diff options
49 files changed, 2457 insertions, 596 deletions
diff --git a/VERSION.txt b/VERSION.txt index 97a124abfa..eb6e1de36f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1,5 +1,25 @@ jetty-7.6.0-SNAPSHOT +jetty-7.6.0.RC2 - 22 December 2011 + + 364638 HttpParser closes if data received while seeking EOF. Tests fixed to + cope + + 364921 Made test less time sensitive for ssl + + 364936 use Resource for opening URL streams + + 365267 NullPointerException in bad Address + + 365375 ResourceHandler should be a HandlerWrapper + + 365750 Support WebSocket over SSL, aka wss:// + + 365932 Produce jetty-websocket aggregate jar for android use + + 365947 Set headers for Auth failure and retry in http-spi + + 366316 Superfluous printStackTrace on 404 + + 366342 Dont persist DosFilter trackers in http session + + 366730 pass the time idle to onIdleExpire + + 367048 test harness for guard on suspended requests + + 367175 SSL 100% CPU spin in case of blocked write and RST. + + 367219 WebSocketClient.open() fails when URI uses default ports. + + JETTY-1460 suppress PrintWriter exceptions + + JETTY-1463 websocket D0 parser should return progress even if no fill done + + JETTY-1465 NPE in ContextHandler.toString + jetty-7.6.0.RC1 - 04 December 2011 + 352565 cookie httponly flag ignored + 353285 ServletSecurity annotation ignored diff --git a/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java b/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java index 899c682bc0..778c9b0b23 100644 --- a/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java +++ b/example-jetty-embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.nio.BlockingChannelConnector; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; import org.eclipse.jetty.server.ssl.SslSocketConnector; @@ -68,6 +69,13 @@ public class LikeJettyXml server.setConnectors(new Connector[] { connector }); + + BlockingChannelConnector bConnector = new BlockingChannelConnector(); + bConnector.setPort(8888); + bConnector.setMaxIdleTime(30000); + bConnector.setConfidentialPort(8443); + bConnector.setAcceptors(1); + server.addConnector(bConnector); SslSelectChannelConnector ssl_connector = new SslSelectChannelConnector(); ssl_connector.setPort(8443); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpConnection.java index 00571cad0b..a032b38b17 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpConnection.java @@ -179,7 +179,7 @@ public abstract class AbstractHttpConnection extends AbstractConnection implemen _generator.setVersion(_exchange.getVersion()); String method=_exchange.getMethod(); - String uri = _exchange.getURI(); + String uri = _exchange.getRequestURI(); if (_destination.isProxied() && !HttpMethods.CONNECT.equals(method) && uri.startsWith("/")) { boolean secure = _destination.isSecure(); @@ -394,7 +394,11 @@ public abstract class AbstractHttpConnection extends AbstractConnection implemen } } - _endp.close(); + if (_endp.isOpen()) + { + _endp.close(); + _destination.returnConnection(this, true); + } } public void setIdleTimeout() diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpExchangeCancelTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpExchangeCancelTest.java index 923d686766..c65525583d 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpExchangeCancelTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpExchangeCancelTest.java @@ -14,15 +14,8 @@ package org.eclipse.jetty.client; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - import java.io.IOException; import java.net.SocketTimeoutException; - import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; @@ -36,18 +29,22 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.StdErrLog; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + /** */ public abstract class AbstractHttpExchangeCancelTest { - private static final Logger LOG = Log.getLogger(AbstractHttpExchangeCancelTest.TestHttpExchange.class); - private Server server; private Connector connector; @@ -362,7 +359,7 @@ public abstract class AbstractHttpExchangeCancelTest int status = exchange.waitForDone(); long end = System.currentTimeMillis(); - + assertTrue(HttpExchange.STATUS_EXPIRED==status||HttpExchange.STATUS_EXCEPTED==status); assertFalse(exchange.isResponseCompleted()); assertTrue(end-start<4000); @@ -371,6 +368,26 @@ public abstract class AbstractHttpExchangeCancelTest assertFalse(exchange.isAssociated()); } + @Test + public void testHttpExchangeCancelReturnsConnection() throws Exception + { + TestHttpExchange exchange = new TestHttpExchange(); + Address address = newAddress(); + exchange.setAddress(address); + long delay = 5000; + exchange.setRequestURI("/?action=wait" + delay); + + HttpClient httpClient = getHttpClient(); + HttpDestination destination = httpClient.getDestination(address, false); + int connections = destination.getConnections(); + httpClient.send(exchange); + Thread.sleep(delay / 2); + Assert.assertEquals(connections + 1, destination.getConnections()); + + exchange.cancel(); + Assert.assertEquals(connections, destination.getConnections()); + } + /* ------------------------------------------------------------ */ protected abstract HttpClient getHttpClient(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java index f2f04f738f..abcdce4d00 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/SslBytesServerTest.java @@ -5,7 +5,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; -import java.net.SocketException; import java.net.SocketTimeoutException; import java.nio.channels.SocketChannel; import java.util.Arrays; @@ -40,6 +39,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.OS; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.After; import org.junit.Assert; @@ -53,17 +53,16 @@ import static org.hamcrest.Matchers.lessThan; public class SslBytesServerTest extends SslBytesTest { - private final static int MAX_IDLE_TIME=5000; - private final AtomicInteger sslHandles = new AtomicInteger(); private final AtomicInteger sslFlushes = new AtomicInteger(); private final AtomicInteger httpParses = new AtomicInteger(); + private final AtomicReference<EndPoint> serverEndPoint = new AtomicReference<EndPoint>(); + private final int idleTimeout = 2000; private ExecutorService threadPool; private Server server; private SSLContext sslContext; private SimpleProxy proxy; - private final AtomicReference<SslConnection> sslConnection = new AtomicReference<SslConnection>(); - + @Before public void init() throws Exception { @@ -75,7 +74,8 @@ public class SslBytesServerTest extends SslBytesTest @Override protected SslConnection newSslConnection(AsyncEndPoint endPoint, SSLEngine engine) { - SslConnection connection = new SslConnection(engine, endPoint) + serverEndPoint.set(endPoint); + return new SslConnection(engine, endPoint) { @Override public Connection handle() throws IOException @@ -98,8 +98,6 @@ public class SslBytesServerTest extends SslBytesTest }; } }; - sslConnection.set(connection); - return connection; } @Override @@ -123,7 +121,7 @@ public class SslBytesServerTest extends SslBytesTest }; } }; - connector.setMaxIdleTime(MAX_IDLE_TIME); + connector.setMaxIdleTime(idleTimeout); // connector.setPort(5870); connector.setPort(0); @@ -571,6 +569,21 @@ public class SslBytesServerTest extends SslBytesTest @Test public void testRequestWithCloseAlert() throws Exception { + if ( !OS.IS_LINUX ) + { + // currently we are ignoring this test on anything other then linux + + //http://tools.ietf.org/html/rfc2246#section-7.2.1 + + // TODO (react to this portion which seems to allow win/mac behavior) + //It is required that the other party respond with a close_notify alert of its own + //and close down the connection immediately, discarding any pending writes. It is not + //required for the initiator of the close to wait for the responding + //close_notify alert before closing the read side of the connection. + return; + } + + final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); @@ -746,6 +759,20 @@ public class SslBytesServerTest extends SslBytesTest @Test public void testRequestWithCloseAlertWithSplitBoundary() throws Exception { + if ( !OS.IS_LINUX ) + { + // currently we are ignoring this test on anything other then linux + + //http://tools.ietf.org/html/rfc2246#section-7.2.1 + + // TODO (react to this portion which seems to allow win/mac behavior) + //It is required that the other party respond with a close_notify alert of its own + //and close down the connection immediately, discarding any pending writes. It is not + //required for the initiator of the close to wait for the responding + //close_notify alert before closing the read side of the connection. + return; + } + final SSLSocket client = newClient(); SimpleProxy.AutomaticFlow automaticProxyFlow = proxy.startAutomaticFlow(); @@ -1245,7 +1272,7 @@ public class SslBytesServerTest extends SslBytesTest } @Test - public void testServerCloseClientDoesNotClose() throws Exception + public void testServerShutdownOutputClientDoesNotCloseServerCloses() throws Exception { final SSLSocket client = newClient(); final OutputStream clientOutput = client.getOutputStream(); @@ -1267,7 +1294,6 @@ public class SslBytesServerTest extends SslBytesTest "\r\n" + content).getBytes("UTF-8")); clientOutput.flush(); - Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8")); String line = reader.readLine(); @@ -1278,52 +1304,25 @@ public class SslBytesServerTest extends SslBytesTest if (line.trim().length() == 0) break; } + Assert.assertTrue(automaticProxyFlow.stop(5, TimeUnit.SECONDS)); // Check client is at EOF Assert.assertEquals(-1,client.getInputStream().read()); - + // Client should close the socket, but let's hold it open. // Check that we did not spin TimeUnit.MILLISECONDS.sleep(100); Assert.assertThat(sslHandles.get(), lessThan(20)); + Assert.assertThat(sslFlushes.get(), lessThan(20)); Assert.assertThat(httpParses.get(), lessThan(50)); - - // Check it is still half closed on the server. - Assert.assertTrue(((AsyncEndPoint)sslConnection.get().getEndPoint()).isOpen()); - Assert.assertTrue(((AsyncEndPoint)sslConnection.get().getEndPoint()).isOutputShutdown()); - Assert.assertFalse(((AsyncEndPoint)sslConnection.get().getEndPoint()).isInputShutdown()); - - // wait for a bit more than MAX_IDLE_TIME - TimeUnit.MILLISECONDS.sleep(MAX_IDLE_TIME+1000); - - // Server should have closed the endpoint - Assert.assertFalse(((AsyncEndPoint)sslConnection.get().getEndPoint()).isOpen()); - Assert.assertTrue(((AsyncEndPoint)sslConnection.get().getEndPoint()).isOutputShutdown()); - Assert.assertTrue(((AsyncEndPoint)sslConnection.get().getEndPoint()).isInputShutdown()); - - - // writing to client will eventually get broken pipe exception or similar - try - { - for (int i=0;i<100;i++) - { - clientOutput.write(("" + - "POST / HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Content-Type: text/plain\r\n" + - "Content-Length: " + content.length() + "\r\n" + - "Connection: close\r\n" + - "\r\n" + - content).getBytes("UTF-8")); - clientOutput.flush(); - } - Assert.fail("Client should have seen server close"); - } - catch(SocketException e) - { - // this was expected. - } + + // The server has shutdown the output since the client sent a Connection: close + // but the client does not close, so the server must idle timeout the endPoint. + + TimeUnit.MILLISECONDS.sleep(idleTimeout + idleTimeout/2); + + Assert.assertFalse(serverEndPoint.get().isOpen()); } private void assumeJavaVersionSupportsTLSRenegotiations() diff --git a/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/HttpSpiContextHandler.java b/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/HttpSpiContextHandler.java index 62c30cac16..7cf8011240 100644 --- a/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/HttpSpiContextHandler.java +++ b/jetty-http-spi/src/main/java/org/eclipse/jetty/http/spi/HttpSpiContextHandler.java @@ -15,6 +15,8 @@ package org.eclipse.jetty.http.spi; import java.io.IOException;
import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -25,6 +27,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import com.sun.net.httpserver.Authenticator;
import com.sun.net.httpserver.Authenticator.Result;
+import com.sun.net.httpserver.BasicAuthenticator;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
@@ -108,12 +111,23 @@ public class HttpSpiContextHandler extends ContextHandler if (result instanceof Authenticator.Failure)
{
int rc = ((Authenticator.Failure)result).getResponseCode();
+ for (Map.Entry<String,List<String>> header : httpExchange.getResponseHeaders().entrySet())
+ {
+ for (String value : header.getValue())
+ resp.addHeader(header.getKey(),value);
+ }
resp.sendError(rc);
}
else if (result instanceof Authenticator.Retry)
{
int rc = ((Authenticator.Retry)result).getResponseCode();
- resp.sendError(rc);
+ for (Map.Entry<String,List<String>> header : httpExchange.getResponseHeaders().entrySet())
+ {
+ for (String value : header.getValue())
+ resp.addHeader(header.getKey(),value);
+ }
+ resp.setStatus(rc);
+ resp.flushBuffer();
}
else if (result instanceof Authenticator.Success)
{
diff --git a/jetty-http-spi/src/test/java/org/eclipse/jetty/http/spi/TestSPIServer.java b/jetty-http-spi/src/test/java/org/eclipse/jetty/http/spi/TestSPIServer.java new file mode 100644 index 0000000000..27110811a6 --- /dev/null +++ b/jetty-http-spi/src/test/java/org/eclipse/jetty/http/spi/TestSPIServer.java @@ -0,0 +1,71 @@ +package org.eclipse.jetty.http.spi; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import com.sun.net.httpserver.BasicAuthenticator; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + + +public class TestSPIServer +{ + public static void main(String[] args) throws Exception + { + String host="localhost"; + int port = 8080; + + HttpServer server = new JettyHttpServerProvider().createHttpServer(new + InetSocketAddress(host, port), 10); + server.start(); + + final HttpContext httpContext = server.createContext("/", + new HttpHandler() + { + + @Override + public void handle(HttpExchange exchange) throws IOException + { + Headers responseHeaders = exchange.getResponseHeaders(); + responseHeaders.set("Content-Type","text/plain"); + exchange.sendResponseHeaders(200,0); + + OutputStream responseBody = exchange.getResponseBody(); + Headers requestHeaders = exchange.getRequestHeaders(); + Set<String> keySet = requestHeaders.keySet(); + Iterator<String> iter = keySet.iterator(); + while (iter.hasNext()) + { + String key = iter.next(); + List values = requestHeaders.get(key); + String s = key + " = " + values.toString() + "\n"; + responseBody.write(s.getBytes()); + } + responseBody.close(); + + } + }); + + httpContext.setAuthenticator(new BasicAuthenticator("Test") + { + @Override + public boolean checkCredentials(String username, String password) + { + if ("username".equals(username) && password.equals("password")) + return true; + return false; + } + }); + + + Thread.sleep(10000000); + + } +} diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java index 1f10532588..ab5d9ff75c 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java @@ -257,15 +257,6 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo getSelectSet().scheduleTimeout(task,timeoutMs); } - - /* ------------------------------------------------------------ */ - @Override - public boolean isOutputShutdown() - { - setCheckForIdle(true); - return super.isOutputShutdown(); - } - /* ------------------------------------------------------------ */ public void setCheckForIdle(boolean check) { @@ -289,10 +280,11 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo public void checkIdleTimestamp(long now) { long idleTimestamp=_idleTimestamp; - + if (idleTimestamp!=0 && _maxIdleTime>0) { long idleForMs=now-idleTimestamp; + if (idleForMs>_maxIdleTime) { onIdleExpired(idleForMs); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java index d9af5953ca..876db22112 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java @@ -96,6 +96,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection _sslEndPoint = newSslEndPoint(); } + /* ------------------------------------------------------------ */ protected SslEndPoint newSslEndPoint() { return new SslEndPoint(); @@ -583,6 +584,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection return _sslEndPoint; } + /* ------------------------------------------------------------ */ public String toString() { return String.format("%s %s", super.toString(), _sslEndPoint); @@ -596,6 +598,11 @@ public class SslConnection extends AbstractConnection implements AsyncConnection { return _engine; } + + public AsyncEndPoint getEndpoint() + { + return _aEndp; + } public void shutdownOutput() throws IOException { @@ -822,5 +829,6 @@ public class SslConnection extends AbstractConnection implements AsyncConnection _ishut, _oshut, _connection); } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java index 8ec0737e5a..1ddedf015a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncHttpConnection.java @@ -54,6 +54,10 @@ public class AsyncHttpConnection extends AbstractHttpConnection implements Async try { setCurrentConnection(this); + + // don't check for idle while dispatched (unless blocking IO is done). + _asyncEndp.setCheckForIdle(false); + // While progress and the connection has not changed while (progress && connection==this) @@ -133,10 +137,16 @@ public class AsyncHttpConnection extends AbstractHttpConnection implements Async finally { setCurrentConnection(null); + + // If we are not suspended if (!_request.isAsyncStarted()) { + // return buffers _parser.returnBuffers(); _generator.returnBuffers(); + + // resuming checking for idle + _asyncEndp.setCheckForIdle(true); } // Safety net to catch spinning diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java index ec15289f91..6b5c8284da 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingHttpConnection.java @@ -110,6 +110,13 @@ public class BlockingHttpConnection extends AbstractHttpConnection _endp.shutdownOutput(); } } + + // If we don't have a committed response and we are not suspended + if (_endp.isInputShutdown() && _generator.isIdle() && !_request.getAsyncContinuation().isSuspended()) + { + // then no more can happen, so close. + _endp.close(); + } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 84ec0e2449..ca8789de20 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -101,6 +101,14 @@ import org.eclipse.jetty.util.log.Logger; * to avoid reparsing headers and cookies that are likely to be the same for * requests from the same connection. * + * <p> + * The form content that a request can process is limited to protect from Denial of Service + * attacks. The size in bytes is limited by {@link ContextHandler#getMaxFormContentSize()} or if there is no + * context then the "org.eclipse.jetty.server.Request.maxFormContentSize" {@link Server} attribute. + * The number of parameters keys is limited by {@link ContextHandler#getMaxFormKeys()} or if there is no + * context then the "org.eclipse.jetty.server.Request.maxFormKeys" {@link Server} attribute. + * + * */ public class Request implements HttpServletRequest { @@ -231,7 +239,7 @@ public class Request implements HttpServletRequest if (content_type != null && content_type.length() > 0) { content_type = HttpFields.valueParameters(content_type, null); - + if (MimeTypes.FORM_ENCODED.equalsIgnoreCase(content_type) && _inputState==__NONE && (HttpMethods.POST.equals(getMethod()) || HttpMethods.PUT.equals(getMethod()))) { @@ -241,16 +249,21 @@ public class Request implements HttpServletRequest try { int maxFormContentSize=-1; + int maxFormKeys=-1; if (_context!=null) + { maxFormContentSize=_context.getContextHandler().getMaxFormContentSize(); + maxFormKeys=_context.getContextHandler().getMaxFormKeys(); + } else { - Integer size = (Integer)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormContentSize"); - if (size!=null) - maxFormContentSize =size.intValue(); + Number size = (Number)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormContentSize"); + maxFormContentSize=size==null?200000:size.intValue(); + Number keys = (Number)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormKeys"); + maxFormKeys =keys==null?1000:keys.intValue(); } - + if (content_length>maxFormContentSize && maxFormContentSize > 0) { throw new IllegalStateException("Form too large"+content_length+">"+maxFormContentSize); @@ -258,7 +271,7 @@ public class Request implements HttpServletRequest InputStream in = getInputStream(); // Add form params to query params - UrlEncoded.decodeTo(in, _baseParameters, encoding,content_length<0?maxFormContentSize:-1); + UrlEncoded.decodeTo(in, _baseParameters, encoding,content_length<0?maxFormContentSize:-1,maxFormKeys); } catch (IOException e) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 8af3177194..3538d75f96 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; - import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; import javax.servlet.ServletContext; @@ -70,13 +69,13 @@ import org.eclipse.jetty.util.resource.Resource; /* ------------------------------------------------------------ */ /** * ContextHandler. - * + * * This handler wraps a call to handle by setting the context and servlet path, plus setting the context classloader. - * + * * <p> * If the context init parameter "org.eclipse.jetty.server.context.ManagedAttributes" is set to a comma separated list of names, then they are treated as * context attribute names, which if set as attributes are passed to the servers Container so that they may be managed with JMX. - * + * * @org.apache.xbean.XBean description="Creates a basic HTTP context" */ public class ContextHandler extends ScopedHandler implements Attributes, Server.Graceful @@ -95,7 +94,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** * Get the current ServletContext implementation. - * + * * @return ServletContext implementation */ public static Context getCurrentContext() @@ -121,6 +120,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. private EventListener[] _eventListeners; private Logger _logger; private boolean _allowNullPathInfo; + private int _maxFormKeys = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormKeys",1000).intValue(); private int _maxFormContentSize = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormContentSize",200000).intValue(); private boolean _compactPath = false; private boolean _aliases = false; @@ -244,7 +244,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. * Set the virtual hosts for the context. Only requests that have a matching host header or fully qualified URL will be passed to that context with a * virtual host name. A context with no virtual host names or a null virtual host name is available to all requests that are not served by a context with a * matching virtual host name. - * + * * @param vhosts * Array of virtual hosts that this context responds to. A null host name or null/empty array means any hostname is acceptable. Host names may be * String representation of IP addresses. Host names may start with '*.' to wildcard one level of names. @@ -265,7 +265,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** Either set virtual hosts or add to an existing set of virtual hosts. - * + * * @param virtualHosts * Array of virtual hosts that this context responds to. A null host name or null/empty array means any hostname is acceptable. Host names may be * String representation of IP addresses. Host names may start with '*.' to wildcard one level of names. @@ -287,7 +287,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. { currentVirtualHosts = new ArrayList<String>(); } - + for (int i = 0; i < virtualHosts.length; i++) { String normVhost = normalizeHostname(virtualHosts[i]); @@ -303,7 +303,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** * Removes an array of virtual host entries, if this removes all entries the _vhosts will be set to null - * + * * @param virtualHosts * Array of virtual hosts that this context responds to. A null host name or null/empty array means any hostname is acceptable. Host names may be * String representation of IP addresses. Host names may start with '*.' to wildcard one level of names. @@ -321,7 +321,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. else { List<String> existingVirtualHosts = new ArrayList<String>(Arrays.asList(_vhosts)); - + for (int i = 0; i < virtualHosts.length; i++) { String toRemoveVirtualHost = normalizeHostname(virtualHosts[i]); @@ -330,7 +330,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. existingVirtualHosts.remove(toRemoveVirtualHost); } } - + if (existingVirtualHosts.isEmpty()) { _vhosts = null; // if we ended up removing them all, just null out _vhosts @@ -341,13 +341,13 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. } } } - + /* ------------------------------------------------------------ */ /** * Get the virtual hosts for the context. Only requests that have a matching host header or fully qualified URL will be passed to that context with a * virtual host name. A context with no virtual host names or a null virtual host name is available to all requests that are not served by a context with a * matching virtual host name. - * + * * @return Array of virtual hosts that this context responds to. A null host name or empty array means any hostname is acceptable. Host names may be String * representation of IP addresses. Host names may start with '*.' to wildcard one level of names. */ @@ -371,9 +371,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** * Set the names of accepted connectors. - * + * * Names are either "host:port" or a specific configured name for a connector. - * + * * @param connectors * If non null, an array of connector names that this context will accept a request from. */ @@ -425,7 +425,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** * Make best effort to extract a file classpath from the context classloader - * + * * @return Returns the classLoader. */ public String getClassPath() @@ -521,7 +521,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** * Set the context event listeners. - * + * * @param eventListeners * the event listeners * @see ServletContextListener @@ -559,7 +559,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** * Add a context event listeners. - * + * * @see ServletContextListener * @see ServletContextAttributeListener * @see ServletRequestListener @@ -586,7 +586,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /** * Set shutdown status. This field allows for graceful shutdown of a context. A started context may be put into non accepting state so that existing * requests can complete, but no new requests are accepted. - * + * * @param shutdown * true if this context is (not?) accepting new requests */ @@ -694,7 +694,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /** * Extensible startContext. this method is called from {@link ContextHandler#doStart()} instead of a call to super.doStart(). This allows derived classes to * insert additional handling (Eg configuration) before the call to super.doStart by this method will start contained handlers. - * + * * @see org.eclipse.jetty.server.handler.ContextHandler.Context */ protected void startContext() throws Exception @@ -1116,7 +1116,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* * Set a context attribute. Attributes set via this API cannot be overriden by the ServletContext.setAttribute API. Their lifecycle spans the stop/start of * a context. No attribute listener events are triggered by this API. - * + * * @see javax.servlet.ServletContext#setAttribute(java.lang.String, java.lang.Object) */ public void setAttribute(String name, Object value) @@ -1349,12 +1349,32 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. } /* ------------------------------------------------------------ */ + /** + * Set the maximum size of a form post, to protect against DOS attacks from large forms. + * @param maxSize + */ public void setMaxFormContentSize(int maxSize) { _maxFormContentSize = maxSize; } /* ------------------------------------------------------------ */ + public int getMaxFormKeys() + { + return _maxFormKeys; + } + + /* ------------------------------------------------------------ */ + /** + * Set the maximum number of form Keys to protect against DOS attack from crafted hash keys. + * @param max + */ + public void setMaxFormKeys(int max) + { + _maxFormKeys = max; + } + + /* ------------------------------------------------------------ */ /** * @return True if URLs are compacted to replace multiple '/'s with a single '/' */ @@ -1381,14 +1401,17 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. StringBuilder b = new StringBuilder(); - String p = getClass().getPackage().getName(); - if (p != null && p.length() > 0) + Package pkg = getClass().getPackage(); + if (pkg != null) { - String[] ss = p.split("\\."); - for (String s : ss) - b.append(s.charAt(0)).append('.'); + String p = pkg.getName(); + if (p != null && p.length() > 0) + { + String[] ss = p.split("\\."); + for (String s : ss) + b.append(s.charAt(0)).append('.'); + } } - b.append(getClass().getSimpleName()); b.append('{').append(getContextPath()).append(',').append(getBaseResource()); @@ -1431,7 +1454,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /** * Get the character encoding for a locale. The full locale name is first looked up in the map of encodings. If no encoding is found, then the locale * language is looked up. - * + * * @param locale * a <code>Locale</code> value * @return a <code>String</code> representing the character encoding for the locale or null if none found. @@ -1495,7 +1518,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. /* ------------------------------------------------------------ */ /** * Convert a URL or path to a Resource. The default implementation is a wrapper for {@link Resource#newResource(String)}. - * + * * @param urlOrPath * The URL or path to convert * @return The Resource for the URL/path @@ -1557,8 +1580,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Server. * <p> * A partial implementation of {@link javax.servlet.ServletContext}. A complete implementation is provided by the derived {@link ContextHandler}. * </p> - * - * + * + * */ public class Context implements ServletContext { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/BlockingChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/BlockingChannelConnector.java index 98716285b7..8e331142e6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/BlockingChannelConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/BlockingChannelConnector.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.ByteChannel; +import java.nio.channels.SelectionKey; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Set; @@ -341,5 +342,20 @@ public class BlockingChannelConnector extends AbstractNIOConnector } } } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return String.format("BCEP@%x{l(%s)<->r(%s),open=%b,ishut=%b,oshut=%b}-{%s}", + hashCode(), + _socket.getRemoteSocketAddress(), + _socket.getLocalSocketAddress(), + isOpen(), + isInputShutdown(), + isOutputShutdown(), + _connection); + } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java index 998dc76638..b6165d45fd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java @@ -122,7 +122,6 @@ public class SelectChannelConnector extends AbstractNIOConnector public void customize(EndPoint endpoint, Request request) throws IOException { AsyncEndPoint aEndp = ((AsyncEndPoint)endpoint); - aEndp.setCheckForIdle(false); request.setTimeStamp(System.currentTimeMillis()); endpoint.setMaxIdleTime(_maxIdleTime); super.customize(endpoint, request); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java index 7b2e2b5650..f01dfc831a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectorTimeoutTest.java @@ -13,44 +13,48 @@ package org.eclipse.jetty.server; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; - +import java.net.SocketException; +import java.util.concurrent.Exchanger; +import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import junit.framework.Assert; - +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.nio.SslConnection; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.IO; +import org.junit.Assert; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.matchers.JUnitMatchers.containsString; + public abstract class ConnectorTimeoutTest extends HttpServerTestFixture { protected static final int MAX_IDLE_TIME=250; - + static { System.setProperty("org.eclipse.jetty.io.nio.IDLE_TICK","100"); } - - + + @Test public void testMaxIdleWithRequest10() throws Exception - { + { configureServer(new HelloWorldHandler()); Socket client=newSocket(HOST,_connector.getLocalPort()); client.setSoTimeout(10000); assertFalse(client.isClosed()); - + OutputStream os=client.getOutputStream(); InputStream is=client.getInputStream(); @@ -63,7 +67,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture long start = System.currentTimeMillis(); IO.toString(is); - + Thread.sleep(300); assertEquals(-1, is.read()); @@ -73,13 +77,13 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture @Test public void testMaxIdleWithRequest11() throws Exception - { + { configureServer(new EchoHandler()); Socket client=newSocket(HOST,_connector.getLocalPort()); client.setSoTimeout(10000); assertFalse(client.isClosed()); - + OutputStream os=client.getOutputStream(); InputStream is=client.getInputStream(); @@ -96,24 +100,172 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture long start = System.currentTimeMillis(); IO.toString(is); - + Thread.sleep(300); assertEquals(-1, is.read()); Assert.assertTrue(System.currentTimeMillis()-start>200); Assert.assertTrue(System.currentTimeMillis()-start<5000); } - + + @Test + public void testMaxIdleWithRequest10NoClientClose() throws Exception + { + final Exchanger<EndPoint> endpoint = new Exchanger<EndPoint>(); + configureServer(new HelloWorldHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, + ServletException + { + try + { + endpoint.exchange(baseRequest.getConnection().getEndPoint()); + } + catch(Exception e) + {} + super.handle(target,baseRequest,request,response); + } + + }); + Socket client=newSocket(HOST,_connector.getLocalPort()); + client.setSoTimeout(10000); + + assertFalse(client.isClosed()); + + OutputStream os=client.getOutputStream(); + InputStream is=client.getInputStream(); + + os.write(( + "GET / HTTP/1.0\r\n"+ + "host: "+HOST+":"+_connector.getLocalPort()+"\r\n"+ + "connection: close\r\n"+ + "\r\n").getBytes("utf-8")); + os.flush(); + + // Get the server side endpoint + EndPoint endp = endpoint.exchange(null,10,TimeUnit.SECONDS); + if (endp instanceof SslConnection.SslEndPoint) + endp=((SslConnection.SslEndPoint)endp).getEndpoint(); + + // read the response + String result=IO.toString(is); + Assert.assertThat("OK",result,containsString("200 OK")); + + // check client reads EOF + assertEquals(-1, is.read()); + + // wait for idle timeout + TimeUnit.MILLISECONDS.sleep(MAX_IDLE_TIME+MAX_IDLE_TIME/2); + + + // further writes will get broken pipe or similar + try + { + for (int i=0;i<1000;i++) + { + os.write(( + "GET / HTTP/1.0\r\n"+ + "host: "+HOST+":"+_connector.getLocalPort()+"\r\n"+ + "connection: keep-alive\r\n"+ + "\r\n").getBytes("utf-8")); + os.flush(); + } + Assert.fail("half close should have timed out"); + } + catch(SocketException e) + { + // expected + } + // check the server side is closed + Assert.assertFalse(endp.isOpen()); + } + + @Test + public void testMaxIdleWithRequest11NoClientClose() throws Exception + { + final Exchanger<EndPoint> endpoint = new Exchanger<EndPoint>(); + configureServer(new EchoHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, + ServletException + { + try + { + endpoint.exchange(baseRequest.getConnection().getEndPoint()); + } + catch(Exception e) + {} + super.handle(target,baseRequest,request,response); + } + + }); + Socket client=newSocket(HOST,_connector.getLocalPort()); + client.setSoTimeout(10000); + + assertFalse(client.isClosed()); + + OutputStream os=client.getOutputStream(); + InputStream is=client.getInputStream(); + + String content="Wibble"; + byte[] contentB=content.getBytes("utf-8"); + os.write(( + "POST /echo HTTP/1.1\r\n"+ + "host: "+HOST+":"+_connector.getLocalPort()+"\r\n"+ + "content-type: text/plain; charset=utf-8\r\n"+ + "content-length: "+contentB.length+"\r\n"+ + "connection: close\r\n"+ + "\r\n").getBytes("utf-8")); + os.write(contentB); + os.flush(); + + // Get the server side endpoint + EndPoint endp = endpoint.exchange(null,10,TimeUnit.SECONDS); + + // read the response + IO.toString(is); + + // check client reads EOF + assertEquals(-1, is.read()); + + TimeUnit.MILLISECONDS.sleep(MAX_IDLE_TIME+MAX_IDLE_TIME/2); + + + // further writes will get broken pipe or similar + try + { + for (int i=0;i<1000;i++) + { + os.write(( + "GET / HTTP/1.0\r\n"+ + "host: "+HOST+":"+_connector.getLocalPort()+"\r\n"+ + "connection: keep-alive\r\n"+ + "\r\n").getBytes("utf-8")); + os.flush(); + } + Assert.fail("half close should have timed out"); + } + catch(SocketException e) + { + // expected + } + + // check the server side is closed + Assert.assertFalse(endp.isOpen()); + } + @Test public void testMaxIdleNoRequest() throws Exception - { + { configureServer(new EchoHandler()); Socket client=newSocket(HOST,_connector.getLocalPort()); client.setSoTimeout(10000); InputStream is=client.getInputStream(); assertFalse(client.isClosed()); - + Thread.sleep(500); long start = System.currentTimeMillis(); try @@ -123,25 +275,25 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture } catch(SSLException e) { - + } catch(Exception e) { e.printStackTrace(); } Assert.assertTrue(System.currentTimeMillis()-start<5000); - - } + + } @Test public void testMaxIdleWithSlowRequest() throws Exception - { + { configureServer(new EchoHandler()); Socket client=newSocket(HOST,_connector.getLocalPort()); client.setSoTimeout(10000); assertFalse(client.isClosed()); - + OutputStream os=client.getOutputStream(); InputStream is=client.getInputStream(); @@ -163,7 +315,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture os.write(contentB); os.flush(); } - + String in = IO.toString(is); int offset=0; for (int i =0;i<20;i++) @@ -175,13 +327,13 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture @Test public void testMaxIdleWithSlowResponse() throws Exception - { + { configureServer(new SlowResponseHandler()); Socket client=newSocket(HOST,_connector.getLocalPort()); client.setSoTimeout(10000); assertFalse(client.isClosed()); - + OutputStream os=client.getOutputStream(); InputStream is=client.getInputStream(); @@ -192,7 +344,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture "Connection: close\r\n"+ "\r\n").getBytes("utf-8")); os.flush(); - + String in = IO.toString(is); int offset=0; for (int i =0;i<20;i++) @@ -204,13 +356,13 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture @Test public void testMaxIdleWithWait() throws Exception - { + { configureServer(new WaitHandler()); Socket client=newSocket(HOST,_connector.getLocalPort()); client.setSoTimeout(10000); assertFalse(client.isClosed()); - + OutputStream os=client.getOutputStream(); InputStream is=client.getInputStream(); @@ -221,12 +373,12 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture "Connection: close\r\n"+ "\r\n").getBytes("utf-8")); os.flush(); - + String in = IO.toString(is); int offset=in.indexOf("Hello World"); Assert.assertTrue(offset>0); } - + protected static class SlowResponseHandler extends AbstractHandler { public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException @@ -234,7 +386,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture baseRequest.setHandled(true); response.setStatus(200); OutputStream out = response.getOutputStream(); - + for (int i=0;i<20;i++) { out.write("Hello World\r\n".getBytes()); @@ -244,7 +396,7 @@ public abstract class ConnectorTimeoutTest extends HttpServerTestFixture out.close(); } } - + protected static class WaitHandler extends AbstractHandler { public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 71cfdcd236..bc4cbff2cd 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -19,12 +19,16 @@ import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; +import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; @@ -34,9 +38,11 @@ import javax.servlet.http.HttpServletResponse; import junit.framework.Assert; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.log.Log; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -740,6 +746,56 @@ public class RequestTest assertEquals(null,cookie[1]); } + + @Test + public void testHashDOS() throws Exception + { + _server.setAttribute("org.eclipse.jetty.server.Request.maxFormContentSize",-1); + _server.setAttribute("org.eclipse.jetty.server.Request.maxFormKeys",1000); + + // This file is not distributed - as it is dangerous + File evil_keys = new File("/tmp/keys_mapping_to_zero_2m"); + if (!evil_keys.exists()) + { + Log.info("testHashDOS skipped"); + return; + } + + BufferedReader in = new BufferedReader(new FileReader(evil_keys)); + StringBuilder buf = new StringBuilder(4000000); + + String key=null; + buf.append("a=b"); + while((key=in.readLine())!=null) + { + buf.append("&").append(key).append("=").append("x"); + } + buf.append("&c=d"); + + _handler._checker = new RequestTester() + { + public boolean check(HttpServletRequest request,HttpServletResponse response) + { + return "b".equals(request.getParameter("a")) && request.getParameter("c")==null; + } + }; + + String request="POST / HTTP/1.1\r\n"+ + "Host: whatever\r\n"+ + "Content-Type: "+MimeTypes.FORM_ENCODED+"\r\n"+ + "Content-Length: "+buf.length()+"\r\n"+ + "Connection: close\r\n"+ + "\r\n"+ + buf; + + long start=System.currentTimeMillis(); + String response = _connector.getResponses(request); + assertTrue(response.contains("200 OK")); + long now=System.currentTimeMillis(); + assertTrue((now-start)<5000); + } + + interface RequestTester { boolean check(HttpServletRequest request,HttpServletResponse response) throws IOException; @@ -754,13 +810,15 @@ public class RequestTest { ((Request)request).setHandled(true); - if (request.getContentLength()>0) + if (request.getContentLength()>0 && !MimeTypes.FORM_ENCODED.equals(request.getContentType())) _content=IO.toString(request.getInputStream()); - + if (_checker!=null && _checker.check(request,response)) response.setStatus(200); else response.sendError(500); + + } } } diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index 4e2d6115f9..c42b93d710 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -20,6 +20,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; +import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -30,7 +31,6 @@ import java.lang.reflect.Method; import java.net.ConnectException; import java.net.InetAddress; import java.net.Socket; -import java.security.Policy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -40,17 +40,15 @@ import java.util.List; import java.util.Properties; import java.util.Set; - /*-------------------------------------------*/ /** * <p> - * Main start class. This class is intended to be the main class listed in the MANIFEST.MF of the start.jar archive. It - * allows an application to be started with the command "java -jar start.jar". + * Main start class. This class is intended to be the main class listed in the MANIFEST.MF of the start.jar archive. It allows an application to be started with + * the command "java -jar start.jar". * </p> - * + * * <p> - * The behaviour of Main is controlled by the parsing of the {@link Config} "org/eclipse/start/start.config" file - * obtained as a resource or file. + * The behaviour of Main is controlled by the parsing of the {@link Config} "org/eclipse/start/start.config" file obtained as a resource or file. * </p> */ public class Main @@ -67,257 +65,240 @@ public class Main private boolean _dryRun = false; private boolean _exec = false; private final Config _config = new Config(); - private Set<String> _sysProps = new HashSet<String>(); - private List<String> _jvmArgs = new ArrayList<String>(); + private final Set<String> _sysProps = new HashSet<String>(); + private final List<String> _jvmArgs = new ArrayList<String>(); private String _startConfig = null; private String _jettyHome; - public static void main(String[] args) + public static void main(String[] args) + { + try + { + Main main = new Main(); + List<String> arguments = main.expandCommandLine(args); + List<String> xmls = main.processCommandLine(arguments); + if (xmls!=null) + main.start(xmls); + } + catch (Throwable e) + { + usageExit(e,ERR_UNKNOWN); + } + } + + Main() throws IOException { - Main main = new Main(); - main.parseCommandLine(args); + _jettyHome = System.getProperty("jetty.home","."); + _jettyHome = new File(_jettyHome).getCanonicalPath(); } - public void parseCommandLine(String[] args) + public List<String> expandCommandLine(String[] args) throws Exception { - try - { - List<String> arguments = new ArrayList<String>(); + List<String> arguments = new ArrayList<String>(); - // add the command line args and look for start.ini args - boolean ini=false; - for (String arg : args) + // add the command line args and look for start.ini args + boolean ini = false; + for (String arg : args) + { + if (arg.startsWith("--ini=") || arg.equals("--ini")) { - if (arg.startsWith("--ini=")||arg.equals("--ini")) - { - ini=true; - if (arg.length()>6) - { - arguments.addAll(loadStartIni(arg.substring(6))); - continue; - } - } - else if (arg.startsWith("--config=")) + ini = true; + if (arg.length() > 6) { - _startConfig=arg.substring(9); - } - else - { - arguments.add(arg); + arguments.addAll(loadStartIni(new File(arg.substring(6)))); + continue; } } - - // if no non-option inis, add the start.ini - if (!ini) + else if (arg.startsWith("--config=")) { - arguments.addAll(0,loadStartIni(null)); + _startConfig = arg.substring(9); } - - // The XML Configuration Files to initialize with - List<String> xmls = new ArrayList<String>(); + else + { + arguments.add(arg); + } + } - // Process the arguments - int startup=0; - for (String arg : arguments) + // if no non-option inis, add the start.ini and start.d + if (!ini) + { + List<String> ini_args=new ArrayList<String>(); + File start_ini = new File(_jettyHome,"start.ini"); + if (start_ini.exists()) + ini_args.addAll(loadStartIni(start_ini)); + + File start_d = new File(_jettyHome,"start.d"); + if (start_d.isDirectory()) { - if ("--help".equals(arg) || "-?".equals(arg)) + File[] inis = start_d.listFiles(new FilenameFilter() { - _showUsage = true; - continue; - } + public boolean accept(File dir, String name) + { + return name.toLowerCase().endsWith(".ini"); + } + }); + Arrays.sort(inis); + for (File i : inis) + ini_args.addAll(loadStartIni(i)); + } + arguments.addAll(0,ini_args); + } - if ("--stop".equals(arg)) - { - int port = Integer.parseInt(Config.getProperty("STOP.PORT","-1")); - String key = Config.getProperty("STOP.KEY",null); - stop(port,key); - return; - } + return arguments; + } + + public List<String> processCommandLine(List<String> arguments) throws Exception + { + // The XML Configuration Files to initialize with + List<String> xmls = new ArrayList<String>(); - if ("--version".equals(arg) || "-v".equals(arg) || "--info".equals(arg)) - { - _dumpVersions = true; - continue; - } + // Process the arguments + int startup = 0; + for (String arg : arguments) + { + if ("--help".equals(arg) || "-?".equals(arg)) + { + _showUsage = true; + continue; + } - if ("--list-modes".equals(arg) || "--list-options".equals(arg)) - { - _listOptions = true; - continue; - } + if ("--stop".equals(arg)) + { + int port = Integer.parseInt(Config.getProperty("STOP.PORT","-1")); + String key = Config.getProperty("STOP.KEY",null); + stop(port,key); + return null; + } - if ("--list-config".equals(arg)) - { - _listConfig=true; - continue; - } + if ("--version".equals(arg) || "-v".equals(arg) || "--info".equals(arg)) + { + _dumpVersions = true; + continue; + } - if ("--exec-print".equals(arg)||"--dry-run".equals(arg)) - { - _dryRun = true; - continue; - } + if ("--list-modes".equals(arg) || "--list-options".equals(arg)) + { + _listOptions = true; + continue; + } - if ("--exec".equals(arg)) - { - _exec = true; - continue; - } + if ("--list-config".equals(arg)) + { + _listConfig = true; + continue; + } - // Special internal indicator that jetty was started by the jetty.sh Daemon - if ("--daemon".equals(arg)) - { - File startDir = new File(System.getProperty("jetty.logs","logs")); - if (!startDir.exists() || !startDir.canWrite() ) - startDir = new File("."); - File startLog = new File(startDir,"start.log"); - if (!startLog.exists() && !startLog.createNewFile()) - { - // Output about error is lost in majority of cases. - System.err.println("Unable to create: " + startLog.getAbsolutePath()); - // Toss a unique exit code indicating this failure. - usageExit(ERR_LOGGING); - } + if ("--exec-print".equals(arg) || "--dry-run".equals(arg)) + { + _dryRun = true; + continue; + } - if (!startLog.canWrite()) - { - // Output about error is lost in majority of cases. - System.err.println("Unable to write to: " + startLog.getAbsolutePath()); - // Toss a unique exit code indicating this failure. - usageExit(ERR_LOGGING); - } - PrintStream logger = new PrintStream(new FileOutputStream(startLog,false)); - System.setOut(logger); - System.setErr(logger); - System.out.println("Establishing start.log on " + new Date()); - continue; - } - - if (arg.startsWith("--pre=")) - { - xmls.add(startup++,arg.substring(6)); - continue; - } - - if (arg.startsWith("-D")) + if ("--exec".equals(arg)) + { + _exec = true; + continue; + } + + // Special internal indicator that jetty was started by the jetty.sh Daemon + if ("--daemon".equals(arg)) + { + File startDir = new File(System.getProperty("jetty.logs","logs")); + if (!startDir.exists() || !startDir.canWrite()) + startDir = new File("."); + File startLog = new File(startDir,"start.log"); + if (!startLog.exists() && !startLog.createNewFile()) { - String[] assign = arg.substring(2).split("=",2); - _sysProps.add(assign[0]); - switch(assign.length) - { - case 2: - System.setProperty(assign[0],assign[1]); - break; - case 1: - System.setProperty(assign[0],""); - break; - default: - break; - } - continue; + // Output about error is lost in majority of cases. + System.err.println("Unable to create: " + startLog.getAbsolutePath()); + // Toss a unique exit code indicating this failure. + usageExit(ERR_LOGGING); } - if (arg.startsWith("-")) + if (!startLog.canWrite()) { - _jvmArgs.add(arg); - continue; + // Output about error is lost in majority of cases. + System.err.println("Unable to write to: " + startLog.getAbsolutePath()); + // Toss a unique exit code indicating this failure. + usageExit(ERR_LOGGING); } + PrintStream logger = new PrintStream(new FileOutputStream(startLog,false)); + System.setOut(logger); + System.setErr(logger); + System.out.println("Establishing start.log on " + new Date()); + continue; + } - // Is this a Property? - if (arg.indexOf('=') >= 0) - { - String[] assign = arg.split("=",2); + if (arg.startsWith("--pre=")) + { + xmls.add(startup++,arg.substring(6)); + continue; + } - switch(assign.length) - { - case 2: - if ("OPTIONS".equals(assign[0])) - { - String opts[] = assign[1].split(","); - for (String opt : opts) - _config.addActiveOption(opt); - } - else - { - this._config.setProperty(assign[0],assign[1]); - } - break; - case 1: - this._config.setProperty(assign[0],null); - break; - default: - break; - } - - continue; - } - - // Anything else is considered an XML file. - if (xmls.contains(arg)) + if (arg.startsWith("-D")) + { + String[] assign = arg.substring(2).split("=",2); + _sysProps.add(assign[0]); + switch (assign.length) { - System.out.println("WARN: Argument '"+arg+"' specified multiple times. Check start.ini?"); - System.out.println("Use \"java -jar start.jar --help\" for more information."); + case 2: + System.setProperty(assign[0],assign[1]); + break; + case 1: + System.setProperty(assign[0],""); + break; + default: + break; } - xmls.add(arg); + continue; } - - start(xmls); - } - catch (Throwable t) - { - usageExit(t,ERR_UNKNOWN); - } - } - /** - * If a start.ini is present in the CWD, then load it into the argument list. - */ - private List<String> loadStartIni(String ini) - { - String jettyHome=System.getProperty("jetty.home"); - File startIniFile = ini==null?((jettyHome!=null)? new File(jettyHome,"start.ini"):new File("start.ini")):new File(ini); - if (!startIniFile.exists()) - { - if (ini != null) + if (arg.startsWith("-")) { - System.err.println("Warning - can't find ini file: " + ini); + _jvmArgs.add(arg); + continue; } - // No start.ini found, skip load. - return Collections.emptyList(); - } - - List<String> args = new ArrayList<String>(); - - FileReader reader = null; - BufferedReader buf = null; - try - { - reader = new FileReader(startIniFile); - buf = new BufferedReader(reader); - String arg; - while ((arg = buf.readLine()) != null) + // Is this a Property? + if (arg.indexOf('=') >= 0) { - arg = arg.trim(); - if (arg.length() == 0 || arg.startsWith("#")) + String[] assign = arg.split("=",2); + + switch (assign.length) { - continue; + case 2: + if ("OPTIONS".equals(assign[0])) + { + String opts[] = assign[1].split(","); + for (String opt : opts) + _config.addActiveOption(opt); + } + else + { + this._config.setProperty(assign[0],assign[1]); + } + break; + case 1: + this._config.setProperty(assign[0],null); + break; + default: + break; } - args.add(arg); + + continue; } - } - catch (IOException e) - { - usageExit(e,ERR_UNKNOWN); - } - finally - { - close(buf); - close(reader); + + // Anything else is considered an XML file. + if (xmls.contains(arg)) + { + System.out.println("WARN: Argument '" + arg + "' specified multiple times. Check start.ini?"); + System.out.println("Use \"java -jar start.jar --help\" for more information."); + } + xmls.add(arg); } - return args; + return xmls; } private void usage() @@ -339,10 +320,10 @@ public class Main while ((line = buf.readLine()) != null) { - if (line.endsWith("@") && line.indexOf('@')!=line.lastIndexOf('@')) + if (line.endsWith("@") && line.indexOf('@') != line.lastIndexOf('@')) { - String indent=line.substring(0,line.indexOf("@")); - String info=line.substring(line.indexOf('@'),line.lastIndexOf('@')); + String indent = line.substring(0,line.indexOf("@")); + String info = line.substring(line.indexOf('@'),line.lastIndexOf('@')); if (info.equals("@OPTIONS")) { @@ -352,7 +333,7 @@ public class Main for (String option : sortedOptions) { - if ("*".equals(option) || option.trim().length()==0) + if ("*".equals(option) || option.trim().length() == 0) continue; System.out.print(indent); System.out.println(option); @@ -396,7 +377,7 @@ public class Main else if (info.equals("@STARTINI")) { List<String> ini = loadStartIni(null); - if (ini!=null && ini.size()>0) + if (ini != null && ini.size() > 0) { for (String a : ini) { @@ -429,7 +410,7 @@ public class Main } public void invokeMain(ClassLoader classloader, String classname, List<String> args) throws IllegalAccessException, InvocationTargetException, - NoSuchMethodException, ClassNotFoundException + NoSuchMethodException, ClassNotFoundException { Class<?> invoked_class = null; @@ -462,10 +443,12 @@ public class Main String argArray[] = args.toArray(new String[0]); - Class<?>[] method_param_types = new Class[] { argArray.getClass() }; + Class<?>[] method_param_types = new Class[] + { argArray.getClass() }; Method main = invoked_class.getDeclaredMethod("main",method_param_types); - Object[] method_params = new Object[] { argArray }; + Object[] method_params = new Object[] + { argArray }; main.invoke(null,method_params); } @@ -492,13 +475,12 @@ public class Main // Setup Start / Stop Monitoring int port = Integer.parseInt(Config.getProperty("STOP.PORT","-1")); String key = Config.getProperty("STOP.KEY",null); - Monitor monitor=new Monitor(port,key); - + Monitor monitor = new Monitor(port,key); // Load potential Config (start.config) List<String> configuredXmls = loadConfig(xmls); - // No XML defined in start.config or command line. Can't execute. + // No XML defined in start.config or command line. Can't execute. if (configuredXmls.isEmpty()) { throw new FileNotFoundException("No XML configuration files specified in start.config or command line."); @@ -523,7 +505,7 @@ public class Main System.err.println("classloader.parent=" + cl.getParent()); System.err.println("properties=" + Config.getProperties()); } - + // Show the usage information and return if (_showUsage) { @@ -544,7 +526,7 @@ public class Main showAllOptionsWithVersions(classpath); return; } - + if (_listConfig) { listConfig(); @@ -557,7 +539,7 @@ public class Main System.out.println(buildCommandLine(classpath,configuredXmls)); return; } - + // execute Jetty in another JVM if (_exec) { @@ -565,11 +547,12 @@ public class Main final Process process = Runtime.getRuntime().exec(cmd); Runtime.getRuntime().addShutdownHook(new Thread() { + @Override public void run() { Config.debug("Destroying " + process); process.destroy(); - } + } }); copyInThread(process.getErrorStream(),System.err); copyInThread(process.getInputStream(),System.out); @@ -578,7 +561,7 @@ public class Main process.waitFor(); return; } - + if (_jvmArgs.size() > 0 || _sysProps.size() > 0) { System.err.println("WARNING: System properties and/or JVM args set. Consider using --dry-run or --exec"); @@ -617,7 +600,7 @@ public class Main } } - private void copyInThread(final InputStream in,final OutputStream out) + private void copyInThread(final InputStream in, final OutputStream out) { new Thread(new Runnable() { @@ -625,23 +608,23 @@ public class Main { try { - byte[] buf=new byte[1024]; - int len=in.read(buf); - while(len>0) + byte[] buf = new byte[1024]; + int len = in.read(buf); + while (len > 0) { out.write(buf,0,len); - len=in.read(buf); + len = in.read(buf); } } - catch(IOException e) + catch (IOException e) { // e.printStackTrace(); } } - + }).start(); } - + private String resolveXmlConfig(String xmlFilename) throws FileNotFoundException { if (!xmlFilename.toLowerCase().endsWith(".xml")) @@ -675,22 +658,22 @@ public class Main { StringBuilder cmd = new StringBuilder(); cmd.append(findJavaBin()); - for (String x:_jvmArgs) + for (String x : _jvmArgs) cmd.append(' ').append(x); cmd.append(" -Djetty.home=").append(_jettyHome); - for (String p:_sysProps) + for (String p : _sysProps) { cmd.append(" -D").append(p); - String v=System.getProperty(p); - if (v!=null && v.length()>0) + String v = System.getProperty(p); + if (v != null && v.length() > 0) cmd.append('=').append(v); } cmd.append(" -cp ").append(classpath.toString()); cmd.append(" ").append(_config.getMainClassname()); - + // Check if we need to pass properties as a file Properties properties = Config.getProperties(); - if (properties.size()>0) + if (properties.size() > 0) { File prop_file = File.createTempFile("start",".properties"); if (!_dryRun) @@ -698,7 +681,7 @@ public class Main properties.store(new FileOutputStream(prop_file),"start.jar properties"); cmd.append(" ").append(prop_file.getAbsolutePath()); } - + for (String xml : xmls) { cmd.append(' ').append(xml); @@ -875,11 +858,10 @@ public class Main private String getZipVersion(File element) { - // TODO - find version in zip file. Look for META-INF/MANIFEST.MF ? + // TODO - find version in zip file. Look for META-INF/MANIFEST.MF ? return ""; } - private List<String> resolveXmlConfigs(List<String> xmls) throws FileNotFoundException { List<String> ret = new ArrayList<String>(); @@ -896,15 +878,15 @@ public class Main InputStream cfgstream = null; try { - cfgstream=getConfigStream(); - byte[] buf=new byte[4096]; - - int len=0; - - while (len>=0) + cfgstream = getConfigStream(); + byte[] buf = new byte[4096]; + + int len = 0; + + while (len >= 0) { - len=cfgstream.read(buf); - if (len>0) + len = cfgstream.read(buf); + if (len > 0) System.out.write(buf,0,len); } } @@ -917,13 +899,13 @@ public class Main close(cfgstream); } } - + /** * Load Configuration. - * - * No specific configuration is real until a {@link Config#getCombinedClasspath(java.util.Collection)} is used to - * execute the {@link Class} specified by {@link Config#getMainClassname()} is executed. - * + * + * No specific configuration is real until a {@link Config#getCombinedClasspath(java.util.Collection)} is used to execute the {@link Class} specified by + * {@link Config#getMainClassname()} is executed. + * * @param xmls * the command line specified xml configuration options. * @return the list of xml configurations arriving via command line and start.config choices. @@ -935,13 +917,13 @@ public class Main { // Pass in xmls.size into Config so that conditions based on "nargs" work. _config.setArgCount(xmls.size()); - - cfgstream=getConfigStream(); - + + cfgstream = getConfigStream(); + // parse the config _config.parse(cfgstream); - - _jettyHome = Config.getProperty("jetty.home"); + + _jettyHome = Config.getProperty("jetty.home",_jettyHome); if (_jettyHome != null) { _jettyHome = new File(_jettyHome).getCanonicalPath(); @@ -975,12 +957,12 @@ public class Main private InputStream getConfigStream() throws FileNotFoundException { - String config=_startConfig; + String config = _startConfig; if (config == null || config.length() == 0) { config = System.getProperty("START","org/eclipse/jetty/start/start.config"); } - + Config.debug("config=" + config); // Look up config as resource first. @@ -991,11 +973,10 @@ public class Main { cfgstream = new FileInputStream(config); } - + return cfgstream; } - /** * Stop a running jetty instance. */ @@ -1038,7 +1019,6 @@ public class Main usageExit(e,ERR_UNKNOWN); } } - static void usageExit(Throwable t, int exit) { @@ -1048,6 +1028,7 @@ public class Main System.err.println(" java -jar start.jar --help # for more information"); System.exit(exit); } + static void usageExit(int exit) { System.err.println(); @@ -1055,4 +1036,53 @@ public class Main System.err.println(" java -jar start.jar --help # for more information"); System.exit(exit); } + + /** + * Convert a start.ini format file into an argument list. + */ + static List<String> loadStartIni(File ini) + { + File startIniFile = ini; + if (!startIniFile.exists()) + { + if (ini != null) + { + System.err.println("Warning - can't find ini file: " + ini); + } + // No start.ini found, skip load. + return Collections.emptyList(); + } + + List<String> args = new ArrayList<String>(); + + FileReader reader = null; + BufferedReader buf = null; + try + { + reader = new FileReader(ini); + buf = new BufferedReader(reader); + + String arg; + while ((arg = buf.readLine()) != null) + { + arg = arg.trim(); + if (arg.length() == 0 || arg.startsWith("#")) + { + continue; + } + args.add(arg); + } + } + catch (IOException e) + { + // usageExit(e,ERR_UNKNOWN); + } + finally + { + Main.close(buf); + Main.close(reader); + } + + return args; + } } diff --git a/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt b/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt index 7371be3f99..c4c85c4523 100644 --- a/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt +++ b/jetty-start/src/main/resources/org/eclipse/jetty/start/usage.txt @@ -116,8 +116,11 @@ Available Configurations: Defaults: A start.ini file may be used to specify default arguments to start.jar, which are used if no command line arguments are provided and override - the defaults in the start.config file. If --ini options are provided on - the command line, then start.ini will no be read. The current start.ini - arguments are: + the defaults in the start.config file. If the directory start.d exists, + then multiple *.ini files will be read from that directory in alphabetical + order. If --ini options are provided on the command line, then start.ini + and start.d will not be read. + + The current start.ini arguments are: @STARTINI@ diff --git a/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java b/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java new file mode 100644 index 0000000000..246771b4da --- /dev/null +++ b/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java @@ -0,0 +1,76 @@ +// ======================================================================== +// Copyright (c) 2009-2009 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.start; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.net.URL; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +/* ------------------------------------------------------------ */ +/** + */ +public class MainTest +{ + /* ------------------------------------------------------------ */ + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception + { + System.setProperty("jetty.home",this.getClass().getResource("/jetty.home").getFile()); + } + + /** + * Test method for {@link org.eclipse.jetty.start.StartIniParser#loadStartIni(java.lang.String)}. + */ + @Test + public void testLoadStartIni() + { + URL startIni = this.getClass().getResource("/jetty.home/start.ini"); + String startIniFileName = startIni.getFile(); + List<String> args = Main.loadStartIni(new File(startIniFileName)); + assertEquals("Expected 5 uncommented lines in start.ini",5,args.size()); + assertEquals("First uncommented line in start.ini doesn't match expected result","OPTIONS=Server,jsp,resources,websocket,ext",args.get(0)); + } + + @Test + public void testExpandCommandLine() throws Exception + { + Main main = new Main(); + List<String> args = main.expandCommandLine(new String[]{}); + assertEquals("start.ini OPTIONS","OPTIONS=Server,jsp,resources,websocket,ext",args.get(0)); + assertEquals("start.d/jmx OPTIONS","OPTIONS=jmx",args.get(5)); + assertEquals("start.d/jmx XML","--pre=etc/jetty-jmx.xml",args.get(6)); + assertEquals("start.d/websocket OPTIONS","OPTIONS=websocket",args.get(7)); + } + + @Test + public void testProcessCommandLine() throws Exception + { + Main main = new Main(); + List<String> args = main.expandCommandLine(new String[]{}); + List<String> xmls = main.processCommandLine(args); + + assertEquals("jmx --pre","etc/jetty-jmx.xml",xmls.get(0)); + assertEquals("start.ini","etc/jetty.xml",xmls.get(1)); + assertEquals("start.d","etc/jetty-testrealm.xml",xmls.get(5)); + } + +} diff --git a/jetty-start/src/test/resources/jetty.home/start.d/10-jmx.ini b/jetty-start/src/test/resources/jetty.home/start.d/10-jmx.ini new file mode 100644 index 0000000000..827e41bf43 --- /dev/null +++ b/jetty-start/src/test/resources/jetty.home/start.d/10-jmx.ini @@ -0,0 +1,22 @@ +#=========================================================== +# Additional Jetty start.jar arguments +# Each line of this file is prepended to the command line +# arguments # of a call to: +# java -jar start.jar [arg...] +#=========================================================== + + + +#=========================================================== +#----------------------------------------------------------- +OPTIONS=jmx +#----------------------------------------------------------- + + +#=========================================================== +# Configuration files. +# For a full list of available configuration files do +# java -jar start.jar --help +#----------------------------------------------------------- +--pre=etc/jetty-jmx.xml +#=========================================================== diff --git a/jetty-start/src/test/resources/jetty.home/start.d/20-websocket.ini b/jetty-start/src/test/resources/jetty.home/start.d/20-websocket.ini new file mode 100644 index 0000000000..679a221dd7 --- /dev/null +++ b/jetty-start/src/test/resources/jetty.home/start.d/20-websocket.ini @@ -0,0 +1,13 @@ +#=========================================================== +# Additional Jetty start.jar arguments +# Each line of this file is prepended to the command line +# arguments # of a call to: +# java -jar start.jar [arg...] +#=========================================================== + + + +#=========================================================== +#----------------------------------------------------------- +OPTIONS=websocket +#-----------------------------------------------------------
\ No newline at end of file diff --git a/jetty-start/src/test/resources/jetty.home/start.d/90-testrealm.ini b/jetty-start/src/test/resources/jetty.home/start.d/90-testrealm.ini new file mode 100644 index 0000000000..59313d3b99 --- /dev/null +++ b/jetty-start/src/test/resources/jetty.home/start.d/90-testrealm.ini @@ -0,0 +1 @@ +etc/jetty-testrealm.xml
\ No newline at end of file diff --git a/jetty-start/src/test/resources/jetty.home/start.ini b/jetty-start/src/test/resources/jetty.home/start.ini new file mode 100644 index 0000000000..a9b724988e --- /dev/null +++ b/jetty-start/src/test/resources/jetty.home/start.ini @@ -0,0 +1,65 @@ +#=========================================================== +# Jetty start.jar arguments +# Each line of this file is prepended to the command line +# arguments # of a call to: +# java -jar start.jar [arg...] +#=========================================================== + + + +#=========================================================== +# If the arguements in this file include JVM arguments +# (eg -Xmx512m) or JVM System properties (eg com.sun.???), +# then these will not take affect unless the --exec +# parameter is included or if the output from --dry-run +# is executed like: +# eval $(java -jar start.jar --dry-run) +# +# Below are some recommended options for Sun's JRE +#----------------------------------------------------------- +# --exec +# -Dorg.apache.jasper.compiler.disablejsr199=true +# -Dcom.sun.management.jmxremote +# -Dorg.eclipse.jetty.util.log.IGNORED=true +# -Dorg.eclipse.jetty.util.log.stderr.DEBUG=true +# -Dorg.eclipse.jetty.util.log.stderr.SOURCE=true +# -Xmx2000m +# -Xmn512m +# -verbose:gc +# -XX:+PrintGCDateStamps +# -XX:+PrintGCTimeStamps +# -XX:+PrintGCDetails +# -XX:+PrintTenuringDistribution +# -XX:+PrintCommandLineFlags +# -XX:+DisableExplicitGC +# -XX:+UseConcMarkSweepGC +# -XX:ParallelCMSThreads=2 +# -XX:+CMSClassUnloadingEnabled +# -XX:+UseCMSCompactAtFullCollection +# -XX:CMSInitiatingOccupancyFraction=80 +#----------------------------------------------------------- + + +#=========================================================== +# Start classpath OPTIONS. +# These control what classes are on the classpath +# for a full listing do +# java -jar start.jar --list-options +#----------------------------------------------------------- +OPTIONS=Server,jsp,resources,websocket,ext +#----------------------------------------------------------- + + +#=========================================================== +# Configuration files. +# For a full list of available configuration files do +# java -jar start.jar --help +#----------------------------------------------------------- +etc/jetty.xml +# etc/jetty-ssl.xml +# etc/jetty-requestlog.xml +etc/jetty-deploy.xml +#etc/jetty-overlay.xml +etc/jetty-webapps.xml +etc/jetty-contexts.xml +#=========================================================== diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java b/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java index e0ed56cbaf..06741c0a13 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.util; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.Iterator; import java.util.Map; @@ -78,13 +79,13 @@ public class UrlEncoded extends MultiMap implements Cloneable /* ----------------------------------------------------------------- */ public void decode(String query) { - decodeTo(query,this,ENCODING); + decodeTo(query,this,ENCODING,-1); } /* ----------------------------------------------------------------- */ public void decode(String query,String charset) { - decodeTo(query,this,charset); + decodeTo(query,this,charset,-1); } /* -------------------------------------------------------------- */ @@ -178,6 +179,15 @@ public class UrlEncoded extends MultiMap implements Cloneable */ public static void decodeTo(String content, MultiMap map, String charset) { + decodeTo(content,map,charset,-1); + } + + /* -------------------------------------------------------------- */ + /** Decoded parameters to Map. + * @param content the string containing the encoded parameters + */ + public static void decodeTo(String content, MultiMap map, String charset, int maxKeys) + { if (charset==null) charset=ENCODING; @@ -208,6 +218,11 @@ public class UrlEncoded extends MultiMap implements Cloneable } key = null; value=null; + if (maxKeys>0 && map.size()>maxKeys) + { + LOG.warn("maxFormKeys limit exceeded keys>{}",maxKeys); + return; + } break; case '=': if (key!=null) @@ -343,9 +358,10 @@ public class UrlEncoded extends MultiMap implements Cloneable /** Decoded parameters to Map. * @param in InputSteam to read * @param map MultiMap to add parameters to - * @param maxLength maximum length of content to read 0r -1 for no limit + * @param maxLength maximum length of content to read or -1 for no limit + * @param maxLength maximum number of keys to read or -1 for no limit */ - public static void decode88591To(InputStream in, MultiMap map, int maxLength) + public static void decode88591To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException { synchronized(map) @@ -375,6 +391,11 @@ public class UrlEncoded extends MultiMap implements Cloneable } key = null; value=null; + if (maxKeys>0 && map.size()>maxKeys) + { + LOG.warn("maxFormKeys limit exceeded keys>{}",maxKeys); + return; + } break; case '=': @@ -423,9 +444,10 @@ public class UrlEncoded extends MultiMap implements Cloneable /** Decoded parameters to Map. * @param in InputSteam to read * @param map MultiMap to add parameters to - * @param maxLength maximum length of content to read 0r -1 for no limit + * @param maxLength maximum length of content to read or -1 for no limit + * @param maxLength maximum number of keys to read or -1 for no limit */ - public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength) + public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException { synchronized(map) @@ -455,6 +477,11 @@ public class UrlEncoded extends MultiMap implements Cloneable } key = null; value=null; + if (maxKeys>0 && map.size()>maxKeys) + { + LOG.warn("maxFormKeys limit exceeded keys>{}",maxKeys); + return; + } break; case '=': @@ -500,25 +527,20 @@ public class UrlEncoded extends MultiMap implements Cloneable } /* -------------------------------------------------------------- */ - public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength) throws IOException + public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException { InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16); - StringBuffer buf = new StringBuffer(); - - int c; - int length=0; - if (maxLength<0) - maxLength=Integer.MAX_VALUE; - while ((c=input.read())>0 && length++<maxLength) - buf.append((char)c); - decodeTo(buf.toString(),map,ENCODING); + StringWriter buf = new StringWriter(8192); + IO.copy(input,buf,maxLength); + + decodeTo(buf.getBuffer().toString(),map,ENCODING,maxKeys); } /* -------------------------------------------------------------- */ /** Decoded parameters to Map. * @param in the stream containing the encoded parameters */ - public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength) + public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength, int maxKeys) throws IOException { //no charset present, use the configured default @@ -527,22 +549,21 @@ public class UrlEncoded extends MultiMap implements Cloneable charset=ENCODING; } - if (StringUtil.__UTF8.equalsIgnoreCase(charset)) { - decodeUtf8To(in,map,maxLength); + decodeUtf8To(in,map,maxLength,maxKeys); return; } if (StringUtil.__ISO_8859_1.equals(charset)) { - decode88591To(in,map,maxLength); + decode88591To(in,map,maxLength,maxKeys); return; } if (StringUtil.__UTF16.equalsIgnoreCase(charset)) // Should be all 2 byte encodings { - decodeUtf16To(in,map,maxLength); + decodeUtf16To(in,map,maxLength,maxKeys); return; } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java index 5d4c22159a..42bba2b05b 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java @@ -178,7 +178,7 @@ public class URLEncodedTest { ByteArrayInputStream in = new ByteArrayInputStream("name\n=value+%30&name1=&name2&n\u00e3me3=value+3".getBytes(charsets[i][0])); MultiMap m = new MultiMap(); - UrlEncoded.decodeTo(in, m, charsets[i][1], -1); + UrlEncoded.decodeTo(in, m, charsets[i][1], -1,-1); System.err.println(m); assertEquals(i+" stream length",4,m.size()); assertEquals(i+" stream name\\n","value 0",m.getString("name\n")); @@ -192,7 +192,7 @@ public class URLEncodedTest { ByteArrayInputStream in2 = new ByteArrayInputStream ("name=%83e%83X%83g".getBytes()); MultiMap m2 = new MultiMap(); - UrlEncoded.decodeTo(in2, m2, "Shift_JIS", -1); + UrlEncoded.decodeTo(in2, m2, "Shift_JIS", -1,-1); assertEquals("stream length",1,m2.size()); assertEquals("stream name","\u30c6\u30b9\u30c8",m2.getString("name")); } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClient.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClient.java index cc5db43883..743bdc38b6 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClient.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClient.java @@ -348,8 +348,8 @@ public class WebSocketClient return holder; } - - public static final InetSocketAddress toSocketAddress(URI uri) + + public static InetSocketAddress toSocketAddress(URI uri) { String scheme = uri.getScheme(); if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme))) @@ -360,8 +360,7 @@ public class WebSocketClient if (port < 0) port = "ws".equals(scheme) ? 80 : 443; - InetSocketAddress address = new InetSocketAddress(uri.getHost(), port); - return address; + return new InetSocketAddress(uri.getHost(), port); } /* ------------------------------------------------------------ */ @@ -371,16 +370,8 @@ public class WebSocketClient { final WebSocket _websocket; final URI _uri; - final String _protocol; - final String _origin; - final MaskGen _maskGen; - final int _maxIdleTime; - final int _maxTextMessageSize; - final int _maxBinaryMessageSize; - final Map<String,String> _cookies; - final List<String> _extensions; + final WebSocketClient _client; final CountDownLatch _done = new CountDownLatch(1); - ByteChannel _channel; WebSocketConnection _connection; Throwable _exception; @@ -389,14 +380,7 @@ public class WebSocketClient { _websocket=websocket; _uri=uri; - _protocol=client._protocol; - _origin=client._origin; - _maskGen=client._maskGen; - _maxIdleTime=client._maxIdleTime; - _maxTextMessageSize=client._maxTextMessageSize; - _maxBinaryMessageSize=client._maxBinaryMessageSize; - _cookies=client._cookies; - _extensions=client._extensions; + _client=client; _channel=channel; } @@ -404,8 +388,10 @@ public class WebSocketClient { try { - connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize); - connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize); + _client.getFactory().addConnection(connection); + + connection.getConnection().setMaxTextMessageSize(_client.getMaxTextMessageSize()); + connection.getConnection().setMaxBinaryMessageSize(_client.getMaxBinaryMessageSize()); WebSocketConnection con; synchronized (this) @@ -460,12 +446,12 @@ public class WebSocketClient public Map<String,String> getCookies() { - return _cookies; + return _client.getCookies(); } public String getProtocol() { - return _protocol; + return _client.getProtocol(); } public WebSocket getWebSocket() @@ -480,17 +466,17 @@ public class WebSocketClient public int getMaxIdleTime() { - return _maxIdleTime; + return _client.getMaxIdleTime(); } public String getOrigin() { - return _origin; + return _client.getOrigin(); } public MaskGen getMaskGen() { - return _maskGen; + return _client.getMaskGen(); } @Override diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java index 35a1f39479..4b250ab1b2 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java @@ -20,8 +20,11 @@ import java.io.IOException; import java.net.ProtocolException; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; +import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; import javax.net.ssl.SSLEngine; import org.eclipse.jetty.http.HttpFields; @@ -33,6 +36,7 @@ import org.eclipse.jetty.io.Buffers; import org.eclipse.jetty.io.ByteArrayBuffer; import org.eclipse.jetty.io.ConnectedEndPoint; import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.SimpleBuffers; import org.eclipse.jetty.io.nio.AsyncConnection; import org.eclipse.jetty.io.nio.SelectChannelEndPoint; @@ -60,8 +64,8 @@ public class WebSocketClientFactory extends AggregateLifeCycle { private final static Logger __log = org.eclipse.jetty.util.log.Log.getLogger(WebSocketClientFactory.class.getName()); private final static ByteArrayBuffer __ACCEPT = new ByteArrayBuffer.CaseInsensitive("Sec-WebSocket-Accept"); - - private SslContextFactory _sslContextFactory = new SslContextFactory(); + private final Queue<WebSocketConnection> connections = new ConcurrentLinkedQueue<WebSocketConnection>(); + private final SslContextFactory _sslContextFactory = new SslContextFactory(); private final ThreadPool _threadPool; private final WebSocketClientSelector _selector; private MaskGen _maskGen; @@ -200,6 +204,12 @@ public class WebSocketClientFactory extends AggregateLifeCycle return _buffers.getBufferSize(); } + @Override + protected void doStop() throws Exception + { + closeConnections(); + } + /* ------------------------------------------------------------ */ /** * <p>Creates and returns a new instance of a {@link WebSocketClient}, configured with this @@ -231,6 +241,22 @@ public class WebSocketClientFactory extends AggregateLifeCycle return sslEngine; } + protected boolean addConnection(WebSocketConnection connection) + { + return isRunning() && connections.add(connection); + } + + protected boolean removeConnection(WebSocketConnection connection) + { + return connections.remove(connection); + } + + protected void closeConnections() + { + for (WebSocketConnection connection : connections) + connection.shutdown(); + } + /* ------------------------------------------------------------ */ /** * WebSocket Client Selector Manager @@ -457,18 +483,9 @@ public class WebSocketClientFactory extends AggregateLifeCycle } else { - Buffer header = _parser.getHeaderBuffer(); - MaskGen maskGen = _future.getMaskGen(); - WebSocketConnectionRFC6455 connection = - new WebSocketConnectionRFC6455(_future.getWebSocket(), - _endp, - _buffers, System.currentTimeMillis(), - _future.getMaxIdleTime(), - _future.getProtocol(), - null, - WebSocketConnectionRFC6455.VERSION, - maskGen); + WebSocketConnection connection = newWebSocketConnection(); + Buffer header = _parser.getHeaderBuffer(); if (header.hasContent()) connection.fillBuffersFrom(header); _buffers.returnBuffer(header); @@ -483,6 +500,21 @@ public class WebSocketClientFactory extends AggregateLifeCycle return this; } + private WebSocketConnection newWebSocketConnection() throws IOException + { + return new WebSocketClientConnection( + _future._client.getFactory(), + _future.getWebSocket(), + _endp, + _buffers, + System.currentTimeMillis(), + _future.getMaxIdleTime(), + _future.getProtocol(), + null, + WebSocketConnectionRFC6455.VERSION, + _future.getMaskGen()); + } + public void onInputShutdown() throws IOException { _endp.close(); @@ -506,4 +538,22 @@ public class WebSocketClientFactory extends AggregateLifeCycle _future.handshakeFailed(new EOFException()); } } + + private static class WebSocketClientConnection extends WebSocketConnectionRFC6455 + { + private final WebSocketClientFactory factory; + + public WebSocketClientConnection(WebSocketClientFactory factory, WebSocket webSocket, EndPoint endPoint, WebSocketBuffers buffers, long timeStamp, int maxIdleTime, String protocol, List<Extension> extensions, int draftVersion, MaskGen maskGen) throws IOException + { + super(webSocket, endPoint, buffers, timeStamp, maxIdleTime, protocol, extensions, draftVersion, maskGen); + this.factory = factory; + } + + @Override + public void onClose() + { + super.onClose(); + factory.removeConnection(this); + } + } } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java index 6ff8991062..67bfb37e65 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java @@ -24,8 +24,10 @@ import org.eclipse.jetty.io.nio.AsyncConnection; public interface WebSocketConnection extends AsyncConnection { void fillBuffersFrom(Buffer buffer); - + List<Extension> getExtensions(); - + WebSocket.Connection getConnection(); -}
\ No newline at end of file + + void shutdown(); +} diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD00.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD00.java index b634bba1f9..12ec110144 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD00.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD00.java @@ -293,6 +293,11 @@ public class WebSocketConnectionD00 extends AbstractConnection implements WebSoc } } + public void shutdown() + { + close(); + } + /* ------------------------------------------------------------ */ public void fillBuffersFrom(Buffer buffer) { @@ -389,7 +394,7 @@ public class WebSocketConnectionD00 extends AbstractConnection implements WebSoc { return _protocol; } - + protected void onFrameHandshake() { if (_websocket instanceof OnFrame) diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD06.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD06.java index f9e699fa57..c7c5f2b0a7 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD06.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD06.java @@ -274,6 +274,13 @@ public class WebSocketConnectionD06 extends AbstractConnection implements WebSoc } } + public void shutdown() + { + final WebSocket.Connection connection = _connection; + if (connection != null) + connection.close(CLOSE_SHUTDOWN, null); + } + /* ------------------------------------------------------------ */ public void fillBuffersFrom(Buffer buffer) { @@ -294,7 +301,7 @@ public class WebSocketConnectionD06 extends AbstractConnection implements WebSoc { return Collections.emptyList(); } - + protected void onFrameHandshake() { if (_onFrame!=null) @@ -302,7 +309,7 @@ public class WebSocketConnectionD06 extends AbstractConnection implements WebSoc _onFrame.onHandshake(_connection); } } - + protected void onWebSocketOpen() { _webSocket.onOpen(_connection); diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD08.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD08.java index dc342ab4e6..9b282d6f6d 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD08.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionD08.java @@ -370,6 +370,13 @@ public class WebSocketConnectionD08 extends AbstractConnection implements WebSoc } } + public void shutdown() + { + final WebSocket.Connection connection = _connection; + if (connection != null) + connection.close(CLOSE_SHUTDOWN, null); + } + /* ------------------------------------------------------------ */ public void fillBuffersFrom(Buffer buffer) { diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java index 47c45a5922..7f693751be 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java @@ -400,6 +400,13 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We } } + public void shutdown() + { + final WebSocket.Connection connection = _connection; + if (connection != null) + connection.close(CLOSE_SHUTDOWN, null); + } + /* ------------------------------------------------------------ */ public void fillBuffersFrom(Buffer buffer) { @@ -431,7 +438,7 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We /* ------------------------------------------------------------ */ private class WSFrameConnection implements WebSocket.FrameConnection { - volatile boolean _disconnecting; + private volatile boolean _disconnecting; /* ------------------------------------------------------------ */ public void sendMessage(String content) throws IOException diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java index 25f58c1ef0..28dd23f296 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java @@ -30,12 +30,12 @@ package org.eclipse.jetty.websocket; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; - +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -45,15 +45,17 @@ import org.eclipse.jetty.io.ConnectedEndPoint; import org.eclipse.jetty.server.AbstractHttpConnection; import org.eclipse.jetty.server.BlockingHttpConnection; import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** * Factory to create WebSocket connections */ -public class WebSocketFactory +public class WebSocketFactory extends AbstractLifeCycle { private static final Logger LOG = Log.getLogger(WebSocketFactory.class); + private final Queue<WebSocketServletConnection> connections = new ConcurrentLinkedQueue<WebSocketServletConnection>(); public interface Acceptor { @@ -87,7 +89,7 @@ public class WebSocketFactory private final Acceptor _acceptor; private WebSocketBuffers _buffers; private int _maxIdleTime = 300000; - private int _maxTextMessageSize = 16*1024; + private int _maxTextMessageSize = 16 * 1024; private int _maxBinaryMessageSize = -1; public WebSocketFactory(Acceptor acceptor) @@ -101,7 +103,6 @@ public class WebSocketFactory _acceptor = acceptor; } - /** * @return A modifiable map of extension name to extension class */ @@ -187,6 +188,12 @@ public class WebSocketFactory _maxBinaryMessageSize = maxBinaryMessageSize; } + @Override + protected void doStop() throws Exception + { + closeConnections(); + } + /** * Upgrade the request/response to a WebSocket Connection. * <p>This method will not normally return, but will instead throw a @@ -230,40 +237,49 @@ public class WebSocketFactory } final WebSocketServletConnection connection; - final List<Extension> extensions; switch (draft) { case -1: // unspecified draft/version case 0: // Old school draft/version - extensions=Collections.emptyList(); - connection = new WebSocketServletConnectionD00(websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol); + { + connection = new WebSocketServletConnectionD00(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol); break; + } case 1: case 2: case 3: case 4: case 5: case 6: - extensions=Collections.emptyList(); - connection = new WebSocketServletConnectionD06(websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol); + { + connection = new WebSocketServletConnectionD06(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol); break; + } case 7: case 8: - extensions= initExtensions(extensions_requested,8-WebSocketConnectionD08.OP_EXT_DATA, 16-WebSocketConnectionD08.OP_EXT_CTRL,3); - connection = new WebSocketServletConnectionD08(websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol,extensions,draft); + { + List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionD08.OP_EXT_DATA, 16 - WebSocketConnectionD08.OP_EXT_CTRL, 3); + connection = new WebSocketServletConnectionD08(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft); break; + } case WebSocketConnectionRFC6455.VERSION: // RFC 6455 Version - extensions= initExtensions(extensions_requested,8-WebSocketConnectionRFC6455.OP_EXT_DATA, 16-WebSocketConnectionRFC6455.OP_EXT_CTRL,3); - connection = new WebSocketServletConnectionRFC6455(websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol,extensions,draft); + { + List<Extension> extensions = initExtensions(extensions_requested, 8 - WebSocketConnectionRFC6455.OP_EXT_DATA, 16 - WebSocketConnectionRFC6455.OP_EXT_CTRL, 3); + connection = new WebSocketServletConnectionRFC6455(this, websocket, endp, _buffers, http.getTimeStamp(), _maxIdleTime, protocol, extensions, draft); break; + } default: - LOG.warn("Unsupported Websocket version: "+draft); + { + LOG.warn("Unsupported Websocket version: " + draft); // Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol // Using the examples as outlined - response.setHeader("Sec-WebSocket-Version","13, 8, 6, 0"); + response.setHeader("Sec-WebSocket-Version", "13, 8, 6, 0"); throw new HttpException(400, "Unsupported websocket version specification: " + draft); + } } + addConnection(connection); + // Set the defaults connection.getConnection().setMaxBinaryMessageSize(_maxBinaryMessageSize); connection.getConnection().setMaxTextMessageSize(_maxTextMessageSize); @@ -281,8 +297,6 @@ public class WebSocketFactory request.setAttribute("org.eclipse.jetty.io.Connection", connection); } - /** - */ protected String[] parseProtocols(String protocol) { if (protocol == null) @@ -296,8 +310,6 @@ public class WebSocketFactory return protocols; } - /** - */ public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -353,8 +365,6 @@ public class WebSocketFactory return false; } - /** - */ public List<Extension> initExtensions(List<String> requested,int maxDataOpcodes,int maxControlOpcodes,int maxReservedBits) { List<Extension> extensions = new ArrayList<Extension>(); @@ -386,8 +396,6 @@ public class WebSocketFactory return extensions; } - /** - */ private Extension newExtension(String name) { try @@ -403,4 +411,20 @@ public class WebSocketFactory return null; } + + protected boolean addConnection(WebSocketServletConnection connection) + { + return isRunning() && connections.add(connection); + } + + protected boolean removeConnection(WebSocketServletConnection connection) + { + return connections.remove(connection); + } + + protected void closeConnections() + { + for (WebSocketServletConnection connection : connections) + connection.shutdown(); + } } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServlet.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServlet.java index ce9edc5c8e..1e765263e6 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServlet.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServlet.java @@ -34,31 +34,33 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; /* ------------------------------------------------------------ */ /** * Servlet to upgrade connections to WebSocket - * <p> + * <p/> * The request must have the correct upgrade headers, else it is * handled as a normal servlet request. - * <p> + * <p/> * The initParameter "bufferSize" can be used to set the buffer size, * which is also the max frame byte size (default 8192). - * <p> + * <p/> * The initParameter "maxIdleTime" can be used to set the time in ms * that a websocket may be idle before closing. - * <p> + * <p/> * The initParameter "maxTextMessagesSize" can be used to set the size in characters * that a websocket may be accept before closing. - * <p> + * <p/> * The initParameter "maxBinaryMessagesSize" can be used to set the size in bytes * that a websocket may be accept before closing. - * */ @SuppressWarnings("serial") public abstract class WebSocketServlet extends HttpServlet implements WebSocketFactory.Acceptor { - WebSocketFactory _webSocketFactory; + private final Logger LOG = Log.getLogger(getClass()); + private WebSocketFactory _webSocketFactory; /* ------------------------------------------------------------ */ /** @@ -67,20 +69,32 @@ public abstract class WebSocketServlet extends HttpServlet implements WebSocketF @Override public void init() throws ServletException { - String bs=getInitParameter("bufferSize"); - _webSocketFactory = new WebSocketFactory(this,bs==null?8192:Integer.parseInt(bs)); - String max=getInitParameter("maxIdleTime"); - if (max!=null) - _webSocketFactory.setMaxIdleTime(Integer.parseInt(max)); + try + { + String bs = getInitParameter("bufferSize"); + _webSocketFactory = new WebSocketFactory(this, bs == null ? 8192 : Integer.parseInt(bs)); + _webSocketFactory.start(); - max=getInitParameter("maxTextMessageSize"); - if (max!=null) - _webSocketFactory.setMaxTextMessageSize(Integer.parseInt(max)); + String max = getInitParameter("maxIdleTime"); + if (max != null) + _webSocketFactory.setMaxIdleTime(Integer.parseInt(max)); - max=getInitParameter("maxBinaryMessageSize"); - if (max!=null) - _webSocketFactory.setMaxBinaryMessageSize(Integer.parseInt(max)); + max = getInitParameter("maxTextMessageSize"); + if (max != null) + _webSocketFactory.setMaxTextMessageSize(Integer.parseInt(max)); + max = getInitParameter("maxBinaryMessageSize"); + if (max != null) + _webSocketFactory.setMaxBinaryMessageSize(Integer.parseInt(max)); + } + catch (ServletException x) + { + throw x; + } + catch (Exception x) + { + throw new ServletException(x); + } } /* ------------------------------------------------------------ */ @@ -90,9 +104,9 @@ public abstract class WebSocketServlet extends HttpServlet implements WebSocketF @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (_webSocketFactory.acceptWebSocket(request,response) || response.isCommitted()) + if (_webSocketFactory.acceptWebSocket(request, response) || response.isCommitted()) return; - super.service(request,response); + super.service(request, response); } /* ------------------------------------------------------------ */ @@ -101,6 +115,17 @@ public abstract class WebSocketServlet extends HttpServlet implements WebSocketF return true; } - - + /* ------------------------------------------------------------ */ + @Override + public void destroy() + { + try + { + _webSocketFactory.stop(); + } + catch (Exception x) + { + LOG.ignore(x); + } + } } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD00.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD00.java index aa2a65f745..72632beddb 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD00.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD00.java @@ -25,10 +25,13 @@ import org.eclipse.jetty.util.QuotedStringTokenizer; public class WebSocketServletConnectionD00 extends WebSocketConnectionD00 implements WebSocketServletConnection { - public WebSocketServletConnectionD00(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol) + private final WebSocketFactory factory; + + public WebSocketServletConnectionD00(WebSocketFactory factory, WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol) throws IOException { super(websocket,endpoint,buffers,timestamp,maxIdleTime,protocol); + this.factory = factory; } public void handshake(HttpServletRequest request, HttpServletResponse response, String subprotocol) throws IOException @@ -70,7 +73,7 @@ public class WebSocketServletConnectionD00 extends WebSocketConnectionD00 implem { response.addHeader("Sec-WebSocket-Protocol",subprotocol); } - response.sendError(101,"WebSocket Protocol Handshake"); + response.sendError(101, "WebSocket Protocol Handshake"); } else { @@ -89,4 +92,11 @@ public class WebSocketServletConnectionD00 extends WebSocketConnectionD00 implem onWebsocketOpen(); } } + + @Override + public void onClose() + { + super.onClose(); + factory.removeConnection(this); + } } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD06.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD06.java index 741abe6967..d75e153f41 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD06.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD06.java @@ -23,10 +23,13 @@ import org.eclipse.jetty.io.EndPoint; public class WebSocketServletConnectionD06 extends WebSocketConnectionD06 implements WebSocketServletConnection { - public WebSocketServletConnectionD06(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol) + private final WebSocketFactory factory; + + public WebSocketServletConnectionD06(WebSocketFactory factory, WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol) throws IOException { super(websocket,endpoint,buffers,timestamp,maxIdleTime,protocol); + this.factory = factory; } /* ------------------------------------------------------------ */ @@ -47,4 +50,11 @@ public class WebSocketServletConnectionD06 extends WebSocketConnectionD06 implem onFrameHandshake(); onWebSocketOpen(); } + + @Override + public void onClose() + { + super.onClose(); + factory.removeConnection(this); + } } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD08.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD08.java index ba2f97eb6a..daa54334c3 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD08.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionD08.java @@ -24,16 +24,13 @@ import org.eclipse.jetty.io.EndPoint; public class WebSocketServletConnectionD08 extends WebSocketConnectionD08 implements WebSocketServletConnection { - public WebSocketServletConnectionD08(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol, - List<Extension> extensions, int draft, MaskGen maskgen) throws IOException - { - super(websocket,endpoint,buffers,timestamp,maxIdleTime,protocol,extensions,draft,maskgen); - } + private final WebSocketFactory factory; - public WebSocketServletConnectionD08(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol, + public WebSocketServletConnectionD08(WebSocketFactory factory, WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol, List<Extension> extensions, int draft) throws IOException { super(websocket,endpoint,buffers,timestamp,maxIdleTime,protocol,extensions,draft); + this.factory = factory; } /* ------------------------------------------------------------ */ @@ -59,4 +56,11 @@ public class WebSocketServletConnectionD08 extends WebSocketConnectionD08 implem onFrameHandshake(); onWebSocketOpen(); } + + @Override + public void onClose() + { + super.onClose(); + factory.removeConnection(this); + } } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionRFC6455.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionRFC6455.java index c93b41c267..b6e44fcbec 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionRFC6455.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketServletConnectionRFC6455.java @@ -24,16 +24,13 @@ import org.eclipse.jetty.io.EndPoint; public class WebSocketServletConnectionRFC6455 extends WebSocketConnectionRFC6455 implements WebSocketServletConnection { - public WebSocketServletConnectionRFC6455(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol, - List<Extension> extensions, int draft, MaskGen maskgen) throws IOException - { - super(websocket,endpoint,buffers,timestamp,maxIdleTime,protocol,extensions,draft,maskgen); - } + private final WebSocketFactory factory; - public WebSocketServletConnectionRFC6455(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol, + public WebSocketServletConnectionRFC6455(WebSocketFactory factory, WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol, List<Extension> extensions, int draft) throws IOException { super(websocket,endpoint,buffers,timestamp,maxIdleTime,protocol,extensions,draft); + this.factory = factory; } /* ------------------------------------------------------------ */ @@ -59,4 +56,11 @@ public class WebSocketServletConnectionRFC6455 extends WebSocketConnectionRFC645 onFrameHandshake(); onWebSocketOpen(); } + + @Override + public void onClose() + { + super.onClose(); + factory.removeConnection(this); + } } diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java index 2ab060ac23..571a454e0d 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java @@ -15,8 +15,6 @@ *******************************************************************************/ package org.eclipse.jetty.websocket; -import static org.hamcrest.Matchers.*; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -46,6 +44,9 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; + public class WebSocketClientTest { private WebSocketClientFactory _factory = new WebSocketClientFactory(); @@ -717,7 +718,7 @@ public class WebSocketClientTest Assert.assertThat("URI (" + uri + ").host", addr.getHostName(), is("localhost")); Assert.assertThat("URI (" + uri + ").port", addr.getPort(), is(80)); } - + @Test public void testURIWithDefaultWSSPort() throws Exception { diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageD00Test.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageD00Test.java index 0bcd6ce4b7..fd2c02cbf2 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageD00Test.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageD00Test.java @@ -25,57 +25,80 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; +import java.net.SocketException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import javax.servlet.http.HttpServletRequest; +import junit.framework.Assert; + import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.Utf8StringBuilder; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.StdErrLog; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public class WebSocketMessageD00Test { - private static Server _server; - private static Connector _connector; - private static TestWebSocket _serverWebSocket; + private static Server __server; + private static Connector __connector; + private static TestWebSocket __serverWebSocket; + private static CountDownLatch __latch; + private static AtomicInteger __textCount = new AtomicInteger(0); + @BeforeClass public static void startServer() throws Exception { - _server = new Server(); - _connector = new SelectChannelConnector(); - _server.addConnector(_connector); + __server = new Server(); + __connector = new SelectChannelConnector(); + __server.addConnector(__connector); WebSocketHandler wsHandler = new WebSocketHandler() { public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { - _serverWebSocket = new TestWebSocket(); - _serverWebSocket.onConnect=("onConnect".equals(protocol)); - return _serverWebSocket; + __serverWebSocket = new TestWebSocket(); + __serverWebSocket._onConnect=("onConnect".equals(protocol)); + __serverWebSocket._echo=("echo".equals(protocol)); + __serverWebSocket._latch=("latch".equals(protocol)); + if (__serverWebSocket._latch) + __latch=new CountDownLatch(1); + return __serverWebSocket; } }; wsHandler.setHandler(new DefaultHandler()); - _server.setHandler(wsHandler); - _server.start(); + __server.setHandler(wsHandler); + __server.start(); } @AfterClass public static void stopServer() throws Exception { - _server.stop(); - _server.join(); + __server.stop(); + __server.join(); } + @Before + public void reset() + { + __textCount.set(0); + } + @Test public void testServerSendBigStringMessage() throws Exception { - Socket socket = new Socket("localhost", _connector.getLocalPort()); + Socket socket = new Socket("localhost", __connector.getLocalPort()); OutputStream output = socket.getOutputStream(); output.write( ("GET /test HTTP/1.1\r\n" + @@ -94,7 +117,6 @@ public class WebSocketMessageD00Test InputStream input = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(input, "ISO-8859-1")); String responseLine = reader.readLine(); - System.err.println(responseLine); assertTrue(responseLine.startsWith("HTTP/1.1 101 WebSocket Protocol Handshake")); // Read until we find an empty line, which signals the end of the http response String line; @@ -102,8 +124,8 @@ public class WebSocketMessageD00Test if (line.length() == 0) break; - assertTrue(_serverWebSocket.awaitConnected(1000)); - assertNotNull(_serverWebSocket.outbound); + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.outbound); // read the hixie bytes char[] hixie=new char[16]; // should be bytes, but we know this example is all ascii @@ -125,7 +147,7 @@ public class WebSocketMessageD00Test String text = "0123456789ABCDEF"; for (int i = 0; i < 64 * 1024 / text.length(); ++i) message.append(text); - _serverWebSocket.outbound.sendMessage(message.toString()); + __serverWebSocket.outbound.sendMessage(message.toString()); // Read until we get 0xFF ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -147,7 +169,7 @@ public class WebSocketMessageD00Test @Test public void testServerSendOnConnect() throws Exception { - Socket socket = new Socket("localhost", _connector.getLocalPort()); + Socket socket = new Socket("localhost", __connector.getLocalPort()); OutputStream output = socket.getOutputStream(); output.write( ("GET /test HTTP/1.1\r\n" + @@ -200,8 +222,8 @@ public class WebSocketMessageD00Test } - assertTrue(_serverWebSocket.awaitConnected(1000)); - assertNotNull(_serverWebSocket.outbound); + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.outbound); looking_for="8jKS'y:G*Co,Wxa-"; while(true) @@ -232,17 +254,498 @@ public class WebSocketMessageD00Test assertEquals(0xff,input.read()); } + + + @Test + public void testServerEcho() throws Exception + { + // Log.getLogger("org.eclipse.jetty.websocket").setDebugEnabled(true); + + Socket socket = new Socket("localhost", __connector.getLocalPort()); + socket.setSoTimeout(1000); + OutputStream output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + output.write( + ("GET /test HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Protocol: echo\r\n" + + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + + "\r\n"+ + "^n:ds[4U").getBytes("ISO-8859-1")); + output.flush(); + + lookFor("HTTP/1.1 101 WebSocket Protocol Handshake\r\n",input); + skipTo("\r\n\r\n",input); + lookFor("8jKS'y:G*Co,Wxa-",input); + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + + + output.write(0x00); + byte[] bytes="this is an echo".getBytes(StringUtil.__ISO_8859_1); + for (int i=0;i<bytes.length;i++) + output.write(bytes[i]); + output.write(0xff); + output.flush(); + + assertEquals("00",TypeUtil.toHexString((byte)(0xff&input.read()))); + lookFor("this is an echo",input); + assertEquals(0xff,input.read()); + } + + @Test + public void testBlockedConsumer() throws Exception + { + + // Log.getLogger("org.eclipse.jetty.websocket").setDebugEnabled(true); + + Socket socket = new Socket("localhost", __connector.getLocalPort()); + socket.setSoTimeout(60000); + OutputStream output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + output.write( + ("GET /test HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Protocol: latch\r\n" + + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + + "\r\n"+ + "^n:ds[4U").getBytes("ISO-8859-1")); + output.flush(); + + lookFor("HTTP/1.1 101 WebSocket Protocol Handshake\r\n",input); + skipTo("\r\n\r\n",input); + lookFor("8jKS'y:G*Co,Wxa-",input); + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + __serverWebSocket.connection.setMaxIdleTime(60000); + + + byte[] bytes="This is a long message of text that we will send again and again".getBytes(StringUtil.__ISO_8859_1); + byte[] mesg=new byte[bytes.length+2]; + mesg[0]=(byte)0x00; + for (int i=0;i<bytes.length;i++) + mesg[i+1]=(byte)(bytes[i]); + mesg[mesg.length-1]=(byte)0xFF; + + final int count = 100000; + + + // Send and receive 1 message + output.write(mesg); + output.flush(); + while(__textCount.get()==0) + Thread.sleep(10); + + // unblock the latch in 4s + new Thread() + { + @Override + public void run() + { + try + { + Thread.sleep(4000); + __latch.countDown(); + //System.err.println("latched"); + } + catch(Exception e) + { + e.printStackTrace(); + } + } + }.start(); + + // Send enough messages to fill receive buffer + long max=0; + long start=System.currentTimeMillis(); + for (int i=0;i<count;i++) + { + output.write(mesg); + if (i%100==0) + { + // System.err.println(">>> "+i); + output.flush(); + + long now=System.currentTimeMillis(); + long duration=now-start; + start=now; + if (max<duration) + max=duration; + } + } + + Thread.sleep(50); + while(__textCount.get()<count+1) + { + System.err.println(__textCount.get()+"<"+(count+1)); + Thread.sleep(10); + } + assertEquals(count+1,__textCount.get()); // all messages + assertTrue(max>2000); // was blocked + } + + @Test + public void testBlockedProducer() throws Exception + { + // Log.getLogger("org.eclipse.jetty.websocket").setDebugEnabled(true); + + final Socket socket = new Socket("localhost", __connector.getLocalPort()); + socket.setSoTimeout(60000); + OutputStream output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + output.write( + ("GET /test HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Protocol: latch\r\n" + + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + + "\r\n"+ + "^n:ds[4U").getBytes("ISO-8859-1")); + output.flush(); + + lookFor("HTTP/1.1 101 WebSocket Protocol Handshake\r\n",input); + skipTo("\r\n\r\n",input); + lookFor("8jKS'y:G*Co,Wxa-",input); + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + + + final int count = 100000; + + __serverWebSocket.connection.setMaxIdleTime(60000); + __latch.countDown(); - private static class TestWebSocket implements WebSocket + // wait 2s and then consume messages + final AtomicLong totalB=new AtomicLong(); + new Thread() + { + @Override + public void run() + { + try + { + Thread.sleep(2000); + + byte[] recv = new byte[32*1024]; + + int len=0; + while (len>=0) + { + totalB.addAndGet(len); + len=socket.getInputStream().read(recv,0,recv.length); + Thread.sleep(10); + } + } + catch(Exception e) + { + e.printStackTrace(); + } + } + }.start(); + + + // Send enough messages to fill receive buffer + long max=0; + long start=System.currentTimeMillis(); + String mesg="How Now Brown Cow"; + for (int i=0;i<count;i++) + { + __serverWebSocket.connection.sendMessage(mesg); + if (i%100==0) + { + output.flush(); + + long now=System.currentTimeMillis(); + long duration=now-start; + start=now; + if (max<duration) + max=duration; + } + } + + while(totalB.get()<(count*(mesg.length()+2))) + Thread.sleep(100); + + assertEquals(count*(mesg.length()+2),totalB.get()); // all messages + assertTrue(max>1000); // was blocked + } + + + + @Test + public void testIdle() throws Exception + { + // Log.getLogger("org.eclipse.jetty.websocket").setDebugEnabled(true); + + final Socket socket = new Socket("localhost", __connector.getLocalPort()); + socket.setSoTimeout(10000); + OutputStream output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + output.write( + ("GET /test HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Protocol: onConnect\r\n" + + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + + "\r\n"+ + "^n:ds[4U").getBytes("ISO-8859-1")); + output.flush(); + + lookFor("HTTP/1.1 101 WebSocket Protocol Handshake\r\n",input); + skipTo("\r\n\r\n",input); + lookFor("8jKS'y:G*Co,Wxa-",input); + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + __serverWebSocket.connection.setMaxIdleTime(250); + assertEquals(0x00,input.read()); + lookFor("sent on connect",input); + assertEquals(0xff,input.read()); + + assertEquals(-1,input.read()); + socket.close(); + + assertTrue(__serverWebSocket.awaitDisconnected(100)); + } + + @Test + public void testIdleBadClient() throws Exception { - boolean onConnect=false; - private final CountDownLatch latch = new CountDownLatch(1); + // Log.getLogger("org.eclipse.jetty.websocket").setDebugEnabled(true); + + final Socket socket = new Socket("localhost", __connector.getLocalPort()); + socket.setSoTimeout(10000); + OutputStream output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + output.write( + ("GET /test HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Protocol: onConnect\r\n" + + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + + "\r\n"+ + "^n:ds[4U").getBytes("ISO-8859-1")); + output.flush(); + + lookFor("HTTP/1.1 101 WebSocket Protocol Handshake\r\n",input); + skipTo("\r\n\r\n",input); + lookFor("8jKS'y:G*Co,Wxa-",input); + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + __serverWebSocket.connection.setMaxIdleTime(250); + assertEquals(0x00,input.read()); + lookFor("sent on connect",input); + assertEquals(0xff,input.read()); + + assertEquals(-1,input.read()); + + assertTrue(__serverWebSocket.disconnected.getCount()>0); + assertTrue(__serverWebSocket.awaitDisconnected(1000)); + } + + @Test + public void testTCPClose() throws Exception + { + // Log.getLogger("org.eclipse.jetty.websocket").setDebugEnabled(true); + + final Socket socket = new Socket("localhost", __connector.getLocalPort()); + socket.setSoTimeout(10000); + OutputStream output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + output.write( + ("GET /test HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Protocol: onConnect\r\n" + + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + + "\r\n"+ + "^n:ds[4U").getBytes("ISO-8859-1")); + output.flush(); + + lookFor("HTTP/1.1 101 WebSocket Protocol Handshake\r\n",input); + skipTo("\r\n\r\n",input); + lookFor("8jKS'y:G*Co,Wxa-",input); + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + __serverWebSocket.connection.setMaxIdleTime(250); + assertEquals(0x00,input.read()); + lookFor("sent on connect",input); + assertEquals(0xff,input.read()); + + + + socket.close(); + + assertTrue(__serverWebSocket.awaitDisconnected(500)); + + try + { + __serverWebSocket.connection.sendMessage("Don't send"); + assertTrue(false); + } + catch(IOException e) + { + assertTrue(true); + } + } + + @Test + public void testTCPHalfClose() throws Exception + { + // Log.getLogger("org.eclipse.jetty.websocket").setDebugEnabled(true); + + final Socket socket = new Socket("localhost", __connector.getLocalPort()); + socket.setSoTimeout(10000); + OutputStream output = socket.getOutputStream(); + InputStream input = socket.getInputStream(); + output.write( + ("GET /test HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Protocol: onConnect\r\n" + + "Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\n" + + "Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\n" + + "\r\n"+ + "^n:ds[4U").getBytes("ISO-8859-1")); + output.flush(); + + lookFor("HTTP/1.1 101 WebSocket Protocol Handshake\r\n",input); + skipTo("\r\n\r\n",input); + lookFor("8jKS'y:G*Co,Wxa-",input); + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + __serverWebSocket.connection.setMaxIdleTime(250); + assertEquals(0x00,input.read()); + lookFor("sent on connect",input); + assertEquals(0xff,input.read()); + + + socket.shutdownOutput(); + + assertTrue(__serverWebSocket.awaitDisconnected(500)); + + assertEquals(-1,input.read()); + + // look for broken pipe + try + { + for (int i=0;i<1000;i++) + output.write(0); + Assert.fail(); + } + catch(SocketException e) + { + // expected + } + } + + + + + + + + + + + + + + private void lookFor(String string,InputStream in) + throws IOException + { + String orig=string; + Utf8StringBuilder scanned=new Utf8StringBuilder(); + try + { + while(true) + { + int b = in.read(); + if (b<0) + throw new EOFException(); + scanned.append((byte)b); + assertEquals("looking for\""+orig+"\" in '"+scanned+"'",(int)string.charAt(0),b); + if (string.length()==1) + break; + string=string.substring(1); + } + } + catch(IOException e) + { + System.err.println("IOE while looking for \""+orig+"\" in '"+scanned+"'"); + throw e; + } + } + + private void skipTo(String string,InputStream in) + throws IOException + { + int state=0; + + while(true) + { + int b = in.read(); + if (b<0) + throw new EOFException(); + + if (b==string.charAt(state)) + { + state++; + if (state==string.length()) + break; + } + else + state=0; + } + } + + + + private static class TestWebSocket implements WebSocket.OnFrame, WebSocket, WebSocket.OnTextMessage + { + protected boolean _latch; + boolean _echo=true; + boolean _onConnect=false; private volatile Connection outbound; + private final CountDownLatch connected = new CountDownLatch(1); + private final CountDownLatch disconnected = new CountDownLatch(1); + private volatile FrameConnection connection; + public FrameConnection getConnection() + { + return connection; + } + + public void onHandshake(FrameConnection connection) + { + this.connection = connection; + } + public void onOpen(Connection outbound) { this.outbound = outbound; - if (onConnect) + if (_onConnect) { try { @@ -253,16 +756,55 @@ public class WebSocketMessageD00Test e.printStackTrace(); } } - latch.countDown(); + connected.countDown(); } private boolean awaitConnected(long time) throws InterruptedException { - return latch.await(time, TimeUnit.MILLISECONDS); + return connected.await(time, TimeUnit.MILLISECONDS); + } + + private boolean awaitDisconnected(long time) throws InterruptedException + { + return disconnected.await(time, TimeUnit.MILLISECONDS); } public void onClose(int code,String message) { + disconnected.countDown(); + } + + public boolean onFrame(byte flags, byte opcode, byte[] data, int offset, int length) + { + return true; + } + + public void onMessage(String data) + { + __textCount.incrementAndGet(); + if (_latch) + { + try + { + __latch.await(); + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + if (_echo) + { + try + { + outbound.sendMessage(data); + } + catch (IOException e) + { + e.printStackTrace(); + } + } } } } diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java index 113fc83012..6c017e9317 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; +import java.net.SocketException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -32,6 +33,8 @@ import java.util.zip.Inflater; import javax.servlet.http.HttpServletRequest; +import junit.framework.Assert; + import org.eclipse.jetty.io.Buffer; import org.eclipse.jetty.io.ByteArrayEndPoint; import org.eclipse.jetty.server.Connector; @@ -1314,7 +1317,7 @@ public class WebSocketMessageRFC6455Test } @Test - public void testClose() throws Exception + public void testTCPClose() throws Exception { Socket socket = new Socket("localhost", __connector.getLocalPort()); OutputStream output = socket.getOutputStream(); @@ -1350,7 +1353,6 @@ public class WebSocketMessageRFC6455Test socket.close(); assertTrue(__serverWebSocket.awaitDisconnected(500)); - try { @@ -1362,6 +1364,64 @@ public class WebSocketMessageRFC6455Test assertTrue(true); } } + + @Test + public void testTCPHalfClose() throws Exception + { + Socket socket = new Socket("localhost", __connector.getLocalPort()); + OutputStream output = socket.getOutputStream(); + output.write( + ("GET /chat HTTP/1.1\r\n"+ + "Host: server.example.com\r\n"+ + "Upgrade: websocket\r\n"+ + "Connection: Upgrade\r\n"+ + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"+ + "Sec-WebSocket-Origin: http://example.com\r\n"+ + "Sec-WebSocket-Protocol: onConnect\r\n" + + "Sec-WebSocket-Version: "+WebSocketConnectionRFC6455.VERSION+"\r\n"+ + "\r\n").getBytes("ISO-8859-1")); + output.flush(); + + // Make sure the read times out if there are problems with the implementation + socket.setSoTimeout(1000); + + InputStream input = socket.getInputStream(); + + lookFor("HTTP/1.1 101 Switching Protocols\r\n",input); + skipTo("Sec-WebSocket-Accept: ",input); + lookFor("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",input); + skipTo("\r\n\r\n",input); + + + assertTrue(__serverWebSocket.awaitConnected(1000)); + assertNotNull(__serverWebSocket.connection); + + assertEquals(0x81,input.read()); + assertEquals(0x0f,input.read()); + lookFor("sent on connect",input); + + socket.shutdownOutput(); + + assertTrue(__serverWebSocket.awaitDisconnected(500)); + + assertEquals(0x88,input.read()); + assertEquals(0x00,input.read()); + assertEquals(-1,input.read()); + + // look for broken pipe + try + { + for (int i=0;i<1000;i++) + output.write(0); + Assert.fail(); + } + catch(SocketException e) + { + // expected + } + } + + @Test public void testParserAndGenerator() throws Exception diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketRedeployTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketRedeployTest.java new file mode 100644 index 0000000000..39f218085d --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketRedeployTest.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2011 Intalio, Inc. + * ====================================================================== + * 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.websocket; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +public class WebSocketRedeployTest +{ + private Server server; + private ServletContextHandler context; + private String uri; + private WebSocketClientFactory wsFactory; + + public void init(final WebSocket webSocket) throws Exception + { + server = new Server(); + SelectChannelConnector connector = new SelectChannelConnector(); +// connector.setPort(8080); + server.addConnector(connector); + + HandlerCollection handlers = new HandlerCollection(); + server.setHandler(handlers); + + String contextPath = "/test_context"; + context = new ServletContextHandler(handlers, contextPath, ServletContextHandler.SESSIONS); + + WebSocketServlet servlet = new WebSocketServlet() + { + public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) + { + return webSocket; + } + }; + String servletPath = "/websocket"; + context.addServlet(new ServletHolder(servlet), servletPath); + + server.start(); + + uri = "ws://localhost:" + connector.getLocalPort() + contextPath + servletPath; + + wsFactory = new WebSocketClientFactory(); + wsFactory.start(); + } + + @After + public void destroy() throws Exception + { + if (wsFactory != null) + wsFactory.stop(); + if (server != null) + { + server.stop(); + server.join(); + } + } + + @Test + public void testStoppingContextClosesConnections() throws Exception + { + final CountDownLatch openLatch = new CountDownLatch(2); + final CountDownLatch closeLatch = new CountDownLatch(2); + init(new WebSocket.OnTextMessage() + { + public void onOpen(Connection connection) + { + openLatch.countDown(); + } + + public void onMessage(String data) + { + } + + public void onClose(int closeCode, String message) + { + closeLatch.countDown(); + } + }); + + WebSocketClient client = wsFactory.newWebSocketClient(); + client.open(new URI(uri), new WebSocket.OnTextMessage() + { + public void onOpen(Connection connection) + { + openLatch.countDown(); + } + + public void onMessage(String data) + { + } + + public void onClose(int closeCode, String message) + { + closeLatch.countDown(); + } + }, 5, TimeUnit.SECONDS); + + Assert.assertTrue(openLatch.await(5, TimeUnit.SECONDS)); + + context.stop(); + + Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testStoppingClientFactoryClosesConnections() throws Exception + { + final CountDownLatch openLatch = new CountDownLatch(2); + final CountDownLatch closeLatch = new CountDownLatch(2); + init(new WebSocket.OnTextMessage() + { + public void onOpen(Connection connection) + { + openLatch.countDown(); + } + + public void onMessage(String data) + { + } + + public void onClose(int closeCode, String message) + { + closeLatch.countDown(); + } + }); + + WebSocketClient client = wsFactory.newWebSocketClient(); + client.open(new URI(uri), new WebSocket.OnTextMessage() + { + public void onOpen(Connection connection) + { + openLatch.countDown(); + } + + public void onMessage(String data) + { + } + + public void onClose(int closeCode, String message) + { + closeLatch.countDown(); + } + }, 5, TimeUnit.SECONDS); + + Assert.assertTrue(openLatch.await(5, TimeUnit.SECONDS)); + + wsFactory.stop(); + + Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + } +} 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 6da0355acf..d5f4df3915 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 @@ -110,20 +110,22 @@ public class XmlConfiguration __parser = new XmlParser(); try { - URL configURL = Loader.getResource(XmlConfiguration.class,"org/eclipse/jetty/xml/configure_6_0.dtd",true); - __parser.redirectEntity("configure.dtd",configURL); - __parser.redirectEntity("configure_1_0.dtd",configURL); - __parser.redirectEntity("configure_1_1.dtd",configURL); - __parser.redirectEntity("configure_1_2.dtd",configURL); - __parser.redirectEntity("configure_1_3.dtd",configURL); - __parser.redirectEntity("configure_6_0.dtd",configURL); - - __parser.redirectEntity("http://jetty.mortbay.org/configure.dtd",configURL); - __parser.redirectEntity("http://jetty.eclipse.org/configure.dtd",configURL); - __parser.redirectEntity("http://www.eclipse.org/jetty/configure.dtd",configURL); - - __parser.redirectEntity("-//Mort Bay Consulting//DTD Configure//EN",configURL); - __parser.redirectEntity("-//Jetty//Configure//EN",configURL); + URL config60 = Loader.getResource(XmlConfiguration.class,"org/eclipse/jetty/xml/configure_6_0.dtd",true); + URL config71 = Loader.getResource(XmlConfiguration.class,"org/eclipse/jetty/xml/configure_7_1.dtd",true); + __parser.redirectEntity("configure.dtd",config71); + __parser.redirectEntity("configure_1_0.dtd",config60); + __parser.redirectEntity("configure_1_1.dtd",config60); + __parser.redirectEntity("configure_1_2.dtd",config60); + __parser.redirectEntity("configure_1_3.dtd",config60); + __parser.redirectEntity("configure_6_0.dtd",config60); + __parser.redirectEntity("configure_7_1.dtd",config71); + + __parser.redirectEntity("http://jetty.mortbay.org/configure.dtd",config71); + __parser.redirectEntity("http://jetty.eclipse.org/configure.dtd",config71); + __parser.redirectEntity("http://www.eclipse.org/jetty/configure.dtd",config71); + + __parser.redirectEntity("-//Mort Bay Consulting//DTD Configure//EN",config71); + __parser.redirectEntity("-//Jetty//Configure//EN",config71); } catch (ClassNotFoundException e) { @@ -902,7 +904,7 @@ public class XmlConfiguration /* ------------------------------------------------------------ */ /* - * Create a new value object. + * Get a Property. * * @param obj @param node @return @exception Exception */ @@ -922,6 +924,7 @@ public class XmlConfiguration configure(prop,node,0); return prop; } + /* ------------------------------------------------------------ */ /* @@ -1091,6 +1094,14 @@ public class XmlConfiguration String defaultValue = node.getAttribute("default"); return System.getProperty(name,defaultValue); } + + if ("Env".equals(tag)) + { + String name = node.getAttribute("name"); + String defaultValue = node.getAttribute("default"); + String value=System.getenv(name); + return value==null?defaultValue:value; + } LOG.warn("Unknown value tag: " + node,new Throwable()); return null; diff --git a/jetty-xml/src/main/resources/org/eclipse/jetty/xml/configure_6_0.dtd b/jetty-xml/src/main/resources/org/eclipse/jetty/xml/configure_6_0.dtd index f79d4b7f00..e2139c162e 100644 --- a/jetty-xml/src/main/resources/org/eclipse/jetty/xml/configure_6_0.dtd +++ b/jetty-xml/src/main/resources/org/eclipse/jetty/xml/configure_6_0.dtd @@ -19,7 +19,7 @@ my be specified if a match is not achieved. --> <!ENTITY % CONFIG "Set|Get|Put|Call|New|Ref|Array|Map|Property"> -<!ENTITY % VALUE "#PCDATA|Get|Call|New|Ref|Array|Map|SystemProperty|Property"> +<!ENTITY % VALUE "#PCDATA|Get|Call|New|Ref|Array|Map|SystemProperty|Env|Property"> <!ENTITY % TYPEATTR "type CDATA #IMPLIED " > <!-- String|Character|Short|Byte|Integer|Long|Boolean|Float|Double|char|short|byte|int|long|boolean|float|double|URL|InetAddress|InetAddrPort| #classname --> <!ENTITY % IMPLIEDCLASSATTR "class NMTOKEN #IMPLIED" > @@ -245,6 +245,24 @@ This is equivalent to: <!ELEMENT SystemProperty EMPTY> <!ATTLIST SystemProperty %NAMEATTR; %DEFAULTATTR; %IDATTR;> +<!-- +Environment variable Element. +This element allows OS Environment variables to be retrieved as +part of the value of elements such as Set, Put, Arg, etc. +The name attribute specifies the env variable name and the optional +default argument provides a default value. + + <Env name="Test" default="value" /> + +This is equivalent to: + + String v=System.getEnv("Test"); + if (v==null) v="value"; + +--> +<!ELEMENT Env EMPTY> +<!ATTLIST Env %NAMEATTR; %DEFAULTATTR; %IDATTR;> + <!-- Property Element. diff --git a/jetty-xml/src/main/resources/org/eclipse/jetty/xml/configure_7_6.dtd b/jetty-xml/src/main/resources/org/eclipse/jetty/xml/configure_7_6.dtd new file mode 100644 index 0000000000..f79d4b7f00 --- /dev/null +++ b/jetty-xml/src/main/resources/org/eclipse/jetty/xml/configure_7_6.dtd @@ -0,0 +1,265 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<!-- +This is the document type descriptor for the +org.eclipse.XmlConfiguration class. It allows a java object to be +configured by with a sequence of Set, Put and Call elements. These tags are +mapped to methods on the object to be configured as follows: + + <Set name="Test">value</Set> == obj.setTest("value"); + <Put name="Test">value</Put> == obj.put("Test","value"); + <Call name="test"><Arg>value</Arg></Call> == obj.test("value"); + +Values themselves may be configured objects that are created with the +<New> tag or returned from a <Call> tag. + +Values are matched to arguments on a best effort approach, but types +my be specified if a match is not achieved. + +--> + +<!ENTITY % CONFIG "Set|Get|Put|Call|New|Ref|Array|Map|Property"> +<!ENTITY % VALUE "#PCDATA|Get|Call|New|Ref|Array|Map|SystemProperty|Property"> + +<!ENTITY % TYPEATTR "type CDATA #IMPLIED " > <!-- String|Character|Short|Byte|Integer|Long|Boolean|Float|Double|char|short|byte|int|long|boolean|float|double|URL|InetAddress|InetAddrPort| #classname --> +<!ENTITY % IMPLIEDCLASSATTR "class NMTOKEN #IMPLIED" > +<!ENTITY % CLASSATTR "class NMTOKEN #REQUIRED" > +<!ENTITY % NAMEATTR "name NMTOKEN #REQUIRED" > +<!ENTITY % IMPLIEDNAMEATTR "name NMTOKEN #IMPLIED" > +<!ENTITY % DEFAULTATTR "default CDATA #IMPLIED" > +<!ENTITY % IDATTR "id NMTOKEN #IMPLIED" > +<!ENTITY % REQUIREDIDATTR "id NMTOKEN #REQUIRED" > + + +<!-- +Configure Element. +This is the root element that specifies the class of object that +can be configured: + + <Configure class="com.acme.MyClass"> ... </Configure> +--> +<!ELEMENT Configure (%CONFIG;)* > +<!ATTLIST Configure %IMPLIEDCLASSATTR; %IDATTR; > + + +<!-- +Set Element. +This element maps to a call to a setter method or field on the current object. +The name and optional type attributes are used to select the setter +method. If the name given is xxx, then a setXxx method is used, or +the xxx field is used of setXxx cannot be found. +A Set element can contain value text and/or the value objects returned +by other elements such as Call, New, SystemProperty, etc. +If no value type is specified, then white +space is trimmed out of the value. If it contains multiple value +elements they are added as strings before being converted to any +specified type. + +A Set with a class attribute is treated as a static set method invocation. +--> +<!ELEMENT Set ( %VALUE; )* > +<!ATTLIST Set %NAMEATTR; %TYPEATTR; %IMPLIEDCLASSATTR; > + + +<!-- +Get Element. +This element maps to a call to a getter method or field on the current object. +The name attribute is used to select the get method. +If the name given is xxx, then a getXxx method is used, or +the xxx field is used if getXxx cannot be found. +A Get element can contain other elements such as Set, Put, Call, etc. +which act on the object returned by the get call. + +A Get with a class attribute is treated as a static get method or field. +--> +<!ELEMENT Get (%CONFIG;)*> +<!ATTLIST Get %NAMEATTR; %IMPLIEDCLASSATTR; %IDATTR; > + + +<!-- +Put Element. +This element maps to a call to a put method on the current object, +which must implement the Map interface. The name attribute is used +as the put key and the optional type attribute can force the type +of the value. + +A Put element can contain value text and/or value elements such as Call, +New, SystemProperty, etc. If no value type is specified, then white +space is trimmed out of the value. If it contains multiple value +elements they are added as strings before being converted to any +specified type. +--> +<!ELEMENT Put ( %VALUE; )* > +<!ATTLIST Put %NAMEATTR; %TYPEATTR; > + + +<!-- +Call Element. +This element maps to an arbitrary call to a method on the current object, +The name attribute and Arg elements are used to select the method. + +A Call element can contain a sequence of Arg elements followed by +a sequence of other elements such as Set, Put, Call, etc. which act on any object +returned by the original call: + + <Call id="o2" name="test"> + <Arg>value1</Arg> + <Set name="Test">Value2</Set> + </Call> + +This is equivalent to: + + Object o2 = o1.test("value1"); + o2.setTest("value2"); + +A Call with a class attribute is treated as a static call. +--> +<!ELEMENT Call (Arg*,(%CONFIG;)*)> +<!ATTLIST Call %NAMEATTR; %IMPLIEDCLASSATTR; %IDATTR;> + + +<!-- +Arg Element. +This element defines a positional argument for the Call element. +The optional type attribute can force the type of the value. + +An Arg element can contain value text and/or value elements such as Call, +New, SystemProperty, etc. If no value type is specified, then white +space is trimmed out of the value. If it contains multiple value +elements they are added as strings before being converted to any +specified type. +--> +<!ELEMENT Arg ( %VALUE; )* > +<!ATTLIST Arg %TYPEATTR; %IMPLIEDNAMEATTR; > + + +<!-- +New Element. +This element allows the creation of a new object as part of a +value for elements such as Set, Put, Arg, etc. The class attribute determines +the type of the new object and the contained Arg elements +are used to select the constructor for the new object. + +A New element can contain a sequence of Arg elements followed by +a sequence of elements such as Set, Put, Call, etc. elements +which act on the new object: + + <New id="o" class="com.acme.MyClass"> + <Arg>value1</Arg> + <Set name="test">Value2</Set> + </New> + +This is equivalent to: + + Object o = new com.acme.MyClass("value1"); + o.setTest("Value2"); +--> +<!ELEMENT New (Arg*,(%CONFIG;)*)> +<!ATTLIST New %CLASSATTR; %IDATTR;> + + +<!-- +Ref Element. +This element allows a previously created object to be referenced by id. +A Ref element can contain a sequence of elements such as Set, Put, Call, etc. +which act on the referenced object: + + <Ref id="myobject"> + <Set name="Test">Value2</Set> + </New> +--> +<!ELEMENT Ref ((%CONFIG;)*)> +<!ATTLIST Ref %REQUIREDIDATTR;> + + +<!-- +Array Element. +This element allows the creation of a new array as part of a +value of elements such as Set, Put, Arg, etc. The type attribute determines +the type of the new array and the contained Item elements +are used for each element of the array: + + <Array type="java.lang.String"> + <Item>value0</Item> + <Item><New class="java.lang.String"><Arg>value1</Arg></New></Item> + </Array> + +This is equivalent to: + String[] a = new String[] { "value0", new String("value1") }; +--> +<!ELEMENT Array (Item*)> +<!ATTLIST Array %TYPEATTR; %IDATTR; > + + +<!-- +Map Element. +This element allows the creation of a new map as part of a +value of elements such as Set, Put, Arg, etc. The type attribute determines +the type of the new array and the contained Item elements +are used for each element of the array: + + <Map> + <Entry> + <Item>keyName</Item> + <Item><New class="java.lang.String"><Arg>value1</Arg></New></Item> + </Entry> + </Map> + +This is equivalent to: + Map m = new HashMap(); + m.put("keyName", new String("value1")); +--> +<!ELEMENT Map (Entry*)> +<!ATTLIST Map %IDATTR; > +<!ELEMENT Entry (Item,Item)> + + +<!-- +Item Element. +This element defines an entry for the Array or Map Entry elements. +The optional type attribute can force the type of the value. + +An Item element can contain value text and/or the value object of +elements such as Call, New, SystemProperty, etc. If no value type +is specified, then white space is trimmed out of the value. +If it contains multiple value elements they are added as strings +before being converted to any specified type. +--> +<!ELEMENT Item ( %VALUE; )* > +<!ATTLIST Item %TYPEATTR; %IDATTR; > + + +<!-- +System Property Element. +This element allows JVM System properties to be retrieved as +part of the value of elements such as Set, Put, Arg, etc. +The name attribute specifies the property name and the optional +default argument provides a default value. + + <SystemProperty name="Test" default="value" /> + +This is equivalent to: + + System.getProperty("Test","value"); +--> +<!ELEMENT SystemProperty EMPTY> +<!ATTLIST SystemProperty %NAMEATTR; %DEFAULTATTR; %IDATTR;> + + +<!-- +Property Element. +This element allows arbitrary properties to be retrieved by name. +The name attribute specifies the property name and the optional +default argument provides a default value. + +A Property element can contain a sequence of elements such as Set, Put, Call, etc. +which act on the retrieved object: + + <Property name="Server"> + <Call id="jdbcIdMgr" name="getAttribute"> + <Arg>jdbcIdMgr</Arg> + </Call> + </Property> +--> +<!ELEMENT Property ((%CONFIG;)*)> +<!ATTLIST Property %NAMEATTR; %DEFAULTATTR; %IDATTR;> 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 6608b1639c..700d750880 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 @@ -63,6 +63,8 @@ public class XmlConfigurationTest assertEquals( "ObjectsWhiteString", "-1\n String",tc.get("ObjectsWhiteString")); assertEquals( "SystemProperty", System.getProperty("user.dir")+"/stuff",tc.get("SystemProperty")); + assertEquals( "Env", System.getenv("HOME"),tc.get("Env")); + assertEquals( "Property", "xxx", tc.get("Property")); diff --git a/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configure.xml b/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configure.xml index ec26b3ea94..fd0316bff6 100644 --- a/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configure.xml +++ b/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configure.xml @@ -81,6 +81,7 @@ <Put name="SomethingElse"><SystemProperty name="floople" default="xxx"/></Put> <Put name="Boolean" type="Boolean">True</Put> <Put name="Float" type="Float">2.3</Put> + <Put name="Env"><Env name="HOME"/></Put> <Call name="call"> </Call> @@ -311,6 +311,7 @@ </plugin> </plugins> </reporting> +<!-- <repositories> <repository> <snapshots> @@ -321,6 +322,7 @@ <url>http://oss.sonatype.org/content/groups/jetty</url> </repository> </repositories> +--> <modules> <module>jetty-util</module> <module>jetty-jmx</module> |