diff --git a/VERSION.txt b/VERSION.txt index b0306ee3b47..b5c40ca2bc7 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1,228 +1,6 @@ jetty-10.0.0-SNAPSHOT -jetty-10.0.0.alpha1 - 03 October 2019 - + 113 Add support for NCSA Extended Log File Format - + 114 Bring back overlay deployer - + 132 ClientConnector abstraction - + 207 Support javax.websocket version 1.1 - + 215 Add Conscrypt for native ALPN/TLS/SSL - + 250 Implement HTTP CONNECT for HTTP/2 - + 300 Implement Deflater / Inflater Object Pool - + 482 [jetty-osgi] The CCL while parsing the xml files should be set to a - combination of Jetty and Bundle-Classloader - + 592 Support no-value Host header in HttpParser - + 675 Slf4jLog.ignore() should produce at DEBUG level - + 676 JavaUtilLog.ignore() should produce at DEBUG level - + 677 Logging of .ignore() should indicate that it was an "Ignored Exception" - + 746 Implement Servlet 4.0 Request.getMappings - + 801 Jetty respond with status 200 instead of 304 while using Servlet 4.0 - PushBuilder - + 809 NPE in WebInfConfiguration for webapp deploy in osgi - + 987 Can GzipHandler check if .gz file exists only by some paths? - + 1135 Avoid allocations from Method.getParameterTypes() if possible - + 1200 Use PathWatcher in DeploymentManager - + 1350 Dynamic selection of the transport to use based on ALPN on the client - side - + 1368 Need to support KeyStore/TrustStore with null passwords - + 1384 Expose StatisticsServlet to webapp - + 1468 Configure PKIX Revocation Checker for SslContextFactory - + 1485 Add systemd service file - + 1498 Add JRTResource to support future Java 9 classloader behaviors - + 1499 ClasspathPattern needs MODULE ruleset to support future Java 9 - classloader behaviors - + 1503 IPv6 address needs normalization (without brackets) in - ForwardedRequestCustomizer - + 1551 Move CookieCutter to jetty-http - + 1571 Support Hazelcast session management in 9.4 - + 1591 JDBCSessionDataStore doesn't work with root context on Oracle DB - + 1592 CompressedContentFormat.tagEquals() - incorrect comparison of entity - tag hashes - + 1595 HTTP/2: Avoid sending unnecessary stream WINDOW_UPDATE frames - + 1599 WebSocketCloseTest fails - + 1600 Update jndi.mod and plus.mod - + 1603 WebSocketServerFactory NPE in toString() - + 1604 WebSocketContainer stop needs improvement - + 1605 ContainerProvider.getWebSocketContainer() behavior is not to spec - + 1615 Password defaults in jetty-ssl-context.xml should be removed - + 1618 AsyncContext.dispatch() does not use raw/encoded URI - + 1622 HeaderFilter doesn't work if the response has been committed - + 1623 JettyRunMojo use dependencies from reactor (outputdirectory) - + 1625 Support new IANA declared Websocket Close Status Codes - + 1637 Thread per connection retained in HTTP/2 - + 1642 Using RewriteHandler with AsyncContext.dispatch() and - HttpServletRequestWrapper not possible - + 1643 ProxyServlet always uses default number of selector threads - - constructor should allow to overwrite the default. - + 1645 NotSerializableException: DoSFilter when using Non-Clustered Session - Management: File System - + 1656 Improve configurability of ConnectionPools - + 1671 Asymmetric usage of trailers in MetaData.Request - + 1675 Session id should not be logged with INFO level in AbstractSessionCache - + 1676 Remove Deprecated classes & methods - + 1679 DeploymentManagerMBean not usable through JMX - + 1682 Jetty-WarFragmentFolderPath directive has no effect in eclipse runtime - mode except for the first launch - + 1692 Annotation scanning should ignore `module-info.class` files - + 1698 Missing WWW-Authenticate from SpnegoAuthenticator when other - Authorization header provided - + 1746 Remove LICENSE-CONTRIBUTOR? - + 1836 Migrate Locker implementation to JVM ReentrantLock implementation - + 1838 Servlet 4.0.0 artifact now available on central.maven.org - + 1852 Fix quickstart generation for servlet 4.0 - + 1898 Request.getCookie() should ignore invalid cookies - + 1956 Store and report build information of Jetty - + 2075 Deprecating MultiException - + 2095 Remove FastCGI multiplexing - + 2103 Server should open connectors early in start sequence - + 2108 Update licence headers and plugin for 2018 - + 2172 Support javax.websocket 1.1 - + 2175 Refactor WebSocket close handling - + 2191 JPMS Support - + 2431 Upgrade to Junit 5 - + 2868 Adding SPNEGO authentication support for Jetty Client - + 2901 Introduce HttpConnectionUpgrader as a conversation component in - HttpClient - + 2909 Remove B64Code - + 2948 Require JDK 11 for Jetty 10.x - + 2978 Add module-info.java to relevant Jetty modules - + 2983 Jetty 10 Configuration abstraction - + 2985 Jetty 10 Configuration replacement algorithm incorrect - + 2996 ContextHandler.setDefaultContextPath() not implemented for quickstart. - + 3009 Update Jetty 10 to use non-LEGACY Compliance Modes - + 3010 Move old MultiPart parsing implementation to jetty-http - + 3011 Move HttpCompliance to HttpConfiguration - + 3012 Refactor HttpCompliance and HttpComplianceSection to be friendlier to - customization - + 3106 Websocket connection stats and request stats - + 3129 javax-websocket-common pom.xml is wrong - + 3139 NPE on - WebSocketServerContainerInitializer.configureContext(ServletContextHandler) - + 3154 Add support for javax.net.ssl.HostnameVerifier to HttpClient - + 3159 WebSocket permessage-deflate RSV1 validity check - + 3162 Use Jetty specific Servlet API jar - + 3167 JavaxWebSocketServerContainerInitializer always creates a HttpClient - + 3170 WebSocket proxy PoC - + 3182 Restore websocket example files - + 3186 Jetty maven plugin - javax.annotation.jar picked up from jetty plugin - rather than from applications classpath - + 3197 Use jetty specific websocket API jar - + 3216 Autobahn WebSocketServer failures in jetty 10 - + 3225 Response.sendError should not set reason. - + 3249 Update to apache jasper 9.0.14 for jetty-10 - + 3274 OSGi versions of java.base classes in - org.apache.felix:org.osgi.foundation:jar conflicts with new rules on Java 9+ - + 3279 WebSocket write may hang forever - + 3288 Correct websocket artifactIds on jetty-10.0.x - + 3290 async websocket onOpen, onError and onClose in 10.0.x - + 3298 Review jetty-10 websocket CompletableFuture usage. - + 3303 Update to jakarta ee javax artifacts for jetty-10 - + 3308 Remove deprecated methods from sessions - + 3320 Review Jetty 10 module-info.java - + 3333 Jetty 10 standalone cannot start on the module-path - + 3340 Update PushCacheFilter to use Servlet 4.0 APIs - + 3341 XmlBasedHttpClientProvider in Jetty 10 - + 3351 Restructure jetty-unixsocket module - + 3374 JSR356 RemoteEndpoint.Async.setSendTimeout() logic invalid in Jetty - 10.0.x - + 3379 Tracking of WebSocket Sessions in WebSocket containers - + 3380 WebSocket should support jetty-io Connection.Listener - + 3382 Jetty WebSocket Session.suspend() not implemented - + 3399 XmlConfiguration jetty.webapps.uri is the uri of the webapp not the - parent dir - + 3412 problems with jetty 10 WebSocket session customizer - + 3446 allow jetty WebSockets to be upgraded using WebSocketUpgradeFilter in - jetty-10 - + 3453 Removing moved Extension classes from jetty-websocket-api - + 3458 ensure users of the jetty-websocket-api do not have to see - websocket-core classes - + 3462 client validation of websocket upgrade response - + 3465 websocket negotiation of extension configuration parameters - + 3479 review and cleanup of jetty-websocket-api in jetty-10 - + 3484 ClassCastException when using websocket-core classes in - websocket-servlet - + 3564 Update jetty-10.0.x to apache jsp 9.0.19 - + 3608 Reply with 400 Bad request to malformed WebSocket handshake - + 3616 Backport WebSocket SessionTracker from Jetty 10 - + 3661 JettyWebSocketServerContainer exposes websocket common classes - + 3666 WebSocket - Handling sent 1009 close frame. - + 3696 Unwrap JavaxWebSocketClientContainer.connectToServer() exceptions - + 3705 Review ClientUpgradeRequest exception handling - + 3712 change maxIdleTime to idleTimeout in jetty-10 websockets - + 3719 Clean up jetty-10 modules - + 3726 Remove OSGi export uses of servlet-api from jetty-util - + 3751 Modern Configure DTD / FPI is used inconsistently - + 3787 Jetty client sometimes returns EOFException instead of - SSLHandshakeException on certificate errors. - + 3789 XmlConfiguration set from property - + 3804 Weld/CDI XML backwards compat? - + 3809 sending WebSocket close frame with error StatusCode does not do a hard - close (Jetty-10) - + 3839 JavaxWebSocketServletContainerInitializer fails - + 3872 Review exposure of JavaxWebSocketServletContainerInitializer - + 3952 Server configuration for direct/heap ByteBuffers - + 4003 Quickstart broken in jetty-10 - + 4058 Review Locker - + 4076 Restarting quickstarted webapp throws IllegalStateException: - ServletContainerInitializersStarter already exists - + 4096 thread in ReservedThreadExecutor does not exit when stopped - + 4104 frames are sent through ExtensionStack even if WebSocket Session is - closed - + 4105 QueuedThreadPool should reuse oldest threads first, to allow idle - threads to expire - + 4121 QueuedThreadPool should support ThreadFactory behaviors - + 4122 QueuedThreadPool should reset thread interrupted on failed run - + 4141 ClassCastException with non-async Servlet + async Filter + - HttpServletRequestWrapper - + 4144 Naked cast to Request should be avoided - + 4150 Module org.eclipse.jetty.alpn.client not found, required by - org.eclipse.jetty.proxy - -jetty-9.4.21.v20190926 - 26 September 2019 - + 97 Permanent UnavailableException thrown during servlet request handling - should cause servlet destroy - + 137 Support OAuth - + 155 No way to set keystore for JSR 356 websocket clients, needed for SSL - client authentication - + 1036 Allow easy configuration of Scheduler-Threads and name them more - appropriate - + 2815 HPack fields are opaque octets - + 3040 Allow RFC6265 Cookies to include optional SameSite attribute - + 3106 WebSocket connection stats and request stats - + 3734 WebSocket suspend when input closed - + 3747 Make Jetty Demo work with JPMS - + 3806 Error Page handling Async race with ProxyServlet - + 3913 Clustered HttpSession IllegalStateException: Invalid for read - + 3936 Race condition when modifying session + sendRedirect() - + 3956 Remove and warn on use of illegal HTTP/2 response headers - + 3964 Improve efficiency of listeners - + 3968 WebSocket sporadic ReadPendingException using suspend/resume - + 3978 HTTP/2 fixes for robustly handling abnormal traffic and resource - exhaustion - + 3983 JarFileResource incorrectly lists the contents of directories with - spaces - + 3985 Improve lenient Cookie parsing - + 3989 Inform custom ManagedSelector of dead selector via optional - onFailedSelect() - + 4000 Add SameFileAliasChecker to help with FileSystem static file access - normalization on Mac and Windows - + 4007 NullPointerException while trying to run jetty start.run on Windows - + 4009 ServletContextHandler setSecurityHandler broke handler chain - + 4020 Revert WebSocket ExtensionFactory change to interface - + 4022 Servlet which is added by ServletRegistration can't be started - + 4025 Provide more write-through behaviours for DefaultSessionCache - + 4027 Ensure AbstractSessionDataStore cannot be used unless it is started - + 4033 Ignore bad percent encodings in paths during - URIUtil.equalsIgnoreEncodings() - + 4047 Gracefully stopped Jetty not flushing all response data - + 4048 Multiple values in X-Forwarded-Port throw NumberFormatException - + 4057 NullPointerException in o.e.j.h.HttpFields - + 4064 NullPointerException initializing embedded servlet - + 4075 Do not fail on servlet-mapping with url-pattern /On* - + 4082 NullPointerExceptoin while Debug logging in client - + 4084 Use of HttpConfiguration.setBlockingTimeout(long) in jetty.xml produces - warning on jetty-home startup - + 4105 Cleanup of Idle thread count in QueuedThreadPool - + 4113 HttpClient fails with JDK 13 and TLS 1.3 jetty-10.0.0-alpha0 - 11 July +jetty-10.0.0-alpha0 - 11 July 2019 + 113 Add support for NCSA Extended Log File Format + 114 Bring back overlay deployer @@ -415,6 +193,98 @@ jetty-9.4.21.v20190926 - 26 September 2019 + 3849 ClosedChannelException from jetty-test-webapp javax websocket chat example +jetty-9.4.22.v20191022 - 22 October 2019 + + 2429 HttpClient backpressure improved + + 3558 Error notifications can be received after a successful websocket + + 3787 Jetty client sometimes returns EOFException instead of + SSLHandshakeException on certificate errors. + + 3913 Clustered HttpSession IllegalStateException: Invalid for read + + 3989 Inform custom ManagedSelector of dead selector via optional + onFailedSelect() + + 4096 Thread in ReservedThreadExecutor does not exit when stopped + + 4104 Frames are sent through ExtensionStack even if WebSocket Session is + closed + + 4105 QueuedThreadPool increased thread usage and no idle thread decay + + 4115 Drop HTTP/2 pseudo headers + + 4121 QueuedThreadPool should support ThreadFactory behaviors + + 4122 QueuedThreadPool should reset thread interrupted on failed run + + 4128 OpenIdCredetials can't decode JWT ID token + + 4132 Should be possible to use OIDC without metadata + + 4141 ClassCastException with non-async Servlet + async Filter + + HttpServletRequestWrapper + + 4142 Configurable HTTP/2 RateControl + + 4144 Naked cast to Request should be avoided + + 4156 IllegalStateException when forwarding to jsp with new session + + 4158 Behaviour change in session handling in 9.4.21.v20190926 + + 4170 Client-side alias selection based on SSLEngine + + 4174 ConcurrentModificationException when stopping jetty:run-war + + 4176 Should not set header if sendError has been called + + 4177 Configure HTTP proxy with SslContextFactory + + 4179 Improve HttpChannel$SendCallback references for GC + + 4183 Jetty considers bootstrap injected class to be a "server class" + + 4188 Spin in HttpOutput.close + + 4190 Jetty hangs after thread blocked in SharedBlockingCallback.block() + called by HttpOutput.close + + 4191 Increase GzipHandler minGzipSize default size + + 4193 InetAccessHandler - new includeConnectors/excludeConnectors not quite + correct anymore + + 4201 Throw SSLHandshakeException in case of TLS handshake failures + + 4203 Some Transfer-Encoding and Content-Length combinations do not result in + expected 400 Bad Request + + 4204 Transfer-Encoding behavior does not follow RFC7230 + + 4208 Regression in Jetty 9.4.21: 304 response with Content-Length fails + + 4209 Unused TLS connection is not closed in Java 11 + + 4217 SslConnection.DecryptedEnpoint.flush eternal busy loop + + 4227 First authorization request produced by OIDC module fails due to + inclusion of sessionid + +jetty-9.4.21.v20190926 - 26 September 2019 + + 97 Permanent UnavailableException thrown during servlet request handling + should cause servlet destroy + + 137 Support OAuth + + 155 No way to set keystore for JSR 356 websocket clients, needed for SSL + client authentication + + 1036 Allow easy configuration of Scheduler-Threads and name them more + appropriate + + 2815 HPack fields are opaque octets + + 3040 Allow RFC6265 Cookies to include optional SameSite attribute + + 3106 WebSocket connection stats and request stats + + 3734 WebSocket suspend when input closed + + 3747 Make Jetty Demo work with JPMS + + 3806 Error Page handling Async race with ProxyServlet + + 3913 Clustered HttpSession IllegalStateException: Invalid for read + + 3936 Race condition when modifying session + sendRedirect() + + 3956 Remove and warn on use of illegal HTTP/2 response headers + + 3964 Improve efficiency of listeners + + 3968 WebSocket sporadic ReadPendingException using suspend/resume + + 3978 HTTP/2 fixes for robustly handling abnormal traffic and resource + exhaustion + + 3983 JarFileResource incorrectly lists the contents of directories with + spaces + + 3985 Improve lenient Cookie parsing + + 3989 Inform custom ManagedSelector of dead selector via optional + onFailedSelect() + + 4000 Add SameFileAliasChecker to help with FileSystem static file access + normalization on Mac and Windows + + 4007 NullPointerException while trying to run jetty start.run on Windows + + 4009 ServletContextHandler setSecurityHandler broke handler chain + + 4020 Revert WebSocket ExtensionFactory change to interface + + 4022 Servlet which is added by ServletRegistration can't be started + + 4025 Provide more write-through behaviours for DefaultSessionCache + + 4027 Ensure AbstractSessionDataStore cannot be used unless it is started + + 4033 Ignore bad percent encodings in paths during + URIUtil.equalsIgnoreEncodings() + + 4047 Gracefully stopped Jetty not flushing all response data + + 4048 Multiple values in X-Forwarded-Port throw NumberFormatException + + 4057 NullPointerException in o.e.j.h.HttpFields + + 4064 NullPointerException initializing embedded servlet + + 4075 Do not fail on servlet-mapping with url-pattern /On* + + 4082 NullPointerExceptoin while Debug logging in client + + 4084 Use of HttpConfiguration.setBlockingTimeout(long) in jetty.xml produces + warning on jetty-home startup + + 4105 Cleanup of Idle thread count in QueuedThreadPool + + 4113 HttpClient fails with JDK 13 and TLS 1.3 + jetty-9.4.20.v20190813 - 13 August 2019 + 300 Implement Deflater / Inflater Object Pool + 2061 WebSocket hangs in blockingWrite diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java index 30b5d92406b..839aad4c317 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java @@ -19,18 +19,25 @@ package org.eclipse.jetty.client; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; @@ -39,17 +46,26 @@ import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; +import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.ExecutorThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnJre; @@ -68,7 +84,6 @@ public class HttpClientTLSTest private Server server; private ServerConnector connector; private HttpClient client; - private SSLSocket sslSocket; private void startServer(SslContextFactory.Server sslContextFactory, Handler handler) throws Exception { @@ -424,16 +439,16 @@ public class HttpClientTLSTest String host = "localhost"; int port = connector.getLocalPort(); - Socket socket = new Socket(host, port); - sslSocket = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket, host, port, true); + Socket socket1 = new Socket(host, port); + SSLSocket sslSocket1 = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket1, host, port, true); CountDownLatch handshakeLatch1 = new CountDownLatch(1); AtomicReference session1 = new AtomicReference<>(); - sslSocket.addHandshakeCompletedListener(event -> + sslSocket1.addHandshakeCompletedListener(event -> { session1.set(event.getSession().getId()); handshakeLatch1.countDown(); }); - sslSocket.startHandshake(); + sslSocket1.startHandshake(); assertTrue(handshakeLatch1.await(5, TimeUnit.SECONDS)); // In TLS 1.3 the server sends a NewSessionTicket post-handshake message @@ -441,29 +456,29 @@ public class HttpClientTLSTest assertThrows(SocketTimeoutException.class, () -> { - sslSocket.setSoTimeout(1000); - sslSocket.getInputStream().read(); + sslSocket1.setSoTimeout(1000); + sslSocket1.getInputStream().read(); }); // The client closes abruptly. - socket.close(); + socket1.close(); // Try again and compare the session ids. - socket = new Socket(host, port); - sslSocket = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket, host, port, true); + Socket socket2 = new Socket(host, port); + SSLSocket sslSocket2 = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket2, host, port, true); CountDownLatch handshakeLatch2 = new CountDownLatch(1); AtomicReference session2 = new AtomicReference<>(); - sslSocket.addHandshakeCompletedListener(event -> + sslSocket2.addHandshakeCompletedListener(event -> { session2.set(event.getSession().getId()); handshakeLatch2.countDown(); }); - sslSocket.startHandshake(); + sslSocket2.startHandshake(); assertTrue(handshakeLatch2.await(5, TimeUnit.SECONDS)); assertArrayEquals(session1.get(), session2.get()); - sslSocket.close(); + sslSocket2.close(); } @Test @@ -485,7 +500,7 @@ public class HttpClientTLSTest protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory) { SslClientConnectionFactory ssl = (SslClientConnectionFactory)super.newSslClientConnectionFactory(sslContextFactory, connectionFactory); - ssl.setAllowMissingCloseMessage(false); + ssl.setRequireCloseMessage(true); return ssl; } }; @@ -512,19 +527,19 @@ public class HttpClientTLSTest break; } - // If the response is Content-Length delimited, allowing the - // missing TLS Close Message is fine because the application - // will see a EOFException anyway. - // If the response is connection delimited, allowing the - // missing TLS Close Message is bad because the application - // will see a successful response with truncated content. + // If the response is Content-Length delimited, the lack of + // the TLS Close Message is fine because the application + // will see a EOFException anyway: the Content-Length and + // the actual content bytes count won't match. + // If the response is connection delimited, the lack of the + // TLS Close Message is bad because the application will + // see a successful response, but with truncated content. - // Verify that by not allowing the missing - // TLS Close Message we get a response failure. + // Verify that by requiring the TLS Close Message we get + // a response failure. byte[] half = new byte[8]; String response = "HTTP/1.1 200 OK\r\n" + -// "Content-Length: " + (half.length * 2) + "\r\n" + "Connection: close\r\n" + "\r\n"; OutputStream output = sslSocket.getOutputStream(); @@ -564,4 +579,368 @@ public class HttpClientTLSTest assertTrue(latch.await(5, TimeUnit.SECONDS)); } + + @Test + public void testNeverUsedConnectionThenServerIdleTimeout() throws Exception + { + long idleTimeout = 2000; + + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + AtomicLong serverBytes = new AtomicLong(); + SslConnectionFactory ssl = new SslConnectionFactory(serverTLSFactory, http.getProtocol()) + { + @Override + protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine) + { + return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) + { + @Override + protected int networkFill(ByteBuffer input) throws IOException + { + int n = super.networkFill(input); + if (n > 0) + serverBytes.addAndGet(n); + return n; + } + }; + } + }; + connector = new ServerConnector(server, 1, 1, ssl, http); + connector.setIdleTimeout(idleTimeout); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(clientTLSFactory); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + AtomicLong clientBytes = new AtomicLong(); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)) + { + @Override + protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory) + { + if (sslContextFactory == null) + sslContextFactory = getSslContextFactory(); + return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory) + { + @Override + protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine) + { + return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) + { + @Override + protected int networkFill(ByteBuffer input) throws IOException + { + int n = super.networkFill(input); + if (n > 0) + clientBytes.addAndGet(n); + return n; + } + }; + } + }; + } + }; + client.setExecutor(clientThreads); + client.start(); + + // Create a connection but don't use it. + Origin origin = new Origin(HttpScheme.HTTPS.asString(), "localhost", connector.getLocalPort()); + HttpDestination destination = client.resolveDestination(new HttpDestination.Key(origin, null)); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + // Trigger the creation of a new connection, but don't use it. + connectionPool.tryCreate(-1); + // Verify that the connection has been created. + while (true) + { + Thread.sleep(50); + if (connectionPool.getConnectionCount() == 1) + break; + } + + // Wait for the server to idle timeout the connection. + Thread.sleep(idleTimeout + idleTimeout / 2); + + // The connection should be gone from the connection pool. + assertEquals(0, connectionPool.getConnectionCount(), connectionPool.dump()); + assertEquals(0, serverBytes.get()); + assertEquals(0, clientBytes.get()); + } + + @Test + public void testNeverUsedConnectionThenClientIdleTimeout() throws Exception + { + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + AtomicLong serverBytes = new AtomicLong(); + SslConnectionFactory ssl = new SslConnectionFactory(serverTLSFactory, http.getProtocol()) + { + @Override + protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine) + { + return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) + { + @Override + protected int networkFill(ByteBuffer input) throws IOException + { + int n = super.networkFill(input); + if (n > 0) + serverBytes.addAndGet(n); + return n; + } + }; + } + }; + connector = new ServerConnector(server, 1, 1, ssl, http); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + long idleTimeout = 2000; + + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(clientTLSFactory); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + AtomicLong clientBytes = new AtomicLong(); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)) + { + @Override + protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory) + { + if (sslContextFactory == null) + sslContextFactory = getSslContextFactory(); + return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory) + { + @Override + protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine) + { + return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) + { + @Override + protected int networkFill(ByteBuffer input) throws IOException + { + int n = super.networkFill(input); + if (n > 0) + clientBytes.addAndGet(n); + return n; + } + }; + } + }; + } + }; + client.setIdleTimeout(idleTimeout); + client.setExecutor(clientThreads); + client.start(); + + // Create a connection but don't use it. + Origin origin = new Origin(HttpScheme.HTTPS.asString(), "localhost", connector.getLocalPort()); + HttpDestination destination = client.resolveDestination(new HttpDestination.Key(origin, null)); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + // Trigger the creation of a new connection, but don't use it. + connectionPool.tryCreate(-1); + // Verify that the connection has been created. + while (true) + { + Thread.sleep(50); + if (connectionPool.getConnectionCount() == 1) + break; + } + + // Wait for the client to idle timeout the connection. + Thread.sleep(idleTimeout + idleTimeout / 2); + + // The connection should be gone from the connection pool. + assertEquals(0, connectionPool.getConnectionCount(), connectionPool.dump()); + assertEquals(0, serverBytes.get()); + assertEquals(0, clientBytes.get()); + } + + @Test + public void testSSLEngineClosedDuringHandshake() throws Exception + { + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); + startServer(serverTLSFactory, new EmptyServerHandler()); + + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(clientTLSFactory); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)) + { + @Override + protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory) + { + if (sslContextFactory == null) + sslContextFactory = getSslContextFactory(); + return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory) + { + @Override + protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine) + { + return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) + { + @Override + protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuffer output) throws SSLException + { + sslEngine.closeOutbound(); + return super.wrap(sslEngine, input, output); + } + }; + } + }; + } + }; + client.setExecutor(clientThreads); + client.start(); + + ExecutionException failure = assertThrows(ExecutionException.class, () -> client.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) + .send()); + Throwable cause = failure.getCause(); + assertThat(cause, Matchers.instanceOf(SSLHandshakeException.class)); + } + + @Test + public void testTLSLargeFragments() throws Exception + { + CountDownLatch serverLatch = new CountDownLatch(1); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + SslConnectionFactory ssl = new SslConnectionFactory(serverTLSFactory, http.getProtocol()) + { + @Override + protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine) + { + return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) + { + @Override + protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException + { + int inputBytes = input.remaining(); + SSLEngineResult result = super.unwrap(sslEngine, input, output); + if (inputBytes == 5) + serverLatch.countDown(); + return result; + } + }; + } + }; + connector = new ServerConnector(server, 1, 1, ssl, http); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + long idleTimeout = 2000; + + CountDownLatch clientLatch = new CountDownLatch(1); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(clientTLSFactory); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)) + { + @Override + protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory) + { + if (sslContextFactory == null) + sslContextFactory = getSslContextFactory(); + return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory) + { + @Override + protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine) + { + return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption()) + { + @Override + protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuffer output) throws SSLException + { + try + { + clientLatch.countDown(); + assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); + return super.wrap(sslEngine, input, output); + } + catch (InterruptedException x) + { + throw new SSLException(x); + } + } + }; + } + }; + } + }; + client.setIdleTimeout(idleTimeout); + client.setExecutor(clientThreads); + client.start(); + + String host = "localhost"; + int port = connector.getLocalPort(); + + CountDownLatch responseLatch = new CountDownLatch(1); + client.newRequest(host, port) + .scheme(HttpScheme.HTTPS.asString()) + .send(result -> + { + assertTrue(result.isSucceeded()); + assertEquals(HttpStatus.OK_200, result.getResponse().getStatus()); + responseLatch.countDown(); + }); + // Wait for the TLS buffers to be acquired by the client, then the + // HTTP request will be paused waiting for the TLS buffer to be expanded. + assertTrue(clientLatch.await(5, TimeUnit.SECONDS)); + + // Send the large frame bytes that will enlarge the TLS buffers. + try (Socket socket = new Socket(host, port)) + { + OutputStream output = socket.getOutputStream(); + byte[] largeFrameBytes = new byte[5]; + largeFrameBytes[0] = 22; // Type = handshake + largeFrameBytes[1] = 3; // Major TLS version + largeFrameBytes[2] = 3; // Minor TLS version + // Frame length is 0x7FFF == 32767, i.e. a "large fragment". + // Maximum allowed by RFC 8446 is 16384, but SSLEngine supports up to 33093. + largeFrameBytes[3] = 0x7F; // Length hi byte + largeFrameBytes[4] = (byte)0xFF; // Length lo byte + output.write(largeFrameBytes); + output.flush(); + // Just close the connection now, the large frame + // length was enough to trigger the buffer expansion. + } + + // The HTTP request will resume and be forced to handle the TLS buffer expansion. + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } } diff --git a/jetty-documentation/src/main/asciidoc/distribution-guide/annotations/using-annotations.adoc b/jetty-documentation/src/main/asciidoc/distribution-guide/annotations/using-annotations.adoc index 43e070094a2..3385c4c5c0f 100644 --- a/jetty-documentation/src/main/asciidoc/distribution-guide/annotations/using-annotations.adoc +++ b/jetty-documentation/src/main/asciidoc/distribution-guide/annotations/using-annotations.adoc @@ -139,7 +139,7 @@ Here is an example, setting the context attribute in code (although you can also ---- WebAppContext context = new WebAppContext(); context.setAttribute("org.eclipse.jetty.containerInitializerOrder", - "org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer, com.acme.Foo.MySCI, *"); + "org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer, com.acme.Foo.MySCI, *"); ---- In this example, we ensure that the `WebSocketServerContainerInitializer` is the very first `ServletContainerInitializer` that is called, followed by MySCI and then any other `ServletContainerInitializer` instances that were discovered but not yet called. diff --git a/jetty-documentation/src/main/asciidoc/distribution-guide/security/serving-aliased-files.adoc b/jetty-documentation/src/main/asciidoc/distribution-guide/security/serving-aliased-files.adoc index 290cad1e31d..5bf22dfbe5f 100644 --- a/jetty-documentation/src/main/asciidoc/distribution-guide/security/serving-aliased-files.adoc +++ b/jetty-documentation/src/main/asciidoc/distribution-guide/security/serving-aliased-files.adoc @@ -19,8 +19,8 @@ [[serving-aliased-files]] === Aliased Files and Symbolic links -Web applications will often server static content from the file system provided by the operating system running underneath the JVM. -However because file systems often implement multiple aliased names for the same file, then security constraints and other servlet URI space mappings my inadvertently be bypassed by aliases. +Web applications will often serve static content from the file system provided by the operating system running underneath the JVM. +However, because file systems often implement multiple aliased names for the same file, then security constraints and other servlet URI space mappings may inadvertently be bypassed by aliases. A key example of this is case insensitivity and 8.3 filenames implemented by the Windows file system. If a file within a web application called `/mysecretfile.txt` is protected by a security constraint on the URI `/mysecretfile.txt`, then a request to `/MySecretFile.TXT` will not match the URI constraint because URIs are case sensitive, but the Windows file system will report that a file does exist at that name and it will be served despite the security constraint. diff --git a/jetty-documentation/src/main/asciidoc/distribution-guide/startup/startup-base-vs-home.adoc b/jetty-documentation/src/main/asciidoc/distribution-guide/startup/startup-base-vs-home.adoc index 8f91ad557f3..13763bd3732 100644 --- a/jetty-documentation/src/main/asciidoc/distribution-guide/startup/startup-base-vs-home.adoc +++ b/jetty-documentation/src/main/asciidoc/distribution-guide/startup/startup-base-vs-home.adoc @@ -145,7 +145,7 @@ Properties: jetty.secure.port = 8443 jetty.truststore = etc/keystore jetty.truststore.password = OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4 - org.eclipse.jetty.websocket.jsr356 = false + org.eclipse.jetty.websocket.javax = false threads.max = 200 threads.min = 10 threads.timeout = 60000 @@ -235,7 +235,7 @@ etc/demo-rewrite-rules.xml # Websocket chat examples needs websocket enabled # Don't start for all contexts (set to true in test.xml context) -org.eclipse.jetty.websocket.jsr356=false +org.eclipse.jetty.websocket.javax=false --module=websocket # Create and configure the test realm diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/intro/chapter.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/intro/chapter.adoc index f5783ef1c3b..0b21590b25f 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/intro/chapter.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/intro/chapter.adoc @@ -107,18 +107,18 @@ To enable Websocket, you need to enable the `websocket` link:#enabling-modules[m Once this module is enabled for your Jetty base, it will apply to all webapps deployed to that base. If you want to be more selective about which webapps use Websocket, then you can: -Disable JSR-356 for a particular webapp::: - You can disable jsr-356 for a particular webapp by setting the link:#context_attributes[context attribute] `org.eclipse.jetty.websocket.jsr356` to `false`. +Disable Websocket for a particular webapp::: + You can disable jsr-356 for a particular webapp by setting the link:#context_attributes[context attribute] `org.eclipse.jetty.websocket.javax` to `false`. This will mean that websockets are not available to your webapp, however deployment time scanning for websocket-related classes such as endpoints will still occur. This can be a significant impost if your webapp contains a lot of classes and/or jar files. To completely disable websockets and avoid all setup costs associated with it for a particular webapp, use instead the context attribute `org.eclipse.jetty.containerInitializerExclusionPattern`, described next, which allows you to exclude the websocket ServletContainerInitializer that causes the scanning. -Completely disable jsr-356 for a particular webapp::: - Set the `org.eclipse.jetty.containerInitializerExclusionPattern` link:#context_attributes[context attribute] to include `org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer`. +Completely disable Websocket for a particular webapp::: + Set the `org.eclipse.jetty.containerInitializerExclusionPattern` link:#context_attributes[context attribute] to include `org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer`. Here's an example of doing this in code, although you can do the link:#intro-jetty-configuration-webapps[same in xml]: + [source, java, subs="{sub-order}"] ---- WebAppContext context = new WebAppContext(); -context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern", - "org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer|com.acme.*"); +context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern", + "org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer|com.acme.*"); ---- diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-send-message.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-send-message.adoc index a8b6e11957f..bccad70f3a3 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-send-message.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-send-message.adoc @@ -115,10 +115,54 @@ catch (IOException e) How to send a Text message in 2 parts, using the partial message support in RemoteEndpoint. This will block until each part of the message is sent, possibly throwing an IOException if unable to send the partial message. +[[websocket-async-send]] +==== Async Send Message + +There are also four (4) async send message methods available: + +* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendBytes(java.nio.ByteBuffer,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendBytes(ByteBuffer message, WriteCallback callback)`] +* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendPartialBytes(java.nio.ByteBuffer,boolean,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendPartialBytes(ByteBuffer message, boolean isLast, WriteCallback callback)`] +* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendString(java.lang.String,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendString(String message, WriteCallback callback)`] +* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendPartialString(java.lang.String,boolean,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendPartialString(String message, boolean isLast, WriteCallback callback)`] + +All these async send methods use `WriteCallback`, which allows you to be notified when the write either succeeds or fails. + +[source, java, subs="{sub-order}"] +---- +WriteCallback callback = new WriteCallback() +{ + @Override + public void writeSuccess() + { + // Notification that the write has succeeded. + } + + @Override + public void writeFailed(Throwable x) + { + // Notification that the write has failed. + t.printStackTrace(); + } +}; +---- + +The async send methods can be used in a similar way to the blocking send methods, however the method will return before the message is transmitted, and you are notified of the final result of the message transmission through the `WriteCallback`. +The static `WriteCallback.NOOP` can be used to do nothing on success / failure of the callback. + +[source, java, subs="{sub-order}"] +---- +RemoteEndpoint remote = session.getRemote(); + +// Async Send of a BINARY message to remote endpoint +ByteBuffer message = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); +remote.sendBytes(message, callback); +---- + + [[pingpong]] ==== Send Ping / Pong Control Frame -You can also send Ping and Pong control frames using the RemoteEndpoint. +You can also send Ping and Pong control frames using the `RemoteEndpoint`. [source, java, subs="{sub-order}"] ---- @@ -138,7 +182,7 @@ catch (IOException e) ---- How to send a Ping control frame, with a payload of `"You There?"` (arriving at Remote Endpoint as a byte array payload). -This will block until the message is sent, possibly throwing an IOException if unable to send the ping frame. +This will block until the message is sent, possibly throwing an `IOException` if unable to send the ping frame. [source, java, subs="{sub-order}"] ---- @@ -158,143 +202,23 @@ catch (IOException e) ---- How to send a Pong control frame, with a payload of `"Yup I'm here"` (arriving at Remote Endpoint as a byte array payload). -This will block until the message is sent, possibly throwing an IOException if unable to send the pong frame. +This will block until the message is sent, possibly throwing an `IOException` if unable to send the pong frame. To be correct in your usage of Pong frames, you should return the same byte array data that you received in the Ping frame. -[[async]] -==== Async Send Message - -However there are also 2 Async send message methods available: - -* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendBytesByFuture(java.nio.ByteBuffer)[`RemoteEndpoint.sendBytesByFuture(ByteBuffer message)`] -* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendStringByFuture(java.lang.String)[`RemoteEndpoint.sendStringByFuture(String message)`] - -Both return a `Future` that can be used to test for success and failure of the message send using standard http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html[`java.util.concurrent.Future`] behavior. +You can also asynchronously send Ping and Pong frames using the `WriteCallback`, this will return before the Ping/Pong is +transmitted and notify you of the result in `WriteCallback` `writeSuccess()` or `writeFailed()`. [source, java, subs="{sub-order}"] ---- RemoteEndpoint remote = session.getRemote(); -// Async Send of a BINARY message to remote endpoint -ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); -remote.sendBytesByFuture(buf); +String pingData = "You There?"; +ByteBuffer pingPayload = ByteBuffer.wrap(data.getBytes()); + +String pongData = "Yup, I'm here"; +ByteBuffer pongPayload = ByteBuffer.wrap(data.getBytes()); + +remote.sendPing(pingPayload, WriteCallback.NOOP); +remote.sendPong(pongPayload, WriteCallback.NOOP); ---- - -How to send a simple Binary message using the RemoteEndpoint. -The message will be enqueued for outgoing write, but you will not know if it succeeded or failed. - -[source, java, subs="{sub-order}"] ----- -RemoteEndpoint remote = session.getRemote(); - -// Async Send of a BINARY message to remote endpoint -ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); -try -{ - Future fut = remote.sendBytesByFuture(buf); - // wait for completion (forever) - fut.get(); -} -catch (ExecutionException | InterruptedException e) -{ - // Send failed - e.printStackTrace(); -} ----- - -How to send a simple Binary message using the RemoteEndpoint, tracking the `Future` to know if the send succeeded or failed. - -[source, java, subs="{sub-order}"] ----- -RemoteEndpoint remote = session.getRemote(); - -// Async Send of a BINARY message to remote endpoint -ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); -Future fut = null; -try -{ - fut = remote.sendBytesByFuture(buf); - // wait for completion (timeout) - fut.get(2,TimeUnit.SECONDS); -} -catch (ExecutionException | InterruptedException e) -{ - // Send failed - e.printStackTrace(); -} -catch (TimeoutException e) -{ - // timeout - e.printStackTrace(); - if (fut != null) - { - // cancel the message - fut.cancel(true); - } -} ----- - -How to send a simple Binary message using the RemoteEndpoint, tracking the `Future` and waiting only prescribed amount of time for the send to complete, cancelling the message if the timeout occurs. - -[source, java, subs="{sub-order}"] ----- -RemoteEndpoint remote = session.getRemote(); - -// Async Send of a TEXT message to remote endpoint -remote.sendStringByFuture("Hello World"); ----- - -How to send a simple Text message using the RemoteEndpoint. -The message will be enqueued for outgoing write, but you will not know if it succeeded or failed. - -[source, java, subs="{sub-order}"] ----- -RemoteEndpoint remote = session.getRemote(); - -// Async Send of a TEXT message to remote endpoint -try -{ - Future fut = remote.sendStringByFuture("Hello World"); - // wait for completion (forever) - fut.get(); -} -catch (ExecutionException | InterruptedException e) -{ - // Send failed - e.printStackTrace(); -} ----- - -How to send a simple Binary message using the RemoteEndpoint, tracking the `Future` to know if the send succeeded or failed. - -[source, java, subs="{sub-order}"] ----- -RemoteEndpoint remote = session.getRemote(); - -// Async Send of a TEXT message to remote endpoint -Future fut = null; -try -{ - fut = remote.sendStringByFuture("Hello World"); - // wait for completion (timeout) - fut.get(2,TimeUnit.SECONDS); -} -catch (ExecutionException | InterruptedException e) -{ - // Send failed - e.printStackTrace(); -} -catch (TimeoutException e) -{ - // timeout - e.printStackTrace(); - if (fut != null) - { - // cancel the message - fut.cancel(true); - } -} ----- - -How to send a simple Binary message using the RemoteEndpoint, tracking the `Future` and waiting only prescribed amount of time for the send to complete, cancelling the message if the timeout occurs. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-session.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-session.adoc index 67c34790b40..13f96ef9c72 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-session.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-api-session.adoc @@ -54,12 +54,12 @@ What is the Local and Remote Address. [source, java, subs="{sub-order}"] ---- -InetSocketAddress remoteAddr = session.getRemoteAddress(); +SocketAddress remoteAddr = session.getRemoteAddress(); ---- Get and Set the Idle Timeout [source, java, subs="{sub-order}"] ---- -session.setIdleTimeout(2000); // 2 second timeout +session.setIdleTimeout(Duration.ofMillis(2000)); ---- diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-server-api.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-server-api.adoc index d18910ae134..acc99bd7a54 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-server-api.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/websockets/jetty/jetty-websocket-server-api.adoc @@ -19,7 +19,7 @@ [[jetty-websocket-server-api]] === Jetty WebSocket Server API -Jetty provides the ability to wire up WebSocket endpoints to Servlet Path Specs via the use of a WebSocketServlet bridge servlet. +Jetty provides the ability to wire up WebSocket endpoints to Servlet Path Specs via the use of a `JettyWebSocketServlet` bridge servlet. Internally, Jetty manages the HTTP Upgrade to WebSocket and migration from a HTTP Connection to a WebSocket Connection. @@ -27,7 +27,7 @@ This will only work when running within the Jetty Container (unlike past Jetty t ==== The Jetty WebSocketServlet -To wire up your WebSocket to a specific path via the WebSocketServlet, you will need to extend org.eclipse.jetty.websocket.servlet.WebSocketServlet and specify what WebSocket object should be created with incoming Upgrade requests. +To wire up your WebSocket to a specific path via the `JettyWebSocketServlet`, you will need to extend `org.eclipse.jetty.websocket.servlet.JettyWebSocketServlet` and specify what `WebSocket` object should be created with incoming Upgrade requests. [source, java, subs="{sub-order}"] ---- @@ -36,8 +36,8 @@ include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclips This example will create a Servlet mapped via the http://docs.oracle.com/javaee/6/api/javax/servlet/annotation/WebServlet.html[@WebServlet] annotation to the Servlet path spec of `"/echo"` (or you can do this manually in the `WEB-INF/web.xml` of your web application) which will create MyEchoSocket instances when encountering HTTP Upgrade requests. -The link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServlet.html#configure(org.eclipse.jetty.websocket.servlet.WebSocketServletFactory)[`WebSocketServlet.configure(WebSocketServletFactory factory)`] is where you put your specific configuration for your WebSocket. -In the example we specify a 10 second idle timeout and register MyEchoSocket with the default WebSocketCreator the WebSocket class we want to be created on Upgrade. +The link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServlet.html#configure(org.eclipse.jetty.websocket.servlet.JettyWebSocketServletFactory)[`JettyWebSocketServlet.configure(JettyWebSocketServletFactory factory)`] is where you put your specific configuration for your WebSocket. +In the example we specify a 10 second idle timeout and register MyEchoSocket with the default JettyWebSocketCreator the WebSocket class we want to be created on Upgrade. ____ [NOTE] @@ -46,21 +46,21 @@ when configuring websockets. Be sure the websocket configuration is lower than your firewall or router. ____ -==== Using the WebSocketCreator +==== Using the JettyWebSocketCreator -All WebSocket's are created via whatever link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketCreator.html[WebSocketCreator] you have registered with the link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.html[WebSocketServletFactory]. +All WebSocket's are created via whatever link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketCreator.html[JettyWebSocketCreator] you have registered with the link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServletFactory.html[JettyWebSocketServletFactory]. -By default, the WebSocketServletFactory is a simple WebSocketCreator capable of creating a single WebSocket object. -Use link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.html#register(java.lang.Class)[`WebSocketCreator.register(Class websocket)`] to tell the WebSocketServletFactory which class it should instantiate (make sure it has a default constructor). +By default, the `JettyWebSocketServletFactory` is a simple `JettyWebSocketCreator` capable of creating a single WebSocket object. +Use link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServletFactory.html#register(java.lang.Class)[`JettyWebSocketCreator.register(Class websocket)`] to tell the `JettyWebSocketServletFactory` which class it should instantiate (make sure it has a default constructor). -If you have a more complicated creation scenario, you might want to provide your own WebSocketCreator that bases the WebSocket it creates off of information present in the UpgradeRequest object. +If you have a more complicated creation scenario, you might want to provide your own `JettyWebSocketCreator` that bases the WebSocket it creates off of information present in the `UpgradeRequest` object. [source, java, subs="{sub-order}"] ---- include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java[] ---- -Here we show a WebSocketCreator that will utilize the http://tools.ietf.org/html/rfc6455#section-1.9[WebSocket subprotocol] information from request to determine what WebSocket type should be +Here we show a `JettyWebSocketCreator` that will utilize the http://tools.ietf.org/html/rfc6455#section-1.9[WebSocket subprotocol] information from request to determine what WebSocket type should be created. [source, java, subs="{sub-order}"] @@ -68,9 +68,9 @@ created. include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java[] ---- -When you want a custom WebSocketCreator, use link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.html#setCreator(org.eclipse.jetty.websocket.servlet.WebSocketCreator)[`WebSocketServletFactory.setCreator(WebSocketCreator creator)`] and the WebSocketServletFactory will use your creator for all incoming Upgrade requests on this servlet. +When you want a custom `JettyWebSocketCreator`, use link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServletFactory.html#setCreator(org.eclipse.jetty.websocket.servlet.JettyWebSocketCreator)[`JettyWebSocketServletFactory.setCreator(JettyWebSocketCreator creator)`] and the `JettyWebSocketServletFactory` will use your creator for all incoming Upgrade requests on this servlet. -Other uses for a WebSocketCreator: +Other uses for a `JettyWebSocketCreator`: * Controlling the selection of WebSocket subprotocol * Performing any WebSocket origin you deem important. @@ -78,4 +78,4 @@ Other uses for a WebSocketCreator: * Obtaining the Servlet HttpSession object (if it exists) * Specifying a response status code and reason -If you don't want to accept the upgrade, simply return null from the link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketCreator.html#createWebSocket(org.eclipse.jetty.websocket.api.UpgradeRequest, org.eclipse.jetty.websocket.api.UpgradeResponse)[`WebSocketCreator.createWebSocket(UpgradeRequest req, UpgradeResponse resp)`] method. +If you don't want to accept the upgrade, simply return null from the link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketCreator.html#createWebSocket(org.eclipse.jetty.websocket.api.UpgradeRequest,org.eclipse.jetty.websocket.api.UpgradeResponse)[`JettyWebSocketCreator.createWebSocket(UpgradeRequest req, UpgradeResponse resp)`] method. diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java index 48a80eda6f5..5799ac621b1 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java @@ -34,7 +34,7 @@ import static java.util.EnumSet.noneOf; */ public class CookieCompliance implements ComplianceViolation.Mode { - enum Violation implements ComplianceViolation + public enum Violation implements ComplianceViolation { COMMA_NOT_VALID_OCTET("https://tools.ietf.org/html/rfc6265#section-4.1.1", "Comma not valid as cookie-octet or separator"), RESERVED_NAMES_NOT_DOLLAR_PREFIXED("https://tools.ietf.org/html/rfc6265#section-4.1.1", "Reserved names no longer use '$' prefix"); @@ -57,13 +57,13 @@ public class CookieCompliance implements ComplianceViolation.Mode @Override public String getURL() { - return null; + return url; } @Override public String getDescription() { - return null; + return description; } } @@ -87,9 +87,8 @@ public class CookieCompliance implements ComplianceViolation.Mode private CookieCompliance(String name, Set violations) { - Objects.nonNull(violations); _name = name; - _violations = unmodifiableSet(copyOf(violations)); + _violations = unmodifiableSet(copyOf(Objects.requireNonNull(violations))); } @Override diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index e1d4e13b497..fe4aa04696d 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -686,17 +686,24 @@ public class HttpGenerator _endOfContent = EndOfContent.NO_CONTENT; // But it is an error if there actually is content - if (_contentPrepared > 0 || contentLength > 0) + if (_contentPrepared > 0) + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response"); + + if (contentLengthField) { - if (_contentPrepared == 0 && last) + if (response != null && response.getStatus() == HttpStatus.NOT_MODIFIED_304) + putContentLength(header, contentLength); + else if (contentLength > 0) { - // TODO discard content for backward compatibility with 9.3 releases - // TODO review if it is still needed in 9.4 or can we just throw. - content.clear(); - contentLength = 0; + if (_contentPrepared == 0 && last) + { + // TODO discard content for backward compatibility with 9.3 releases + // TODO review if it is still needed in 9.4 or can we just throw. + content.clear(); + } + else + throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response"); } - else - throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response"); } } // Else if we are HTTP/1.1 and the content length is unknown and we are either persistent diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 1093feae375..e0442162ad3 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -170,6 +170,7 @@ public class HttpParser private Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune? private EndOfContent _endOfContent; private boolean _hasContentLength; + private boolean _hasTransferEncoding; private long _contentLength = -1; private long _contentPosition; private int _chunkLength; @@ -916,6 +917,9 @@ public class HttpParser switch (_header) { case CONTENT_LENGTH: + if (_hasTransferEncoding) + checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH); + if (_hasContentLength) { checkViolation(MULTIPLE_CONTENT_LENGTHS); @@ -924,9 +928,6 @@ public class HttpParser } _hasContentLength = true; - if (_endOfContent == EndOfContent.CHUNKED_CONTENT) - checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH); - if (_endOfContent != EndOfContent.CHUNKED_CONTENT) { _contentLength = convertContentLength(_valueString); @@ -938,9 +939,15 @@ public class HttpParser break; case TRANSFER_ENCODING: + _hasTransferEncoding = true; + if (_hasContentLength) checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH); + // we encountered another Transfer-Encoding header, but chunked was already set + if (_endOfContent == EndOfContent.CHUNKED_CONTENT) + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last"); + if (HttpHeaderValue.CHUNKED.is(_valueString)) { _endOfContent = EndOfContent.CHUNKED_CONTENT; @@ -949,15 +956,26 @@ public class HttpParser else { List values = new QuotedCSV(_valueString).getValues(); - if (!values.isEmpty() && HttpHeaderValue.CHUNKED.is(values.get(values.size() - 1))) + int chunked = -1; + int len = values.size(); + for (int i = 0; i < len; i++) { - _endOfContent = EndOfContent.CHUNKED_CONTENT; - _contentLength = -1; + if (HttpHeaderValue.CHUNKED.is(values.get(i))) + { + if (chunked != -1) + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, multiple chunked tokens"); + chunked = i; + // declared chunked + _endOfContent = EndOfContent.CHUNKED_CONTENT; + _contentLength = -1; + } + // we have a non-chunked token after a declared chunked token + else if (_endOfContent == EndOfContent.CHUNKED_CONTENT) + { + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last"); + } } - else if (values.stream().anyMatch(HttpHeaderValue.CHUNKED::is)) - throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad chunking"); } - break; case HOST: @@ -1098,6 +1116,17 @@ public class HttpParser return _handler.messageComplete(); } + // We found Transfer-Encoding headers, but none declared the 'chunked' token + if (_hasTransferEncoding && _endOfContent != EndOfContent.CHUNKED_CONTENT) + { + if (_responseHandler == null || _endOfContent != EndOfContent.EOF_CONTENT) + { + // Transfer-Encoding chunked not specified + // https://tools.ietf.org/html/rfc7230#section-3.3.1 + throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last"); + } + } + // Was there a required host header? if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null) { @@ -1779,6 +1808,7 @@ public class HttpParser _endOfContent = EndOfContent.UNKNOWN_CONTENT; _contentLength = -1; _hasContentLength = false; + _hasTransferEncoding = false; _contentPosition = 0; _responseStatus = 0; _contentChunk = null; diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java index 0b791dca9f6..95887d28cdc 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java @@ -979,7 +979,7 @@ public class HttpParserTest assertEquals("GET", _methodOrVersion); assertEquals("/chunk", _uriOrStatus); assertEquals("HTTP/1.0", _versionOrReason); - assertThat(_bad, containsString("Bad chunking")); + assertThat(_bad, containsString("Bad Transfer-Encoding")); } @Test diff --git a/jetty-http2/http2-client/pom.xml b/jetty-http2/http2-client/pom.xml index d8dcc21ccc3..e7753a1fc70 100644 --- a/jetty-http2/http2-client/pom.xml +++ b/jetty-http2/http2-client/pom.xml @@ -20,7 +20,7 @@ maven-surefire-plugin - @{argLine} ${jetty.surefire.argLine} --add-reads org.eclipse.jetty.http2.client=jetty.servlet.api --add-modules jetty.servlet.api + @{argLine} ${jetty.surefire.argLine} --add-reads org.eclipse.jetty.http2.client=jetty.servlet.api,org.eclipse.jetty.http2.hpack --add-modules jetty.servlet.api diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java index bab0b867fba..df88b4cda68 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java @@ -46,7 +46,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory private final ClientConnectionFactory connectionFactory; private boolean _directBuffersForEncryption = true; private boolean _directBuffersForDecryption = true; - private boolean allowMissingCloseMessage = true; + private boolean _requireCloseMessage; public SslClientConnectionFactory(SslContextFactory sslContextFactory, ByteBufferPool byteBufferPool, Executor executor, ClientConnectionFactory connectionFactory) { @@ -76,14 +76,22 @@ public class SslClientConnectionFactory implements ClientConnectionFactory return _directBuffersForEncryption; } - public boolean isAllowMissingCloseMessage() + /** + * @return whether peers must send the TLS {@code close_notify} message + * @see SslConnection#isRequireCloseMessage() + */ + public boolean isRequireCloseMessage() { - return allowMissingCloseMessage; + return _requireCloseMessage; } - public void setAllowMissingCloseMessage(boolean allowMissingCloseMessage) + /** + * @param requireCloseMessage whether peers must send the TLS {@code close_notify} message + * @see SslConnection#setRequireCloseMessage(boolean) + */ + public void setRequireCloseMessage(boolean requireCloseMessage) { - this.allowMissingCloseMessage = allowMissingCloseMessage; + _requireCloseMessage = requireCloseMessage; } @Override @@ -118,7 +126,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory SslConnection sslConnection = (SslConnection)connection; sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); sslConnection.setRenegotiationLimit(sslContextFactory.getRenegotiationLimit()); - sslConnection.setAllowMissingCloseMessage(isAllowMissingCloseMessage()); + sslConnection.setRequireCloseMessage(isRequireCloseMessage()); ContainerLifeCycle client = (ContainerLifeCycle)context.get(ClientConnectionFactory.CLIENT_CONTEXT_KEY); if (client != null) client.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java index 4e830f3fc3f..cf7db648b58 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java @@ -25,12 +25,14 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.ToIntFunction; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSession; import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.AbstractEndPoint; @@ -80,9 +82,10 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr private static final Logger LOG = Log.getLogger(SslConnection.class); private static final String TLS_1_3 = "TLSv1.3"; - private enum Handshake + private enum HandshakeState { INITIAL, + HANDSHAKE, SUCCEEDED, FAILED } @@ -113,10 +116,10 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr private boolean _renegotiationAllowed; private int _renegotiationLimit = -1; private boolean _closedOutbound; - private boolean _allowMissingCloseMessage = true; + private boolean _requireCloseMessage; private FlushState _flushState = FlushState.IDLE; private FillState _fillState = FillState.IDLE; - private AtomicReference _handshake = new AtomicReference<>(Handshake.INITIAL); + private AtomicReference _handshake = new AtomicReference<>(HandshakeState.INITIAL); private boolean _underflown; private abstract class RunnableTask implements Runnable, Invocable @@ -231,7 +234,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } /** - * @return The number of renegotions allowed for this connection. When the limit + * @return The number of renegotiations allowed for this connection. When the limit * is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied. */ public int getRenegotiationLimit() @@ -240,7 +243,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } /** - * @param renegotiationLimit The number of renegotions allowed for this connection. + * @param renegotiationLimit The number of renegotiations allowed for this connection. * When the limit is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied. * Default -1. */ @@ -249,20 +252,75 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr _renegotiationLimit = renegotiationLimit; } - public boolean isAllowMissingCloseMessage() + /** + * @return whether peers must send the TLS {@code close_notify} message + */ + public boolean isRequireCloseMessage() { - return _allowMissingCloseMessage; + return _requireCloseMessage; } - public void setAllowMissingCloseMessage(boolean allowMissingCloseMessage) + /** + *

Sets whether it is required that a peer send the TLS {@code close_notify} message + * to indicate the will to close the connection, otherwise it may be interpreted as a + * truncation attack.

+ *

This option is only useful on clients, since typically servers cannot accept + * connection-delimited content that may be truncated.

+ * + * @param requireCloseMessage whether peers must send the TLS {@code close_notify} message + */ + public void setRequireCloseMessage(boolean requireCloseMessage) { - this._allowMissingCloseMessage = allowMissingCloseMessage; + _requireCloseMessage = requireCloseMessage; + } + + private boolean isHandshakeInitial() + { + return _handshake.get() == HandshakeState.INITIAL; + } + + private boolean isHandshakeSucceeded() + { + return _handshake.get() == HandshakeState.SUCCEEDED; + } + + private boolean isHandshakeComplete() + { + HandshakeState state = _handshake.get(); + return state == HandshakeState.SUCCEEDED || state == HandshakeState.FAILED; + } + + private int getApplicationBufferSize() + { + return getBufferSize(SSLSession::getApplicationBufferSize); + } + + private int getPacketBufferSize() + { + return getBufferSize(SSLSession::getPacketBufferSize); + } + + private int getBufferSize(ToIntFunction bufferSizeFn) + { + SSLSession hsSession = _sslEngine.getHandshakeSession(); + SSLSession session = _sslEngine.getSession(); + int size = bufferSizeFn.applyAsInt(session); + if (hsSession == null || hsSession == session) + return size; + int hsSize = bufferSizeFn.applyAsInt(hsSession); + return Math.max(hsSize, size); } private void acquireEncryptedInput() { if (_encryptedInput == null) - _encryptedInput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers); + _encryptedInput = _bufferPool.acquire(getPacketBufferSize(), _encryptedDirectBuffers); + } + + private void acquireEncryptedOutput() + { + if (_encryptedOutput == null) + _encryptedOutput = _bufferPool.acquire(getPacketBufferSize(), _encryptedDirectBuffers); } @Override @@ -329,6 +387,16 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr _decryptedEndPoint.onFillableFail(cause == null ? new IOException() : cause); } + protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuffer output) throws SSLException + { + return sslEngine.wrap(input, output); + } + + protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException + { + return sslEngine.unwrap(input, output); + } + @Override public String toConnectionString() { @@ -350,6 +418,24 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr connection instanceof AbstractConnection ? ((AbstractConnection)connection).toConnectionString() : connection); } + private void releaseEncryptedInputBuffer() + { + if (_encryptedInput != null && !_encryptedInput.hasRemaining()) + { + _bufferPool.release(_encryptedInput); + _encryptedInput = null; + } + } + + protected void releaseDecryptedInputBuffer() + { + if (_decryptedInput != null && !_decryptedInput.hasRemaining()) + { + _bufferPool.release(_decryptedInput); + _decryptedInput = null; + } + } + private void releaseEncryptedOutputBuffer() { if (!Thread.holdsLock(_decryptedEndPoint)) @@ -361,6 +447,16 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } } + protected int networkFill(ByteBuffer input) throws IOException + { + return getEndPoint().fill(input); + } + + protected boolean networkFlush(ByteBuffer output) throws IOException + { + return getEndPoint().flush(output); + } + public class DecryptedEndPoint extends AbstractEndPoint { private final Callback _incompleteWriteCallback = new IncompleteWriteCallback(); @@ -475,9 +571,12 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr { if (connection instanceof AbstractConnection) { - AbstractConnection a = (AbstractConnection)connection; - if (a.getInputBufferSize() < _sslEngine.getSession().getApplicationBufferSize()) - a.setInputBufferSize(_sslEngine.getSession().getApplicationBufferSize()); + // This is an optimization to avoid that upper layer connections use small + // buffers and we need to copy decrypted data rather than decrypting in place. + AbstractConnection c = (AbstractConnection)connection; + int appBufferSize = getApplicationBufferSize(); + if (c.getInputBufferSize() < appBufferSize) + c.setInputBufferSize(appBufferSize); } super.setConnection(connection); } @@ -544,12 +643,13 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr // can we use the passed buffer if it is big enough ByteBuffer appIn; + int appBufferSize = getApplicationBufferSize(); if (_decryptedInput == null) { - if (BufferUtil.space(buffer) > _sslEngine.getSession().getApplicationBufferSize()) + if (BufferUtil.space(buffer) > appBufferSize) appIn = buffer; else - appIn = _decryptedInput = _bufferPool.acquire(_sslEngine.getSession().getApplicationBufferSize(), _decryptedDirectBuffers); + appIn = _decryptedInput = _bufferPool.acquire(appBufferSize, _decryptedDirectBuffers); } else { @@ -558,14 +658,23 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } // Let's try reading some encrypted data... even if we have some already. - int netFilled = getEndPoint().fill(_encryptedInput); - + int netFilled = networkFill(_encryptedInput); if (LOG.isDebugEnabled()) LOG.debug("net filled={}", netFilled); - if (netFilled > 0 && _handshake.get() == Handshake.INITIAL && isOutboundDone()) + // Workaround for Java 11 behavior. + if (netFilled < 0 && isHandshakeInitial() && BufferUtil.isEmpty(_encryptedInput)) + closeInbound(); + + if (netFilled > 0 && !isHandshakeComplete() && isOutboundDone()) throw new SSLHandshakeException("Closed during handshake"); + if (_handshake.compareAndSet(HandshakeState.INITIAL, HandshakeState.HANDSHAKE)) + { + if (LOG.isDebugEnabled()) + LOG.debug("fill starting handshake {}", SslConnection.this); + } + // Let's unwrap even if we have no net data because in that // case we want to fall through to the handshake handling int pos = BufferUtil.flipToFill(appIn); @@ -573,7 +682,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr try { _underflown = false; - unwrapResult = _sslEngine.unwrap(_encryptedInput, appIn); + unwrapResult = unwrap(_sslEngine, _encryptedInput, appIn); } finally { @@ -620,8 +729,21 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } return filled = netFilled; + case BUFFER_OVERFLOW: + // It's possible that SSLSession.applicationBufferSize has been expanded + // by the SSLEngine implementation. Unwrapping a large encrypted buffer + // causes BUFFER_OVERFLOW because the (old) applicationBufferSize is + // too small. Release the decrypted input buffer so it will be re-acquired + // with the larger capacity. + // See also system property "jsse.SSLEngine.acceptLargeFragments". + if (BufferUtil.isEmpty(_decryptedInput) && appBufferSize < getApplicationBufferSize()) + { + releaseDecryptedInputBuffer(); + continue; + } + throw new IllegalStateException("Unexpected unwrap result " + unwrap); + case OK: - { if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) handshakeSucceeded(); @@ -639,7 +761,6 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } break; - } default: throw new IllegalStateException("Unexpected unwrap result " + unwrap); @@ -648,8 +769,8 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } catch (Throwable x) { - Throwable failure = handleException(x, "fill"); - handshakeFailed(failure); + Throwable f = handleException(x, "fill"); + Throwable failure = handshakeFailed(f); if (_flushState == FlushState.WAIT_FOR_FILL) { _flushState = FlushState.IDLE; @@ -659,17 +780,8 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } finally { - if (_encryptedInput != null && !_encryptedInput.hasRemaining()) - { - _bufferPool.release(_encryptedInput); - _encryptedInput = null; - } - - if (_decryptedInput != null && !_decryptedInput.hasRemaining()) - { - _bufferPool.release(_decryptedInput); - _decryptedInput = null; - } + releaseEncryptedInputBuffer(); + releaseDecryptedInputBuffer(); if (_flushState == FlushState.WAIT_FOR_FILL) { @@ -771,7 +883,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr private void handshakeSucceeded() throws SSLException { - if (_handshake.compareAndSet(Handshake.INITIAL, Handshake.SUCCEEDED)) + if (_handshake.compareAndSet(HandshakeState.HANDSHAKE, HandshakeState.SUCCEEDED)) { if (LOG.isDebugEnabled()) LOG.debug("handshake succeeded {} {} {}/{}", SslConnection.this, @@ -779,16 +891,16 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr _sslEngine.getSession().getProtocol(), _sslEngine.getSession().getCipherSuite()); notifyHandshakeSucceeded(_sslEngine); } - else if (_handshake.get() == Handshake.SUCCEEDED) + else if (isHandshakeSucceeded()) { if (_renegotiationLimit > 0) _renegotiationLimit--; } } - private void handshakeFailed(Throwable failure) + private Throwable handshakeFailed(Throwable failure) { - if (_handshake.compareAndSet(Handshake.INITIAL, Handshake.FAILED)) + if (_handshake.compareAndSet(HandshakeState.HANDSHAKE, HandshakeState.FAILED)) { if (LOG.isDebugEnabled()) LOG.debug("handshake failed {} {}", SslConnection.this, failure); @@ -796,6 +908,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr failure = new SSLHandshakeException(failure.getMessage()).initCause(failure); notifyHandshakeFailed(_sslEngine, failure); } + return failure; } private void terminateInput() @@ -820,7 +933,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } catch (SSLException x) { - if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && !isAllowMissingCloseMessage()) + if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && isRequireCloseMessage()) throw x; LOG.ignore(x); return x; @@ -850,7 +963,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } // finish of any previous flushes - if (BufferUtil.hasContent(_encryptedOutput) && !getEndPoint().flush(_encryptedOutput)) + if (BufferUtil.hasContent(_encryptedOutput) && !networkFlush(_encryptedOutput)) return false; boolean isEmpty = BufferUtil.isEmpty(appOuts); @@ -878,6 +991,9 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr continue; case NEED_UNWRAP: + // Workaround for Java 11 behavior. + if (isHandshakeInitial() && isOutboundDone()) + break; if (_fillState == FillState.IDLE) { int filled = fill(BufferUtil.EMPTY_BUFFER); @@ -892,16 +1008,23 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr throw new IllegalStateException("Unexpected HandshakeStatus " + status); } - if (_encryptedOutput == null) - _encryptedOutput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers); + int packetBufferSize = getPacketBufferSize(); + acquireEncryptedOutput(); - // We call sslEngine.wrap to try to take bytes from appOut buffers and encrypt them into the _netOut buffer + if (_handshake.compareAndSet(HandshakeState.INITIAL, HandshakeState.HANDSHAKE)) + { + if (LOG.isDebugEnabled()) + LOG.debug("flush starting handshake {}", SslConnection.this); + } + + // We call sslEngine.wrap to try to take bytes from appOuts + // buffers and encrypt them into the _encryptedOutput buffer. BufferUtil.compact(_encryptedOutput); int pos = BufferUtil.flipToFill(_encryptedOutput); SSLEngineResult wrapResult; try { - wrapResult = _sslEngine.wrap(appOuts, _encryptedOutput); + wrapResult = wrap(_sslEngine, appOuts, _encryptedOutput); } finally { @@ -920,7 +1043,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr // if we have net bytes, let's try to flush them boolean flushed = true; if (BufferUtil.hasContent(_encryptedOutput)) - flushed = getEndPoint().flush(_encryptedOutput); + flushed = networkFlush(_encryptedOutput); if (LOG.isDebugEnabled()) LOG.debug("net flushed={}, ac={}", flushed, isEmpty); @@ -944,7 +1067,18 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr case BUFFER_OVERFLOW: if (!flushed) return result = false; - continue; + // It's possible that SSLSession.packetBufferSize has been expanded + // by the SSLEngine implementation. Wrapping a large application buffer + // causes BUFFER_OVERFLOW because the (old) packetBufferSize is + // too small. Release the encrypted output buffer so that it will + // be re-acquired with the larger capacity. + // See also system property "jsse.SSLEngine.acceptLargeFragments". + if (packetBufferSize < getPacketBufferSize()) + { + releaseEncryptedOutputBuffer(); + continue; + } + throw new IllegalStateException("Unexpected wrap result " + wrap); case OK: if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) @@ -980,8 +1114,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr catch (Throwable x) { Throwable failure = handleException(x, "flush"); - handshakeFailed(failure); - throw failure; + throw handshakeFailed(failure); } finally { @@ -1096,15 +1229,15 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr @Override public void doShutdownOutput() { - final EndPoint endp = getEndPoint(); + EndPoint endPoint = getEndPoint(); try { boolean close; boolean flush = false; synchronized (_decryptedEndPoint) { - boolean ishut = endp.isInputShutdown(); - boolean oshut = endp.isOutputShutdown(); + boolean ishut = endPoint.isInputShutdown(); + boolean oshut = endPoint.isOutputShutdown(); if (LOG.isDebugEnabled()) LOG.debug("shutdownOutput: {} oshut={}, ishut={}", SslConnection.this, oshut, ishut); @@ -1128,19 +1261,19 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr // let's just flush the encrypted output in the background. ByteBuffer write = _encryptedOutput; if (BufferUtil.hasContent(write)) - endp.write(Callback.from(Callback.NOOP::succeeded, t -> endp.close()), write); + endPoint.write(Callback.from(Callback.NOOP::succeeded, t -> endPoint.close()), write); } } if (close) - endp.close(); + endPoint.close(); else ensureFillInterested(); } catch (Throwable x) { LOG.ignore(x); - endp.close(); + endPoint.close(); } } @@ -1152,7 +1285,8 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } catch (Throwable x) { - LOG.ignore(x); + if (LOG.isDebugEnabled()) + LOG.debug(x); } } @@ -1258,7 +1392,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr private boolean isRenegotiating() { - if (_handshake.get() == Handshake.INITIAL) + if (!isHandshakeComplete()) return false; if (isTLS13()) return false; diff --git a/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/TestForkedChild.java b/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/TestForkedChild.java index 6a3114e88a5..597ef61145d 100644 --- a/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/TestForkedChild.java +++ b/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/TestForkedChild.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.maven.plugin; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStreamReader; import java.io.LineNumberReader; @@ -31,8 +32,10 @@ import java.util.List; import java.util.Locale; import java.util.Random; +import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.resource.Resource; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -51,7 +54,6 @@ public class TestForkedChild File baseDir; File tmpDir; File tokenFile; - File forkWebXml; File webappPropsFile; int stopPort; String stopKey = "FERMATI"; @@ -82,6 +84,7 @@ public class TestForkedChild JettyWebAppContext webapp = new JettyWebAppContext(); webapp.setContextPath("/foo"); webapp.setTempDirectory(tmpDir); + webapp.setBaseResource(Resource.newResource(baseDir)); WebAppPropertyConverter.toProperties(webapp, webappPropsFile, null); child = new JettyForkedChild(cmd.toArray(new String[cmd.size()])); child.jetty.setExitVm(false); //ensure jetty doesn't stop vm for testing @@ -100,10 +103,9 @@ public class TestForkedChild baseDir = MavenTestingUtils.getTestResourceDir("root"); testDir = MavenTestingUtils.getTargetTestingDir("forkedChild"); if (testDir.exists()) - IO.delete(testDir); + FS.delete(testDir); testDir.mkdirs(); tmpDir = new File(testDir, "tmp"); - forkWebXml = new File(testDir, "fork-web.xml"); webappPropsFile = new File(testDir, "webapp.props"); stopPort = Integer.valueOf(System.getProperty("stop.port")); @@ -165,6 +167,9 @@ public class TestForkedChild connection = (HttpURLConnection)url.openConnection(); connection.connect(); assertThat(connection.getResponseCode(), Matchers.is(200)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IO.copy(connection.getInputStream(), baos); + assertThat(baos.toString(), Matchers.containsString("ROOT")); } finally { diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java index 41fce0be5ee..0642b422a24 100644 --- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java @@ -246,6 +246,16 @@ public class OpenIdAuthenticator extends LoginAuthenticator try { + if (request.isRequestedSessionIdFromURL()) + { + if (LOG.isDebugEnabled()) + LOG.debug("Session ID should be cookie for OpenID authentication to work"); + + int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); + baseResponse.sendRedirect(redirectCode, URIUtil.addPaths(request.getContextPath(), _errorPage)); + return Authentication.SEND_FAILURE; + } + // Handle a request for authentication. if (isJSecurityCheck(uri)) { @@ -288,7 +298,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator response.setContentLength(0); int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); - baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(nuri)); + baseResponse.sendRedirect(redirectCode, nuri); return openIdAuth; } } @@ -308,7 +318,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator if (LOG.isDebugEnabled()) LOG.debug("auth failed {}", _errorPage); int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); - baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _errorPage))); + baseResponse.sendRedirect(redirectCode, URIUtil.addPaths(request.getContextPath(), _errorPage)); } return Authentication.SEND_FAILURE; @@ -399,7 +409,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator if (LOG.isDebugEnabled()) LOG.debug("challenge {}->{}", session.getId(), challengeUri); int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); - baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(challengeUri)); + baseResponse.sendRedirect(redirectCode, challengeUri); return Authentication.SEND_CONTINUE; } diff --git a/jetty-server/src/main/config/modules/gzip.mod b/jetty-server/src/main/config/modules/gzip.mod index 24c045a37c4..96be4bb9c01 100644 --- a/jetty-server/src/main/config/modules/gzip.mod +++ b/jetty-server/src/main/config/modules/gzip.mod @@ -15,7 +15,7 @@ etc/jetty-gzip.xml [ini-template] ## Minimum content length after which gzip is enabled -# jetty.gzip.minGzipSize=2048 +# jetty.gzip.minGzipSize=32 ## Check whether a file with *.gz extension exists # jetty.gzip.checkGzExists=false diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index e8a6e3f8d86..f0018f7e745 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -500,7 +500,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } // RFC 7230, section 3.3. - if (!_request.isHead() && !_response.isContentComplete(_response.getHttpOutput().getWritten())) + if (!_request.isHead() && + _response.getStatus() != HttpStatus.NOT_MODIFIED_304 && + !_response.isContentComplete(_response.getHttpOutput().getWritten())) { if (sendErrorOrAbort("Insufficient content written")) break; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 88c7d839faf..feaf21afd12 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -364,6 +364,13 @@ public class HttpOutput extends ServletOutputStream implements Runnable State state = _state.get(); switch (state) { + case CLOSING: + { + if (!_state.compareAndSet(state, State.CLOSED)) + break; + releaseBuffer(); + return; + } case CLOSED: { return; 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 6f2b3c563aa..875b532cfb3 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 @@ -110,8 +110,8 @@ import org.eclipse.jetty.util.resource.Resource; @ManagedObject("URI Context") public class ContextHandler extends ScopedHandler implements Attributes, Graceful { - public static final int SERVLET_MAJOR_VERSION = 3; - public static final int SERVLET_MINOR_VERSION = 1; + public static final int SERVLET_MAJOR_VERSION = 4; + public static final int SERVLET_MINOR_VERSION = 0; public static final Class[] SERVLET_LISTENER_TYPES = { ServletContextListener.class, diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java index bf2a96cffe6..156a92debbf 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java @@ -188,13 +188,16 @@ public class InetAccessHandler extends HandlerWrapper protected boolean isAllowed(InetAddress addr, Request baseRequest, HttpServletRequest request) { String name = baseRequest.getHttpChannel().getConnector().getName(); + boolean filterAppliesToConnector = _names.test(name); + boolean allowedByAddr = _addrs.test(addr); if (LOG.isDebugEnabled()) { - Boolean allowedByName = _names.isIncludedAndNotExcluded(name); - Boolean allowedByAddr = _addrs.isIncludedAndNotExcluded(addr); - LOG.debug("{} allowedByName={} allowedByAddr={} for {}/{}", this, allowedByName, allowedByAddr, addr, request); + LOG.debug("name = {}/{} addr={}/{} appliesToConnector={} allowedByAddr={}", + name, _names, addr, _addrs, filterAppliesToConnector, allowedByAddr); } - return _names.test(name) && _addrs.test(addr); + if (!filterAppliesToConnector) + return true; + return allowedByAddr; } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index 4ad1bfdb844..86a8995f9ab 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -25,7 +25,6 @@ import java.util.ListIterator; import java.util.Set; import java.util.regex.Pattern; import java.util.zip.Deflater; - import javax.servlet.DispatcherType; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -92,8 +91,7 @@ import org.eclipse.jetty.util.log.Logger; * *
  • * Is the Response {@code Content-Length} header present, and does its - * value meet the minimum gzip size requirements? - *
    (Default: 16 bytes. see {@link GzipHandler#DEFAULT_MIN_GZIP_SIZE}) + * value meet the minimum gzip size requirements (default 32 bytes)? *
  • *
  • * Is the Request {@code Accept} header present and does it contain the @@ -155,8 +153,8 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory { public static final String GZIP = "gzip"; public static final String DEFLATE = "deflate"; - public static final int DEFAULT_MIN_GZIP_SIZE = 2048; - public static final int COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION; + public static final int DEFAULT_MIN_GZIP_SIZE = 32; + public static final int BREAK_EVEN_GZIP_SIZE = 23; private static final Logger LOG = Log.getLogger(GzipHandler.class); private static final HttpField X_CE_GZIP = new PreEncodedHttpField("X-Content-Encoding", "gzip"); private static final HttpField TE_CHUNKED = new PreEncodedHttpField(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED.asString()); @@ -877,13 +875,19 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory } /** - * Set the minimum response size to trigger dynamic compression + * Set the minimum response size to trigger dynamic compression. + *

    + * Sizes below {@link #BREAK_EVEN_GZIP_SIZE} will result a compressed response that is larger than the + * original data. + *

    * - * @param minGzipSize minimum response size in bytes + * @param minGzipSize minimum response size in bytes (not allowed to be lower then {@link #BREAK_EVEN_GZIP_SIZE}) */ public void setMinGzipSize(int minGzipSize) { - _minGzipSize = minGzipSize; + if (minGzipSize < BREAK_EVEN_GZIP_SIZE) + LOG.warn("minGzipSize of {} is inefficient for short content, break even is size {}", minGzipSize, BREAK_EVEN_GZIP_SIZE); + _minGzipSize = Math.max(0, minGzipSize); } /** diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java index fb899eb0c9a..f0d3c859f06 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java @@ -113,7 +113,7 @@ public class DumpHandler extends AbstractHandler.ErrorDispatchHandler writer.write("
    \nlocal=" + request.getLocalAddr() + ":" + request.getLocalPort() + "\n
    \n"); writer.write("
    \nremote=" + request.getRemoteAddr() + ":" + request.getRemotePort() + "\n
    \n"); writer.write("

    Header:

    ");
    -        writer.write(request.getMethod() + " " + request.getRequestURI() + " " + request.getProtocol() + "\n");
    +        writer.write(String.format("%4s %s %s\n", request.getMethod(), request.getRequestURI(), request.getProtocol()));
             Enumeration headers = request.getHeaderNames();
             while (headers.hasMoreElements())
             {
    diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
    index 2d5560efc95..4fd88f1cfa4 100644
    --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
    +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
    @@ -30,10 +30,13 @@ import java.io.IOException;
     import java.io.OutputStream;
     import java.io.PrintWriter;
     import java.io.StringReader;
    +import java.util.Arrays;
     import java.util.HashSet;
    +import java.util.List;
     import java.util.Set;
     import java.util.concurrent.CountDownLatch;
     import java.util.concurrent.TimeUnit;
    +import java.util.stream.Stream;
     import javax.servlet.ServletException;
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
    @@ -54,6 +57,9 @@ import org.hamcrest.Matchers;
     import org.junit.jupiter.api.AfterEach;
     import org.junit.jupiter.api.BeforeEach;
     import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.params.ParameterizedTest;
    +import org.junit.jupiter.params.provider.Arguments;
    +import org.junit.jupiter.params.provider.MethodSource;
     
     import static org.hamcrest.MatcherAssert.assertThat;
     import static org.hamcrest.Matchers.containsString;
    @@ -265,43 +271,172 @@ public class HttpConnectionTest
             }
         }
     
    +    static final int CHUNKED = -1;
    +    static final int DQUOTED_CHUNKED = -2;
    +    static final int BAD_CHUNKED = -3;
    +    static final int UNKNOWN_TE = -4;
    +
    +    public static Stream http11ContentLengthAndChunkedData()
    +    {
    +        return Stream.of(
    +            Arguments.of(new int[]{CHUNKED, 8}),
    +            Arguments.of(new int[]{8, CHUNKED}),
    +            Arguments.of(new int[]{8, CHUNKED, 8}),
    +            Arguments.of(new int[]{DQUOTED_CHUNKED, 8}),
    +            Arguments.of(new int[]{8, DQUOTED_CHUNKED}),
    +            Arguments.of(new int[]{8, DQUOTED_CHUNKED, 8}),
    +            Arguments.of(new int[]{BAD_CHUNKED, 8}),
    +            Arguments.of(new int[]{8, BAD_CHUNKED}),
    +            Arguments.of(new int[]{8, BAD_CHUNKED, 8}),
    +            Arguments.of(new int[]{UNKNOWN_TE, 8}),
    +            Arguments.of(new int[]{8, UNKNOWN_TE}),
    +            Arguments.of(new int[]{8, UNKNOWN_TE, 8}),
    +            Arguments.of(new int[]{8, UNKNOWN_TE, CHUNKED, DQUOTED_CHUNKED, BAD_CHUNKED, 8})
    +        );
    +    }
    +
         /**
          * More then 1 Content-Length is a bad requests per HTTP rfcs.
          */
    -    @Test
    -    public void testHttp11ContentLengthAndChunk() throws Exception
    +    @ParameterizedTest
    +    @MethodSource("http11ContentLengthAndChunkedData")
    +    public void testHttp11ContentLengthAndChunk(int[] contentLengths) throws Exception
         {
             HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED...");
    -        int[][] contentLengths = {
    -            {-1, 8},
    -            {8, -1},
    -            {8, -1, 8},
    -            };
     
    -        for (int x = 0; x < contentLengths.length; x++)
    +        StringBuilder request = new StringBuilder();
    +        request.append("POST / HTTP/1.1\r\n");
    +        request.append("Host: local\r\n");
    +        for (int n = 0; n < contentLengths.length; n++)
             {
    -            StringBuilder request = new StringBuilder();
    -            request.append("POST /?id=").append(Integer.toString(x)).append(" HTTP/1.1\r\n");
    -            request.append("Host: local\r\n");
    -            int[] clen = contentLengths[x];
    -            for (int n = 0; n < clen.length; n++)
    +            switch (contentLengths[n])
                 {
    -                if (clen[n] == -1)
    +                case CHUNKED:
                         request.append("Transfer-Encoding: chunked\r\n");
    -                else
    -                    request.append("Content-Length: ").append(Integer.toString(clen[n])).append("\r\n");
    +                    break;
    +                case DQUOTED_CHUNKED:
    +                    request.append("Transfer-Encoding: \"chunked\"\r\n");
    +                    break;
    +                case BAD_CHUNKED:
    +                    request.append("Transfer-Encoding: 'chunked'\r\n");
    +                    break;
    +                case UNKNOWN_TE:
    +                    request.append("Transfer-Encoding: bogus\r\n");
    +                    break;
    +                default:
    +                    request.append("Content-Length: ").append(contentLengths[n]).append("\r\n");
    +                    break;
                 }
    -            request.append("Content-Type: text/plain\r\n");
    -            request.append("Connection: close\r\n");
    -            request.append("\r\n");
    -            request.append("8;\r\n"); // chunk header
    -            request.append("abcdefgh"); // actual content of 8 bytes
    -            request.append("\r\n0;\r\n"); // last chunk
    -
    -            String rawResponse = connector.getResponse(request.toString());
    -            HttpTester.Response response = HttpTester.parseResponse(rawResponse);
    -            assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
             }
    +        request.append("Content-Type: text/plain\r\n");
    +        request.append("\r\n");
    +        request.append("8;\r\n"); // chunk header
    +        request.append("abcdefgh"); // actual content of 8 bytes
    +        request.append("\r\n0;\r\n\r\n"); // last chunk
    +
    +        String rawResponse = connector.getResponse(request.toString());
    +        HttpTester.Response response = HttpTester.parseResponse(rawResponse);
    +        assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
    +    }
    +
    +    /**
    +     * Examples of valid Chunked behaviors.
    +     */
    +    public static Stream http11TransferEncodingChunked()
    +    {
    +        return Stream.of(
    +            Arguments.of(Arrays.asList("chunked, ")), // results in 1 entry
    +            Arguments.of(Arrays.asList(", chunked")),
    +
    +            // invalid tokens with chunked as last
    +            // no conflicts, chunked token is specified and is last, will result in chunked
    +            Arguments.of(Arrays.asList("bogus, chunked")),
    +            Arguments.of(Arrays.asList("'chunked', chunked")), // apostrophe characters with and without
    +            Arguments.of(Arrays.asList("identity, chunked")), // identity was removed in RFC2616 errata and has been dropped in RFC7230
    +
    +            // multiple headers
    +            Arguments.of(Arrays.asList("identity", "chunked")), // 2 separate headers
    +            Arguments.of(Arrays.asList("", "chunked")) // 2 separate headers
    +        );
    +    }
    +
    +    /**
    +     * Test Chunked Transfer-Encoding behavior indicated by
    +     * https://tools.ietf.org/html/rfc7230#section-3.3.1
    +     */
    +    @ParameterizedTest
    +    @MethodSource("http11TransferEncodingChunked")
    +    public void testHttp11TransferEncodingChunked(List tokens) throws Exception
    +    {
    +        StringBuilder request = new StringBuilder();
    +        request.append("POST / HTTP/1.1\r\n");
    +        request.append("Host: local\r\n");
    +        tokens.forEach((token) -> request.append("Transfer-Encoding: ").append(token).append("\r\n"));
    +        request.append("Content-Type: text/plain\r\n");
    +        request.append("\r\n");
    +        request.append("8;\r\n"); // chunk header
    +        request.append("abcdefgh"); // actual content of 8 bytes
    +        request.append("\r\n0;\r\n\r\n"); // last chunk
    +
    +        System.out.println(request.toString());
    +
    +        String rawResponse = connector.getResponse(request.toString());
    +        HttpTester.Response response = HttpTester.parseResponse(rawResponse);
    +        assertThat("Response.status (" + response.getReason() + ")", response.getStatus(), is(HttpServletResponse.SC_OK));
    +    }
    +
    +    public static Stream http11TransferEncodingInvalidChunked()
    +    {
    +        return Stream.of(
    +            // == Results in 400 Bad Request
    +            Arguments.of(Arrays.asList("bogus", "identity")), // 2 separate headers
    +
    +            Arguments.of(Arrays.asList("bad")),
    +            Arguments.of(Arrays.asList("identity")),  // identity was removed in RFC2616 errata and has been dropped in RFC7230
    +            Arguments.of(Arrays.asList("'chunked'")), // apostrophe characters
    +            Arguments.of(Arrays.asList("`chunked`")), // backtick "quote" characters
    +            Arguments.of(Arrays.asList("[chunked]")), // bracketed (seen as mistake in several REST libraries)
    +            Arguments.of(Arrays.asList("{chunked}")), // json'd (seen as mistake in several REST libraries)
    +            Arguments.of(Arrays.asList("\u201Cchunked\u201D")), // opening and closing (fancy) double quotes characters
    +
    +            // invalid tokens with chunked not as last
    +            Arguments.of(Arrays.asList("chunked, bogus")),
    +            Arguments.of(Arrays.asList("chunked, 'chunked'")),
    +            Arguments.of(Arrays.asList("chunked, identity")),
    +            Arguments.of(Arrays.asList("chunked, identity, chunked")), // duplicate chunked
    +            Arguments.of(Arrays.asList("chunked", "identity")), // 2 separate header lines
    +
    +            // multiple chunked tokens present
    +            Arguments.of(Arrays.asList("chunked", "identity", "chunked")), // 3 separate header lines
    +            Arguments.of(Arrays.asList("chunked", "chunked")), // 2 separate header lines
    +            Arguments.of(Arrays.asList("chunked, chunked")) // on same line
    +        );
    +    }
    +
    +    /**
    +     * Test bad Transfer-Encoding behavior as indicated by
    +     * https://tools.ietf.org/html/rfc7230#section-3.3.1
    +     */
    +    @ParameterizedTest
    +    @MethodSource("http11TransferEncodingInvalidChunked")
    +    public void testHttp11TransferEncodingInvalidChunked(List tokens) throws Exception
    +    {
    +        HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED...");
    +        StringBuilder request = new StringBuilder();
    +        request.append("POST / HTTP/1.1\r\n");
    +        request.append("Host: local\r\n");
    +        tokens.forEach((token) -> request.append("Transfer-Encoding: ").append(token).append("\r\n"));
    +        request.append("Content-Type: text/plain\r\n");
    +        request.append("\r\n");
    +        request.append("8;\r\n"); // chunk header
    +        request.append("abcdefgh"); // actual content of 8 bytes
    +        request.append("\r\n0;\r\n\r\n"); // last chunk
    +
    +        System.out.println(request.toString());
    +
    +        String rawResponse = connector.getResponse(request.toString());
    +        HttpTester.Response response = HttpTester.parseResponse(rawResponse);
    +        assertThat("Response.status", response.getStatus(), is(HttpServletResponse.SC_BAD_REQUEST));
         }
     
         @Test
    @@ -549,11 +684,10 @@ public class HttpConnectionTest
                 "Host: localhost\r\n" +
                 "Transfer-Encoding: chunked\r\n" +
                 "Content-Type: text/plain\r\n" +
    -            "Connection: close\r\n" +
                 "\r\n" +
                 "A\r\n" +
                 "0123456789\r\n" +
    -            "0\r\n");
    +            "0\r\n\r\n");
     
             int offset = 0;
             offset = checkContains(response, offset, "HTTP/1.1 200");
    diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java
    index 874a7449aba..a0adebfe366 100644
    --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java
    +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpManyWaysToCommitTest.java
    @@ -24,6 +24,7 @@ import javax.servlet.ServletException;
     import javax.servlet.http.HttpServletRequest;
     import javax.servlet.http.HttpServletResponse;
     
    +import org.eclipse.jetty.http.HttpStatus;
     import org.eclipse.jetty.http.HttpVersion;
     import org.eclipse.jetty.http.tools.HttpTester;
     import org.eclipse.jetty.server.handler.AbstractHandler;
    @@ -432,6 +433,21 @@ public class HttpManyWaysToCommitTest extends AbstractHttpTest
             }
         }
     
    +    @ParameterizedTest
    +    @MethodSource("httpVersions")
    +    public void testSetContentLengthAnd304Status(HttpVersion httpVersion) throws Exception
    +    {
    +        server.setHandler(new SetContentLength304Handler());
    +        server.start();
    +
    +        HttpTester.Response response = executeRequest(httpVersion);
    +        assertThat("response code", response.getStatus(), is(304));
    +        assertThat(response, containsHeaderValue("content-length", "32768"));
    +        byte[] content = response.getContentBytes();
    +        assertThat(content.length, is(0));
    +        assertFalse(response.isEarlyEOF());
    +    }
    +
         @ParameterizedTest
         @MethodSource("httpVersions")
         public void testSetContentLengthFlushAndWriteInsufficientBytes(HttpVersion httpVersion) throws Exception
    @@ -519,6 +535,21 @@ public class HttpManyWaysToCommitTest extends AbstractHttpTest
             }
         }
     
    +    private class SetContentLength304Handler extends AbstractHandler
    +    {
    +        private SetContentLength304Handler()
    +        {
    +        }
    +
    +        @Override
    +        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
    +        {
    +            baseRequest.setHandled(true);
    +            response.setContentLength(32768);
    +            response.setStatus(HttpStatus.NOT_MODIFIED_304);
    +        }
    +    }
    +
         private class SetContentLengthAndWriteThatAmountOfBytesHandler extends ThrowExceptionOnDemandHandler
         {
             private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException)
    diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java b/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java
    index 47c62ec54bf..5a08087b163 100644
    --- a/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java
    +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/PartialRFC2616Test.java
    @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test;
     
     import static org.hamcrest.MatcherAssert.assertThat;
     import static org.hamcrest.Matchers.containsString;
    +import static org.hamcrest.Matchers.is;
     import static org.hamcrest.Matchers.not;
     import static org.hamcrest.Matchers.nullValue;
     import static org.junit.jupiter.api.Assertions.assertEquals;
    @@ -106,6 +107,33 @@ public class PartialRFC2616Test
             }
         }
     
    +
    +    @Test
    +    public void test3_3_2()
    +    {
    +        try
    +        {
    +            String get = connector.getResponse("GET /R1 HTTP/1.0\n" + "Host: localhost\n" + "\n");
    +            checkContains(get, 0, "HTTP/1.1 200", "GET");
    +            checkContains(get, 0, "Content-Type: text/html", "GET _content");
    +            checkContains(get, 0, "", "GET body");
    +            int cli = get.indexOf("Content-Length");
    +            String contentLength = get.substring(cli,get.indexOf("\r",cli));
    +
    +            String head = connector.getResponse("HEAD /R1 HTTP/1.0\n" + "Host: localhost\n" + "\n");
    +            checkContains(head, 0, "HTTP/1.1 200", "HEAD");
    +            checkContains(head, 0, "Content-Type: text/html", "HEAD _content");
    +            assertEquals(-1, head.indexOf(""), "HEAD no body");
    +            checkContains(head, 0, contentLength, "3.3.2 HEAD");
    +        }
    +        catch (Exception e)
    +        {
    +            e.printStackTrace();
    +            assertTrue(false);
    +        }
    +    }
    +
    +
         @Test
         public void test3_6_a() throws Exception
         {
    @@ -324,12 +352,10 @@ public class PartialRFC2616Test
                     "\n");
             offset = 0;
             response = endp.getResponse();
    -        offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10;
    -        offset = checkContains(response, offset, "/R1", "2. identity") + 3;
    +        offset = checkContains(response, offset, "HTTP/1.1 400 ", "2. identity") + 10;
             offset = 0;
             response = endp.getResponse();
    -        offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10;
    -        offset = checkContains(response, offset, "/R2", "2. identity") + 3;
    +        assertThat("There should be no next response as first one closed connection", response, is(nullValue()));
         }
     
         @Test
    @@ -361,7 +387,7 @@ public class PartialRFC2616Test
                     "\n" +
                     "abcdef");
             response = endp.getResponse();
    -        offset = checkContains(response, offset, "HTTP/1.1 400 Bad", "3. ignore c-l") + 1;
    +        offset = checkContains(response, offset, "HTTP/1.1 400 ", "3. ignore c-l") + 1;
             checkNotContained(response, offset, "/R2", "3. _content-length");
         }
     
    diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ServletWriterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ServletWriterTest.java
    new file mode 100644
    index 00000000000..3728880a5f6
    --- /dev/null
    +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ServletWriterTest.java
    @@ -0,0 +1,127 @@
    +//
    +//  ========================================================================
    +//  Copyright (c) 1995-2019 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.server;
    +
    +import java.io.BufferedReader;
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.InputStreamReader;
    +import java.io.OutputStream;
    +import java.io.PrintWriter;
    +import java.net.Socket;
    +import java.util.Arrays;
    +import java.util.concurrent.CountDownLatch;
    +import java.util.concurrent.TimeUnit;
    +import java.util.concurrent.atomic.AtomicReference;
    +import javax.servlet.ServletException;
    +import javax.servlet.http.HttpServletRequest;
    +import javax.servlet.http.HttpServletResponse;
    +
    +import org.eclipse.jetty.server.handler.AbstractHandler;
    +import org.junit.jupiter.api.AfterEach;
    +import org.junit.jupiter.api.Test;
    +
    +import static java.nio.charset.StandardCharsets.UTF_8;
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.containsString;
    +import static org.junit.jupiter.api.Assertions.assertTrue;
    +
    +public class ServletWriterTest
    +{
    +    private Server server;
    +    private ServerConnector connector;
    +
    +    private void start(int aggregationSize, Handler handler) throws Exception
    +    {
    +        server = new Server();
    +        HttpConfiguration httpConfig = new HttpConfiguration();
    +        httpConfig.setOutputBufferSize(2 * aggregationSize);
    +        httpConfig.setOutputAggregationSize(2 * aggregationSize);
    +        connector = new ServerConnector(server, 1, 1, new HttpConnectionFactory(httpConfig));
    +        server.addConnector(connector);
    +        server.setHandler(handler);
    +        server.start();
    +    }
    +
    +    @AfterEach
    +    public void dispose() throws Exception
    +    {
    +        server.stop();
    +    }
    +
    +    @Test
    +    public void testTCPCongestedCloseDoesNotDeadlock() throws Exception
    +    {
    +        // Write a large content so it gets TCP congested when calling close().
    +        char[] chars = new char[128 * 1024 * 1024];
    +        CountDownLatch latch = new CountDownLatch(1);
    +        AtomicReference serverThreadRef = new AtomicReference<>();
    +        start(chars.length, new AbstractHandler.ErrorDispatchHandler() {
    +            @Override
    +            protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
    +            {
    +                serverThreadRef.set(Thread.currentThread());
    +                jettyRequest.setHandled(true);
    +                response.setContentType("text/plain; charset=utf-8");
    +                PrintWriter writer = response.getWriter();
    +                Arrays.fill(chars, '0');
    +                // The write is entirely buffered.
    +                writer.write(chars);
    +                latch.countDown();
    +                // Closing will trigger the write over the network.
    +                writer.close();
    +            }
    +        });
    +
    +        try (Socket socket = new Socket("localhost", connector.getLocalPort()))
    +        {
    +            String request = "GET / HTTP/1.1\r\n" +
    +                "Host: localhost\r\n" +
    +                "Connection: close\r\n" +
    +                "\r\n";
    +            OutputStream output = socket.getOutputStream();
    +            output.write(request.getBytes(UTF_8));
    +            output.flush();
    +
    +            // Wait until the response is buffered, so close() will write it.
    +            assertTrue(latch.await(5, TimeUnit.SECONDS));
    +            // Don't read the response yet to trigger TCP congestion.
    +            Thread.sleep(1000);
    +
    +            // Now read the response.
    +            socket.setSoTimeout(5000);
    +            InputStream input = socket.getInputStream();
    +            BufferedReader reader = new BufferedReader(new InputStreamReader(input, UTF_8));
    +            String line = reader.readLine();
    +            assertThat(line, containsString(" 200 "));
    +            // Consume all the content, we should see EOF.
    +            while (line != null)
    +            {
    +                line = reader.readLine();
    +            }
    +        }
    +        catch (Throwable x)
    +        {
    +            Thread thread = serverThreadRef.get();
    +            if (thread != null)
    +                thread.interrupt();
    +            throw x;
    +        }
    +    }
    +}
    diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java
    index 277d2155fe9..a96c48a82e1 100644
    --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java
    +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java
    @@ -21,7 +21,9 @@ package org.eclipse.jetty.server.handler;
     import java.io.IOException;
     import java.net.Socket;
     import java.nio.ByteBuffer;
    +import java.util.ArrayList;
     import java.util.Arrays;
    +import java.util.List;
     import java.util.stream.Stream;
     import javax.servlet.ServletException;
     import javax.servlet.http.HttpServletRequest;
    @@ -45,17 +47,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
     public class InetAccessHandlerTest
     {
         private static Server _server;
    -    private static ServerConnector _connector;
    +    private static ServerConnector _connector1;
    +    private static ServerConnector _connector2;
         private static InetAccessHandler _handler;
     
         @BeforeAll
         public static void setUp() throws Exception
         {
             _server = new Server();
    -        _connector = new ServerConnector(_server);
    -        _connector.setName("http");
    +        _connector1 = new ServerConnector(_server);
    +        _connector1.setName("http_connector1");
    +        _connector2 = new ServerConnector(_server);
    +        _connector2.setName("http_connector2");
             _server.setConnectors(new Connector[]
    -            {_connector});
    +            {_connector1, _connector2});
     
             _handler = new InetAccessHandler();
             _handler.setHandler(new AbstractHandler()
    @@ -113,7 +118,21 @@ public class InetAccessHandlerTest
                 }
             }
     
    -        try (Socket socket = new Socket("127.0.0.1", _connector.getLocalPort());)
    +        List codePerConnector = new ArrayList<>();
    +        for (String nextCode : code.split(";", -1))
    +        {
    +            if (nextCode.length() > 0)
    +            {
    +                codePerConnector.add(nextCode);
    +            }
    +        }
    +
    +        testConnector(_connector1.getLocalPort(), include, exclude, includeConnectors, excludeConnectors, codePerConnector.get(0));
    +        testConnector(_connector2.getLocalPort(), include, exclude, includeConnectors, excludeConnectors, codePerConnector.get(1));
    +    }
    +
    +    private void testConnector(int port, String include, String exclude, String includeConnectors, String excludeConnectors, String code) throws IOException {
    +        try (Socket socket = new Socket("127.0.0.1", port);)
             {
                 socket.setSoTimeout(5000);
     
    @@ -136,39 +155,68 @@ public class InetAccessHandlerTest
             }
         }
     
    +    /**
    +     * Data for this test.
    +     * @return Format of data: include;exclude;includeConnectors;excludeConnectors;assertionStatusCodePerConnector
    +     */
         public static Stream data()
         {
             Object[][] data = new Object[][]
                 {
    -                // Empty lists
    -                {"", "", "", "", "200"},
    +                // Empty lists 1
    +                {"", "", "", "", "200;200"},
     
                     // test simple filters
    -                {"127.0.0.1", "", "", "", "200"},
    -                {"127.0.0.1-127.0.0.254", "", "", "", "200"},
    -                {"192.0.0.1", "", "", "", "403"},
    -                {"192.0.0.1-192.0.0.254", "", "", "", "403"},
    +                {"127.0.0.1", "", "", "", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "", "", "200;200"},
    +                {"192.0.0.1", "", "", "", "403;403"},
    +                {"192.0.0.1-192.0.0.254", "", "", "", "403;403"},
     
    -                // test connector name filters
    -                {"127.0.0.1", "", "http", "", "200"},
    -                {"127.0.0.1-127.0.0.254", "", "http", "", "200"},
    -                {"192.0.0.1", "", "http", "", "403"},
    -                {"192.0.0.1-192.0.0.254", "", "http", "", "403"},
    +                // test includeConnector
    +                {"127.0.0.1", "", "http_connector1", "", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "http_connector1", "", "200;200"},
    +                {"192.0.0.1", "", "http_connector1", "", "403;200"},
    +                {"192.0.0.1-192.0.0.254", "", "http_connector1", "", "403;200"},
    +                {"192.0.0.1", "", "http_connector2", "", "200;403"},
    +                {"192.0.0.1-192.0.0.254", "", "http_connector2", "", "200;403"},
     
    -                {"127.0.0.1", "", "nothttp", "", "403"},
    -                {"127.0.0.1-127.0.0.254", "", "nothttp", "", "403"},
    -                {"192.0.0.1", "", "nothttp", "", "403"},
    -                {"192.0.0.1-192.0.0.254", "", "nothttp", "", "403"},
    +                // test includeConnector names where none of them match
    +                {"127.0.0.1", "", "nothttp", "", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "nothttp", "", "200;200"},
    +                {"192.0.0.1", "", "nothttp", "", "200;200"},
    +                {"192.0.0.1-192.0.0.254", "", "nothttp", "", "200;200"},
     
    -                {"127.0.0.1", "", "", "http", "403"},
    -                {"127.0.0.1-127.0.0.254", "", "", "http", "403"},
    -                {"192.0.0.1", "", "", "http", "403"},
    -                {"192.0.0.1-192.0.0.254", "", "", "http", "403"},
    +                // text excludeConnector
    +                {"127.0.0.1", "", "", "http_connector1", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "", "http_connector1", "200;200"},
    +                {"192.0.0.1", "", "", "http_connector1", "200;403"},
    +                {"192.0.0.1-192.0.0.254", "", "", "http_connector1", "200;403"},
    +                {"192.0.0.1", "", "", "http_connector2", "403;200"},
    +                {"192.0.0.1-192.0.0.254", "", "", "http_connector2", "403;200"},
     
    -                {"127.0.0.1", "", "", "nothttp", "200"},
    -                {"127.0.0.1-127.0.0.254", "", "", "nothttp", "200"},
    -                {"192.0.0.1", "", "", "nothttp", "403"},
    -                {"192.0.0.1-192.0.0.254", "", "", "nothttp", "403"},
    +                // test excludeConnector where none of them match.
    +                {"127.0.0.1", "", "", "nothttp", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "", "nothttp", "200;200"},
    +                {"192.0.0.1", "", "", "nothttp", "403;403"},
    +                {"192.0.0.1-192.0.0.254", "", "", "nothttp", "403;403"},
    +
    +                // both connectors are excluded
    +                {"127.0.0.1", "", "", "http_connector1;http_connector2", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "", "http_connector1;http_connector2", "200;200"},
    +                {"192.0.0.1", "", "", "http_connector1;http_connector2", "200;200"},
    +                {"192.0.0.1-192.0.0.254", "", "", "http_connector1;http_connector2", "200;200"},
    +
    +                // both connectors are included
    +                {"127.0.0.1", "", "http_connector1;http_connector2", "", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "http_connector1;http_connector2", "", "200;200"},
    +                {"192.0.0.1", "", "http_connector1;http_connector2", "", "403;403"},
    +                {"192.0.0.1-192.0.0.254", "", "http_connector1;http_connector2", "", "403;403"},
    +                
    +                // exclude takes precedence over include
    +                {"127.0.0.1", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"},
    +                {"127.0.0.1-127.0.0.254", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"},
    +                {"192.0.0.1", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"},
    +                {"192.0.0.1-192.0.0.254", "", "http_connector1;http_connector2", "http_connector1;http_connector2", "200;200"},
                 };
             return Arrays.asList(data).stream().map(Arguments::of);
         }
    diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerBreakEvenSizeTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerBreakEvenSizeTest.java
    new file mode 100644
    index 00000000000..70771c53e2c
    --- /dev/null
    +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerBreakEvenSizeTest.java
    @@ -0,0 +1,121 @@
    +//
    +//  ========================================================================
    +//  Copyright (c) 1995-2019 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.servlet;
    +
    +import java.io.IOException;
    +import java.net.URI;
    +import java.util.Arrays;
    +import javax.servlet.ServletException;
    +import javax.servlet.http.HttpServlet;
    +import javax.servlet.http.HttpServletRequest;
    +import javax.servlet.http.HttpServletResponse;
    +
    +import org.eclipse.jetty.client.HttpClient;
    +import org.eclipse.jetty.client.api.ContentResponse;
    +import org.eclipse.jetty.http.HttpHeader;
    +import org.eclipse.jetty.server.Server;
    +import org.eclipse.jetty.server.ServerConnector;
    +import org.eclipse.jetty.server.handler.gzip.GzipHandler;
    +import org.eclipse.jetty.util.StringUtil;
    +import org.eclipse.jetty.util.component.LifeCycle;
    +import org.junit.jupiter.api.AfterEach;
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.params.ParameterizedTest;
    +import org.junit.jupiter.params.provider.ValueSource;
    +
    +import static java.nio.charset.StandardCharsets.UTF_8;
    +import static org.hamcrest.MatcherAssert.assertThat;
    +import static org.hamcrest.Matchers.is;
    +import static org.hamcrest.Matchers.lessThanOrEqualTo;
    +
    +public class GzipHandlerBreakEvenSizeTest
    +{
    +    private Server server;
    +    private HttpClient client;
    +
    +    @BeforeEach
    +    public void startServerAndClient() throws Exception
    +    {
    +        server = new Server();
    +        ServerConnector connector = new ServerConnector(server);
    +        connector.setPort(0);
    +        server.addConnector(connector);
    +
    +        GzipHandler gzipHandler = new GzipHandler();
    +        gzipHandler.setExcludedAgentPatterns();
    +        gzipHandler.setMinGzipSize(0);
    +
    +        ServletContextHandler context = new ServletContextHandler(gzipHandler, "/");
    +        context.addServlet(VeryCompressibleContentServlet.class, "/content");
    +        gzipHandler.setHandler(context);
    +        server.setHandler(gzipHandler);
    +
    +        server.start();
    +
    +        client = new HttpClient();
    +        client.start();
    +    }
    +
    +    @AfterEach
    +    public void stopServerAndClient()
    +    {
    +        LifeCycle.stop(client);
    +        LifeCycle.stop(server);
    +    }
    +
    +    @ParameterizedTest
    +    @ValueSource(ints = {0, 1, 2, 3, 4, 5, 10, 15, 20, 21, 22, 23, 24, 25, 50, 100, 300, 500})
    +    public void testRequestSized(int size) throws Exception
    +    {
    +        URI uri = server.getURI().resolve("/content?size=" + size);
    +        ContentResponse response = client.newRequest(uri)
    +            .header(HttpHeader.ACCEPT_ENCODING, "gzip")
    +            .send();
    +
    +        assertThat("Status Code", response.getStatus(), is(200));
    +        assertThat("Size Requested", response.getHeaders().getField("X-SizeRequested").getIntValue(), is(size));
    +
    +        if (size > GzipHandler.BREAK_EVEN_GZIP_SIZE)
    +            assertThat("Response Size", response.getHeaders().getField(HttpHeader.CONTENT_LENGTH).getIntValue(), lessThanOrEqualTo(size));
    +    }
    +
    +    public static class VeryCompressibleContentServlet extends HttpServlet
    +    {
    +        @Override
    +        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
    +        {
    +            resp.setContentType("text/plain");
    +            resp.setCharacterEncoding("utf-8");
    +            String sizeStr = req.getParameter("size");
    +            int size = 0;
    +            if (!StringUtil.isBlank(sizeStr))
    +            {
    +                size = Integer.parseInt(sizeStr);
    +            }
    +            resp.setHeader("X-SizeRequested", String.valueOf(size));
    +            if (size > 0)
    +            {
    +                byte[] buf = new byte[size];
    +                Arrays.fill(buf, (byte)'x');
    +                resp.getWriter().print(new String(buf, UTF_8));
    +            }
    +            resp.getWriter().close();
    +        }
    +    }
    +}
    diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
    index 7020fbb8145..4adad29c950 100644
    --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
    +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java
    @@ -25,6 +25,7 @@ import java.io.OutputStream;
     import java.io.OutputStreamWriter;
     import java.io.Writer;
     import java.nio.charset.Charset;
    +import java.nio.charset.StandardCharsets;
     import java.util.Enumeration;
     import java.util.HashMap;
     import java.util.Locale;
    @@ -251,7 +252,11 @@ public class CGI extends HttpServlet
                     String parameterName = names.nextElement();
                     parameterMap.addValues(parameterName, req.getParameterValues(parameterName));
                 }
    -            bodyFormEncoded = UrlEncoded.encode(parameterMap, Charset.forName(req.getCharacterEncoding()), true);
    +
    +            String characterEncoding = req.getCharacterEncoding();
    +            Charset charset = characterEncoding != null
    +                ? Charset.forName(characterEncoding) : StandardCharsets.UTF_8;
    +            bodyFormEncoded = UrlEncoded.encode(parameterMap, charset, true);
             }
     
             EnvList env = new EnvList(_env);
    diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java
    index 5881009dfce..1f8d82f1705 100644
    --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java
    +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DoSFilter.java
    @@ -121,7 +121,7 @@ import org.eclipse.jetty.util.thread.Scheduler;
      * 
      * 

    * This filter should be configured for {@link DispatcherType#REQUEST} and {@link DispatcherType#ASYNC} and with - * <async-supported>true</async-supported>. + * {@code true}. *

    */ @ManagedObject("limits exposure to abuse from request flooding, whether malicious, or as a result of a misconfigured client") @@ -146,7 +146,6 @@ public class DoSFilter implements Filter private static final long __DEFAULT_MAX_REQUEST_MS_INIT_PARAM = 30000L; private static final long __DEFAULT_MAX_IDLE_TRACKER_MS_INIT_PARAM = 30000L; - static final String NAME = "name"; static final String MANAGED_ATTR_INIT_PARAM = "managedAttr"; static final String MAX_REQUESTS_PER_S_INIT_PARAM = "maxRequestsPerSec"; static final String DELAY_MS_INIT_PARAM = "delayMs"; @@ -384,14 +383,13 @@ public class DoSFilter implements Filter long throttleMs = getThrottleMs(); if (!Boolean.TRUE.equals(throttled) && throttleMs > 0) { - final int priority = getPriority(request, tracker); + int priority = getPriority(request, tracker); request.setAttribute(__THROTTLED, Boolean.TRUE); if (isInsertHeaders()) response.addHeader("DoSFilter", "throttled"); AsyncContext asyncContext = request.startAsync(); request.setAttribute(_suspended, Boolean.TRUE); - if (throttleMs > 0) - asyncContext.setTimeout(throttleMs); + asyncContext.setTimeout(throttleMs); asyncContext.addListener(_listeners[priority]); _queues[priority].add(asyncContext); if (LOG.isDebugEnabled()) @@ -467,14 +465,7 @@ public class DoSFilter implements Filter protected void doFilterChain(FilterChain chain, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException { final Thread thread = Thread.currentThread(); - Runnable requestTimeout = new Runnable() - { - @Override - public void run() - { - onRequestTimeout(request, response, thread); - } - }; + Runnable requestTimeout = () -> onRequestTimeout(request, response, thread); Scheduler.Task task = _scheduler.schedule(requestTimeout, getMaxRequestMs(), TimeUnit.MILLISECONDS); try { @@ -527,7 +518,7 @@ public class DoSFilter implements Filter * @param tracker the rate tracker for this request * @return the priority for this request */ - protected int getPriority(HttpServletRequest request, RateTracker tracker) + private int getPriority(HttpServletRequest request, RateTracker tracker) { if (extractUserId(request) != null) return USER_AUTH; @@ -544,7 +535,7 @@ public class DoSFilter implements Filter return USER_AUTH; } - public void schedule(RateTracker tracker) + private void schedule(RateTracker tracker) { _scheduler.schedule(tracker, getMaxIdleTrackerMs(), TimeUnit.MILLISECONDS); } @@ -565,7 +556,7 @@ public class DoSFilter implements Filter * @param request the current request * @return the request rate tracker for the current connection */ - public RateTracker getRateTracker(ServletRequest request) + RateTracker getRateTracker(ServletRequest request) { HttpSession session = ((HttpServletRequest)request).getSession(false); @@ -617,7 +608,7 @@ public class DoSFilter implements Filter return tracker; } - public void addToRateTracker(RateTracker tracker) + private void addToRateTracker(RateTracker tracker) { _rateTrackers.put(tracker.getId(), tracker); } @@ -700,7 +691,7 @@ public class DoSFilter implements Filter byte[] result = new byte[4]; for (int i = 0; i < result.length; ++i) { - result[i] = ((Integer)Integer.parseInt(ipv4Matcher.group(i + 1))).byteValue(); + result[i] = Integer.valueOf(ipv4Matcher.group(i + 1)).byteValue(); } return result; } @@ -1237,7 +1228,7 @@ public class DoSFilter implements Filter LOG.debug("Tracker removed: {}", getId()); } - protected void addToRateTrackers(DoSFilter filter, RateTracker tracker) + private void addToRateTrackers(DoSFilter filter, RateTracker tracker) { if (filter == null) return; @@ -1277,7 +1268,7 @@ public class DoSFilter implements Filter } } - class FixedRateTracker extends RateTracker + private static class FixedRateTracker extends RateTracker { public FixedRateTracker(ServletContext context, String filterName, String id, int type, int numRecentRequestsTracked) { @@ -1306,15 +1297,15 @@ public class DoSFilter implements Filter } } - private class DoSTimeoutAsyncListener implements AsyncListener + private static class DoSTimeoutAsyncListener implements AsyncListener { @Override - public void onStartAsync(AsyncEvent event) throws IOException + public void onStartAsync(AsyncEvent event) { } @Override - public void onComplete(AsyncEvent event) throws IOException + public void onComplete(AsyncEvent event) { } @@ -1325,7 +1316,7 @@ public class DoSFilter implements Filter } @Override - public void onError(AsyncEvent event) throws IOException + public void onError(AsyncEvent event) { } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java b/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java index d0b88b1aa19..6f9d838993d 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/IncludeExcludeSet.java @@ -56,6 +56,12 @@ public class IncludeExcludeSet implements Predicate

    { return set.contains(item); } + + @Override + public String toString() + { + return "CONTAINS"; + } } /** @@ -227,4 +233,34 @@ public class IncludeExcludeSet implements Predicate

    { return _includes.isEmpty() && _excludes.isEmpty(); } + + /** + * Match items in combined IncludeExcludeSets. + * @param item1 The item to match against set1 + * @param set1 A IncludeExcludeSet to match item1 against + * @param item2 The item to match against set2 + * @param set2 A IncludeExcludeSet to match item2 against + * @param The type of item1 + * @param The type of item2 + * @return True IFF

      + *
    • Neither item is excluded from their respective sets
    • + *
    • Both sets have no includes OR at least one of the items is included in its respective set
    • + *
    + */ + public static boolean matchCombined(T1 item1, IncludeExcludeSet set1, T2 item2, IncludeExcludeSet set2) + { + Boolean match1 = set1.isIncludedAndNotExcluded(item1); + Boolean match2 = set2.isIncludedAndNotExcluded(item2); + + // if we are excluded from either set, then we do not match + if (match1 == Boolean.FALSE || match2 == Boolean.FALSE) + return false; + + // If either set has any includes, then we must be included by one of them + if (set1.hasIncludes() || set2.hasIncludes()) + return match1 == Boolean.TRUE || match2 == Boolean.TRUE; + + // If not excluded and no includes, then we match + return true; + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/InetAddressSet.java b/jetty-util/src/main/java/org/eclipse/jetty/util/InetAddressSet.java index 58240e8590f..3101dd62719 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/InetAddressSet.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/InetAddressSet.java @@ -56,7 +56,7 @@ public class InetAddressSet extends AbstractSet implements Set, return _patterns.put(pattern, newInetRange(pattern)) == null; } - protected InetPattern newInetRange(String pattern) + private InetPattern newInetRange(String pattern) { if (pattern == null) return null; diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java index 81e94d01d37..c7829256734 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java @@ -49,11 +49,11 @@ public class Scanner extends AbstractLifeCycle private static int __scannerId = 0; private int _scanInterval; private int _scanCount = 0; - private final List _listeners = new ArrayList(); - private final Map _prevScan = new HashMap(); - private final Map _currentScan = new HashMap(); + private final List _listeners = new ArrayList<>(); + private final Map _prevScan = new HashMap<>(); + private final Map _currentScan = new HashMap<>(); private FilenameFilter _filter; - private final List _scanDirs = new ArrayList(); + private final List _scanDirs = new ArrayList<>(); private volatile boolean _running = false; private boolean _reportExisting = true; private boolean _reportDirs = true; @@ -66,8 +66,7 @@ public class Scanner extends AbstractLifeCycle ADDED, CHANGED, REMOVED } - ; - private final Map _notifications = new HashMap(); + private final Map _notifications = new HashMap<>(); static class TimeNSize { @@ -412,11 +411,7 @@ public class Scanner extends AbstractLifeCycle if (l instanceof ScanListener) ((ScanListener)l).scan(); } - catch (Exception e) - { - LOG.warn(e); - } - catch (Error e) + catch (Throwable e) { LOG.warn(e); } @@ -428,16 +423,11 @@ public class Scanner extends AbstractLifeCycle */ public synchronized void scanFiles() { - if (_scanDirs == null) - return; - _currentScan.clear(); - Iterator itor = _scanDirs.iterator(); - while (itor.hasNext()) + for (File dir : _scanDirs) { - File dir = itor.next(); - if ((dir != null) && (dir.exists())) + { try { scanFile(dir.getCanonicalFile(), _currentScan, 0); @@ -446,6 +436,7 @@ public class Scanner extends AbstractLifeCycle { LOG.warn("Error scanning files.", e); } + } } } @@ -455,11 +446,11 @@ public class Scanner extends AbstractLifeCycle * @param currentScan the info from the most recent pass * @param oldScan info from the previous pass */ - public synchronized void reportDifferences(Map currentScan, Map oldScan) + private synchronized void reportDifferences(Map currentScan, Map oldScan) { // scan the differences and add what was found to the map of notifications: - Set oldScanKeys = new HashSet(oldScan.keySet()); + Set oldScanKeys = new HashSet<>(oldScan.keySet()); // Look for new and changed files for (Map.Entry entry : currentScan.entrySet()) @@ -484,17 +475,8 @@ public class Scanner extends AbstractLifeCycle else if (!oldScan.get(file).equals(currentScan.get(file))) { Notification old = _notifications.put(file, Notification.CHANGED); - if (old != null) - { - switch (old) - { - case ADDED: - _notifications.put(file, Notification.ADDED); - break; - default: - break; - } - } + if (old == Notification.ADDED) + _notifications.put(file, Notification.ADDED); } } @@ -504,17 +486,8 @@ public class Scanner extends AbstractLifeCycle if (!currentScan.containsKey(file)) { Notification old = _notifications.put(file, Notification.REMOVED); - if (old != null) - { - switch (old) - { - case ADDED: - _notifications.remove(file); - break; - default: - break; - } - } + if (old == Notification.ADDED) + _notifications.remove(file); } } @@ -523,7 +496,7 @@ public class Scanner extends AbstractLifeCycle // Process notifications // Only process notifications that are for stable files (ie same in old and current scan). - List bulkChanges = new ArrayList(); + List bulkChanges = new ArrayList<>(); for (Iterator> iter = _notifications.entrySet().iterator(); iter.hasNext(); ) { Entry entry = iter.next(); @@ -577,7 +550,7 @@ public class Scanner extends AbstractLifeCycle if (f.isFile() || depth > 0 && _reportDirs && f.isDirectory()) { - if ((_filter == null) || ((_filter != null) && _filter.accept(f.getParentFile(), f.getName()))) + if (_filter == null || _filter.accept(f.getParentFile(), f.getName())) { if (LOG.isDebugEnabled()) LOG.debug("scan accepted {}", f); @@ -597,9 +570,9 @@ public class Scanner extends AbstractLifeCycle File[] files = f.listFiles(); if (files != null) { - for (int i = 0; i < files.length; i++) + for (File file : files) { - scanFile(files[i], scanInfoMap, depth + 1); + scanFile(file, scanInfoMap, depth + 1); } } else @@ -624,20 +597,14 @@ public class Scanner extends AbstractLifeCycle */ private void reportAddition(String filename) { - Iterator itor = _listeners.iterator(); - while (itor.hasNext()) + for (Listener l : _listeners) { - Listener l = itor.next(); try { if (l instanceof DiscreteListener) ((DiscreteListener)l).fileAdded(filename); } - catch (Exception e) - { - warn(l, filename, e); - } - catch (Error e) + catch (Throwable e) { warn(l, filename, e); } @@ -651,20 +618,14 @@ public class Scanner extends AbstractLifeCycle */ private void reportRemoval(String filename) { - Iterator itor = _listeners.iterator(); - while (itor.hasNext()) + for (Object l : _listeners) { - Object l = itor.next(); try { if (l instanceof DiscreteListener) ((DiscreteListener)l).fileRemoved(filename); } - catch (Exception e) - { - warn(l, filename, e); - } - catch (Error e) + catch (Throwable e) { warn(l, filename, e); } @@ -678,20 +639,14 @@ public class Scanner extends AbstractLifeCycle */ private void reportChange(String filename) { - Iterator itor = _listeners.iterator(); - while (itor.hasNext()) + for (Listener l : _listeners) { - Listener l = itor.next(); try { if (l instanceof DiscreteListener) ((DiscreteListener)l).fileChanged(filename); } - catch (Exception e) - { - warn(l, filename, e); - } - catch (Error e) + catch (Throwable e) { warn(l, filename, e); } @@ -700,20 +655,14 @@ public class Scanner extends AbstractLifeCycle private void reportBulkChanges(List filenames) { - Iterator itor = _listeners.iterator(); - while (itor.hasNext()) + for (Listener l : _listeners) { - Listener l = itor.next(); try { if (l instanceof BulkListener) ((BulkListener)l).filesChanged(filenames); } - catch (Exception e) - { - warn(l, filenames.toString(), e); - } - catch (Error e) + catch (Throwable e) { warn(l, filenames.toString(), e); } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java b/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java index 689f2a51d6f..b39fd4b5beb 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java @@ -333,7 +333,7 @@ public class ContainerLifeCycle extends AbstractLifeCycle implements Container, return addBean(o, managed ? Managed.POJO : Managed.UNMANAGED); } - public boolean addBean(Object o, Managed managed) + private boolean addBean(Object o, Managed managed) { if (o == null || contains(o)) return false; diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java index 65c51f0d4a1..c55ceb01764 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java @@ -72,7 +72,7 @@ public class ClassMatcher extends AbstractSet { private static final Logger LOG = Log.getLogger(ClassMatcher.class); - static class Entry + public static class Entry { private final String _pattern; private final String _name; diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java index a1d2ccc5dd1..907443dfa01 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java @@ -41,7 +41,7 @@ public class WebDescriptor extends Descriptor protected static XmlParser _nonValidatingStaticParser; protected MetaDataComplete _metaDataComplete; - protected int _majorVersion = 3; //default to container version + protected int _majorVersion = 4; //default to container version protected int _minorVersion = 0; protected ArrayList _classNames = new ArrayList(); protected boolean _distributable; diff --git a/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java index f159c34fbb4..1a90957dddf 100644 --- a/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java +++ b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java @@ -239,6 +239,7 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli public void setIdleTimeout(Duration duration) { configurationCustomizer.setIdleTimeout(duration); + getHttpClient().setIdleTimeout(duration.toMillis()); } @Override diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java index 444eb448569..597af68b6f2 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java @@ -26,6 +26,7 @@ import java.net.SocketTimeoutException; import java.net.URI; import java.time.Duration; import java.util.EnumSet; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -70,6 +71,7 @@ public class ClientConnectTest { private Server server; private WebSocketClient client; + private CountDownLatch serverLatch = new CountDownLatch(1); @SuppressWarnings("unchecked") private E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher errorMatcher) @@ -97,7 +99,7 @@ public class ClientConnectTest { client = new WebSocketClient(); client.setConnectTimeout(TimeUnit.SECONDS.toMillis(3)); - client.setIdleTimeout(Duration.ofSeconds(10)); + client.setIdleTimeout(Duration.ofSeconds(3)); client.start(); } @@ -124,6 +126,19 @@ public class ClientConnectTest return new EchoSocket(); }); container.addMapping("/get-auth-header", (req, resp) -> new GetAuthHeaderEndpoint()); + + container.addMapping("/noResponse", (req, resp) -> + { + try + { + serverLatch.await(); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + return null; + }); }); context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); @@ -367,35 +382,32 @@ public class ClientConnectTest @Test public void testConnectionTimeout_Concurrent() throws Exception { + client.setConnectTimeout(1000); + client.setIdleTimeout(Duration.ofSeconds(1)); CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); - try (ServerSocket serverSocket = new ServerSocket()) - { - InetAddress addr = InetAddress.getByName("localhost"); - InetSocketAddress endpoint = new InetSocketAddress(addr, 0); - serverSocket.bind(endpoint, 1); - int port = serverSocket.getLocalPort(); - URI wsUri = URI.create(String.format("ws://%s:%d/", addr.getHostAddress(), port)); - Future future = client.connect(cliSock, wsUri); + // Connect to endpoint which waits and does not send back a response. + URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/noResponse")); + Future future = client.connect(cliSock, wsUri); - // Accept the connection, but do nothing on it (no response, no upgrade, etc) - serverSocket.accept(); + // The attempt to get upgrade response future should throw error + Exception e = assertThrows(Exception.class, + () -> future.get(5, TimeUnit.SECONDS)); - // The attempt to get upgrade response future should throw error - Exception e = assertThrows(Exception.class, - () -> future.get(5, TimeUnit.SECONDS)); + // Allow server to exit now we have failed. + serverLatch.countDown(); - if (e instanceof ExecutionException) - { - assertExpectedError((ExecutionException)e, cliSock, anyOf( - instanceOf(ConnectException.class), - instanceOf(UpgradeException.class) - )); - } - else - { - assertThat("Should have been a TimeoutException", e, instanceOf(TimeoutException.class)); - } - } + // Unwrap the exception to test if it was what we expected. + assertThat(e, instanceOf(ExecutionException.class)); + + Throwable jettyUpgradeException = e.getCause(); + assertThat(jettyUpgradeException, instanceOf(UpgradeException.class)); + + Throwable coreUpgradeException = jettyUpgradeException.getCause(); + assertThat(coreUpgradeException, instanceOf(org.eclipse.jetty.websocket.core.UpgradeException.class)); + + Throwable timeoutException = coreUpgradeException.getCause(); + assertThat(timeoutException, instanceOf(TimeoutException.class)); + assertThat(timeoutException.getMessage(), containsString("Idle timeout")); } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/Parser.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/Parser.java index 176adf382dd..486aaaffbbc 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/Parser.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/Parser.java @@ -256,12 +256,17 @@ public class Parser protected void checkFrameSize(byte opcode, int payloadLength) throws MessageTooLargeException, ProtocolException { - if (OpCode.isControlFrame(opcode) && payloadLength > Frame.MAX_CONTROL_PAYLOAD) - throw new ProtocolException("Invalid control frame payload length, [" + payloadLength + "] cannot exceed [" + Frame.MAX_CONTROL_PAYLOAD + "]"); - - long maxFrameSize = configuration.getMaxFrameSize(); - if (!configuration.isAutoFragment() && maxFrameSize > 0 && payloadLength > maxFrameSize) - throw new MessageTooLargeException("Cannot handle payload lengths larger than " + maxFrameSize); + if (OpCode.isControlFrame(opcode)) + { + if (payloadLength > Frame.MAX_CONTROL_PAYLOAD) + throw new ProtocolException("Invalid control frame payload length, [" + payloadLength + "] cannot exceed [" + Frame.MAX_CONTROL_PAYLOAD + "]"); + } + else + { + long maxFrameSize = configuration.getMaxFrameSize(); + if (!configuration.isAutoFragment() && maxFrameSize > 0 && payloadLength > maxFrameSize) + throw new MessageTooLargeException("Cannot handle payload lengths larger than " + maxFrameSize); + } } protected ParsedFrame newFrame(byte firstByte, byte[] mask, ByteBuffer payload, boolean releaseable) @@ -279,6 +284,32 @@ public class Parser return new ParsedFrame(firstByte, mask, payload, releaseable); } + private ParsedFrame autoFragment(ByteBuffer buffer, int fragmentSize) + { + payloadLength -= fragmentSize; + + byte[] nextMask = null; + if (mask != null) + { + int shift = fragmentSize % 4; + nextMask = new byte[4]; + nextMask[0] = mask[(0 + shift) % 4]; + nextMask[1] = mask[(1 + shift) % 4]; + nextMask[2] = mask[(2 + shift) % 4]; + nextMask[3] = mask[(3 + shift) % 4]; + } + + ByteBuffer content = buffer.slice(); + content.limit(fragmentSize); + buffer.position(buffer.position() + fragmentSize); + + final ParsedFrame frame = newFrame((byte)(firstByte & 0x7F), mask, content, false); + mask = nextMask; + firstByte = (byte)((firstByte & 0x80) | OpCode.CONTINUATION); + state = State.FRAGMENT; + return frame; + } + private ParsedFrame parsePayload(ByteBuffer buffer) { if (payloadLength == 0) @@ -288,35 +319,21 @@ public class Parser return null; int available = buffer.remaining(); + boolean isDataFrame = OpCode.isDataFrame(OpCode.getOpCode(firstByte)); + + // Always autoFragment data frames if payloadLength is greater than maxFrameSize. + long maxFrameSize = configuration.getMaxFrameSize(); + if (maxFrameSize > 0 && isDataFrame && payloadLength > maxFrameSize) + return autoFragment(buffer, (int)Math.min(available, maxFrameSize)); if (aggregate == null) { if (available < payloadLength) { - // not enough to complete this frame - + // not enough to complete this frame // Can we auto-fragment - if (configuration.isAutoFragment() && OpCode.isDataFrame(OpCode.getOpCode(firstByte))) - { - payloadLength -= available; - - byte[] nextMask = null; - if (mask != null) - { - int shift = available % 4; - nextMask = new byte[4]; - nextMask[0] = mask[(0 + shift) % 4]; - nextMask[1] = mask[(1 + shift) % 4]; - nextMask[2] = mask[(2 + shift) % 4]; - nextMask[3] = mask[(3 + shift) % 4]; - } - final ParsedFrame frame = newFrame((byte)(firstByte & 0x7F), mask, buffer.slice(), false); - buffer.position(buffer.limit()); - mask = nextMask; - firstByte = (byte)((firstByte & 0x80) | OpCode.CONTINUATION); - state = State.FRAGMENT; - return frame; - } + if (configuration.isAutoFragment() && isDataFrame) + return autoFragment(buffer, available); // No space in the buffer, so we have to copy the partial payload aggregate = bufferPool.acquire(payloadLength, false); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/AutoFragmentTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/AutoFragmentTest.java new file mode 100644 index 00000000000..139ef9db8f8 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/AutoFragmentTest.java @@ -0,0 +1,164 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 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.websocket.core; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.core.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AutoFragmentTest +{ + private WebSocketServer server; + private TestFrameHandler serverHandler; + private URI serverUri; + + private WebSocketCoreClient client; + + @BeforeEach + public void setup() throws Exception + { + serverHandler = new TestFrameHandler(); + + server = new WebSocketServer(serverHandler); + server.start(); + serverUri = new URI("ws://localhost:" + server.getLocalPort()); + + client = new WebSocketCoreClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + @Test + public void testAutoFragmentToMaxFrameSize() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + CompletableFuture connect = client.connect(clientHandler, serverUri); + connect.get(5, TimeUnit.SECONDS); + + // Turn off fragmentation on the client. + clientHandler.coreSession.setMaxFrameSize(0); + clientHandler.coreSession.setAutoFragment(false); + + // Set the server should fragment to the maxFrameSize. + int maxFrameSize = 30; + assertTrue(serverHandler.open.await(5, TimeUnit.SECONDS)); + serverHandler.coreSession.setMaxFrameSize(maxFrameSize); + serverHandler.coreSession.setAutoFragment(true); + + // Send a message which is too large. + int size = maxFrameSize * 2; + byte[] message = new byte[size]; + Arrays.fill(message, 0, size, (byte)'X'); + clientHandler.coreSession.sendFrame(new Frame(OpCode.BINARY, BufferUtil.toBuffer(message)), Callback.NOOP, false); + + // We should not receive any frames larger than the max frame size. + // So our message should be split into two frames. + Frame frame = serverHandler.receivedFrames.poll(5, TimeUnit.SECONDS); + assertNotNull(frame); + assertThat(frame.getOpCode(), is(OpCode.BINARY)); + assertThat(frame.getPayloadLength(), is(maxFrameSize)); + assertThat(frame.isFin(), is(false)); + + // Second frame should be final and contain rest of the data. + frame = serverHandler.receivedFrames.poll(5, TimeUnit.SECONDS); + assertNotNull(frame); + assertThat(frame.getOpCode(), is(OpCode.CONTINUATION)); + assertThat(frame.getPayloadLength(), is(maxFrameSize)); + assertThat(frame.isFin(), is(true)); + + clientHandler.sendClose(); + assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); + assertTrue(clientHandler.closed.await(5, TimeUnit.SECONDS)); + } + + @Disabled("permessage-deflate autoFragment not implemented yet") + @Test + public void testAutoFragmentWithPermessageDeflate() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, serverUri, clientHandler); + upgradeRequest.addExtensions("permessage-deflate"); + CompletableFuture connect = client.connect(upgradeRequest); + connect.get(5, TimeUnit.SECONDS); + + // Turn off fragmentation on the client. + clientHandler.coreSession.setMaxFrameSize(0); + clientHandler.coreSession.setAutoFragment(false); + + // Set a small maxFrameSize on the server. + int maxFrameSize = 10; + assertTrue(serverHandler.open.await(5, TimeUnit.SECONDS)); + serverHandler.coreSession.setMaxFrameSize(maxFrameSize); + serverHandler.coreSession.setAutoFragment(true); + + // Generate a large random payload. + int payloadSize = 1000; + Random rand = new Random(); + ByteBuffer payload = BufferUtil.allocate(payloadSize); + BufferUtil.clearToFill(payload); + for (int i=0; i(), new LinkedList<>()); this.coreSession = new WebSocketCoreSession(new TestMessageHandler(), behavior, Negotiated.from(exStack)); + coreSession.setAutoFragment(false); + coreSession.setMaxFrameSize(0); this.parser = new Parser(components.getBufferPool(), coreSession); } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java index f267a0e8549..d3aa8f058ef 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserTest.java @@ -1542,6 +1542,7 @@ public class ParserTest ByteBuffer buffer = BufferUtil.allocate(32); ParserCapture capture = new ParserCapture(false, Behavior.SERVER); + capture.getCoreSession().setAutoFragment(true); data.limit(6 + 5); BufferUtil.append(buffer, data); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java index 7b0c3f40bcf..aab128409a8 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java @@ -57,7 +57,8 @@ public class TestFrameHandler implements SynchronousFrameHandler @Override public void onOpen(CoreSession coreSession) { - LOG.info("onOpen {}", coreSession); + if (LOG.isDebugEnabled()) + LOG.debug("onOpen {}", coreSession); this.coreSession = coreSession; open.countDown(); } @@ -65,41 +66,47 @@ public class TestFrameHandler implements SynchronousFrameHandler @Override public void onFrame(Frame frame) { - LOG.info("onFrame: " + OpCode.name(frame.getOpCode()) + ":" + BufferUtil.toDetailString(frame.getPayload())); + if (LOG.isDebugEnabled()) + LOG.debug("onFrame: " + OpCode.name(frame.getOpCode()) + ":" + BufferUtil.toDetailString(frame.getPayload())); receivedFrames.offer(Frame.copy(frame)); } @Override public void onClosed(CloseStatus closeStatus) { - LOG.info("onClosed {}", closeStatus); + if (LOG.isDebugEnabled()) + LOG.debug("onClosed {}", closeStatus); closed.countDown(); } @Override public void onError(Throwable cause) { - LOG.info("onError {} ", cause == null ? null : cause.toString()); + if (LOG.isDebugEnabled()) + LOG.debug("onError {} ", cause == null ? null : cause.toString()); failure = cause; error.countDown(); } public void sendText(String text) { - LOG.info("sendText {} ", text); + if (LOG.isDebugEnabled()) + LOG.debug("sendText {} ", text); Frame frame = new Frame(OpCode.TEXT, text); getCoreSession().sendFrame(frame, Callback.NOOP, false); } public void sendFrame(Frame frame) { - LOG.info("sendFrame {} ", frame); + if (LOG.isDebugEnabled()) + LOG.debug("sendFrame {} ", frame); getCoreSession().sendFrame(frame, Callback.NOOP, false); } public void sendClose() { - LOG.info("sendClose"); + if (LOG.isDebugEnabled()) + LOG.debug("sendClose"); Frame frame = new Frame(OpCode.CLOSE); getCoreSession().sendFrame(frame, Callback.NOOP, false); } diff --git a/pom.xml b/pom.xml index a6e68ce953c..4a1cffe88f6 100644 --- a/pom.xml +++ b/pom.xml @@ -671,7 +671,7 @@ org.jacoco jacoco-maven-plugin - 0.8.4 + 0.8.5 com.agilejava.docbkx diff --git a/scripts/release-jetty.sh b/scripts/release-jetty.sh index 6dedce27661..ae0cb0d2f67 100755 --- a/scripts/release-jetty.sh +++ b/scripts/release-jetty.sh @@ -190,7 +190,7 @@ if proceedyn "Are you sure you want to release using above? (y/N)" n; then fi if proceedyn "Push git commits to remote $GIT_REMOTE_ID? (Y/n)" y; then git push $GIT_REMOTE_ID $GIT_BRANCH_ID - git push $GIT_REMOTE_ID --tags + git push $GIT_REMOTE_ID $TAG_NAME fi else echo "Not performing release" diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java index e4549f5fe97..b867a714b90 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java @@ -366,20 +366,11 @@ public abstract class RFC2616BaseTest req1.append("\n"); req1.append("123\r\n"); - req1.append("GET /echo/R2 HTTP/1.1\n"); - req1.append("Host: localhost\n"); - req1.append("Connection: close\n"); - req1.append("\n"); - List responses = http.requests(req1); - assertEquals(2, responses.size(), "Response Count"); + assertEquals(1, responses.size(), "Response Count"); HttpTester.Response response = responses.get(0); - assertThat("4.4.2 Message Length / Response Code", response.getStatus(), is(HttpStatus.OK_200)); - assertThat("4.4.2 Message Length / Body", response.getContent(), Matchers.containsString("123\n")); - response = responses.get(1); - assertThat("4.4.2 Message Length / Response Code", response.getStatus(), is(HttpStatus.OK_200)); - assertEquals("", response.getContent(), "4.4.2 Message Length / No Body"); + assertThat("4.4.2 Message Length / Response Code", response.getStatus(), is(HttpStatus.BAD_REQUEST_400)); // 4.4.3 - // Client - do not send 'Content-Length' if entity-length diff --git a/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml index 75523dfb47a..05e4f1d4f6f 100644 --- a/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml +++ b/tests/test-webapps/test-jetty-webapp/src/main/webapp/WEB-INF/web.xml @@ -125,6 +125,7 @@ CGI org.eclipse.jetty.servlets.CGI 1 + true