diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java b/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java index 6c243a3a873..4dee1d083ba 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultAuthenticatorFactory.java @@ -62,6 +62,8 @@ public class DefaultAuthenticatorFactory implements Authenticator.Factory authenticator=new FormAuthenticator(); else if ( Constraint.__SPNEGO_AUTH.equalsIgnoreCase(auth) ) authenticator = new SpnegoAuthenticator(); + else if ( Constraint.__NEGOTIATE_AUTH.equalsIgnoreCase(auth) ) // see Bug #377076 + authenticator = new SpnegoAuthenticator(Constraint.__NEGOTIATE_AUTH); if (Constraint.__CERT_AUTH.equalsIgnoreCase(auth)||Constraint.__CERT_AUTH2.equalsIgnoreCase(auth)) authenticator=new ClientCertAuthenticator(); diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java index 9df7448468a..412d3fd30cb 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java @@ -36,9 +36,25 @@ public class SpnegoAuthenticator extends LoginAuthenticator { private static final Logger LOG = Log.getLogger(SpnegoAuthenticator.class); + private String _authMethod = Constraint.__SPNEGO_AUTH; + + public SpnegoAuthenticator() + { + + } + + /** + * Allow for a custom authMethod value to be set for instances where SPENGO may not be appropriate + * @param authMethod + */ + public SpnegoAuthenticator( String authMethod ) + { + _authMethod = authMethod; + } + public String getAuthMethod() { - return Constraint.__SPNEGO_AUTH; + return _authMethod; } public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java index 7a6397bf8fc..de3f797e167 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java @@ -492,24 +492,9 @@ public class ServletHolder extends Holder implements UserIdentity.Scope } // Handle configuring servlets that implement org.apache.jasper.servlet.JspServlet - if (isJspServlet(_servlet)) + if (isJspServlet()) { - ContextHandler ch = ((ContextHandler.Context)getServletHandler().getServletContext()).getContextHandler(); - - /* Set the webapp's classpath for Jasper */ - ch.setAttribute("org.apache.catalina.jsp_classpath", ch.getClassPath()); - - /* Set the system classpath for Jasper */ - setInitParameter("com.sun.appserv.jsp.classpath", Loader.getClassPath(ch.getClassLoader())); - - /* Set up other classpath attribute */ - if ("?".equals(getInitParameter("classpath"))) - { - String classpath = ch.getClassPath(); - LOG.debug("classpath=" + classpath); - if (classpath != null) - setInitParameter("classpath", classpath); - } + initJspServlet(); } _servlet.init(_config); @@ -544,6 +529,31 @@ public class ServletHolder extends Holder implements UserIdentity.Scope } + /* ------------------------------------------------------------ */ + /** + * @throws Exception + */ + protected void initJspServlet () throws Exception + { + ContextHandler ch = ((ContextHandler.Context)getServletHandler().getServletContext()).getContextHandler(); + + /* Set the webapp's classpath for Jasper */ + ch.setAttribute("org.apache.catalina.jsp_classpath", ch.getClassPath()); + + /* Set the system classpath for Jasper */ + setInitParameter("com.sun.appserv.jsp.classpath", Loader.getClassPath(ch.getClassLoader().getParent())); + + /* Set up other classpath attribute */ + if ("?".equals(getInitParameter("classpath"))) + { + String classpath = ch.getClassPath(); + LOG.debug("classpath=" + classpath); + if (classpath != null) + setInitParameter("classpath", classpath); + } + } + + /* ------------------------------------------------------------ */ /** * @see org.eclipse.jetty.server.UserIdentity.Scope#getContextPath() @@ -642,12 +652,12 @@ public class ServletHolder extends Holder implements UserIdentity.Scope /* ------------------------------------------------------------ */ - private boolean isJspServlet (Servlet servlet) + private boolean isJspServlet () { - if (servlet == null) + if (_servlet == null) return false; - Class c = servlet.getClass(); + Class c = _servlet.getClass(); boolean result = false; while (c != null && !result) diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/Promise.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/Promise.java index 6f107874d54..bb5393014ff 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/Promise.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/Promise.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.spdy; +import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -89,6 +90,8 @@ public class Promise implements Handler, Future private T result() throws ExecutionException { + if (isCancelled()) + throw new CancellationException(); Throwable failure = this.failure; if (failure != null) throw new ExecutionException(failure); diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java index d97d41db003..446a9103e98 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -64,6 +65,7 @@ import org.eclipse.jetty.spdy.frames.SynStreamFrame; import org.eclipse.jetty.spdy.frames.WindowUpdateFrame; import org.eclipse.jetty.spdy.generator.Generator; import org.eclipse.jetty.spdy.parser.Parser; +import org.eclipse.jetty.util.Atomics; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -79,6 +81,7 @@ public class StandardSession implements ISession, Parser.Listener, Handler attributes = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); private final ConcurrentMap streams = new ConcurrentHashMap<>(); private final LinkedList queue = new LinkedList<>(); @@ -208,7 +211,7 @@ public class StandardSession implements ISession, Parser.Listener, Handler handler) { SettingsFrame frame = new SettingsFrame(version,settingsInfo.getFlags(),settingsInfo.getSettings()); - control(null,frame,timeout,unit,handler,null); + control(null, frame, timeout, unit, handler, null); } @Override @@ -244,7 +247,7 @@ public class StandardSession implements ISession, Parser.Listener, Handler handler) { - goAway(SessionStatus.OK,timeout,unit,handler); + goAway(SessionStatus.OK, timeout, unit, handler); } private void goAway(SessionStatus sessionStatus, long timeout, TimeUnit unit, Handler handler) @@ -269,6 +272,30 @@ public class StandardSession implements ISession, Parser.Listener, Handler oldValue) - { - if (lastStreamId.compareAndSet(oldValue,streamId)) - break; - oldValue = lastStreamId.get(); - } - } + if (streamId % 2 != streamIds.get() % 2) + Atomics.updateMax(lastStreamId, streamId); } @Override diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java index e655afe1452..d1731409681 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java @@ -29,7 +29,6 @@ import org.eclipse.jetty.spdy.api.Handler; import org.eclipse.jetty.spdy.api.HeadersInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.RstInfo; -import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StreamStatus; @@ -37,7 +36,6 @@ import org.eclipse.jetty.spdy.api.SynInfo; import org.eclipse.jetty.spdy.frames.ControlFrame; import org.eclipse.jetty.spdy.frames.HeadersFrame; import org.eclipse.jetty.spdy.frames.SynReplyFrame; -import org.eclipse.jetty.spdy.frames.SynStreamFrame; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -45,9 +43,10 @@ public class StandardStream implements IStream { private static final Logger logger = Log.getLogger(Stream.class); private final Map attributes = new ConcurrentHashMap<>(); - private final IStream associatedStream; - private final SynStreamFrame frame; + private final int id; + private final byte priority; private final ISession session; + private final IStream associatedStream; private final AtomicInteger windowSize = new AtomicInteger(); private final Set pushedStreams = Collections.newSetFromMap(new ConcurrentHashMap()); private volatile StreamFrameListener listener; @@ -55,9 +54,10 @@ public class StandardStream implements IStream private volatile CloseState closeState = CloseState.OPENED; private volatile boolean reset = false; - public StandardStream(SynStreamFrame frame, ISession session, IStream associatedStream) + public StandardStream(int id, byte priority, ISession session, IStream associatedStream) { - this.frame = frame; + this.id = id; + this.priority = priority; this.session = session; this.associatedStream = associatedStream; } @@ -65,7 +65,7 @@ public class StandardStream implements IStream @Override public int getId() { - return frame.getStreamId(); + return id; } @Override @@ -95,7 +95,7 @@ public class StandardStream implements IStream @Override public byte getPriority() { - return frame.getPriority(); + return priority; } @Override @@ -112,7 +112,7 @@ public class StandardStream implements IStream } @Override - public Session getSession() + public ISession getSession() { return session; } @@ -150,7 +150,7 @@ public class StandardStream implements IStream { case OPENED: { - closeState = local?CloseState.LOCALLY_CLOSED:CloseState.REMOTELY_CLOSED; + closeState = local ? CloseState.LOCALLY_CLOSED : CloseState.REMOTELY_CLOSED; break; } case LOCALLY_CLOSED: @@ -191,16 +191,16 @@ public class StandardStream implements IStream { openState = OpenState.REPLY_RECV; SynReplyFrame synReply = (SynReplyFrame)frame; - updateCloseState(synReply.isClose(),false); - ReplyInfo replyInfo = new ReplyInfo(synReply.getHeaders(),synReply.isClose()); + updateCloseState(synReply.isClose(), false); + ReplyInfo replyInfo = new ReplyInfo(synReply.getHeaders(), synReply.isClose()); notifyOnReply(replyInfo); break; } case HEADERS: { HeadersFrame headers = (HeadersFrame)frame; - updateCloseState(headers.isClose(),false); - HeadersInfo headersInfo = new HeadersInfo(headers.getHeaders(),headers.isClose(),headers.isResetCompression()); + updateCloseState(headers.isClose(), false); + HeadersInfo headersInfo = new HeadersInfo(headers.getHeaders(), headers.isClose(), headers.isResetCompression()); notifyOnHeaders(headersInfo); break; } @@ -269,7 +269,7 @@ public class StandardStream implements IStream { if (listener != null) { - logger.debug("Invoking headers callback with {} on listener {}", frame, listener); + logger.debug("Invoking headers callback with {} on listener {}", headersInfo, listener); listener.onHeaders(this, headersInfo); } } @@ -320,11 +320,11 @@ public class StandardStream implements IStream { if (isClosed() || isReset()) { - handler.failed(this, new StreamException(getId(),StreamStatus.STREAM_ALREADY_CLOSED)); + handler.failed(this, new StreamException(getId(), StreamStatus.STREAM_ALREADY_CLOSED)); return; } - PushSynInfo pushSynInfo = new PushSynInfo(getId(),synInfo); - session.syn(pushSynInfo,null,timeout,unit,handler); + PushSynInfo pushSynInfo = new PushSynInfo(getId(), synInfo); + session.syn(pushSynInfo, null, timeout, unit, handler); } @Override @@ -341,9 +341,9 @@ public class StandardStream implements IStream if (isUnidirectional()) throw new IllegalStateException("Protocol violation: cannot send SYN_REPLY frames in unidirectional streams"); openState = OpenState.REPLY_SENT; - updateCloseState(replyInfo.isClose(),true); - SynReplyFrame frame = new SynReplyFrame(session.getVersion(),replyInfo.getFlags(),getId(),replyInfo.getHeaders()); - session.control(this,frame,timeout,unit,handler,null); + updateCloseState(replyInfo.isClose(), true); + SynReplyFrame frame = new SynReplyFrame(session.getVersion(), replyInfo.getFlags(), getId(), replyInfo.getHeaders()); + session.control(this, frame, timeout, unit, handler, null); } @Override @@ -359,18 +359,18 @@ public class StandardStream implements IStream { if (!canSend()) { - session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); + session.rst(new RstInfo(getId(), StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a DATA frame before a SYN_REPLY frame"); } if (isLocallyClosed()) { - session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); + session.rst(new RstInfo(getId(), StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a DATA frame on a closed stream"); } // Cannot update the close state here, because the data that we send may // be flow controlled, so we need the stream to update the window size. - session.data(this,dataInfo,timeout,unit,handler,null); + session.data(this, dataInfo, timeout, unit, handler, null); } @Override @@ -386,18 +386,18 @@ public class StandardStream implements IStream { if (!canSend()) { - session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); + session.rst(new RstInfo(getId(), StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a HEADERS frame before a SYN_REPLY frame"); } if (isLocallyClosed()) { - session.rst(new RstInfo(getId(),StreamStatus.PROTOCOL_ERROR)); + session.rst(new RstInfo(getId(), StreamStatus.PROTOCOL_ERROR)); throw new IllegalStateException("Protocol violation: cannot send a HEADERS frame on a closed stream"); } - updateCloseState(headersInfo.isClose(),true); - HeadersFrame frame = new HeadersFrame(session.getVersion(),headersInfo.getFlags(),getId(),headersInfo.getHeaders()); - session.control(this,frame,timeout,unit,handler,null); + updateCloseState(headersInfo.isClose(), true); + HeadersFrame frame = new HeadersFrame(session.getVersion(), headersInfo.getFlags(), getId(), headersInfo.getHeaders()); + session.control(this, frame, timeout, unit, handler, null); } @Override @@ -440,7 +440,7 @@ public class StandardStream implements IStream @Override public String toString() { - return String.format("stream=%d v%d %s",getId(),session.getVersion(),closeState); + return String.format("stream=%d v%d %s", getId(), session.getVersion(), closeState); } private boolean canSend() diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/ByteBufferDataInfo.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/ByteBufferDataInfo.java index 678ff516e36..77c858ba9b8 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/ByteBufferDataInfo.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/ByteBufferDataInfo.java @@ -68,4 +68,10 @@ public class ByteBufferDataInfo extends DataInfo } return space; } + + @Override + protected ByteBuffer allocate(int size) + { + return buffer.isDirect() ? ByteBuffer.allocateDirect(size) : super.allocate(size); + } } diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java index 31615006042..25e49af4ce3 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java @@ -268,6 +268,21 @@ public class Headers implements Iterable return values; } + /** + * @return the values as a comma separated list + */ + public String valuesAsString() + { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < values.length; ++i) + { + if (i > 0) + result.append(", "); + result.append(values[i]); + } + return result.toString(); + } + /** * @return whether the header has multiple values */ diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Session.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Session.java index 5e2e5e281d7..5f4ff011492 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Session.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Session.java @@ -75,7 +75,7 @@ public interface Session * @see #syn(SynInfo, StreamFrameListener, long, TimeUnit, Handler) */ public Future syn(SynInfo synInfo, StreamFrameListener listener); - + /** *

Sends asynchronously a SYN_FRAME to create a new {@link Stream SPDY stream}.

*

Callers may pass a non-null completion handler to be notified of when the @@ -90,7 +90,7 @@ public interface Session */ public void syn(SynInfo synInfo, StreamFrameListener listener, long timeout, TimeUnit unit, Handler handler); - + /** *

Sends asynchronously a RST_STREAM to abort a stream.

*

Callers may use the returned future to wait for the reset to be sent.

@@ -180,10 +180,40 @@ public interface Session public void goAway(long timeout, TimeUnit unit, Handler handler); /** - * @return the streams currently active in this session + * @return a snapshot of the streams currently active in this session + * @see #getStream(int) */ public Set getStreams(); + /** + * @param streamId the id of the stream to retrieve + * @return the stream with the given stream id + * @see #getStreams() + */ + public Stream getStream(int streamId); + + /** + * @param key the attribute key + * @return an arbitrary object associated with the given key to this session + * @see #setAttribute(String, Object) + */ + public Object getAttribute(String key); + + /** + * @param key the attribute key + * @param value an arbitrary object to associate with the given key to this session + * @see #getAttribute(String) + * @see #removeAttribute(String) + */ + public void setAttribute(String key, Object value); + + /** + * @param key the attribute key + * @return the arbitrary object associated with the given key to this session + * @see #setAttribute(String, Object) + */ + public Object removeAttribute(String key); + /** *

Super interface for listeners with callbacks that are invoked on specific session events.

*/ diff --git a/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardSessionTest.java b/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardSessionTest.java index 07822e26676..af9804aa0db 100644 --- a/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardSessionTest.java +++ b/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardSessionTest.java @@ -405,7 +405,7 @@ public class StandardSessionTest final CountDownLatch failedCalledLatch = new CountDownLatch(2); SynStreamFrame synStreamFrame = new SynStreamFrame(SPDY.V2, SynInfo.FLAG_CLOSE, 1, 0, (byte)0, (short)0, null); - IStream stream = new StandardStream(synStreamFrame, session, null); + IStream stream = new StandardStream(synStreamFrame.getStreamId(), synStreamFrame.getPriority(), session, null); stream.updateWindowSize(8192); Handler.Adapter handler = new Handler.Adapter() { diff --git a/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardStreamTest.java b/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardStreamTest.java index 314905bf4a3..e6ac88862e5 100644 --- a/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardStreamTest.java +++ b/jetty-spdy/spdy-core/src/test/java/org/eclipse/jetty/spdy/StandardStreamTest.java @@ -46,15 +46,13 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - -/* ------------------------------------------------------------ */ -/** - */ @RunWith(MockitoJUnitRunner.class) public class StandardStreamTest { - @Mock private ISession session; - @Mock private SynStreamFrame synStreamFrame; + @Mock + private ISession session; + @Mock + private SynStreamFrame synStreamFrame; /** * Test method for {@link org.eclipse.jetty.spdy.StandardStream#syn(org.eclipse.jetty.spdy.api.SynInfo)}. @@ -63,17 +61,18 @@ public class StandardStreamTest @Test public void testSyn() { - Stream stream = new StandardStream(synStreamFrame,session,null); + Stream stream = new StandardStream(synStreamFrame.getStreamId(), synStreamFrame.getPriority(), session, null); Set streams = new HashSet<>(); streams.add(stream); when(synStreamFrame.isClose()).thenReturn(false); SynInfo synInfo = new SynInfo(false); when(session.getStreams()).thenReturn(streams); stream.syn(synInfo); - verify(session).syn(argThat(new PushSynInfoMatcher(stream.getId(),synInfo)),any(StreamFrameListener.class),anyLong(),any(TimeUnit.class),any(Handler.class)); + verify(session).syn(argThat(new PushSynInfoMatcher(stream.getId(), synInfo)), any(StreamFrameListener.class), anyLong(), any(TimeUnit.class), any(Handler.class)); } - private class PushSynInfoMatcher extends ArgumentMatcher{ + private class PushSynInfoMatcher extends ArgumentMatcher + { int associatedStreamId; SynInfo synInfo; @@ -82,15 +81,18 @@ public class StandardStreamTest this.associatedStreamId = associatedStreamId; this.synInfo = synInfo; } + @Override public boolean matches(Object argument) { PushSynInfo pushSynInfo = (PushSynInfo)argument; - if(pushSynInfo.getAssociatedStreamId() != associatedStreamId){ + if (pushSynInfo.getAssociatedStreamId() != associatedStreamId) + { System.out.println("streamIds do not match!"); return false; } - if(pushSynInfo.isClose() != synInfo.isClose()){ + if (pushSynInfo.isClose() != synInfo.isClose()) + { System.out.println("isClose doesn't match"); return false; } @@ -99,13 +101,14 @@ public class StandardStreamTest } @Test - public void testSynOnClosedStream(){ - IStream stream = new StandardStream(synStreamFrame,session,null); - stream.updateCloseState(true,true); - stream.updateCloseState(true,false); - assertThat("stream expected to be closed",stream.isClosed(),is(true)); + public void testSynOnClosedStream() + { + IStream stream = new StandardStream(synStreamFrame.getStreamId(), synStreamFrame.getPriority(), session, null); + stream.updateCloseState(true, true); + stream.updateCloseState(true, false); + assertThat("stream expected to be closed", stream.isClosed(), is(true)); final CountDownLatch failedLatch = new CountDownLatch(1); - stream.syn(new SynInfo(false),1,TimeUnit.SECONDS,new Handler.Adapter() + stream.syn(new SynInfo(false), 1, TimeUnit.SECONDS, new Handler.Adapter() { @Override public void failed(Stream stream, Throwable x) @@ -121,7 +124,7 @@ public class StandardStreamTest public void testSendDataOnHalfClosedStream() throws InterruptedException, ExecutionException, TimeoutException { SynStreamFrame synStreamFrame = new SynStreamFrame(SPDY.V2, SynInfo.FLAG_CLOSE, 1, 0, (byte)0, (short)0, null); - IStream stream = new StandardStream(synStreamFrame, session, null); + IStream stream = new StandardStream(synStreamFrame.getStreamId(), synStreamFrame.getPriority(), session, null); stream.updateWindowSize(8192); stream.updateCloseState(synStreamFrame.isClose(), true); assertThat("stream is half closed", stream.isHalfClosed(), is(true)); diff --git a/jetty-spdy/spdy-jetty-http-webapp/pom.xml b/jetty-spdy/spdy-jetty-http-webapp/pom.xml index 4ccfed9c3e1..150217e98cb 100644 --- a/jetty-spdy/spdy-jetty-http-webapp/pom.xml +++ b/jetty-spdy/spdy-jetty-http-webapp/pom.xml @@ -60,4 +60,45 @@ --> + + + diff --git a/jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy-proxy.xml b/jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy-proxy.xml new file mode 100644 index 00000000000..7d848686196 --- /dev/null +++ b/jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy-proxy.xml @@ -0,0 +1,88 @@ + + + + + + + src/main/resources/keystore.jks + storepwd + src/main/resources/truststore.jks + storepwd + TLSv1 + + + + + + + + + 9090 + + + spdy/2 + + + + + + + + + + + + + + + + + localhost + + + 2 + 127.0.0.1 + 9090 + + + + + + + + + + + + + 8080 + + + + + + + + + 8443 + + + + + + diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/AbstractHTTPSPDYServerConnector.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/AbstractHTTPSPDYServerConnector.java new file mode 100644 index 00000000000..783bd70b484 --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/AbstractHTTPSPDYServerConnector.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.http; + +import java.io.IOException; + +import org.eclipse.jetty.http.HttpSchemes; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.spdy.SPDYServerConnector; +import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +public class AbstractHTTPSPDYServerConnector extends SPDYServerConnector +{ + public AbstractHTTPSPDYServerConnector(ServerSessionFrameListener listener, SslContextFactory sslContextFactory) + { + super(listener, sslContextFactory); + } + + @Override + public void customize(EndPoint endPoint, Request request) throws IOException + { + super.customize(endPoint, request); + if (getSslContextFactory() != null) + request.setScheme(HttpSchemes.HTTPS); + } + + @Override + public boolean isConfidential(Request request) + { + if (getSslContextFactory() != null) + { + int confidentialPort = getConfidentialPort(); + return confidentialPort == 0 || confidentialPort == request.getServerPort(); + } + return super.isConfidential(request); + } + + @Override + public boolean isIntegral(Request request) + { + if (getSslContextFactory() != null) + { + int integralPort = getIntegralPort(); + return integralPort == 0 || integralPort == request.getServerPort(); + } + return super.isIntegral(request); + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java index 4fb73d9d1ab..2cf6e68fd48 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java @@ -16,16 +16,10 @@ package org.eclipse.jetty.spdy.http; -import java.io.IOException; - -import org.eclipse.jetty.http.HttpSchemes; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.spdy.SPDYServerConnector; import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.util.ssl.SslContextFactory; -public class HTTPSPDYServerConnector extends SPDYServerConnector +public class HTTPSPDYServerConnector extends AbstractHTTPSPDYServerConnector { public HTTPSPDYServerConnector() { @@ -47,43 +41,14 @@ public class HTTPSPDYServerConnector extends SPDYServerConnector // We pass a null ServerSessionFrameListener because for // HTTP over SPDY we need one that references the endPoint super(null, sslContextFactory); - // Override the "spdy/3" protocol by handling HTTP over SPDY + clearAsyncConnectionFactories(); + // The "spdy/3" protocol handles HTTP over SPDY putAsyncConnectionFactory("spdy/3", new ServerHTTPSPDYAsyncConnectionFactory(SPDY.V3, getByteBufferPool(), getExecutor(), getScheduler(), this, pushStrategy)); - // Override the "spdy/2" protocol by handling HTTP over SPDY + // The "spdy/2" protocol handles HTTP over SPDY putAsyncConnectionFactory("spdy/2", new ServerHTTPSPDYAsyncConnectionFactory(SPDY.V2, getByteBufferPool(), getExecutor(), getScheduler(), this, pushStrategy)); - // Add the "http/1.1" protocol for browsers that support NPN but not SPDY + // The "http/1.1" protocol handles browsers that support NPN but not SPDY putAsyncConnectionFactory("http/1.1", new ServerHTTPAsyncConnectionFactory(this)); - // Override the default connection factory for non-SSL connections to speak plain HTTP + // The default connection factory handles plain HTTP on non-SSL or non-NPN connections setDefaultAsyncConnectionFactory(getAsyncConnectionFactory("http/1.1")); } - - @Override - public void customize(EndPoint endPoint, Request request) throws IOException - { - super.customize(endPoint, request); - if (getSslContextFactory() != null) - request.setScheme(HttpSchemes.HTTPS); - } - - @Override - public boolean isConfidential(Request request) - { - if (getSslContextFactory() != null) - { - int confidentialPort = getConfidentialPort(); - return confidentialPort == 0 || confidentialPort == request.getServerPort(); - } - return super.isConfidential(request); - } - - @Override - public boolean isIntegral(Request request) - { - if (getSslContextFactory() != null) - { - int integralPort = getIntegralPort(); - return integralPort == 0 || integralPort == request.getServerPort(); - } - return super.isIntegral(request); - } } diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPAsyncConnectionFactory.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPAsyncConnectionFactory.java index 3622b2fdf69..e5a63c753ea 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPAsyncConnectionFactory.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPAsyncConnectionFactory.java @@ -21,18 +21,23 @@ import java.nio.channels.SocketChannel; import org.eclipse.jetty.io.AsyncEndPoint; import org.eclipse.jetty.io.nio.AsyncConnection; import org.eclipse.jetty.server.AsyncHttpConnection; -import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.spdy.AsyncConnectionFactory; +import org.eclipse.jetty.spdy.SPDYServerConnector; public class ServerHTTPAsyncConnectionFactory implements AsyncConnectionFactory { - private final Connector connector; + private final SPDYServerConnector connector; - public ServerHTTPAsyncConnectionFactory(Connector connector) + public ServerHTTPAsyncConnectionFactory(SPDYServerConnector connector) { this.connector = connector; } + public SPDYServerConnector getConnector() + { + return connector; + } + @Override public AsyncConnection newAsyncConnection(SocketChannel channel, AsyncEndPoint endPoint, Object attachment) { diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/HTTPSPDYProxyConnector.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/HTTPSPDYProxyConnector.java new file mode 100644 index 00000000000..0858aedd89b --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/HTTPSPDYProxyConnector.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.proxy; + +import org.eclipse.jetty.spdy.ServerSPDYAsyncConnectionFactory; +import org.eclipse.jetty.spdy.api.SPDY; +import org.eclipse.jetty.spdy.http.AbstractHTTPSPDYServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +public class HTTPSPDYProxyConnector extends AbstractHTTPSPDYServerConnector +{ + public HTTPSPDYProxyConnector(SPDYProxyEngine proxyEngine) + { + this(proxyEngine, null); + } + + public HTTPSPDYProxyConnector(SPDYProxyEngine proxyEngine, SslContextFactory sslContextFactory) + { + super(proxyEngine, sslContextFactory); + clearAsyncConnectionFactories(); + + putAsyncConnectionFactory("spdy/3", new ServerSPDYAsyncConnectionFactory(SPDY.V3, getByteBufferPool(), getExecutor(), getScheduler(), proxyEngine)); + putAsyncConnectionFactory("spdy/2", new ServerSPDYAsyncConnectionFactory(SPDY.V2, getByteBufferPool(), getExecutor(), getScheduler(), proxyEngine)); + putAsyncConnectionFactory("http/1.1", new ProxyHTTPAsyncConnectionFactory(this, SPDY.V3, proxyEngine)); + setDefaultAsyncConnectionFactory(getAsyncConnectionFactory("http/1.1")); + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyEngine.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyEngine.java new file mode 100644 index 00000000000..1013430f17f --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyEngine.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.proxy; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.eclipse.jetty.spdy.api.Headers; +import org.eclipse.jetty.spdy.api.StreamFrameListener; +import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + *

{@link ProxyEngine} is the base class for SPDY proxy functionalities, that is a proxy that + * accepts SPDY from its client side and converts to any protocol to its server side.

+ *

This class listens for SPDY events sent by clients; subclasses are responsible for translating + * these SPDY client events into appropriate events to forward to the server, in the appropriate + * protocol that is understood by the server.

+ *

This class also provides configuration for the proxy rules.

+ */ +public abstract class ProxyEngine extends ServerSessionFrameListener.Adapter implements StreamFrameListener +{ + protected final Logger logger = Log.getLogger(getClass()); + private final ConcurrentMap proxyInfos = new ConcurrentHashMap<>(); + private final String name; + + protected ProxyEngine() + { + this(name()); + } + + private static String name() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException x) + { + return "localhost"; + } + } + + protected ProxyEngine(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + + protected void addRequestProxyHeaders(Headers headers) + { + String newValue = ""; + Headers.Header header = headers.get("via"); + if (header != null) + newValue = header.valuesAsString() + ", "; + newValue += "http/1.1 " + getName(); + headers.put("via", newValue); + } + + protected void addResponseProxyHeaders(Headers headers) + { + // TODO: add Via header + } + + public Map getProxyInfos() + { + return new HashMap<>(proxyInfos); + } + + public void setProxyInfos(Map proxyInfos) + { + this.proxyInfos.clear(); + this.proxyInfos.putAll(proxyInfos); + } + + public void putProxyInfo(String host, ProxyInfo proxyInfo) + { + proxyInfos.put(host, proxyInfo); + } + + protected ProxyInfo getProxyInfo(String host) + { + return proxyInfos.get(host); + } + + public static class ProxyInfo + { + private final short version; + private final InetSocketAddress address; + + public ProxyInfo(short version, String host, int port) + { + this.version = version; + this.address = new InetSocketAddress(host, port); + } + + public short getVersion() + { + return version; + } + + public InetSocketAddress getAddress() + { + return address; + } + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPAsyncConnectionFactory.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPAsyncConnectionFactory.java new file mode 100644 index 00000000000..44e7e7f95fa --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPAsyncConnectionFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.proxy; + +import java.nio.channels.SocketChannel; + +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.nio.AsyncConnection; +import org.eclipse.jetty.spdy.SPDYServerConnector; +import org.eclipse.jetty.spdy.http.ServerHTTPAsyncConnectionFactory; + +public class ProxyHTTPAsyncConnectionFactory extends ServerHTTPAsyncConnectionFactory +{ + private final short version; + private final ProxyEngine proxyEngine; + + public ProxyHTTPAsyncConnectionFactory(SPDYServerConnector connector, short version, ProxyEngine proxyEngine) + { + super(connector); + this.version = version; + this.proxyEngine = proxyEngine; + } + + @Override + public AsyncConnection newAsyncConnection(SocketChannel channel, AsyncEndPoint endPoint, Object attachment) + { + return new ProxyHTTPSPDYAsyncConnection(getConnector(), endPoint, version, proxyEngine); + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYAsyncConnection.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYAsyncConnection.java new file mode 100644 index 00000000000..e7253094e0c --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYAsyncConnection.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.proxy; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpGenerator; +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.nio.DirectNIOBuffer; +import org.eclipse.jetty.io.nio.IndirectNIOBuffer; +import org.eclipse.jetty.io.nio.NIOBuffer; +import org.eclipse.jetty.server.AsyncHttpConnection; +import org.eclipse.jetty.spdy.ISession; +import org.eclipse.jetty.spdy.IStream; +import org.eclipse.jetty.spdy.SPDYServerConnector; +import org.eclipse.jetty.spdy.StandardSession; +import org.eclipse.jetty.spdy.StandardStream; +import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; +import org.eclipse.jetty.spdy.api.BytesDataInfo; +import org.eclipse.jetty.spdy.api.DataInfo; +import org.eclipse.jetty.spdy.api.GoAwayInfo; +import org.eclipse.jetty.spdy.api.Handler; +import org.eclipse.jetty.spdy.api.Headers; +import org.eclipse.jetty.spdy.api.HeadersInfo; +import org.eclipse.jetty.spdy.api.ReplyInfo; +import org.eclipse.jetty.spdy.api.RstInfo; +import org.eclipse.jetty.spdy.api.SessionStatus; +import org.eclipse.jetty.spdy.api.Stream; +import org.eclipse.jetty.spdy.api.SynInfo; +import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; + +public class ProxyHTTPSPDYAsyncConnection extends AsyncHttpConnection +{ + private final Headers headers = new Headers(); + private final short version; + private final ProxyEngine proxyEngine; + private final HttpGenerator generator; + private final ISession session; + private Stream stream; + private Buffer content; + + public ProxyHTTPSPDYAsyncConnection(SPDYServerConnector connector, EndPoint endpoint, short version, ProxyEngine proxyEngine) + { + super(connector, endpoint, connector.getServer()); + this.version = version; + this.proxyEngine = proxyEngine; + this.generator = (HttpGenerator)_generator; + this.session = new HTTPSession(version, connector); + } + + @Override + public AsyncEndPoint getEndPoint() + { + return (AsyncEndPoint)super.getEndPoint(); + } + + @Override + protected void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException + { + SPDYServerConnector connector = (SPDYServerConnector)getConnector(); + String scheme = connector.getSslContextFactory() != null ? "https" : "http"; + headers.put(HTTPSPDYHeader.SCHEME.name(version), scheme); + headers.put(HTTPSPDYHeader.METHOD.name(version), method.toString("UTF-8")); + headers.put(HTTPSPDYHeader.URI.name(version), uri.toString("UTF-8")); + headers.put(HTTPSPDYHeader.VERSION.name(version), httpVersion.toString("UTF-8")); + } + + @Override + protected void parsedHeader(Buffer name, Buffer value) throws IOException + { + String headerName = name.toString("UTF-8").toLowerCase(); + String headerValue = value.toString("UTF-8"); + switch (headerName) + { + case "host": + headers.put(HTTPSPDYHeader.HOST.name(version), headerValue); + break; + default: + headers.put(headerName, headerValue); + break; + } + } + + @Override + protected void headerComplete() throws IOException + { + } + + @Override + protected void content(Buffer buffer) throws IOException + { + if (content == null) + { + stream = syn(false); + content = buffer; + } + else + { + proxyEngine.onData(stream, toDataInfo(buffer, false)); + } + } + + @Override + public void messageComplete(long contentLength) throws IOException + { + if (stream == null) + { + assert content == null; + if (headers.isEmpty()) + proxyEngine.onGoAway(session, new GoAwayInfo(0, SessionStatus.OK)); + else + syn(true); + } + else + { + proxyEngine.onData(stream, toDataInfo(content, true)); + } + headers.clear(); + stream = null; + content = null; + } + + private Stream syn(boolean close) + { + Stream stream = new HTTPStream(1, (byte)0, session, null); + proxyEngine.onSyn(stream, new SynInfo(headers, close)); + return stream; + } + + private DataInfo toDataInfo(Buffer buffer, boolean close) + { + if (buffer instanceof ByteArrayBuffer) + return new BytesDataInfo(buffer.array(), buffer.getIndex(), buffer.length(), close); + + if (buffer instanceof NIOBuffer) + { + ByteBuffer byteBuffer = ((NIOBuffer)buffer).getByteBuffer(); + byteBuffer.limit(buffer.putIndex()); + byteBuffer.position(buffer.getIndex()); + return new ByteBufferDataInfo(byteBuffer, close); + } + + return new BytesDataInfo(buffer.asArray(), close); + } + + private class HTTPSession extends StandardSession + { + private HTTPSession(short version, SPDYServerConnector connector) + { + super(version, connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), null, null, 1, proxyEngine, null, null); + } + + @Override + public void rst(RstInfo rstInfo, long timeout, TimeUnit unit, Handler handler) + { + // Not much we can do in HTTP land: just close the connection + goAway(timeout, unit, handler); + } + + @Override + public void goAway(long timeout, TimeUnit unit, Handler handler) + { + try + { + getEndPoint().close(); + handler.completed(null); + } + catch (IOException x) + { + handler.failed(null, x); + } + } + } + + /** + *

This stream will convert the SPDY invocations performed by the proxy into HTTP to be sent to the client.

+ */ + private class HTTPStream extends StandardStream + { + private final Pattern statusRegexp = Pattern.compile("(\\d{3})\\s*(.*)"); + + private HTTPStream(int id, byte priority, ISession session, IStream associatedStream) + { + super(id, priority, session, associatedStream); + } + + @Override + public void syn(SynInfo synInfo, long timeout, TimeUnit unit, Handler handler) + { + // HTTP does not support pushed streams + handler.completed(new HTTPPushStream(2, getPriority(), getSession(), this)); + } + + @Override + public void headers(HeadersInfo headersInfo, long timeout, TimeUnit unit, Handler handler) + { + // TODO + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + @Override + public void reply(ReplyInfo replyInfo, long timeout, TimeUnit unit, Handler handler) + { + try + { + Headers headers = new Headers(replyInfo.getHeaders(), false); + + headers.remove(HTTPSPDYHeader.SCHEME.name(version)); + + String status = headers.remove(HTTPSPDYHeader.STATUS.name(version)).value(); + Matcher matcher = statusRegexp.matcher(status); + matcher.matches(); + int code = Integer.parseInt(matcher.group(1)); + String reason = matcher.group(2); + generator.setResponse(code, reason); + + String httpVersion = headers.remove(HTTPSPDYHeader.VERSION.name(version)).value(); + generator.setVersion(Integer.parseInt(httpVersion.replaceAll("\\D", ""))); + + Headers.Header host = headers.remove(HTTPSPDYHeader.HOST.name(version)); + if (host != null) + headers.put("host", host.value()); + + HttpFields fields = new HttpFields(); + for (Headers.Header header : headers) + { + String name = camelize(header.name()); + fields.put(name, header.value()); + } + generator.completeHeader(fields, replyInfo.isClose()); + + if (replyInfo.isClose()) + complete(); + + handler.completed(null); + } + catch (IOException x) + { + handler.failed(null, x); + } + } + + private String camelize(String name) + { + char[] chars = name.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + + for (int i = 0; i < chars.length; ++i) + { + char c = chars[i]; + int j = i + 1; + if (c == '-' && j < chars.length) + chars[j] = Character.toUpperCase(chars[j]); + } + return new String(chars); + } + + @Override + public void data(DataInfo dataInfo, long timeout, TimeUnit unit, Handler handler) + { + try + { + // Data buffer must be copied, as the ByteBuffer is pooled + ByteBuffer byteBuffer = dataInfo.asByteBuffer(false); + + Buffer buffer = byteBuffer.isDirect() ? + new DirectNIOBuffer(byteBuffer, false) : + new IndirectNIOBuffer(byteBuffer, false); + + generator.addContent(buffer, dataInfo.isClose()); + generator.flush(unit.toMillis(timeout)); + + if (dataInfo.isClose()) + complete(); + + handler.completed(null); + } + catch (IOException x) + { + handler.failed(null, x); + } + } + + private void complete() throws IOException + { + generator.complete(); + // We need to call asyncDispatch() as if the HTTP request + // has been suspended and now we complete the response + getEndPoint().asyncDispatch(); + } + } + + private class HTTPPushStream extends StandardStream + { + private HTTPPushStream(int id, byte priority, ISession session, IStream associatedStream) + { + super(id, priority, session, associatedStream); + } + + @Override + public void headers(HeadersInfo headersInfo, long timeout, TimeUnit unit, Handler handler) + { + // Ignore pushed headers + handler.completed(null); + } + + @Override + public void data(DataInfo dataInfo, long timeout, TimeUnit unit, Handler handler) + { + // Ignore pushed data + handler.completed(null); + } + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/SPDYProxyEngine.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/SPDYProxyEngine.java new file mode 100644 index 00000000000..23b38b0cdbb --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/SPDYProxyEngine.java @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.proxy; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.spdy.SPDYClient; +import org.eclipse.jetty.spdy.api.ByteBufferDataInfo; +import org.eclipse.jetty.spdy.api.DataInfo; +import org.eclipse.jetty.spdy.api.GoAwayInfo; +import org.eclipse.jetty.spdy.api.Handler; +import org.eclipse.jetty.spdy.api.Headers; +import org.eclipse.jetty.spdy.api.HeadersInfo; +import org.eclipse.jetty.spdy.api.PingInfo; +import org.eclipse.jetty.spdy.api.ReplyInfo; +import org.eclipse.jetty.spdy.api.RstInfo; +import org.eclipse.jetty.spdy.api.Session; +import org.eclipse.jetty.spdy.api.SessionFrameListener; +import org.eclipse.jetty.spdy.api.Stream; +import org.eclipse.jetty.spdy.api.StreamFrameListener; +import org.eclipse.jetty.spdy.api.StreamStatus; +import org.eclipse.jetty.spdy.api.SynInfo; +import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; + +/** + *

{@link SPDYProxyEngine} implements a SPDY to SPDY proxy, that is, converts SPDY events received by + * clients into SPDY events for the servers.

+ */ +public class SPDYProxyEngine extends ProxyEngine +{ + private static final String STREAM_HANDLER_ATTRIBUTE = "org.eclipse.jetty.spdy.http.proxy.streamHandler"; + private static final String CLIENT_STREAM_ATTRIBUTE = "org.eclipse.jetty.spdy.http.proxy.clientStream"; + private static final String CLIENT_SESSIONS_ATTRIBUTE = "org.eclipse.jetty.spdy.http.proxy.clientSessions"; + + private final ConcurrentMap serverSessions = new ConcurrentHashMap<>(); + private final SessionFrameListener sessionListener = new ProxySessionFrameListener(); + private final SPDYClient.Factory factory; + private volatile long connectTimeout = 15000; + private volatile long timeout = 60000; + + public SPDYProxyEngine(SPDYClient.Factory factory) + { + this.factory = factory; + } + + public long getConnectTimeout() + { + return connectTimeout; + } + + public void setConnectTimeout(long connectTimeout) + { + this.connectTimeout = connectTimeout; + } + + public long getTimeout() + { + return timeout; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } + + @Override + public void onPing(Session clientSession, PingInfo pingInfo) + { + // We do not know to which upstream server + // to send the PING so we just ignore it + } + + @Override + public void onGoAway(Session clientSession, GoAwayInfo goAwayInfo) + { + for (Session serverSession : serverSessions.values()) + { + @SuppressWarnings("unchecked") + Set sessions = (Set)serverSession.getAttribute(CLIENT_SESSIONS_ATTRIBUTE); + if (sessions.remove(clientSession)) + break; + } + } + + @Override + public StreamFrameListener onSyn(final Stream clientStream, SynInfo clientSynInfo) + { + logger.debug("C -> P {} on {}", clientSynInfo, clientStream); + + final Session clientSession = clientStream.getSession(); + short clientVersion = clientSession.getVersion(); + Headers headers = new Headers(clientSynInfo.getHeaders(), false); + + Headers.Header hostHeader = headers.get(HTTPSPDYHeader.HOST.name(clientVersion)); + if (hostHeader == null) + { + rst(clientStream); + return null; + } + + String host = hostHeader.value(); + int colon = host.indexOf(':'); + if (colon >= 0) + host = host.substring(0, colon); + ProxyInfo proxyInfo = getProxyInfo(host); + if (proxyInfo == null) + { + rst(clientStream); + return null; + } + + // TODO: give a chance to modify headers and rewrite URI + + short serverVersion = proxyInfo.getVersion(); + InetSocketAddress address = proxyInfo.getAddress(); + Session serverSession = produceSession(host, serverVersion, address); + if (serverSession == null) + { + rst(clientStream); + return null; + } + + @SuppressWarnings("unchecked") + Set sessions = (Set)serverSession.getAttribute(CLIENT_SESSIONS_ATTRIBUTE); + sessions.add(clientSession); + + convert(clientVersion, serverVersion, headers); + + addRequestProxyHeaders(headers); + + SynInfo serverSynInfo = new SynInfo(headers, clientSynInfo.isClose()); + logger.debug("P -> S {}", serverSynInfo); + + StreamFrameListener listener = new ProxyStreamFrameListener(clientStream); + StreamHandler handler = new StreamHandler(clientStream); + clientStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); + serverSession.syn(serverSynInfo, listener, timeout, TimeUnit.MILLISECONDS, handler); + return this; + } + + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + // Servers do not receive replies + } + + @Override + public void onHeaders(Stream stream, HeadersInfo headersInfo) + { + // TODO + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + @Override + public void onData(Stream clientStream, final DataInfo clientDataInfo) + { + logger.debug("C -> P {} on {}", clientDataInfo, clientStream); + + ByteBufferDataInfo serverDataInfo = new ByteBufferDataInfo(clientDataInfo.asByteBuffer(false), clientDataInfo.isClose()) + { + @Override + public void consume(int delta) + { + super.consume(delta); + clientDataInfo.consume(delta); + } + }; + + StreamHandler streamHandler = (StreamHandler)clientStream.getAttribute(STREAM_HANDLER_ATTRIBUTE); + streamHandler.data(serverDataInfo); + } + + private Session produceSession(String host, short version, InetSocketAddress address) + { + try + { + Session session = serverSessions.get(host); + if (session == null) + { + SPDYClient client = factory.newSPDYClient(version); + session = client.connect(address, sessionListener).get(getConnectTimeout(), TimeUnit.MILLISECONDS); + session.setAttribute(CLIENT_SESSIONS_ATTRIBUTE, Collections.newSetFromMap(new ConcurrentHashMap())); + logger.debug("Proxy session connected to {}", address); + Session existing = serverSessions.putIfAbsent(host, session); + if (existing != null) + { + session.goAway(getTimeout(), TimeUnit.MILLISECONDS, new Handler.Adapter()); + session = existing; + } + } + return session; + } + catch (Exception x) + { + logger.debug(x); + return null; + } + } + + private void convert(short fromVersion, short toVersion, Headers headers) + { + if (fromVersion != toVersion) + { + for (HTTPSPDYHeader httpHeader : HTTPSPDYHeader.values()) + { + Headers.Header header = headers.remove(httpHeader.name(fromVersion)); + if (header != null) + { + String toName = httpHeader.name(toVersion); + for (String value : header.values()) + headers.add(toName, value); + } + } + } + } + + private void rst(Stream stream) + { + RstInfo rstInfo = new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM); + stream.getSession().rst(rstInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler.Adapter()); + } + + private class ProxyStreamFrameListener extends StreamFrameListener.Adapter + { + private final Stream clientStream; + private volatile ReplyInfo replyInfo; + + public ProxyStreamFrameListener(Stream clientStream) + { + this.clientStream = clientStream; + } + + @Override + public void onReply(final Stream stream, ReplyInfo replyInfo) + { + short serverVersion = stream.getSession().getVersion(); + Headers headers = new Headers(replyInfo.getHeaders(), false); + short clientVersion = this.clientStream.getSession().getVersion(); + convert(serverVersion, clientVersion, headers); + + addResponseProxyHeaders(headers); + + this.replyInfo = new ReplyInfo(headers, replyInfo.isClose()); + if (replyInfo.isClose()) + reply(); + } + + @Override + public void onHeaders(Stream stream, HeadersInfo headersInfo) + { + // TODO + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + @Override + public void onData(final Stream stream, final DataInfo dataInfo) + { + if (replyInfo != null) + { + if (dataInfo.isClose()) + replyInfo.getHeaders().put("content-length", String.valueOf(dataInfo.available())); + reply(); + } + data(dataInfo); + } + + private void reply() + { + clientStream.reply(replyInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler.Adapter() + { + @Override + public void failed(Void context, Throwable x) + { + logger.debug(x); + rst(clientStream); + } + }); + replyInfo = null; + } + + private void data(final DataInfo dataInfo) + { + clientStream.data(dataInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler() + { + @Override + public void completed(Void context) + { + dataInfo.consume(dataInfo.length()); + } + + @Override + public void failed(Void context, Throwable x) + { + logger.debug(x); + rst(clientStream); + } + }); + } + } + + /** + *

{@link StreamHandler} implements the forwarding of DATA frames from the client to the server.

+ *

Instances of this class buffer DATA frames sent by clients and send them to the server. + * The buffering happens between the send of the SYN_STREAM to the server (where DATA frames may arrive + * from the client before the SYN_STREAM has been fully sent), and between DATA frames, if the client + * is a fast producer and the server a slow consumer, or if the client is a SPDY v2 client (and hence + * without flow control) while the server is a SPDY v3 server (and hence with flow control).

+ */ + private class StreamHandler implements Handler + { + private final Queue queue = new LinkedList<>(); + private final Stream clientStream; + private Stream serverStream; + + private StreamHandler(Stream clientStream) + { + this.clientStream = clientStream; + } + + @Override + public void completed(Stream serverStream) + { + serverStream.setAttribute(CLIENT_STREAM_ATTRIBUTE, clientStream); + + DataInfoHandler dataInfoHandler; + synchronized (queue) + { + this.serverStream = serverStream; + dataInfoHandler = queue.peek(); + if (dataInfoHandler != null) + { + if (dataInfoHandler.flushing) + { + logger.debug("SYN completed, flushing {}, queue size {}", dataInfoHandler.dataInfo, queue.size()); + dataInfoHandler = null; + } + else + { + dataInfoHandler.flushing = true; + logger.debug("SYN completed, queue size {}", queue.size()); + } + } + else + { + logger.debug("SYN completed, queue empty"); + } + } + if (dataInfoHandler != null) + flush(serverStream, dataInfoHandler); + } + + @Override + public void failed(Stream serverStream, Throwable x) + { + logger.debug(x); + rst(clientStream); + } + + public void data(DataInfo dataInfo) + { + Stream serverStream; + DataInfoHandler dataInfoHandler = null; + DataInfoHandler item = new DataInfoHandler(dataInfo); + synchronized (queue) + { + queue.offer(item); + serverStream = this.serverStream; + if (serverStream != null) + { + dataInfoHandler = queue.peek(); + if (dataInfoHandler.flushing) + { + logger.debug("Queued {}, flushing {}, queue size {}", dataInfo, dataInfoHandler.dataInfo, queue.size()); + serverStream = null; + } + else + { + dataInfoHandler.flushing = true; + logger.debug("Queued {}, queue size {}", dataInfo, queue.size()); + } + } + else + { + logger.debug("Queued {}, SYN incomplete, queue size {}", dataInfo, queue.size()); + } + } + if (serverStream != null) + flush(serverStream, dataInfoHandler); + } + + private void flush(Stream serverStream, DataInfoHandler dataInfoHandler) + { + logger.debug("P -> S {} on {}", dataInfoHandler.dataInfo, serverStream); + serverStream.data(dataInfoHandler.dataInfo, getTimeout(), TimeUnit.MILLISECONDS, dataInfoHandler); + } + + private class DataInfoHandler implements Handler + { + private final DataInfo dataInfo; + private boolean flushing; + + private DataInfoHandler(DataInfo dataInfo) + { + this.dataInfo = dataInfo; + } + + @Override + public void completed(Void context) + { + Stream serverStream; + DataInfoHandler dataInfoHandler; + synchronized (queue) + { + serverStream = StreamHandler.this.serverStream; + assert serverStream != null; + dataInfoHandler = queue.poll(); + assert dataInfoHandler == this; + dataInfoHandler = queue.peek(); + if (dataInfoHandler != null) + { + assert !dataInfoHandler.flushing; + dataInfoHandler.flushing = true; + logger.debug("Completed {}, queue size {}", dataInfo, queue.size()); + } + else + { + logger.debug("Completed {}, queue empty", dataInfo); + } + } + if (dataInfoHandler != null) + flush(serverStream, dataInfoHandler); + } + + @Override + public void failed(Void context, Throwable x) + { + logger.debug(x); + rst(clientStream); + } + } + } + + private class ProxySessionFrameListener extends SessionFrameListener.Adapter implements StreamFrameListener + { + @Override + public StreamFrameListener onSyn(Stream serverStream, SynInfo serverSynInfo) + { + logger.debug("S -> P pushed {} on {}", serverSynInfo, serverStream); + + Headers headers = new Headers(serverSynInfo.getHeaders(), false); + + Stream clientStream = (Stream)serverStream.getAssociatedStream().getAttribute(CLIENT_STREAM_ATTRIBUTE); + convert(serverStream.getSession().getVersion(), clientStream.getSession().getVersion(), headers); + + addResponseProxyHeaders(headers); + + StreamHandler handler = new StreamHandler(clientStream); + serverStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); + clientStream.syn(new SynInfo(headers, serverSynInfo.isClose()), getTimeout(), TimeUnit.MILLISECONDS, handler); + return this; + } + + @Override + public void onRst(Session serverSession, RstInfo serverRstInfo) + { + Stream serverStream = serverSession.getStream(serverRstInfo.getStreamId()); + if (serverStream != null) + { + Stream clientStream = (Stream)serverStream.getAttribute(CLIENT_STREAM_ATTRIBUTE); + if (clientStream != null) + { + Session clientSession = clientStream.getSession(); + RstInfo clientRstInfo = new RstInfo(clientStream.getId(), serverRstInfo.getStreamStatus()); + clientSession.rst(clientRstInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler.Adapter()); + } + } + } + + @Override + public void onGoAway(Session serverSession, GoAwayInfo goAwayInfo) + { + serverSessions.values().remove(serverSession); + @SuppressWarnings("unchecked") + Set sessions = (Set)serverSession.removeAttribute(CLIENT_SESSIONS_ATTRIBUTE); + for (Session session : sessions) + session.goAway(getTimeout(), TimeUnit.MILLISECONDS, new Handler.Adapter()); + } + + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + // Push streams never send a reply + } + + @Override + public void onHeaders(Stream stream, HeadersInfo headersInfo) + { + throw new UnsupportedOperationException(); + } + + @Override + public void onData(Stream serverStream, final DataInfo serverDataInfo) + { + logger.debug("S -> P pushed {} on {}", serverDataInfo, serverStream); + + ByteBufferDataInfo clientDataInfo = new ByteBufferDataInfo(serverDataInfo.asByteBuffer(false), serverDataInfo.isClose()) + { + @Override + public void consume(int delta) + { + super.consume(delta); + serverDataInfo.consume(delta); + } + }; + + StreamHandler handler = (StreamHandler)serverStream.getAttribute(STREAM_HANDLER_ATTRIBUTE); + handler.data(clientDataInfo); + } + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYv2Test.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYv2Test.java new file mode 100644 index 00000000000..4bf8e02f6ab --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYv2Test.java @@ -0,0 +1,855 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.proxy; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.spdy.SPDYClient; +import org.eclipse.jetty.spdy.SPDYServerConnector; +import org.eclipse.jetty.spdy.ServerSPDYAsyncConnectionFactory; +import org.eclipse.jetty.spdy.api.BytesDataInfo; +import org.eclipse.jetty.spdy.api.DataInfo; +import org.eclipse.jetty.spdy.api.GoAwayInfo; +import org.eclipse.jetty.spdy.api.Handler; +import org.eclipse.jetty.spdy.api.Headers; +import org.eclipse.jetty.spdy.api.PingInfo; +import org.eclipse.jetty.spdy.api.ReplyInfo; +import org.eclipse.jetty.spdy.api.RstInfo; +import org.eclipse.jetty.spdy.api.SPDY; +import org.eclipse.jetty.spdy.api.Session; +import org.eclipse.jetty.spdy.api.SessionFrameListener; +import org.eclipse.jetty.spdy.api.Stream; +import org.eclipse.jetty.spdy.api.StreamFrameListener; +import org.eclipse.jetty.spdy.api.StreamStatus; +import org.eclipse.jetty.spdy.api.SynInfo; +import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; +import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestWatchman; +import org.junit.runners.model.FrameworkMethod; + +public class ProxyHTTPSPDYv2Test +{ + @Rule + public final TestWatchman testName = new TestWatchman() + { + @Override + public void starting(FrameworkMethod method) + { + super.starting(method); + System.err.printf("Running %s.%s()%n", + method.getMethod().getDeclaringClass().getName(), + method.getName()); + } + }; + + private SPDYClient.Factory factory; + private Server server; + private Server proxy; + private SPDYServerConnector proxyConnector; + + protected short version() + { + return SPDY.V2; + } + + protected InetSocketAddress startServer(ServerSessionFrameListener listener) throws Exception + { + server = new Server(); + SPDYServerConnector serverConnector = new SPDYServerConnector(listener); + serverConnector.setDefaultAsyncConnectionFactory(new ServerSPDYAsyncConnectionFactory(version(), serverConnector.getByteBufferPool(), serverConnector.getExecutor(), serverConnector.getScheduler(), listener)); + serverConnector.setPort(0); + server.addConnector(serverConnector); + server.start(); + return new InetSocketAddress("localhost", serverConnector.getLocalPort()); + } + + protected InetSocketAddress startProxy(InetSocketAddress address) throws Exception + { + proxy = new Server(); + SPDYProxyEngine proxyEngine = new SPDYProxyEngine(factory); + proxyEngine.putProxyInfo("localhost", new ProxyEngine.ProxyInfo(version(), address.getHostName(), address.getPort())); + proxyConnector = new HTTPSPDYProxyConnector(proxyEngine); + proxyConnector.setPort(0); + proxy.addConnector(proxyConnector); + proxy.start(); + return new InetSocketAddress("localhost", proxyConnector.getLocalPort()); + } + + @Before + public void init() throws Exception + { + factory = new SPDYClient.Factory(); + factory.start(); + } + + @After + public void destroy() throws Exception + { + if (server != null) + { + server.stop(); + server.join(); + } + if (proxy != null) + { + proxy.stop(); + proxy.join(); + } + factory.stop(); + } + + @Test + public void testClosingClientDoesNotCloseServer() throws Exception + { + final CountDownLatch closeLatch = new CountDownLatch(1); + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + stream.reply(new ReplyInfo(responseHeaders, true)); + return null; + } + + @Override + public void onGoAway(Session session, GoAwayInfo goAwayInfo) + { + closeLatch.countDown(); + } + })); + + Socket client = new Socket(); + client.connect(proxyAddress); + OutputStream output = client.getOutputStream(); + + String request = "" + + "GET / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.flush(); + + InputStream input = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader.readLine(); + Assert.assertFalse(reader.ready()); + + client.close(); + + // Must not close, other clients may still be connected + Assert.assertFalse(closeLatch.await(1, TimeUnit.SECONDS)); + } + + @Test + public void testClosingServerClosesHTTPClient() throws Exception + { + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + stream.reply(new ReplyInfo(responseHeaders, true)); + stream.getSession().goAway(); + return null; + } + })); + + Socket client = new Socket(); + client.connect(proxyAddress); + OutputStream output = client.getOutputStream(); + + String request = "" + + "GET / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.flush(); + + client.setSoTimeout(1000); + InputStream input = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader.readLine(); + Assert.assertFalse(reader.ready()); + + Assert.assertNull(reader.readLine()); + + client.close(); + } + + @Test + public void testClosingServerClosesSPDYClient() throws Exception + { + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + stream.reply(new ReplyInfo(responseHeaders, true)); + stream.getSession().goAway(); + return null; + } + })); + proxyConnector.setDefaultAsyncConnectionFactory(proxyConnector.getAsyncConnectionFactory("spdy/" + version())); + + final CountDownLatch goAwayLatch = new CountDownLatch(1); + Session client = factory.newSPDYClient(version()).connect(proxyAddress, new SessionFrameListener.Adapter() + { + @Override + public void onGoAway(Session session, GoAwayInfo goAwayInfo) + { + goAwayLatch.countDown(); + } + }).get(5, TimeUnit.SECONDS); + + final CountDownLatch replyLatch = new CountDownLatch(1); + Headers headers = new Headers(); + headers.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + headers.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + headers.put(HTTPSPDYHeader.URI.name(version()), "/"); + headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + proxyAddress.getPort()); + client.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + replyLatch.countDown(); + } + }); + + Assert.assertTrue(replyLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(goAwayLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testGETThenNoContentFromTwoClients() throws Exception + { + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(synInfo.isClose()); + Headers requestHeaders = synInfo.getHeaders(); + Assert.assertNotNull(requestHeaders.get("via")); + + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + ReplyInfo replyInfo = new ReplyInfo(responseHeaders, true); + stream.reply(replyInfo); + return null; + } + })); + + Socket client1 = new Socket(); + client1.connect(proxyAddress); + OutputStream output1 = client1.getOutputStream(); + + String request = "" + + "GET / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "\r\n"; + output1.write(request.getBytes("UTF-8")); + output1.flush(); + + InputStream input1 = client1.getInputStream(); + BufferedReader reader1 = new BufferedReader(new InputStreamReader(input1, "UTF-8")); + String line = reader1.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader1.readLine(); + Assert.assertFalse(reader1.ready()); + + // Perform another request with another client + Socket client2 = new Socket(); + client2.connect(proxyAddress); + OutputStream output2 = client2.getOutputStream(); + + output2.write(request.getBytes("UTF-8")); + output2.flush(); + + InputStream input2 = client2.getInputStream(); + BufferedReader reader2 = new BufferedReader(new InputStreamReader(input2, "UTF-8")); + line = reader2.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader2.readLine(); + Assert.assertFalse(reader2.ready()); + + client1.close(); + client2.close(); + } + + @Test + public void testGETThenSmallResponseContent() throws Exception + { + final byte[] data = "0123456789ABCDEF".getBytes("UTF-8"); + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(synInfo.isClose()); + Headers requestHeaders = synInfo.getHeaders(); + Assert.assertNotNull(requestHeaders.get("via")); + + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + ReplyInfo replyInfo = new ReplyInfo(responseHeaders, false); + stream.reply(replyInfo); + stream.data(new BytesDataInfo(data, true)); + + return null; + } + })); + + Socket client = new Socket(); + client.connect(proxyAddress); + OutputStream output = client.getOutputStream(); + + String request = "" + + "GET / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.flush(); + + InputStream input = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader.readLine(); + for (byte datum : data) + Assert.assertEquals(datum, reader.read()); + Assert.assertFalse(reader.ready()); + + // Perform another request so that we are sure we reset the states of parsers and generators + output.write(request.getBytes("UTF-8")); + output.flush(); + + line = reader.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader.readLine(); + for (byte datum : data) + Assert.assertEquals(datum, reader.read()); + Assert.assertFalse(reader.ready()); + + client.close(); + } + + @Test + public void testPOSTWithSmallRequestContentThenRedirect() throws Exception + { + final byte[] data = "0123456789ABCDEF".getBytes("UTF-8"); + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + { + Headers headers = new Headers(); + headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + headers.put(HTTPSPDYHeader.STATUS.name(version()), "303 See Other"); + stream.reply(new ReplyInfo(headers, true)); + } + } + }; + } + })); + + Socket client = new Socket(); + client.connect(proxyAddress); + OutputStream output = client.getOutputStream(); + + String request = "" + + "POST / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "Content-Length: " + data.length + "\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.write(data); + output.flush(); + + InputStream input = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.contains(" 303")); + while (line.length() > 0) + line = reader.readLine(); + Assert.assertFalse(reader.ready()); + + // Perform another request so that we are sure we reset the states of parsers and generators + output.write(request.getBytes("UTF-8")); + output.write(data); + output.flush(); + + line = reader.readLine(); + Assert.assertTrue(line.contains(" 303")); + while (line.length() > 0) + line = reader.readLine(); + Assert.assertFalse(reader.ready()); + + client.close(); + } + + @Test + public void testPOSTWithSmallRequestContentThenSmallResponseContent() throws Exception + { + final byte[] data = "0123456789ABCDEF".getBytes("UTF-8"); + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + { + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + ReplyInfo replyInfo = new ReplyInfo(responseHeaders, false); + stream.reply(replyInfo); + stream.data(new BytesDataInfo(data, true)); + } + } + }; + } + })); + + Socket client = new Socket(); + client.connect(proxyAddress); + OutputStream output = client.getOutputStream(); + + String request = "" + + "POST / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "Content-Length: " + data.length + "\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.write(data); + output.flush(); + + InputStream input = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader.readLine(); + for (byte datum : data) + Assert.assertEquals(datum, reader.read()); + Assert.assertFalse(reader.ready()); + + // Perform another request so that we are sure we reset the states of parsers and generators + output.write(request.getBytes("UTF-8")); + output.write(data); + output.flush(); + + line = reader.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader.readLine(); + for (byte datum : data) + Assert.assertEquals(datum, reader.read()); + Assert.assertFalse(reader.ready()); + + client.close(); + } + + @Test + public void testSYNThenREPLY() throws Exception + { + final String header = "foo"; + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Headers requestHeaders = synInfo.getHeaders(); + Assert.assertNotNull(requestHeaders.get("via")); + Assert.assertNotNull(requestHeaders.get(header)); + + Headers responseHeaders = new Headers(); + responseHeaders.put(header, "baz"); + stream.reply(new ReplyInfo(responseHeaders, true)); + return null; + } + })); + proxyConnector.setDefaultAsyncConnectionFactory(proxyConnector.getAsyncConnectionFactory("spdy/" + version())); + + Session client = factory.newSPDYClient(version()).connect(proxyAddress, null).get(5, TimeUnit.SECONDS); + + final CountDownLatch replyLatch = new CountDownLatch(1); + Headers headers = new Headers(); + headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + proxyAddress.getPort()); + headers.put(header, "bar"); + client.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Headers headers = replyInfo.getHeaders(); + Assert.assertNotNull(headers.get(header)); + replyLatch.countDown(); + } + }); + + Assert.assertTrue(replyLatch.await(5, TimeUnit.SECONDS)); + + client.goAway().get(5, TimeUnit.SECONDS); + } + + @Test + public void testSYNThenREPLYAndDATA() throws Exception + { + final byte[] data = "0123456789ABCDEF".getBytes("UTF-8"); + final String header = "foo"; + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Headers requestHeaders = synInfo.getHeaders(); + Assert.assertNotNull(requestHeaders.get("via")); + Assert.assertNotNull(requestHeaders.get(header)); + + Headers responseHeaders = new Headers(); + responseHeaders.put(header, "baz"); + stream.reply(new ReplyInfo(responseHeaders, false)); + stream.data(new BytesDataInfo(data, true)); + return null; + } + })); + proxyConnector.setDefaultAsyncConnectionFactory(proxyConnector.getAsyncConnectionFactory("spdy/" + version())); + + Session client = factory.newSPDYClient(version()).connect(proxyAddress, null).get(5, TimeUnit.SECONDS); + + final CountDownLatch replyLatch = new CountDownLatch(1); + final CountDownLatch dataLatch = new CountDownLatch(1); + Headers headers = new Headers(); + headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + proxyAddress.getPort()); + headers.put(header, "bar"); + client.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() + { + private final ByteArrayOutputStream result = new ByteArrayOutputStream(); + + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Headers headers = replyInfo.getHeaders(); + Assert.assertNotNull(headers.get(header)); + replyLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + result.write(dataInfo.asBytes(true), 0, dataInfo.length()); + if (dataInfo.isClose()) + { + Assert.assertArrayEquals(data, result.toByteArray()); + dataLatch.countDown(); + } + } + }); + + Assert.assertTrue(replyLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(dataLatch.await(5, TimeUnit.SECONDS)); + + client.goAway().get(5, TimeUnit.SECONDS); + } + + @Test + public void testGETThenSPDYPushIsIgnored() throws Exception + { + final byte[] data = "0123456789ABCDEF".getBytes("UTF-8"); + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + + Headers pushHeaders = new Headers(); + pushHeaders.put(HTTPSPDYHeader.URI.name(version()), "/push"); + stream.syn(new SynInfo(pushHeaders, false), 5, TimeUnit.SECONDS, new Handler.Adapter() + { + @Override + public void completed(Stream pushStream) + { + pushStream.data(new BytesDataInfo(data, true)); + } + }); + + stream.reply(new ReplyInfo(responseHeaders, true)); + return null; + } + })); + + Socket client = new Socket(); + client.connect(proxyAddress); + OutputStream output = client.getOutputStream(); + + String request = "" + + "GET / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.flush(); + + client.setSoTimeout(1000); + InputStream input = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + String line = reader.readLine(); + Assert.assertTrue(line.contains(" 200")); + while (line.length() > 0) + line = reader.readLine(); + Assert.assertFalse(reader.ready()); + + client.close(); + } + + @Test + public void testSYNThenSPDYPushIsReceived() throws Exception + { + final byte[] data = "0123456789ABCDEF".getBytes("UTF-8"); + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Headers responseHeaders = new Headers(); + responseHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + responseHeaders.put(HTTPSPDYHeader.STATUS.name(version()), "200 OK"); + stream.reply(new ReplyInfo(responseHeaders, false)); + + Headers pushHeaders = new Headers(); + pushHeaders.put(HTTPSPDYHeader.URI.name(version()), "/push"); + stream.syn(new SynInfo(pushHeaders, false), 5, TimeUnit.SECONDS, new Handler.Adapter() + { + @Override + public void completed(Stream pushStream) + { + pushStream.data(new BytesDataInfo(data, true)); + } + }); + + stream.data(new BytesDataInfo(data, true)); + + return null; + } + })); + proxyConnector.setDefaultAsyncConnectionFactory(proxyConnector.getAsyncConnectionFactory("spdy/" + version())); + + final CountDownLatch pushSynLatch = new CountDownLatch(1); + final CountDownLatch pushDataLatch = new CountDownLatch(1); + Session client = factory.newSPDYClient(version()).connect(proxyAddress, new SessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + pushSynLatch.countDown(); + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + pushDataLatch.countDown(); + } + }; + } + }).get(5, TimeUnit.SECONDS); + + Headers headers = new Headers(); + headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + proxyAddress.getPort()); + final CountDownLatch replyLatch = new CountDownLatch(1); + final CountDownLatch dataLatch = new CountDownLatch(1); + client.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + replyLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + dataLatch.countDown(); + } + }); + + Assert.assertTrue(replyLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(pushSynLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(dataLatch.await(5, TimeUnit.SECONDS)); + + client.goAway().get(5, TimeUnit.SECONDS); + } + + @Test + public void testPING() throws Exception + { + // PING is per hop, and it does not carry the information to which server to ping to + // We just verify that it works + + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter())); + proxyConnector.setDefaultAsyncConnectionFactory(proxyConnector.getAsyncConnectionFactory("spdy/" + version())); + + final CountDownLatch pingLatch = new CountDownLatch(1); + Session client = factory.newSPDYClient(version()).connect(proxyAddress, new SessionFrameListener.Adapter() + { + @Override + public void onPing(Session session, PingInfo pingInfo) + { + pingLatch.countDown(); + } + }).get(5, TimeUnit.SECONDS); + + client.ping().get(5, TimeUnit.SECONDS); + + Assert.assertTrue(pingLatch.await(5, TimeUnit.SECONDS)); + + client.goAway().get(5, TimeUnit.SECONDS); + } + + @Test + public void testGETThenReset() throws Exception + { + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(synInfo.isClose()); + Headers requestHeaders = synInfo.getHeaders(); + Assert.assertNotNull(requestHeaders.get("via")); + + stream.getSession().rst(new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM)); + + return null; + } + })); + + Socket client = new Socket(); + client.connect(proxyAddress); + OutputStream output = client.getOutputStream(); + + String request = "" + + "GET / HTTP/1.1\r\n" + + "Host: localhost:" + proxyAddress.getPort() + "\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.flush(); + + InputStream input = client.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + Assert.assertNull(reader.readLine()); + + client.close(); + } + + @Test + public void testSYNThenReset() throws Exception + { + InetSocketAddress proxyAddress = startProxy(startServer(new ServerSessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(synInfo.isClose()); + Headers requestHeaders = synInfo.getHeaders(); + Assert.assertNotNull(requestHeaders.get("via")); + + stream.getSession().rst(new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM)); + + return null; + } + })); + proxyConnector.setDefaultAsyncConnectionFactory(proxyConnector.getAsyncConnectionFactory("spdy/" + version())); + + final CountDownLatch resetLatch = new CountDownLatch(1); + Session client = factory.newSPDYClient(version()).connect(proxyAddress, new SessionFrameListener.Adapter() + { + @Override + public void onRst(Session session, RstInfo rstInfo) + { + resetLatch.countDown(); + } + }).get(5, TimeUnit.SECONDS); + + Headers headers = new Headers(); + headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + proxyAddress.getPort()); + client.syn(new SynInfo(headers, true), null); + + Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); + + client.goAway().get(5, TimeUnit.SECONDS); + } +} diff --git a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYClient.java b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYClient.java index 071ed8ea022..eb2a811b096 100644 --- a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYClient.java +++ b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYClient.java @@ -101,7 +101,7 @@ public class SPDYClient channel.socket().setTcpNoDelay(true); channel.configureBlocking(false); - SessionPromise result = new SessionPromise(this, listener); + SessionPromise result = new SessionPromise(channel, this, listener); channel.connect(address); factory.selector.register(channel, result); @@ -419,14 +419,31 @@ public class SPDYClient private static class SessionPromise extends Promise { + private final SocketChannel channel; private final SPDYClient client; private final SessionFrameListener listener; - private SessionPromise(SPDYClient client, SessionFrameListener listener) + private SessionPromise(SocketChannel channel, SPDYClient client, SessionFrameListener listener) { + this.channel = channel; this.client = client; this.listener = listener; } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) + { + try + { + super.cancel(mayInterruptIfRunning); + channel.close(); + return true; + } + catch (IOException x) + { + return true; + } + } } private static class ClientSPDYAsyncConnectionFactory implements AsyncConnectionFactory diff --git a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java index 65ada1e7165..3226ccadeaa 100644 --- a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java +++ b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java @@ -156,6 +156,14 @@ public class SPDYServerConnector extends SelectChannelConnector } } + public void clearAsyncConnectionFactories() + { + synchronized (factories) + { + factories.clear(); + } + } + protected List provideProtocols() { synchronized (factories) diff --git a/jetty-spdy/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/GoAwayTest.java b/jetty-spdy/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/GoAwayTest.java index ec2a0f6f672..6910f56f8b5 100644 --- a/jetty-spdy/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/GoAwayTest.java +++ b/jetty-spdy/spdy-jetty/src/test/java/org/eclipse/jetty/spdy/GoAwayTest.java @@ -219,10 +219,10 @@ public class GoAwayTest extends AbstractTest Assert.assertThat(x.getCause(), CoreMatchers.instanceOf(ClosedChannelException.class)); } - // Be sure the last good stream is the first + // The last good stream is the second, because it was received by the server Assert.assertTrue(goAwayLatch.await(5, TimeUnit.SECONDS)); GoAwayInfo goAway = goAwayRef.get(); Assert.assertNotNull(goAway); - Assert.assertEquals(stream1.getId(), goAway.getLastStreamId()); + Assert.assertEquals(stream2.getId(), goAway.getLastStreamId()); } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Atomics.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Atomics.java new file mode 100644 index 00000000000..97a3e1be52e --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Atomics.java @@ -0,0 +1,68 @@ +// ======================================================================== +// Copyright (c) 2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.util; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class Atomics +{ + private Atomics() + { + } + + public static void updateMin(AtomicLong currentMin, long newValue) + { + long oldValue = currentMin.get(); + while (newValue < oldValue) + { + if (currentMin.compareAndSet(oldValue, newValue)) + break; + oldValue = currentMin.get(); + } + } + + public static void updateMax(AtomicLong currentMax, long newValue) + { + long oldValue = currentMax.get(); + while (newValue > oldValue) + { + if (currentMax.compareAndSet(oldValue, newValue)) + break; + oldValue = currentMax.get(); + } + } + + public static void updateMin(AtomicInteger currentMin, int newValue) + { + int oldValue = currentMin.get(); + while (newValue < oldValue) + { + if (currentMin.compareAndSet(oldValue, newValue)) + break; + oldValue = currentMin.get(); + } + } + + public static void updateMax(AtomicInteger currentMax, int newValue) + { + int oldValue = currentMax.get(); + while (newValue > oldValue) + { + if (currentMax.compareAndSet(oldValue, newValue)) + break; + oldValue = currentMax.get(); + } + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Loader.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Loader.java index a5f3d013473..b02b6cf1fd9 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/Loader.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Loader.java @@ -163,9 +163,6 @@ public class Loader */ public static String getClassPath(ClassLoader loader) throws Exception { - if (loader.getParent() != null) - loader = loader.getParent(); - StringBuilder classpath=new StringBuilder(); while (loader != null && (loader instanceof URLClassLoader)) { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java b/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java index 320625ca96c..ef353c7669a 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java @@ -37,6 +37,8 @@ public class Constraint implements Cloneable, Serializable public final static String __SPNEGO_AUTH = "SPNEGO"; + public final static String __NEGOTIATE_AUTH = "NEGOTIATE"; + public static boolean validateMethod (String method) { if (method == null) @@ -47,7 +49,8 @@ public class Constraint implements Cloneable, Serializable || method.equals (__DIGEST_AUTH) || method.equals (__CERT_AUTH) || method.equals(__CERT_AUTH2) - || method.equals(__SPNEGO_AUTH)); + || method.equals(__SPNEGO_AUTH) + || method.equals(__NEGOTIATE_AUTH)); } /* ------------------------------------------------------------ */ diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/CounterStatistic.java b/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/CounterStatistic.java index 8c9b33d3105..79ec208289d 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/CounterStatistic.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/CounterStatistic.java @@ -4,17 +4,19 @@ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. -// The Eclipse Public License is available at +// The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php -// You may elect to redistribute this code under either of these licenses. +// You may elect to redistribute this code under either of these licenses. // ======================================================================== package org.eclipse.jetty.util.statistic; import java.util.concurrent.atomic.AtomicLong; +import org.eclipse.jetty.util.Atomics; + /* ------------------------------------------------------------ */ /** Statistics on a counter value. @@ -22,9 +24,9 @@ import java.util.concurrent.atomic.AtomicLong; * Keep total, current and maximum values of a counter that * can be incremented and decremented. The total refers only * to increments. - * + * */ -public class CounterStatistic +public class CounterStatistic { protected final AtomicLong _max = new AtomicLong(); protected final AtomicLong _curr = new AtomicLong(); @@ -39,11 +41,11 @@ public class CounterStatistic /* ------------------------------------------------------------ */ public void reset(final long value) { - _max.set(value); + _max.set(value); _curr.set(value); _total.set(0); // total always set to 0 to properly calculate cumulative total } - + /* ------------------------------------------------------------ */ /** * @param delta the amount to add to the count @@ -53,15 +55,9 @@ public class CounterStatistic long value=_curr.addAndGet(delta); if (delta > 0) _total.addAndGet(delta); - long oldValue = _max.get(); - while (value > oldValue) - { - if (_max.compareAndSet(oldValue, value)) - break; - oldValue = _max.get(); - } + Atomics.updateMax(_max,value); } - + /* ------------------------------------------------------------ */ /** * @param delta the amount to subtract the count by. @@ -70,7 +66,7 @@ public class CounterStatistic { add(-delta); } - + /* ------------------------------------------------------------ */ /** */ @@ -78,7 +74,7 @@ public class CounterStatistic { add(1); } - + /* ------------------------------------------------------------ */ /** */ @@ -95,7 +91,7 @@ public class CounterStatistic { return _max.get(); } - + /* ------------------------------------------------------------ */ /** * @return current value @@ -104,7 +100,7 @@ public class CounterStatistic { return _curr.get(); } - + /* ------------------------------------------------------------ */ /** * @return total value @@ -113,9 +109,4 @@ public class CounterStatistic { return _total.get(); } - - /* ------------------------------------------------------------ */ - protected void upxdateMax(long value) - { - } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/SampleStatistic.java b/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/SampleStatistic.java index fbdb422b0b3..b887588cc1b 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/SampleStatistic.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/statistic/SampleStatistic.java @@ -4,23 +4,25 @@ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. -// The Eclipse Public License is available at +// The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php -// You may elect to redistribute this code under either of these licenses. +// You may elect to redistribute this code under either of these licenses. // ======================================================================== package org.eclipse.jetty.util.statistic; import java.util.concurrent.atomic.AtomicLong; +import org.eclipse.jetty.util.Atomics; + /* ------------------------------------------------------------ */ /** * SampledStatistics *

- * Provides max, total, mean, count, variance, and standard + * Provides max, total, mean, count, variance, and standard * deviation of continuous sequence of samples. *

* Calculates estimates of mean, variance, and standard deviation @@ -53,25 +55,17 @@ public class SampleStatistic { long total = _total.addAndGet(sample); long count = _count.incrementAndGet(); - + if (count>1) { long mean10 = total*10/count; long delta10 = sample*10 - mean10; _totalVariance100.addAndGet(delta10*delta10); - } - - long oldMax = _max.get(); - while (sample > oldMax) - { - if (_max.compareAndSet(oldMax, sample)) - break; - oldMax = _max.get(); } - + + Atomics.updateMax(_max, sample); } - /* ------------------------------------------------------------ */ /** * @return the max value */ @@ -80,37 +74,31 @@ public class SampleStatistic return _max.get(); } - /* ------------------------------------------------------------ */ public long getTotal() { return _total.get(); } - /* ------------------------------------------------------------ */ public long getCount() { return _count.get(); } - /* ------------------------------------------------------------ */ public double getMean() { return (double)_total.get()/_count.get(); } - /* ------------------------------------------------------------ */ public double getVariance() { final long variance100 = _totalVariance100.get(); final long count = _count.get(); - + return count>1?((double)variance100)/100.0/(count-1):0.0; } - /* ------------------------------------------------------------ */ public double getStdDev() { return Math.sqrt(getVariance()); } - } diff --git a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java index 292294d6d9a..cee3c27cb0a 100644 --- a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java +++ b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java @@ -71,11 +71,10 @@ public class XmlConfiguration private static final Class[] __primitiveHolders = { Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Void.class }; - private static final Integer ZERO = new Integer(0); - + private static final Class[] __supportedCollections = { ArrayList.class,ArrayQueue.class,HashSet.class,Queue.class,List.class,Set.class,Collection.class,}; - + private static final Iterable __factoryLoader; private static final XmlParser __parser = initParser(); @@ -141,9 +140,11 @@ public class XmlConfiguration /* ------------------------------------------------------------ */ /** - * Constructor. Reads the XML configuration file. + * Reads and parses the XML configuration file. * - * @param configuration + * @param configuration the URL of the XML configuration + * @throws IOException if the configuration could not be read + * @throws SAXException if the configuration could not be parsed */ public XmlConfiguration(URL configuration) throws SAXException, IOException { @@ -157,12 +158,12 @@ public class XmlConfiguration /* ------------------------------------------------------------ */ /** - * Constructor. + * Reads and parses the XML configuration string. * - * @param configuration - * String of XML configuration commands excluding the normal XML preamble. The String should start with a " Apply the XML configuration script to the passed object.

- * - * @param obj - * The object to be configured, which must be of a type or super type of the class attribute of the Configure element. - * @exception Exception + * @param obj The object to be configured, which must be of a type or super type + * of the class attribute of the <Configure> element. + * @throws Exception if the configuration fails + * @return the configured object */ public Object configure(Object obj) throws Exception { @@ -283,10 +284,13 @@ public class XmlConfiguration /* ------------------------------------------------------------ */ /** - * Configure an object. If the configuration has an ID, an object is looked up by ID and it's type check. Otherwise a new object is created. + * Applies the XML configuration script. + * If the root element of the configuration has an ID, an object is looked up by ID and its type checked + * against the root element's type. + * Otherwise a new object of the type specified by the root element is created. * * @return The newly created configured object. - * @exception Exception + * @throws Exception if the configuration fails */ public Object configure() throws Exception { @@ -353,12 +357,13 @@ public class XmlConfiguration /* ------------------------------------------------------------ */ /** - * Recursive configuration step. This method applies the remaining Set, Put and Call elements to the current object. + * Recursive configuration routine. + * This method applies the nested Set, Put, Call, etc. elements to the given object. * - * @param obj - * @param cfg - * @param i - * @exception Exception + * @param obj the object to configure + * @param cfg the XML nodes of the configuration + * @param i the index of the XML nodes + * @throws Exception if the configuration fails */ public void configure(Object obj, XmlParser.Node cfg, int i) throws Exception { @@ -576,7 +581,9 @@ public class XmlConfiguration } /** - * @return a collection if compareValueToClass is a Set or List. null if that's not the case or value can't be converted to a Collection + * @param array the array to convert + * @param collectionType the desired collection type + * @return a collection of the desired type if the array can be converted */ private static Collection convertArrayToCollection(Object array, Class collectionType) { @@ -862,7 +869,7 @@ public class XmlConfiguration XmlParser.Node item = (Node)nodeObject; String nid = item.getAttribute("id"); Object v = value(obj,item); - al = LazyList.add(al,(v == null && aClass.isPrimitive())?ZERO:v); + al = LazyList.add(al,(v == null && aClass.isPrimitive())?0:v); if (nid != null) _idMap.put(nid,v); } @@ -896,7 +903,7 @@ public class XmlConfiguration XmlParser.Node key = null; XmlParser.Node value = null; - for (Object object : node) + for (Object object : entry) { if (object instanceof String) continue; @@ -932,26 +939,26 @@ public class XmlConfiguration * Get a Property. * * @param node - * @return + * @return * @exception Exception */ private Object propertyObj(XmlParser.Node node) throws Exception { String id = node.getAttribute("id"); String name = node.getAttribute("name"); - String defval = node.getAttribute("default"); - Object prop = null; + String defaultValue = node.getAttribute("default"); + Object prop; if (_propertyMap != null && _propertyMap.containsKey(name)) prop = _propertyMap.get(name); else - prop = defval; + prop = defaultValue; if (id != null) _idMap.put(id,prop); if (prop != null) configure(prop,node,0); return prop; } - + /* ------------------------------------------------------------ */ /* @@ -960,7 +967,7 @@ public class XmlConfiguration */ private Object value(Object obj, XmlParser.Node node) throws Exception { - Object value = null; + Object value; // Get the type String type = node.getAttribute("type"); @@ -989,7 +996,7 @@ public class XmlConfiguration if (type == null || !"String".equals(type)) { // Skip leading white - Object item = null; + Object item; while (first <= last) { item = node.get(first); @@ -1084,7 +1091,7 @@ public class XmlConfiguration throw new InvocationTargetException(e); } } - + for (Class collectionClass : __supportedCollections) { if (isTypeMatchingClass(type,collectionClass)) @@ -1093,12 +1100,11 @@ public class XmlConfiguration throw new IllegalStateException("Unknown type " + type); } - + /* ------------------------------------------------------------ */ private static boolean isTypeMatchingClass(String type, Class classToMatch) { - boolean match = classToMatch.getSimpleName().equalsIgnoreCase(type) || classToMatch.getName().equals(type); - return match; + return classToMatch.getSimpleName().equalsIgnoreCase(type) || classToMatch.getName().equals(type); } /* ------------------------------------------------------------ */ @@ -1134,7 +1140,7 @@ public class XmlConfiguration String defaultValue = node.getAttribute("default"); return System.getProperty(name,defaultValue); } - + if ("Env".equals(tag)) { String name = node.getAttribute("name"); @@ -1167,6 +1173,7 @@ public class XmlConfiguration * * @param args * array of property and xml configuration filenames or {@link Resource}s. + * @throws Exception if the XML configurations cannot be run */ public static void main(final String[] args) throws Exception { diff --git a/jetty-xml/src/test/java/org/eclipse/jetty/xml/TestConfiguration.java b/jetty-xml/src/test/java/org/eclipse/jetty/xml/TestConfiguration.java index e87e71060f2..8c35a8e8e26 100644 --- a/jetty-xml/src/test/java/org/eclipse/jetty/xml/TestConfiguration.java +++ b/jetty-xml/src/test/java/org/eclipse/jetty/xml/TestConfiguration.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import org.junit.Ignore; @@ -42,6 +43,7 @@ public class TestConfiguration extends HashMap @SuppressWarnings("rawtypes") private Set set; private ConstructorArgTestClass constructorArgTestClass; + public Map map; public void setTest(Object value) { @@ -52,7 +54,7 @@ public class TestConfiguration extends HashMap { testInt=value; } - + public void setPropertyTest(int value) { propValue=value; @@ -141,4 +143,9 @@ public class TestConfiguration extends HashMap { this.constructorArgTestClass = constructorArgTestClass; } + + public void setMap(Map map) + { + this.map = map; + } } diff --git a/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java b/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java index 79b4038400a..49cb229254b 100644 --- a/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java +++ b/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java @@ -13,20 +13,24 @@ package org.eclipse.jetty.xml; -import static junit.framework.Assert.assertEquals; -import static org.junit.Assert.*; -import static org.hamcrest.CoreMatchers.*; - import java.net.URL; import java.util.HashMap; import java.util.Map; +import org.junit.Assert; import org.junit.Test; +import static junit.framework.Assert.assertEquals; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + public class XmlConfigurationTest { protected String _configure="org/eclipse/jetty/xml/configure.xml"; - + private static final String STRING_ARRAY_XML = "String1String2"; private static final String INT_ARRAY_XML = "12"; @@ -37,7 +41,7 @@ public class XmlConfigurationTest XmlConfiguration configuration = new XmlConfiguration(url); configuration.configure(); } - + @Test public void testPassedObject() throws Exception { @@ -53,7 +57,7 @@ public class XmlConfigurationTest assertEquals("Set String","SetValue",tc.testObject); assertEquals("Set Type",2,tc.testInt); - + assertEquals(18080, tc.propValue); assertEquals("Put","PutValue",tc.get("Test")); @@ -76,7 +80,7 @@ public class XmlConfigurationTest assertEquals( "SystemProperty", System.getProperty("user.dir")+"/stuff",tc.get("SystemProperty")); assertEquals( "Env", System.getenv("HOME"),tc.get("Env")); - + assertEquals( "Property", "xxx", tc.get("Property")); @@ -104,12 +108,12 @@ public class XmlConfigurationTest assertEquals("nested config","Call1",tc2.testObject); assertEquals("nested config",4,tc2.testInt); assertEquals( "nested call", "http://www.eclipse.com/",tc2.url.toString()); - + assertEquals("static to field",tc.testField1,77); assertEquals("field to field",tc.testField2,2); assertEquals("literal to static",TestConfiguration.VALUE,42); } - + @Test public void testNewObject() throws Exception { @@ -124,7 +128,7 @@ public class XmlConfigurationTest assertEquals("Set String","SetValue",tc.testObject); assertEquals("Set Type",2,tc.testInt); - + assertEquals(18080, tc.propValue); assertEquals("Put","PutValue",tc.get("Test")); @@ -173,13 +177,13 @@ public class XmlConfigurationTest assertEquals("nested config","Call1",tc2.testObject); assertEquals("nested config",4,tc2.testInt); assertEquals( "nested call", "http://www.eclipse.com/",tc2.url.toString()); - + assertEquals("static to field",71,tc.testField1); assertEquals("field to field",2,tc.testField2); assertEquals("literal to static",42,TestConfiguration.VALUE); } - - + + @Test public void testStringConfiguration() throws Exception { @@ -314,4 +318,28 @@ public class XmlConfigurationTest xmlConfiguration.configure(tc); assertThat("tc.getSet() has two entries as specified in the xml",tc.getSet().size(),is(2)); } + + @Test + public void testMap() throws Exception + { + XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + "" + + " " + + " " + + " " + + " key1" + + " value1" + + " " + + " " + + " key2" + + " value2" + + " " + + " " + + " " + + ""); + TestConfiguration tc = new TestConfiguration(); + Assert.assertNull("tc.map is null as it's not configured yet", tc.map); + xmlConfiguration.configure(tc); + Assert.assertEquals("tc.map is has two entries as specified in the XML", 2, tc.map.size()); + } }