Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-1743-refactor-maven-plugin-redux

Signed-off-by: Jan Bartel <janb@webtide.com>
This commit is contained in:
Jan Bartel 2019-10-29 13:51:05 +11:00
commit d3153f3277
50 changed files with 1803 additions and 757 deletions

View File

@ -1,228 +1,6 @@
jetty-10.0.0-SNAPSHOT jetty-10.0.0-SNAPSHOT
jetty-10.0.0.alpha1 - 03 October 2019 jetty-10.0.0-alpha0 - 11 July
+ 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
2019 2019
+ 113 Add support for NCSA Extended Log File Format + 113 Add support for NCSA Extended Log File Format
+ 114 Bring back overlay deployer + 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 + 3849 ClosedChannelException from jetty-test-webapp javax websocket chat
example 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 jetty-9.4.20.v20190813 - 13 August 2019
+ 300 Implement Deflater / Inflater Object Pool + 300 Implement Deflater / Inflater Object Pool
+ 2061 WebSocket hangs in blockingWrite + 2061 WebSocket hangs in blockingWrite

View File

@ -19,18 +19,25 @@
package org.eclipse.jetty.client; package org.eclipse.jetty.client;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; 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.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocket; 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.HttpHeader;
import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector; 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.SslClientConnectionFactory;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.io.ssl.SslHandshakeListener; import org.eclipse.jetty.io.ssl.SslHandshakeListener;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler; 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.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ExecutorThreadPool; import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.EnabledOnJre;
@ -68,7 +84,6 @@ public class HttpClientTLSTest
private Server server; private Server server;
private ServerConnector connector; private ServerConnector connector;
private HttpClient client; private HttpClient client;
private SSLSocket sslSocket;
private void startServer(SslContextFactory.Server sslContextFactory, Handler handler) throws Exception private void startServer(SslContextFactory.Server sslContextFactory, Handler handler) throws Exception
{ {
@ -424,16 +439,16 @@ public class HttpClientTLSTest
String host = "localhost"; String host = "localhost";
int port = connector.getLocalPort(); int port = connector.getLocalPort();
Socket socket = new Socket(host, port); Socket socket1 = new Socket(host, port);
sslSocket = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket, host, port, true); SSLSocket sslSocket1 = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket1, host, port, true);
CountDownLatch handshakeLatch1 = new CountDownLatch(1); CountDownLatch handshakeLatch1 = new CountDownLatch(1);
AtomicReference<byte[]> session1 = new AtomicReference<>(); AtomicReference<byte[]> session1 = new AtomicReference<>();
sslSocket.addHandshakeCompletedListener(event -> sslSocket1.addHandshakeCompletedListener(event ->
{ {
session1.set(event.getSession().getId()); session1.set(event.getSession().getId());
handshakeLatch1.countDown(); handshakeLatch1.countDown();
}); });
sslSocket.startHandshake(); sslSocket1.startHandshake();
assertTrue(handshakeLatch1.await(5, TimeUnit.SECONDS)); assertTrue(handshakeLatch1.await(5, TimeUnit.SECONDS));
// In TLS 1.3 the server sends a NewSessionTicket post-handshake message // In TLS 1.3 the server sends a NewSessionTicket post-handshake message
@ -441,29 +456,29 @@ public class HttpClientTLSTest
assertThrows(SocketTimeoutException.class, () -> assertThrows(SocketTimeoutException.class, () ->
{ {
sslSocket.setSoTimeout(1000); sslSocket1.setSoTimeout(1000);
sslSocket.getInputStream().read(); sslSocket1.getInputStream().read();
}); });
// The client closes abruptly. // The client closes abruptly.
socket.close(); socket1.close();
// Try again and compare the session ids. // Try again and compare the session ids.
socket = new Socket(host, port); Socket socket2 = new Socket(host, port);
sslSocket = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket, host, port, true); SSLSocket sslSocket2 = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket2, host, port, true);
CountDownLatch handshakeLatch2 = new CountDownLatch(1); CountDownLatch handshakeLatch2 = new CountDownLatch(1);
AtomicReference<byte[]> session2 = new AtomicReference<>(); AtomicReference<byte[]> session2 = new AtomicReference<>();
sslSocket.addHandshakeCompletedListener(event -> sslSocket2.addHandshakeCompletedListener(event ->
{ {
session2.set(event.getSession().getId()); session2.set(event.getSession().getId());
handshakeLatch2.countDown(); handshakeLatch2.countDown();
}); });
sslSocket.startHandshake(); sslSocket2.startHandshake();
assertTrue(handshakeLatch2.await(5, TimeUnit.SECONDS)); assertTrue(handshakeLatch2.await(5, TimeUnit.SECONDS));
assertArrayEquals(session1.get(), session2.get()); assertArrayEquals(session1.get(), session2.get());
sslSocket.close(); sslSocket2.close();
} }
@Test @Test
@ -485,7 +500,7 @@ public class HttpClientTLSTest
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory) protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{ {
SslClientConnectionFactory ssl = (SslClientConnectionFactory)super.newSslClientConnectionFactory(sslContextFactory, connectionFactory); SslClientConnectionFactory ssl = (SslClientConnectionFactory)super.newSslClientConnectionFactory(sslContextFactory, connectionFactory);
ssl.setAllowMissingCloseMessage(false); ssl.setRequireCloseMessage(true);
return ssl; return ssl;
} }
}; };
@ -512,19 +527,19 @@ public class HttpClientTLSTest
break; break;
} }
// If the response is Content-Length delimited, allowing the // If the response is Content-Length delimited, the lack of
// missing TLS Close Message is fine because the application // the TLS Close Message is fine because the application
// will see a EOFException anyway. // will see a EOFException anyway: the Content-Length and
// If the response is connection delimited, allowing the // the actual content bytes count won't match.
// missing TLS Close Message is bad because the application // If the response is connection delimited, the lack of the
// will see a successful response with truncated content. // TLS Close Message is bad because the application will
// see a successful response, but with truncated content.
// Verify that by not allowing the missing // Verify that by requiring the TLS Close Message we get
// TLS Close Message we get a response failure. // a response failure.
byte[] half = new byte[8]; byte[] half = new byte[8];
String response = "HTTP/1.1 200 OK\r\n" + String response = "HTTP/1.1 200 OK\r\n" +
// "Content-Length: " + (half.length * 2) + "\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n"; "\r\n";
OutputStream output = sslSocket.getOutputStream(); OutputStream output = sslSocket.getOutputStream();
@ -564,4 +579,368 @@ public class HttpClientTLSTest
assertTrue(latch.await(5, TimeUnit.SECONDS)); 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));
}
} }

View File

@ -139,7 +139,7 @@ Here is an example, setting the context attribute in code (although you can also
---- ----
WebAppContext context = new WebAppContext(); WebAppContext context = new WebAppContext();
context.setAttribute("org.eclipse.jetty.containerInitializerOrder", 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. 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.

View File

@ -19,8 +19,8 @@
[[serving-aliased-files]] [[serving-aliased-files]]
=== Aliased Files and Symbolic links === 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. 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 my inadvertently be bypassed by aliases. 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. 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. 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.

View File

@ -145,7 +145,7 @@ Properties:
jetty.secure.port = 8443 jetty.secure.port = 8443
jetty.truststore = etc/keystore jetty.truststore = etc/keystore
jetty.truststore.password = OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4 jetty.truststore.password = OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4
org.eclipse.jetty.websocket.jsr356 = false org.eclipse.jetty.websocket.javax = false
threads.max = 200 threads.max = 200
threads.min = 10 threads.min = 10
threads.timeout = 60000 threads.timeout = 60000
@ -235,7 +235,7 @@ etc/demo-rewrite-rules.xml
# Websocket chat examples needs websocket enabled # Websocket chat examples needs websocket enabled
# Don't start for all contexts (set to true in test.xml context) # 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 --module=websocket
# Create and configure the test realm # Create and configure the test realm

View File

@ -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: 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::: 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.jsr356` to `false`. 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 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. 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. 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::: Completely disable Websocket 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`. 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]: 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}"] [source, java, subs="{sub-order}"]
---- ----
WebAppContext context = new WebAppContext(); WebAppContext context = new WebAppContext();
context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern", context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern",
"org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer|com.acme.*"); "org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer|com.acme.*");
---- ----

View File

@ -115,10 +115,54 @@ catch (IOException e)
How to send a Text message in 2 parts, using the partial message support in RemoteEndpoint. 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. 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]] [[pingpong]]
==== Send Ping / Pong Control Frame ==== 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}"] [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). 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}"] [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). 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. 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]] You can also asynchronously send Ping and Pong frames using the `WriteCallback`, this will return before the Ping/Pong is
==== Async Send Message transmitted and notify you of the result in `WriteCallback` `writeSuccess()` or `writeFailed()`.
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<Void>` 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.
[source, java, subs="{sub-order}"] [source, java, subs="{sub-order}"]
---- ----
RemoteEndpoint remote = session.getRemote(); RemoteEndpoint remote = session.getRemote();
// Async Send of a BINARY message to remote endpoint String pingData = "You There?";
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 }); ByteBuffer pingPayload = ByteBuffer.wrap(data.getBytes());
remote.sendBytesByFuture(buf);
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<Void> 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<Void>` 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<Void> 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<Void>` 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<Void> 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<Void>` 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<Void> 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<Void>` and waiting only prescribed amount of time for the send to complete, cancelling the message if the timeout occurs.

View File

@ -54,12 +54,12 @@ What is the Local and Remote Address.
[source, java, subs="{sub-order}"] [source, java, subs="{sub-order}"]
---- ----
InetSocketAddress remoteAddr = session.getRemoteAddress(); SocketAddress remoteAddr = session.getRemoteAddress();
---- ----
Get and Set the Idle Timeout Get and Set the Idle Timeout
[source, java, subs="{sub-order}"] [source, java, subs="{sub-order}"]
---- ----
session.setIdleTimeout(2000); // 2 second timeout session.setIdleTimeout(Duration.ofMillis(2000));
---- ----

View File

@ -19,7 +19,7 @@
[[jetty-websocket-server-api]] [[jetty-websocket-server-api]]
=== 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. 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 ==== 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}"] [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. 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. 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 WebSocketCreator the WebSocket class we want to be created on Upgrade. 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] [NOTE]
@ -46,21 +46,21 @@ when configuring websockets. Be sure the websocket configuration is
lower than your firewall or router. 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. By default, the `JettyWebSocketServletFactory` is a simple `JettyWebSocketCreator` 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). 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}"] [source, java, subs="{sub-order}"]
---- ----
include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java[] 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. created.
[source, java, subs="{sub-order}"] [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[] 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 * Controlling the selection of WebSocket subprotocol
* Performing any WebSocket origin you deem important. * Performing any WebSocket origin you deem important.
@ -78,4 +78,4 @@ Other uses for a WebSocketCreator:
* Obtaining the Servlet HttpSession object (if it exists) * Obtaining the Servlet HttpSession object (if it exists)
* Specifying a response status code and reason * 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.

View File

@ -34,7 +34,7 @@ import static java.util.EnumSet.noneOf;
*/ */
public class CookieCompliance implements ComplianceViolation.Mode 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"), 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"); 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 @Override
public String getURL() public String getURL()
{ {
return null; return url;
} }
@Override @Override
public String getDescription() public String getDescription()
{ {
return null; return description;
} }
} }
@ -87,9 +87,8 @@ public class CookieCompliance implements ComplianceViolation.Mode
private CookieCompliance(String name, Set<Violation> violations) private CookieCompliance(String name, Set<Violation> violations)
{ {
Objects.nonNull(violations);
_name = name; _name = name;
_violations = unmodifiableSet(copyOf(violations)); _violations = unmodifiableSet(copyOf(Objects.requireNonNull(violations)));
} }
@Override @Override

View File

@ -686,17 +686,24 @@ public class HttpGenerator
_endOfContent = EndOfContent.NO_CONTENT; _endOfContent = EndOfContent.NO_CONTENT;
// But it is an error if there actually is 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 if (_contentPrepared == 0 && last)
// TODO review if it is still needed in 9.4 or can we just throw. {
content.clear(); // TODO discard content for backward compatibility with 9.3 releases
contentLength = 0; // 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 // Else if we are HTTP/1.1 and the content length is unknown and we are either persistent

View File

@ -170,6 +170,7 @@ public class HttpParser
private Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune? private Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune?
private EndOfContent _endOfContent; private EndOfContent _endOfContent;
private boolean _hasContentLength; private boolean _hasContentLength;
private boolean _hasTransferEncoding;
private long _contentLength = -1; private long _contentLength = -1;
private long _contentPosition; private long _contentPosition;
private int _chunkLength; private int _chunkLength;
@ -916,6 +917,9 @@ public class HttpParser
switch (_header) switch (_header)
{ {
case CONTENT_LENGTH: case CONTENT_LENGTH:
if (_hasTransferEncoding)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH);
if (_hasContentLength) if (_hasContentLength)
{ {
checkViolation(MULTIPLE_CONTENT_LENGTHS); checkViolation(MULTIPLE_CONTENT_LENGTHS);
@ -924,9 +928,6 @@ public class HttpParser
} }
_hasContentLength = true; _hasContentLength = true;
if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH);
if (_endOfContent != EndOfContent.CHUNKED_CONTENT) if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
{ {
_contentLength = convertContentLength(_valueString); _contentLength = convertContentLength(_valueString);
@ -938,9 +939,15 @@ public class HttpParser
break; break;
case TRANSFER_ENCODING: case TRANSFER_ENCODING:
_hasTransferEncoding = true;
if (_hasContentLength) if (_hasContentLength)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH); 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)) if (HttpHeaderValue.CHUNKED.is(_valueString))
{ {
_endOfContent = EndOfContent.CHUNKED_CONTENT; _endOfContent = EndOfContent.CHUNKED_CONTENT;
@ -949,15 +956,26 @@ public class HttpParser
else else
{ {
List<String> values = new QuotedCSV(_valueString).getValues(); List<String> 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; if (HttpHeaderValue.CHUNKED.is(values.get(i)))
_contentLength = -1; {
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; break;
case HOST: case HOST:
@ -1098,6 +1116,17 @@ public class HttpParser
return _handler.messageComplete(); 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? // Was there a required host header?
if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null) if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null)
{ {
@ -1779,6 +1808,7 @@ public class HttpParser
_endOfContent = EndOfContent.UNKNOWN_CONTENT; _endOfContent = EndOfContent.UNKNOWN_CONTENT;
_contentLength = -1; _contentLength = -1;
_hasContentLength = false; _hasContentLength = false;
_hasTransferEncoding = false;
_contentPosition = 0; _contentPosition = 0;
_responseStatus = 0; _responseStatus = 0;
_contentChunk = null; _contentChunk = null;

View File

@ -979,7 +979,7 @@ public class HttpParserTest
assertEquals("GET", _methodOrVersion); assertEquals("GET", _methodOrVersion);
assertEquals("/chunk", _uriOrStatus); assertEquals("/chunk", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason); assertEquals("HTTP/1.0", _versionOrReason);
assertThat(_bad, containsString("Bad chunking")); assertThat(_bad, containsString("Bad Transfer-Encoding"));
} }
@Test @Test

View File

@ -20,7 +20,7 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>
<argLine> <argLine>
@{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
</argLine> </argLine>
</configuration> </configuration>
</plugin> </plugin>

View File

@ -46,7 +46,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory
private final ClientConnectionFactory connectionFactory; private final ClientConnectionFactory connectionFactory;
private boolean _directBuffersForEncryption = true; private boolean _directBuffersForEncryption = true;
private boolean _directBuffersForDecryption = true; private boolean _directBuffersForDecryption = true;
private boolean allowMissingCloseMessage = true; private boolean _requireCloseMessage;
public SslClientConnectionFactory(SslContextFactory sslContextFactory, ByteBufferPool byteBufferPool, Executor executor, ClientConnectionFactory connectionFactory) public SslClientConnectionFactory(SslContextFactory sslContextFactory, ByteBufferPool byteBufferPool, Executor executor, ClientConnectionFactory connectionFactory)
{ {
@ -76,14 +76,22 @@ public class SslClientConnectionFactory implements ClientConnectionFactory
return _directBuffersForEncryption; 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 @Override
@ -118,7 +126,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory
SslConnection sslConnection = (SslConnection)connection; SslConnection sslConnection = (SslConnection)connection;
sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed()); sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed());
sslConnection.setRenegotiationLimit(sslContextFactory.getRenegotiationLimit()); sslConnection.setRenegotiationLimit(sslContextFactory.getRenegotiationLimit());
sslConnection.setAllowMissingCloseMessage(isAllowMissingCloseMessage()); sslConnection.setRequireCloseMessage(isRequireCloseMessage());
ContainerLifeCycle client = (ContainerLifeCycle)context.get(ClientConnectionFactory.CLIENT_CONTEXT_KEY); ContainerLifeCycle client = (ContainerLifeCycle)context.get(ClientConnectionFactory.CLIENT_CONTEXT_KEY);
if (client != null) if (client != null)
client.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener); client.getBeans(SslHandshakeListener.class).forEach(sslConnection::addHandshakeListener);

View File

@ -25,12 +25,14 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.ToIntFunction;
import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.AbstractEndPoint; 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 Logger LOG = Log.getLogger(SslConnection.class);
private static final String TLS_1_3 = "TLSv1.3"; private static final String TLS_1_3 = "TLSv1.3";
private enum Handshake private enum HandshakeState
{ {
INITIAL, INITIAL,
HANDSHAKE,
SUCCEEDED, SUCCEEDED,
FAILED FAILED
} }
@ -113,10 +116,10 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
private boolean _renegotiationAllowed; private boolean _renegotiationAllowed;
private int _renegotiationLimit = -1; private int _renegotiationLimit = -1;
private boolean _closedOutbound; private boolean _closedOutbound;
private boolean _allowMissingCloseMessage = true; private boolean _requireCloseMessage;
private FlushState _flushState = FlushState.IDLE; private FlushState _flushState = FlushState.IDLE;
private FillState _fillState = FillState.IDLE; private FillState _fillState = FillState.IDLE;
private AtomicReference<Handshake> _handshake = new AtomicReference<>(Handshake.INITIAL); private AtomicReference<HandshakeState> _handshake = new AtomicReference<>(HandshakeState.INITIAL);
private boolean _underflown; private boolean _underflown;
private abstract class RunnableTask implements Runnable, Invocable 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. * is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
*/ */
public int getRenegotiationLimit() 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. * When the limit is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
* Default -1. * Default -1.
*/ */
@ -249,20 +252,75 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
_renegotiationLimit = renegotiationLimit; _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) /**
* <p>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.</p>
* <p>This option is only useful on clients, since typically servers cannot accept
* connection-delimited content that may be truncated.</p>
*
* @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<SSLSession> 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() private void acquireEncryptedInput()
{ {
if (_encryptedInput == null) 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 @Override
@ -329,6 +387,16 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
_decryptedEndPoint.onFillableFail(cause == null ? new IOException() : cause); _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 @Override
public String toConnectionString() public String toConnectionString()
{ {
@ -350,6 +418,24 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
connection instanceof AbstractConnection ? ((AbstractConnection)connection).toConnectionString() : connection); 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() private void releaseEncryptedOutputBuffer()
{ {
if (!Thread.holdsLock(_decryptedEndPoint)) 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 public class DecryptedEndPoint extends AbstractEndPoint
{ {
private final Callback _incompleteWriteCallback = new IncompleteWriteCallback(); private final Callback _incompleteWriteCallback = new IncompleteWriteCallback();
@ -475,9 +571,12 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
{ {
if (connection instanceof AbstractConnection) if (connection instanceof AbstractConnection)
{ {
AbstractConnection a = (AbstractConnection)connection; // This is an optimization to avoid that upper layer connections use small
if (a.getInputBufferSize() < _sslEngine.getSession().getApplicationBufferSize()) // buffers and we need to copy decrypted data rather than decrypting in place.
a.setInputBufferSize(_sslEngine.getSession().getApplicationBufferSize()); AbstractConnection c = (AbstractConnection)connection;
int appBufferSize = getApplicationBufferSize();
if (c.getInputBufferSize() < appBufferSize)
c.setInputBufferSize(appBufferSize);
} }
super.setConnection(connection); 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 // can we use the passed buffer if it is big enough
ByteBuffer appIn; ByteBuffer appIn;
int appBufferSize = getApplicationBufferSize();
if (_decryptedInput == null) if (_decryptedInput == null)
{ {
if (BufferUtil.space(buffer) > _sslEngine.getSession().getApplicationBufferSize()) if (BufferUtil.space(buffer) > appBufferSize)
appIn = buffer; appIn = buffer;
else else
appIn = _decryptedInput = _bufferPool.acquire(_sslEngine.getSession().getApplicationBufferSize(), _decryptedDirectBuffers); appIn = _decryptedInput = _bufferPool.acquire(appBufferSize, _decryptedDirectBuffers);
} }
else 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. // 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()) if (LOG.isDebugEnabled())
LOG.debug("net filled={}", netFilled); 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"); 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 // Let's unwrap even if we have no net data because in that
// case we want to fall through to the handshake handling // case we want to fall through to the handshake handling
int pos = BufferUtil.flipToFill(appIn); int pos = BufferUtil.flipToFill(appIn);
@ -573,7 +682,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
try try
{ {
_underflown = false; _underflown = false;
unwrapResult = _sslEngine.unwrap(_encryptedInput, appIn); unwrapResult = unwrap(_sslEngine, _encryptedInput, appIn);
} }
finally finally
{ {
@ -620,8 +729,21 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
} }
return filled = netFilled; 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: case OK:
{
if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
handshakeSucceeded(); handshakeSucceeded();
@ -639,7 +761,6 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
} }
break; break;
}
default: default:
throw new IllegalStateException("Unexpected unwrap result " + unwrap); throw new IllegalStateException("Unexpected unwrap result " + unwrap);
@ -648,8 +769,8 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
} }
catch (Throwable x) catch (Throwable x)
{ {
Throwable failure = handleException(x, "fill"); Throwable f = handleException(x, "fill");
handshakeFailed(failure); Throwable failure = handshakeFailed(f);
if (_flushState == FlushState.WAIT_FOR_FILL) if (_flushState == FlushState.WAIT_FOR_FILL)
{ {
_flushState = FlushState.IDLE; _flushState = FlushState.IDLE;
@ -659,17 +780,8 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
} }
finally finally
{ {
if (_encryptedInput != null && !_encryptedInput.hasRemaining()) releaseEncryptedInputBuffer();
{ releaseDecryptedInputBuffer();
_bufferPool.release(_encryptedInput);
_encryptedInput = null;
}
if (_decryptedInput != null && !_decryptedInput.hasRemaining())
{
_bufferPool.release(_decryptedInput);
_decryptedInput = null;
}
if (_flushState == FlushState.WAIT_FOR_FILL) if (_flushState == FlushState.WAIT_FOR_FILL)
{ {
@ -771,7 +883,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
private void handshakeSucceeded() throws SSLException private void handshakeSucceeded() throws SSLException
{ {
if (_handshake.compareAndSet(Handshake.INITIAL, Handshake.SUCCEEDED)) if (_handshake.compareAndSet(HandshakeState.HANDSHAKE, HandshakeState.SUCCEEDED))
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("handshake succeeded {} {} {}/{}", SslConnection.this, LOG.debug("handshake succeeded {} {} {}/{}", SslConnection.this,
@ -779,16 +891,16 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
_sslEngine.getSession().getProtocol(), _sslEngine.getSession().getCipherSuite()); _sslEngine.getSession().getProtocol(), _sslEngine.getSession().getCipherSuite());
notifyHandshakeSucceeded(_sslEngine); notifyHandshakeSucceeded(_sslEngine);
} }
else if (_handshake.get() == Handshake.SUCCEEDED) else if (isHandshakeSucceeded())
{ {
if (_renegotiationLimit > 0) if (_renegotiationLimit > 0)
_renegotiationLimit--; _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()) if (LOG.isDebugEnabled())
LOG.debug("handshake failed {} {}", SslConnection.this, failure); 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); failure = new SSLHandshakeException(failure.getMessage()).initCause(failure);
notifyHandshakeFailed(_sslEngine, failure); notifyHandshakeFailed(_sslEngine, failure);
} }
return failure;
} }
private void terminateInput() private void terminateInput()
@ -820,7 +933,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
} }
catch (SSLException x) catch (SSLException x)
{ {
if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && !isAllowMissingCloseMessage()) if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && isRequireCloseMessage())
throw x; throw x;
LOG.ignore(x); LOG.ignore(x);
return x; return x;
@ -850,7 +963,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
} }
// finish of any previous flushes // finish of any previous flushes
if (BufferUtil.hasContent(_encryptedOutput) && !getEndPoint().flush(_encryptedOutput)) if (BufferUtil.hasContent(_encryptedOutput) && !networkFlush(_encryptedOutput))
return false; return false;
boolean isEmpty = BufferUtil.isEmpty(appOuts); boolean isEmpty = BufferUtil.isEmpty(appOuts);
@ -878,6 +991,9 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
continue; continue;
case NEED_UNWRAP: case NEED_UNWRAP:
// Workaround for Java 11 behavior.
if (isHandshakeInitial() && isOutboundDone())
break;
if (_fillState == FillState.IDLE) if (_fillState == FillState.IDLE)
{ {
int filled = fill(BufferUtil.EMPTY_BUFFER); int filled = fill(BufferUtil.EMPTY_BUFFER);
@ -892,16 +1008,23 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
throw new IllegalStateException("Unexpected HandshakeStatus " + status); throw new IllegalStateException("Unexpected HandshakeStatus " + status);
} }
if (_encryptedOutput == null) int packetBufferSize = getPacketBufferSize();
_encryptedOutput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers); 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); BufferUtil.compact(_encryptedOutput);
int pos = BufferUtil.flipToFill(_encryptedOutput); int pos = BufferUtil.flipToFill(_encryptedOutput);
SSLEngineResult wrapResult; SSLEngineResult wrapResult;
try try
{ {
wrapResult = _sslEngine.wrap(appOuts, _encryptedOutput); wrapResult = wrap(_sslEngine, appOuts, _encryptedOutput);
} }
finally finally
{ {
@ -920,7 +1043,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
// if we have net bytes, let's try to flush them // if we have net bytes, let's try to flush them
boolean flushed = true; boolean flushed = true;
if (BufferUtil.hasContent(_encryptedOutput)) if (BufferUtil.hasContent(_encryptedOutput))
flushed = getEndPoint().flush(_encryptedOutput); flushed = networkFlush(_encryptedOutput);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("net flushed={}, ac={}", flushed, isEmpty); LOG.debug("net flushed={}, ac={}", flushed, isEmpty);
@ -944,7 +1067,18 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
case BUFFER_OVERFLOW: case BUFFER_OVERFLOW:
if (!flushed) if (!flushed)
return result = false; 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: case OK:
if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
@ -980,8 +1114,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
catch (Throwable x) catch (Throwable x)
{ {
Throwable failure = handleException(x, "flush"); Throwable failure = handleException(x, "flush");
handshakeFailed(failure); throw handshakeFailed(failure);
throw failure;
} }
finally finally
{ {
@ -1096,15 +1229,15 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
@Override @Override
public void doShutdownOutput() public void doShutdownOutput()
{ {
final EndPoint endp = getEndPoint(); EndPoint endPoint = getEndPoint();
try try
{ {
boolean close; boolean close;
boolean flush = false; boolean flush = false;
synchronized (_decryptedEndPoint) synchronized (_decryptedEndPoint)
{ {
boolean ishut = endp.isInputShutdown(); boolean ishut = endPoint.isInputShutdown();
boolean oshut = endp.isOutputShutdown(); boolean oshut = endPoint.isOutputShutdown();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("shutdownOutput: {} oshut={}, ishut={}", SslConnection.this, oshut, ishut); 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. // let's just flush the encrypted output in the background.
ByteBuffer write = _encryptedOutput; ByteBuffer write = _encryptedOutput;
if (BufferUtil.hasContent(write)) 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) if (close)
endp.close(); endPoint.close();
else else
ensureFillInterested(); ensureFillInterested();
} }
catch (Throwable x) catch (Throwable x)
{ {
LOG.ignore(x); LOG.ignore(x);
endp.close(); endPoint.close();
} }
} }
@ -1152,7 +1285,8 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr
} }
catch (Throwable x) 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() private boolean isRenegotiating()
{ {
if (_handshake.get() == Handshake.INITIAL) if (!isHandshakeComplete())
return false; return false;
if (isTLS13()) if (isTLS13())
return false; return false;

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.maven.plugin; package org.eclipse.jetty.maven.plugin;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.LineNumberReader; import java.io.LineNumberReader;
@ -31,8 +32,10 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Random; import java.util.Random;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.Resource;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -51,7 +54,6 @@ public class TestForkedChild
File baseDir; File baseDir;
File tmpDir; File tmpDir;
File tokenFile; File tokenFile;
File forkWebXml;
File webappPropsFile; File webappPropsFile;
int stopPort; int stopPort;
String stopKey = "FERMATI"; String stopKey = "FERMATI";
@ -82,6 +84,7 @@ public class TestForkedChild
JettyWebAppContext webapp = new JettyWebAppContext(); JettyWebAppContext webapp = new JettyWebAppContext();
webapp.setContextPath("/foo"); webapp.setContextPath("/foo");
webapp.setTempDirectory(tmpDir); webapp.setTempDirectory(tmpDir);
webapp.setBaseResource(Resource.newResource(baseDir));
WebAppPropertyConverter.toProperties(webapp, webappPropsFile, null); WebAppPropertyConverter.toProperties(webapp, webappPropsFile, null);
child = new JettyForkedChild(cmd.toArray(new String[cmd.size()])); child = new JettyForkedChild(cmd.toArray(new String[cmd.size()]));
child.jetty.setExitVm(false); //ensure jetty doesn't stop vm for testing child.jetty.setExitVm(false); //ensure jetty doesn't stop vm for testing
@ -100,10 +103,9 @@ public class TestForkedChild
baseDir = MavenTestingUtils.getTestResourceDir("root"); baseDir = MavenTestingUtils.getTestResourceDir("root");
testDir = MavenTestingUtils.getTargetTestingDir("forkedChild"); testDir = MavenTestingUtils.getTargetTestingDir("forkedChild");
if (testDir.exists()) if (testDir.exists())
IO.delete(testDir); FS.delete(testDir);
testDir.mkdirs(); testDir.mkdirs();
tmpDir = new File(testDir, "tmp"); tmpDir = new File(testDir, "tmp");
forkWebXml = new File(testDir, "fork-web.xml");
webappPropsFile = new File(testDir, "webapp.props"); webappPropsFile = new File(testDir, "webapp.props");
stopPort = Integer.valueOf(System.getProperty("stop.port")); stopPort = Integer.valueOf(System.getProperty("stop.port"));
@ -165,6 +167,9 @@ public class TestForkedChild
connection = (HttpURLConnection)url.openConnection(); connection = (HttpURLConnection)url.openConnection();
connection.connect(); connection.connect();
assertThat(connection.getResponseCode(), Matchers.is(200)); assertThat(connection.getResponseCode(), Matchers.is(200));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IO.copy(connection.getInputStream(), baos);
assertThat(baos.toString(), Matchers.containsString("ROOT"));
} }
finally finally
{ {

View File

@ -246,6 +246,16 @@ public class OpenIdAuthenticator extends LoginAuthenticator
try 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. // Handle a request for authentication.
if (isJSecurityCheck(uri)) if (isJSecurityCheck(uri))
{ {
@ -288,7 +298,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
response.setContentLength(0); response.setContentLength(0);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); 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; return openIdAuth;
} }
} }
@ -308,7 +318,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("auth failed {}", _errorPage); LOG.debug("auth failed {}", _errorPage);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); 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; return Authentication.SEND_FAILURE;
@ -399,7 +409,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("challenge {}->{}", session.getId(), challengeUri); LOG.debug("challenge {}->{}", session.getId(), challengeUri);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); 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; return Authentication.SEND_CONTINUE;
} }

View File

@ -15,7 +15,7 @@ etc/jetty-gzip.xml
[ini-template] [ini-template]
## Minimum content length after which gzip is enabled ## Minimum content length after which gzip is enabled
# jetty.gzip.minGzipSize=2048 # jetty.gzip.minGzipSize=32
## Check whether a file with *.gz extension exists ## Check whether a file with *.gz extension exists
# jetty.gzip.checkGzExists=false # jetty.gzip.checkGzExists=false

View File

@ -500,7 +500,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
} }
// RFC 7230, section 3.3. // 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")) if (sendErrorOrAbort("Insufficient content written"))
break; break;

View File

@ -364,6 +364,13 @@ public class HttpOutput extends ServletOutputStream implements Runnable
State state = _state.get(); State state = _state.get();
switch (state) switch (state)
{ {
case CLOSING:
{
if (!_state.compareAndSet(state, State.CLOSED))
break;
releaseBuffer();
return;
}
case CLOSED: case CLOSED:
{ {
return; return;

View File

@ -110,8 +110,8 @@ import org.eclipse.jetty.util.resource.Resource;
@ManagedObject("URI Context") @ManagedObject("URI Context")
public class ContextHandler extends ScopedHandler implements Attributes, Graceful public class ContextHandler extends ScopedHandler implements Attributes, Graceful
{ {
public static final int SERVLET_MAJOR_VERSION = 3; public static final int SERVLET_MAJOR_VERSION = 4;
public static final int SERVLET_MINOR_VERSION = 1; public static final int SERVLET_MINOR_VERSION = 0;
public static final Class<?>[] SERVLET_LISTENER_TYPES = public static final Class<?>[] SERVLET_LISTENER_TYPES =
{ {
ServletContextListener.class, ServletContextListener.class,

View File

@ -188,13 +188,16 @@ public class InetAccessHandler extends HandlerWrapper
protected boolean isAllowed(InetAddress addr, Request baseRequest, HttpServletRequest request) protected boolean isAllowed(InetAddress addr, Request baseRequest, HttpServletRequest request)
{ {
String name = baseRequest.getHttpChannel().getConnector().getName(); String name = baseRequest.getHttpChannel().getConnector().getName();
boolean filterAppliesToConnector = _names.test(name);
boolean allowedByAddr = _addrs.test(addr);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
{ {
Boolean allowedByName = _names.isIncludedAndNotExcluded(name); LOG.debug("name = {}/{} addr={}/{} appliesToConnector={} allowedByAddr={}",
Boolean allowedByAddr = _addrs.isIncludedAndNotExcluded(addr); name, _names, addr, _addrs, filterAppliesToConnector, allowedByAddr);
LOG.debug("{} allowedByName={} allowedByAddr={} for {}/{}", this, allowedByName, allowedByAddr, addr, request);
} }
return _names.test(name) && _addrs.test(addr); if (!filterAppliesToConnector)
return true;
return allowedByAddr;
} }
@Override @Override

View File

@ -25,7 +25,6 @@ import java.util.ListIterator;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -92,8 +91,7 @@ import org.eclipse.jetty.util.log.Logger;
* </li> * </li>
* <li> * <li>
* Is the Response {@code Content-Length} header present, and does its * Is the Response {@code Content-Length} header present, and does its
* value meet the minimum gzip size requirements? * value meet the minimum gzip size requirements (default 32 bytes)?
* <br> (Default: 16 bytes. see {@link GzipHandler#DEFAULT_MIN_GZIP_SIZE})
* </li> * </li>
* <li> * <li>
* Is the Request {@code Accept} header present and does it contain the * 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 GZIP = "gzip";
public static final String DEFLATE = "deflate"; public static final String DEFLATE = "deflate";
public static final int DEFAULT_MIN_GZIP_SIZE = 2048; public static final int DEFAULT_MIN_GZIP_SIZE = 32;
public static final int COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION; public static final int BREAK_EVEN_GZIP_SIZE = 23;
private static final Logger LOG = Log.getLogger(GzipHandler.class); 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 X_CE_GZIP = new PreEncodedHttpField("X-Content-Encoding", "gzip");
private static final HttpField TE_CHUNKED = new PreEncodedHttpField(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED.asString()); 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.
* <p>
* Sizes below {@link #BREAK_EVEN_GZIP_SIZE} will result a compressed response that is larger than the
* original data.
* </p>
* *
* @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) 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);
} }
/** /**

View File

@ -113,7 +113,7 @@ public class DumpHandler extends AbstractHandler.ErrorDispatchHandler
writer.write("<pre>\nlocal=" + request.getLocalAddr() + ":" + request.getLocalPort() + "\n</pre>\n"); writer.write("<pre>\nlocal=" + request.getLocalAddr() + ":" + request.getLocalPort() + "\n</pre>\n");
writer.write("<pre>\nremote=" + request.getRemoteAddr() + ":" + request.getRemotePort() + "\n</pre>\n"); writer.write("<pre>\nremote=" + request.getRemoteAddr() + ":" + request.getRemotePort() + "\n</pre>\n");
writer.write("<h3>Header:</h3><pre>"); writer.write("<h3>Header:</h3><pre>");
writer.write(request.getMethod() + " " + request.getRequestURI() + " " + request.getProtocol() + "\n"); writer.write(String.format("%4s %s %s\n", request.getMethod(), request.getRequestURI(), request.getProtocol()));
Enumeration<String> headers = request.getHeaderNames(); Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()) while (headers.hasMoreElements())
{ {

View File

@ -30,10 +30,13 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringReader; import java.io.StringReader;
import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -54,6 +57,9 @@ import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; 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<Arguments> 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. * More then 1 Content-Length is a bad requests per HTTP rfcs.
*/ */
@Test @ParameterizedTest
public void testHttp11ContentLengthAndChunk() throws Exception @MethodSource("http11ContentLengthAndChunkedData")
public void testHttp11ContentLengthAndChunk(int[] contentLengths) throws Exception
{ {
HttpParser.LOG.info("badMessage: 400 Bad messages EXPECTED..."); 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(); switch (contentLengths[n])
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++)
{ {
if (clen[n] == -1) case CHUNKED:
request.append("Transfer-Encoding: chunked\r\n"); request.append("Transfer-Encoding: chunked\r\n");
else break;
request.append("Content-Length: ").append(Integer.toString(clen[n])).append("\r\n"); 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<Arguments> 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<String> 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<Arguments> 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<String> 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 @Test
@ -549,11 +684,10 @@ public class HttpConnectionTest
"Host: localhost\r\n" + "Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" + "Transfer-Encoding: chunked\r\n" +
"Content-Type: text/plain\r\n" + "Content-Type: text/plain\r\n" +
"Connection: close\r\n" +
"\r\n" + "\r\n" +
"A\r\n" + "A\r\n" +
"0123456789\r\n" + "0123456789\r\n" +
"0\r\n"); "0\r\n\r\n");
int offset = 0; int offset = 0;
offset = checkContains(response, offset, "HTTP/1.1 200"); offset = checkContains(response, offset, "HTTP/1.1 200");

View File

@ -24,6 +24,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler; 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 @ParameterizedTest
@MethodSource("httpVersions") @MethodSource("httpVersions")
public void testSetContentLengthFlushAndWriteInsufficientBytes(HttpVersion httpVersion) throws Exception 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 class SetContentLengthAndWriteThatAmountOfBytesHandler extends ThrowExceptionOnDemandHandler
{ {
private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException) private SetContentLengthAndWriteThatAmountOfBytesHandler(boolean throwException)

View File

@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; 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, "<html>", "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("<html>"), "HEAD no body");
checkContains(head, 0, contentLength, "3.3.2 HEAD");
}
catch (Exception e)
{
e.printStackTrace();
assertTrue(false);
}
}
@Test @Test
public void test3_6_a() throws Exception public void test3_6_a() throws Exception
{ {
@ -324,12 +352,10 @@ public class PartialRFC2616Test
"\n"); "\n");
offset = 0; offset = 0;
response = endp.getResponse(); response = endp.getResponse();
offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10; offset = checkContains(response, offset, "HTTP/1.1 400 ", "2. identity") + 10;
offset = checkContains(response, offset, "/R1", "2. identity") + 3;
offset = 0; offset = 0;
response = endp.getResponse(); response = endp.getResponse();
offset = checkContains(response, offset, "HTTP/1.1 200 OK", "2. identity") + 10; assertThat("There should be no next response as first one closed connection", response, is(nullValue()));
offset = checkContains(response, offset, "/R2", "2. identity") + 3;
} }
@Test @Test
@ -361,7 +387,7 @@ public class PartialRFC2616Test
"\n" + "\n" +
"abcdef"); "abcdef");
response = endp.getResponse(); 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"); checkNotContained(response, offset, "/R2", "3. _content-length");
} }

View File

@ -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<Thread> 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;
}
}
}

View File

@ -21,7 +21,9 @@ package org.eclipse.jetty.server.handler;
import java.io.IOException; import java.io.IOException;
import java.net.Socket; import java.net.Socket;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -45,17 +47,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class InetAccessHandlerTest public class InetAccessHandlerTest
{ {
private static Server _server; private static Server _server;
private static ServerConnector _connector; private static ServerConnector _connector1;
private static ServerConnector _connector2;
private static InetAccessHandler _handler; private static InetAccessHandler _handler;
@BeforeAll @BeforeAll
public static void setUp() throws Exception public static void setUp() throws Exception
{ {
_server = new Server(); _server = new Server();
_connector = new ServerConnector(_server); _connector1 = new ServerConnector(_server);
_connector.setName("http"); _connector1.setName("http_connector1");
_connector2 = new ServerConnector(_server);
_connector2.setName("http_connector2");
_server.setConnectors(new Connector[] _server.setConnectors(new Connector[]
{_connector}); {_connector1, _connector2});
_handler = new InetAccessHandler(); _handler = new InetAccessHandler();
_handler.setHandler(new AbstractHandler() _handler.setHandler(new AbstractHandler()
@ -113,7 +118,21 @@ public class InetAccessHandlerTest
} }
} }
try (Socket socket = new Socket("127.0.0.1", _connector.getLocalPort());) List<String> 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); 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<Arguments> data() public static Stream<Arguments> data()
{ {
Object[][] data = new Object[][] Object[][] data = new Object[][]
{ {
// Empty lists // Empty lists 1
{"", "", "", "", "200"}, {"", "", "", "", "200;200"},
// test simple filters // test simple filters
{"127.0.0.1", "", "", "", "200"}, {"127.0.0.1", "", "", "", "200;200"},
{"127.0.0.1-127.0.0.254", "", "", "", "200"}, {"127.0.0.1-127.0.0.254", "", "", "", "200;200"},
{"192.0.0.1", "", "", "", "403"}, {"192.0.0.1", "", "", "", "403;403"},
{"192.0.0.1-192.0.0.254", "", "", "", "403"}, {"192.0.0.1-192.0.0.254", "", "", "", "403;403"},
// test connector name filters // test includeConnector
{"127.0.0.1", "", "http", "", "200"}, {"127.0.0.1", "", "http_connector1", "", "200;200"},
{"127.0.0.1-127.0.0.254", "", "http", "", "200"}, {"127.0.0.1-127.0.0.254", "", "http_connector1", "", "200;200"},
{"192.0.0.1", "", "http", "", "403"}, {"192.0.0.1", "", "http_connector1", "", "403;200"},
{"192.0.0.1-192.0.0.254", "", "http", "", "403"}, {"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"}, // test includeConnector names where none of them match
{"127.0.0.1-127.0.0.254", "", "nothttp", "", "403"}, {"127.0.0.1", "", "nothttp", "", "200;200"},
{"192.0.0.1", "", "nothttp", "", "403"}, {"127.0.0.1-127.0.0.254", "", "nothttp", "", "200;200"},
{"192.0.0.1-192.0.0.254", "", "nothttp", "", "403"}, {"192.0.0.1", "", "nothttp", "", "200;200"},
{"192.0.0.1-192.0.0.254", "", "nothttp", "", "200;200"},
{"127.0.0.1", "", "", "http", "403"}, // text excludeConnector
{"127.0.0.1-127.0.0.254", "", "", "http", "403"}, {"127.0.0.1", "", "", "http_connector1", "200;200"},
{"192.0.0.1", "", "", "http", "403"}, {"127.0.0.1-127.0.0.254", "", "", "http_connector1", "200;200"},
{"192.0.0.1-192.0.0.254", "", "", "http", "403"}, {"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"}, // test excludeConnector where none of them match.
{"127.0.0.1-127.0.0.254", "", "", "nothttp", "200"}, {"127.0.0.1", "", "", "nothttp", "200;200"},
{"192.0.0.1", "", "", "nothttp", "403"}, {"127.0.0.1-127.0.0.254", "", "", "nothttp", "200;200"},
{"192.0.0.1-192.0.0.254", "", "", "nothttp", "403"}, {"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); return Arrays.asList(data).stream().map(Arguments::of);
} }

View File

@ -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();
}
}
}

View File

@ -25,6 +25,7 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Writer; import java.io.Writer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@ -251,7 +252,11 @@ public class CGI extends HttpServlet
String parameterName = names.nextElement(); String parameterName = names.nextElement();
parameterMap.addValues(parameterName, req.getParameterValues(parameterName)); 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); EnvList env = new EnvList(_env);

View File

@ -121,7 +121,7 @@ import org.eclipse.jetty.util.thread.Scheduler;
* </dl> * </dl>
* <p> * <p>
* This filter should be configured for {@link DispatcherType#REQUEST} and {@link DispatcherType#ASYNC} and with * This filter should be configured for {@link DispatcherType#REQUEST} and {@link DispatcherType#ASYNC} and with
* <code>&lt;async-supported&gt;true&lt;/async-supported&gt;</code>. * {@code <async-supported>true</async-supported>}.
* </p> * </p>
*/ */
@ManagedObject("limits exposure to abuse from request flooding, whether malicious, or as a result of a misconfigured client") @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_REQUEST_MS_INIT_PARAM = 30000L;
private static final long __DEFAULT_MAX_IDLE_TRACKER_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 MANAGED_ATTR_INIT_PARAM = "managedAttr";
static final String MAX_REQUESTS_PER_S_INIT_PARAM = "maxRequestsPerSec"; static final String MAX_REQUESTS_PER_S_INIT_PARAM = "maxRequestsPerSec";
static final String DELAY_MS_INIT_PARAM = "delayMs"; static final String DELAY_MS_INIT_PARAM = "delayMs";
@ -384,14 +383,13 @@ public class DoSFilter implements Filter
long throttleMs = getThrottleMs(); long throttleMs = getThrottleMs();
if (!Boolean.TRUE.equals(throttled) && throttleMs > 0) if (!Boolean.TRUE.equals(throttled) && throttleMs > 0)
{ {
final int priority = getPriority(request, tracker); int priority = getPriority(request, tracker);
request.setAttribute(__THROTTLED, Boolean.TRUE); request.setAttribute(__THROTTLED, Boolean.TRUE);
if (isInsertHeaders()) if (isInsertHeaders())
response.addHeader("DoSFilter", "throttled"); response.addHeader("DoSFilter", "throttled");
AsyncContext asyncContext = request.startAsync(); AsyncContext asyncContext = request.startAsync();
request.setAttribute(_suspended, Boolean.TRUE); request.setAttribute(_suspended, Boolean.TRUE);
if (throttleMs > 0) asyncContext.setTimeout(throttleMs);
asyncContext.setTimeout(throttleMs);
asyncContext.addListener(_listeners[priority]); asyncContext.addListener(_listeners[priority]);
_queues[priority].add(asyncContext); _queues[priority].add(asyncContext);
if (LOG.isDebugEnabled()) 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 protected void doFilterChain(FilterChain chain, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
{ {
final Thread thread = Thread.currentThread(); final Thread thread = Thread.currentThread();
Runnable requestTimeout = new Runnable() Runnable requestTimeout = () -> onRequestTimeout(request, response, thread);
{
@Override
public void run()
{
onRequestTimeout(request, response, thread);
}
};
Scheduler.Task task = _scheduler.schedule(requestTimeout, getMaxRequestMs(), TimeUnit.MILLISECONDS); Scheduler.Task task = _scheduler.schedule(requestTimeout, getMaxRequestMs(), TimeUnit.MILLISECONDS);
try try
{ {
@ -527,7 +518,7 @@ public class DoSFilter implements Filter
* @param tracker the rate tracker for this request * @param tracker the rate tracker for this request
* @return the priority 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) if (extractUserId(request) != null)
return USER_AUTH; return USER_AUTH;
@ -544,7 +535,7 @@ public class DoSFilter implements Filter
return USER_AUTH; return USER_AUTH;
} }
public void schedule(RateTracker tracker) private void schedule(RateTracker tracker)
{ {
_scheduler.schedule(tracker, getMaxIdleTrackerMs(), TimeUnit.MILLISECONDS); _scheduler.schedule(tracker, getMaxIdleTrackerMs(), TimeUnit.MILLISECONDS);
} }
@ -565,7 +556,7 @@ public class DoSFilter implements Filter
* @param request the current request * @param request the current request
* @return the request rate tracker for the current connection * @return the request rate tracker for the current connection
*/ */
public RateTracker getRateTracker(ServletRequest request) RateTracker getRateTracker(ServletRequest request)
{ {
HttpSession session = ((HttpServletRequest)request).getSession(false); HttpSession session = ((HttpServletRequest)request).getSession(false);
@ -617,7 +608,7 @@ public class DoSFilter implements Filter
return tracker; return tracker;
} }
public void addToRateTracker(RateTracker tracker) private void addToRateTracker(RateTracker tracker)
{ {
_rateTrackers.put(tracker.getId(), tracker); _rateTrackers.put(tracker.getId(), tracker);
} }
@ -700,7 +691,7 @@ public class DoSFilter implements Filter
byte[] result = new byte[4]; byte[] result = new byte[4];
for (int i = 0; i < result.length; ++i) 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; return result;
} }
@ -1237,7 +1228,7 @@ public class DoSFilter implements Filter
LOG.debug("Tracker removed: {}", getId()); LOG.debug("Tracker removed: {}", getId());
} }
protected void addToRateTrackers(DoSFilter filter, RateTracker tracker) private void addToRateTrackers(DoSFilter filter, RateTracker tracker)
{ {
if (filter == null) if (filter == null)
return; 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) 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 @Override
public void onStartAsync(AsyncEvent event) throws IOException public void onStartAsync(AsyncEvent event)
{ {
} }
@Override @Override
public void onComplete(AsyncEvent event) throws IOException public void onComplete(AsyncEvent event)
{ {
} }
@ -1325,7 +1316,7 @@ public class DoSFilter implements Filter
} }
@Override @Override
public void onError(AsyncEvent event) throws IOException public void onError(AsyncEvent event)
{ {
} }
} }

View File

@ -56,6 +56,12 @@ public class IncludeExcludeSet<T, P> implements Predicate<P>
{ {
return set.contains(item); return set.contains(item);
} }
@Override
public String toString()
{
return "CONTAINS";
}
} }
/** /**
@ -227,4 +233,34 @@ public class IncludeExcludeSet<T, P> implements Predicate<P>
{ {
return _includes.isEmpty() && _excludes.isEmpty(); 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 <T1> The type of item1
* @param <T2> The type of item2
* @return True IFF <ul>
* <li>Neither item is excluded from their respective sets</li>
* <li>Both sets have no includes OR at least one of the items is included in its respective set</li>
* </ul>
*/
public static <T1,T2> boolean matchCombined(T1 item1, IncludeExcludeSet<?,T1> set1, T2 item2, IncludeExcludeSet<?,T2> 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;
}
} }

View File

@ -56,7 +56,7 @@ public class InetAddressSet extends AbstractSet<String> implements Set<String>,
return _patterns.put(pattern, newInetRange(pattern)) == null; return _patterns.put(pattern, newInetRange(pattern)) == null;
} }
protected InetPattern newInetRange(String pattern) private InetPattern newInetRange(String pattern)
{ {
if (pattern == null) if (pattern == null)
return null; return null;

View File

@ -49,11 +49,11 @@ public class Scanner extends AbstractLifeCycle
private static int __scannerId = 0; private static int __scannerId = 0;
private int _scanInterval; private int _scanInterval;
private int _scanCount = 0; private int _scanCount = 0;
private final List<Listener> _listeners = new ArrayList<Listener>(); private final List<Listener> _listeners = new ArrayList<>();
private final Map<String, TimeNSize> _prevScan = new HashMap<String, TimeNSize>(); private final Map<String, TimeNSize> _prevScan = new HashMap<>();
private final Map<String, TimeNSize> _currentScan = new HashMap<String, TimeNSize>(); private final Map<String, TimeNSize> _currentScan = new HashMap<>();
private FilenameFilter _filter; private FilenameFilter _filter;
private final List<File> _scanDirs = new ArrayList<File>(); private final List<File> _scanDirs = new ArrayList<>();
private volatile boolean _running = false; private volatile boolean _running = false;
private boolean _reportExisting = true; private boolean _reportExisting = true;
private boolean _reportDirs = true; private boolean _reportDirs = true;
@ -66,8 +66,7 @@ public class Scanner extends AbstractLifeCycle
ADDED, CHANGED, REMOVED ADDED, CHANGED, REMOVED
} }
; private final Map<String, Notification> _notifications = new HashMap<>();
private final Map<String, Notification> _notifications = new HashMap<String, Notification>();
static class TimeNSize static class TimeNSize
{ {
@ -412,11 +411,7 @@ public class Scanner extends AbstractLifeCycle
if (l instanceof ScanListener) if (l instanceof ScanListener)
((ScanListener)l).scan(); ((ScanListener)l).scan();
} }
catch (Exception e) catch (Throwable e)
{
LOG.warn(e);
}
catch (Error e)
{ {
LOG.warn(e); LOG.warn(e);
} }
@ -428,16 +423,11 @@ public class Scanner extends AbstractLifeCycle
*/ */
public synchronized void scanFiles() public synchronized void scanFiles()
{ {
if (_scanDirs == null)
return;
_currentScan.clear(); _currentScan.clear();
Iterator<File> itor = _scanDirs.iterator(); for (File dir : _scanDirs)
while (itor.hasNext())
{ {
File dir = itor.next();
if ((dir != null) && (dir.exists())) if ((dir != null) && (dir.exists()))
{
try try
{ {
scanFile(dir.getCanonicalFile(), _currentScan, 0); scanFile(dir.getCanonicalFile(), _currentScan, 0);
@ -446,6 +436,7 @@ public class Scanner extends AbstractLifeCycle
{ {
LOG.warn("Error scanning files.", e); 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 currentScan the info from the most recent pass
* @param oldScan info from the previous pass * @param oldScan info from the previous pass
*/ */
public synchronized void reportDifferences(Map<String, TimeNSize> currentScan, Map<String, TimeNSize> oldScan) private synchronized void reportDifferences(Map<String, TimeNSize> currentScan, Map<String, TimeNSize> oldScan)
{ {
// scan the differences and add what was found to the map of notifications: // scan the differences and add what was found to the map of notifications:
Set<String> oldScanKeys = new HashSet<String>(oldScan.keySet()); Set<String> oldScanKeys = new HashSet<>(oldScan.keySet());
// Look for new and changed files // Look for new and changed files
for (Map.Entry<String, TimeNSize> entry : currentScan.entrySet()) for (Map.Entry<String, TimeNSize> entry : currentScan.entrySet())
@ -484,17 +475,8 @@ public class Scanner extends AbstractLifeCycle
else if (!oldScan.get(file).equals(currentScan.get(file))) else if (!oldScan.get(file).equals(currentScan.get(file)))
{ {
Notification old = _notifications.put(file, Notification.CHANGED); Notification old = _notifications.put(file, Notification.CHANGED);
if (old != null) if (old == Notification.ADDED)
{ _notifications.put(file, Notification.ADDED);
switch (old)
{
case ADDED:
_notifications.put(file, Notification.ADDED);
break;
default:
break;
}
}
} }
} }
@ -504,17 +486,8 @@ public class Scanner extends AbstractLifeCycle
if (!currentScan.containsKey(file)) if (!currentScan.containsKey(file))
{ {
Notification old = _notifications.put(file, Notification.REMOVED); Notification old = _notifications.put(file, Notification.REMOVED);
if (old != null) if (old == Notification.ADDED)
{ _notifications.remove(file);
switch (old)
{
case ADDED:
_notifications.remove(file);
break;
default:
break;
}
}
} }
} }
@ -523,7 +496,7 @@ public class Scanner extends AbstractLifeCycle
// Process notifications // Process notifications
// Only process notifications that are for stable files (ie same in old and current scan). // Only process notifications that are for stable files (ie same in old and current scan).
List<String> bulkChanges = new ArrayList<String>(); List<String> bulkChanges = new ArrayList<>();
for (Iterator<Entry<String, Notification>> iter = _notifications.entrySet().iterator(); iter.hasNext(); ) for (Iterator<Entry<String, Notification>> iter = _notifications.entrySet().iterator(); iter.hasNext(); )
{ {
Entry<String, Notification> entry = iter.next(); Entry<String, Notification> entry = iter.next();
@ -577,7 +550,7 @@ public class Scanner extends AbstractLifeCycle
if (f.isFile() || depth > 0 && _reportDirs && f.isDirectory()) 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()) if (LOG.isDebugEnabled())
LOG.debug("scan accepted {}", f); LOG.debug("scan accepted {}", f);
@ -597,9 +570,9 @@ public class Scanner extends AbstractLifeCycle
File[] files = f.listFiles(); File[] files = f.listFiles();
if (files != null) 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 else
@ -624,20 +597,14 @@ public class Scanner extends AbstractLifeCycle
*/ */
private void reportAddition(String filename) private void reportAddition(String filename)
{ {
Iterator<Listener> itor = _listeners.iterator(); for (Listener l : _listeners)
while (itor.hasNext())
{ {
Listener l = itor.next();
try try
{ {
if (l instanceof DiscreteListener) if (l instanceof DiscreteListener)
((DiscreteListener)l).fileAdded(filename); ((DiscreteListener)l).fileAdded(filename);
} }
catch (Exception e) catch (Throwable e)
{
warn(l, filename, e);
}
catch (Error e)
{ {
warn(l, filename, e); warn(l, filename, e);
} }
@ -651,20 +618,14 @@ public class Scanner extends AbstractLifeCycle
*/ */
private void reportRemoval(String filename) private void reportRemoval(String filename)
{ {
Iterator<Listener> itor = _listeners.iterator(); for (Object l : _listeners)
while (itor.hasNext())
{ {
Object l = itor.next();
try try
{ {
if (l instanceof DiscreteListener) if (l instanceof DiscreteListener)
((DiscreteListener)l).fileRemoved(filename); ((DiscreteListener)l).fileRemoved(filename);
} }
catch (Exception e) catch (Throwable e)
{
warn(l, filename, e);
}
catch (Error e)
{ {
warn(l, filename, e); warn(l, filename, e);
} }
@ -678,20 +639,14 @@ public class Scanner extends AbstractLifeCycle
*/ */
private void reportChange(String filename) private void reportChange(String filename)
{ {
Iterator<Listener> itor = _listeners.iterator(); for (Listener l : _listeners)
while (itor.hasNext())
{ {
Listener l = itor.next();
try try
{ {
if (l instanceof DiscreteListener) if (l instanceof DiscreteListener)
((DiscreteListener)l).fileChanged(filename); ((DiscreteListener)l).fileChanged(filename);
} }
catch (Exception e) catch (Throwable e)
{
warn(l, filename, e);
}
catch (Error e)
{ {
warn(l, filename, e); warn(l, filename, e);
} }
@ -700,20 +655,14 @@ public class Scanner extends AbstractLifeCycle
private void reportBulkChanges(List<String> filenames) private void reportBulkChanges(List<String> filenames)
{ {
Iterator<Listener> itor = _listeners.iterator(); for (Listener l : _listeners)
while (itor.hasNext())
{ {
Listener l = itor.next();
try try
{ {
if (l instanceof BulkListener) if (l instanceof BulkListener)
((BulkListener)l).filesChanged(filenames); ((BulkListener)l).filesChanged(filenames);
} }
catch (Exception e) catch (Throwable e)
{
warn(l, filenames.toString(), e);
}
catch (Error e)
{ {
warn(l, filenames.toString(), e); warn(l, filenames.toString(), e);
} }

View File

@ -333,7 +333,7 @@ public class ContainerLifeCycle extends AbstractLifeCycle implements Container,
return addBean(o, managed ? Managed.POJO : Managed.UNMANAGED); 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)) if (o == null || contains(o))
return false; return false;

View File

@ -72,7 +72,7 @@ public class ClassMatcher extends AbstractSet<String>
{ {
private static final Logger LOG = Log.getLogger(ClassMatcher.class); private static final Logger LOG = Log.getLogger(ClassMatcher.class);
static class Entry public static class Entry
{ {
private final String _pattern; private final String _pattern;
private final String _name; private final String _name;

View File

@ -41,7 +41,7 @@ public class WebDescriptor extends Descriptor
protected static XmlParser _nonValidatingStaticParser; protected static XmlParser _nonValidatingStaticParser;
protected MetaDataComplete _metaDataComplete; protected MetaDataComplete _metaDataComplete;
protected int _majorVersion = 3; //default to container version protected int _majorVersion = 4; //default to container version
protected int _minorVersion = 0; protected int _minorVersion = 0;
protected ArrayList<String> _classNames = new ArrayList<String>(); protected ArrayList<String> _classNames = new ArrayList<String>();
protected boolean _distributable; protected boolean _distributable;

View File

@ -239,6 +239,7 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli
public void setIdleTimeout(Duration duration) public void setIdleTimeout(Duration duration)
{ {
configurationCustomizer.setIdleTimeout(duration); configurationCustomizer.setIdleTimeout(duration);
getHttpClient().setIdleTimeout(duration.toMillis());
} }
@Override @Override

View File

@ -26,6 +26,7 @@ import java.net.SocketTimeoutException;
import java.net.URI; import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -70,6 +71,7 @@ public class ClientConnectTest
{ {
private Server server; private Server server;
private WebSocketClient client; private WebSocketClient client;
private CountDownLatch serverLatch = new CountDownLatch(1);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <E extends Throwable> E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher<Throwable> errorMatcher) private <E extends Throwable> E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher<Throwable> errorMatcher)
@ -97,7 +99,7 @@ public class ClientConnectTest
{ {
client = new WebSocketClient(); client = new WebSocketClient();
client.setConnectTimeout(TimeUnit.SECONDS.toMillis(3)); client.setConnectTimeout(TimeUnit.SECONDS.toMillis(3));
client.setIdleTimeout(Duration.ofSeconds(10)); client.setIdleTimeout(Duration.ofSeconds(3));
client.start(); client.start();
} }
@ -124,6 +126,19 @@ public class ClientConnectTest
return new EchoSocket(); return new EchoSocket();
}); });
container.addMapping("/get-auth-header", (req, resp) -> new GetAuthHeaderEndpoint()); 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)); context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
@ -367,35 +382,32 @@ public class ClientConnectTest
@Test @Test
public void testConnectionTimeout_Concurrent() throws Exception public void testConnectionTimeout_Concurrent() throws Exception
{ {
client.setConnectTimeout(1000);
client.setIdleTimeout(Duration.ofSeconds(1));
CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint(); CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint();
try (ServerSocket serverSocket = new ServerSocket()) // Connect to endpoint which waits and does not send back a response.
{ URI wsUri = WSURI.toWebsocket(server.getURI().resolve("/noResponse"));
InetAddress addr = InetAddress.getByName("localhost"); Future<Session> future = client.connect(cliSock, wsUri);
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<Session> future = client.connect(cliSock, wsUri);
// Accept the connection, but do nothing on it (no response, no upgrade, etc) // The attempt to get upgrade response future should throw error
serverSocket.accept(); Exception e = assertThrows(Exception.class,
() -> future.get(5, TimeUnit.SECONDS));
// The attempt to get upgrade response future should throw error // Allow server to exit now we have failed.
Exception e = assertThrows(Exception.class, serverLatch.countDown();
() -> future.get(5, TimeUnit.SECONDS));
if (e instanceof ExecutionException) // Unwrap the exception to test if it was what we expected.
{ assertThat(e, instanceOf(ExecutionException.class));
assertExpectedError((ExecutionException)e, cliSock, anyOf(
instanceOf(ConnectException.class), Throwable jettyUpgradeException = e.getCause();
instanceOf(UpgradeException.class) assertThat(jettyUpgradeException, instanceOf(UpgradeException.class));
));
} Throwable coreUpgradeException = jettyUpgradeException.getCause();
else assertThat(coreUpgradeException, instanceOf(org.eclipse.jetty.websocket.core.UpgradeException.class));
{
assertThat("Should have been a TimeoutException", e, instanceOf(TimeoutException.class)); Throwable timeoutException = coreUpgradeException.getCause();
} assertThat(timeoutException, instanceOf(TimeoutException.class));
} assertThat(timeoutException.getMessage(), containsString("Idle timeout"));
} }
} }

View File

@ -256,12 +256,17 @@ public class Parser
protected void checkFrameSize(byte opcode, int payloadLength) throws MessageTooLargeException, ProtocolException protected void checkFrameSize(byte opcode, int payloadLength) throws MessageTooLargeException, ProtocolException
{ {
if (OpCode.isControlFrame(opcode) && payloadLength > Frame.MAX_CONTROL_PAYLOAD) if (OpCode.isControlFrame(opcode))
throw new ProtocolException("Invalid control frame payload length, [" + payloadLength + "] cannot exceed [" + Frame.MAX_CONTROL_PAYLOAD + "]"); {
if (payloadLength > Frame.MAX_CONTROL_PAYLOAD)
long maxFrameSize = configuration.getMaxFrameSize(); throw new ProtocolException("Invalid control frame payload length, [" + payloadLength + "] cannot exceed [" + Frame.MAX_CONTROL_PAYLOAD + "]");
if (!configuration.isAutoFragment() && maxFrameSize > 0 && payloadLength > maxFrameSize) }
throw new MessageTooLargeException("Cannot handle payload lengths larger than " + maxFrameSize); 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) 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); 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) private ParsedFrame parsePayload(ByteBuffer buffer)
{ {
if (payloadLength == 0) if (payloadLength == 0)
@ -288,35 +319,21 @@ public class Parser
return null; return null;
int available = buffer.remaining(); 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 (aggregate == null)
{ {
if (available < payloadLength) if (available < payloadLength)
{ {
// not enough to complete this frame // not enough to complete this frame
// Can we auto-fragment // Can we auto-fragment
if (configuration.isAutoFragment() && OpCode.isDataFrame(OpCode.getOpCode(firstByte))) if (configuration.isAutoFragment() && isDataFrame)
{ return autoFragment(buffer, available);
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;
}
// No space in the buffer, so we have to copy the partial payload // No space in the buffer, so we have to copy the partial payload
aggregate = bufferPool.acquire(payloadLength, false); aggregate = bufferPool.acquire(payloadLength, false);

View File

@ -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<FrameHandler.CoreSession> 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<FrameHandler.CoreSession> 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<payloadSize; i++)
payload.put((byte)rand.nextInt(Byte.MAX_VALUE));
BufferUtil.flipToFlush(payload, 0);
// Send the large random payload which should be fragmented on the server.
clientHandler.coreSession.sendFrame(new Frame(OpCode.BINARY, BufferUtil.copy(payload)), Callback.NOOP, false);
// Assemble the message from the fragmented frames.
ByteBuffer message = BufferUtil.allocate(payloadSize*2);
Frame frame = serverHandler.receivedFrames.poll(1, TimeUnit.SECONDS);
while (frame != null)
{
int framePayloadLen = frame.getPayloadLength();
int append = BufferUtil.append(message, frame.getPayload());
assertThat(framePayloadLen, lessThanOrEqualTo(maxFrameSize));
assertThat(append, is(framePayloadLen));
frame = serverHandler.receivedFrames.poll(1, TimeUnit.SECONDS);
}
assertThat(message, is(payload));
clientHandler.sendClose();
assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS));
assertTrue(clientHandler.closed.await(5, TimeUnit.SECONDS));
}
}

View File

@ -57,6 +57,8 @@ public class ParserCapture
ExtensionStack exStack = new ExtensionStack(components, Behavior.SERVER); ExtensionStack exStack = new ExtensionStack(components, Behavior.SERVER);
exStack.negotiate(new LinkedList<>(), new LinkedList<>()); exStack.negotiate(new LinkedList<>(), new LinkedList<>());
this.coreSession = new WebSocketCoreSession(new TestMessageHandler(), behavior, Negotiated.from(exStack)); this.coreSession = new WebSocketCoreSession(new TestMessageHandler(), behavior, Negotiated.from(exStack));
coreSession.setAutoFragment(false);
coreSession.setMaxFrameSize(0);
this.parser = new Parser(components.getBufferPool(), coreSession); this.parser = new Parser(components.getBufferPool(), coreSession);
} }

View File

@ -1542,6 +1542,7 @@ public class ParserTest
ByteBuffer buffer = BufferUtil.allocate(32); ByteBuffer buffer = BufferUtil.allocate(32);
ParserCapture capture = new ParserCapture(false, Behavior.SERVER); ParserCapture capture = new ParserCapture(false, Behavior.SERVER);
capture.getCoreSession().setAutoFragment(true);
data.limit(6 + 5); data.limit(6 + 5);
BufferUtil.append(buffer, data); BufferUtil.append(buffer, data);

View File

@ -57,7 +57,8 @@ public class TestFrameHandler implements SynchronousFrameHandler
@Override @Override
public void onOpen(CoreSession coreSession) public void onOpen(CoreSession coreSession)
{ {
LOG.info("onOpen {}", coreSession); if (LOG.isDebugEnabled())
LOG.debug("onOpen {}", coreSession);
this.coreSession = coreSession; this.coreSession = coreSession;
open.countDown(); open.countDown();
} }
@ -65,41 +66,47 @@ public class TestFrameHandler implements SynchronousFrameHandler
@Override @Override
public void onFrame(Frame frame) 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)); receivedFrames.offer(Frame.copy(frame));
} }
@Override @Override
public void onClosed(CloseStatus closeStatus) public void onClosed(CloseStatus closeStatus)
{ {
LOG.info("onClosed {}", closeStatus); if (LOG.isDebugEnabled())
LOG.debug("onClosed {}", closeStatus);
closed.countDown(); closed.countDown();
} }
@Override @Override
public void onError(Throwable cause) 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; failure = cause;
error.countDown(); error.countDown();
} }
public void sendText(String text) public void sendText(String text)
{ {
LOG.info("sendText {} ", text); if (LOG.isDebugEnabled())
LOG.debug("sendText {} ", text);
Frame frame = new Frame(OpCode.TEXT, text); Frame frame = new Frame(OpCode.TEXT, text);
getCoreSession().sendFrame(frame, Callback.NOOP, false); getCoreSession().sendFrame(frame, Callback.NOOP, false);
} }
public void sendFrame(Frame frame) public void sendFrame(Frame frame)
{ {
LOG.info("sendFrame {} ", frame); if (LOG.isDebugEnabled())
LOG.debug("sendFrame {} ", frame);
getCoreSession().sendFrame(frame, Callback.NOOP, false); getCoreSession().sendFrame(frame, Callback.NOOP, false);
} }
public void sendClose() public void sendClose()
{ {
LOG.info("sendClose"); if (LOG.isDebugEnabled())
LOG.debug("sendClose");
Frame frame = new Frame(OpCode.CLOSE); Frame frame = new Frame(OpCode.CLOSE);
getCoreSession().sendFrame(frame, Callback.NOOP, false); getCoreSession().sendFrame(frame, Callback.NOOP, false);
} }

View File

@ -671,7 +671,7 @@
<plugin> <plugin>
<groupId>org.jacoco</groupId> <groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.4</version> <version>0.8.5</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>com.agilejava.docbkx</groupId> <groupId>com.agilejava.docbkx</groupId>

View File

@ -190,7 +190,7 @@ if proceedyn "Are you sure you want to release using above? (y/N)" n; then
fi fi
if proceedyn "Push git commits to remote $GIT_REMOTE_ID? (Y/n)" y; then 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 $GIT_BRANCH_ID
git push $GIT_REMOTE_ID --tags git push $GIT_REMOTE_ID $TAG_NAME
fi fi
else else
echo "Not performing release" echo "Not performing release"

View File

@ -366,20 +366,11 @@ public abstract class RFC2616BaseTest
req1.append("\n"); req1.append("\n");
req1.append("123\r\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<HttpTester.Response> responses = http.requests(req1); List<HttpTester.Response> responses = http.requests(req1);
assertEquals(2, responses.size(), "Response Count"); assertEquals(1, responses.size(), "Response Count");
HttpTester.Response response = responses.get(0); 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 / Response Code", response.getStatus(), is(HttpStatus.BAD_REQUEST_400));
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");
// 4.4.3 - // 4.4.3 -
// Client - do not send 'Content-Length' if entity-length // Client - do not send 'Content-Length' if entity-length

View File

@ -125,6 +125,7 @@
<servlet-name>CGI</servlet-name> <servlet-name>CGI</servlet-name>
<servlet-class>org.eclipse.jetty.servlets.CGI</servlet-class> <servlet-class>org.eclipse.jetty.servlets.CGI</servlet-class>
<load-on-startup>1</load-on-startup> <load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet> </servlet>
<servlet-mapping> <servlet-mapping>