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:
commit
d3153f3277
316
VERSION.txt
316
VERSION.txt
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.*");
|
||||||
----
|
----
|
||||||
|
|
|
@ -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.
|
|
||||||
|
|
|
@ -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));
|
||||||
----
|
----
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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())
|
||||||
{
|
{
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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><async-supported>true</async-supported></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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue