Merged branch 'jetty-10.0.x' into 'jetty-10.0.x-3537-bootstrap_websocket_http2'.

This commit is contained in:
Simone Bordet 2019-11-14 12:42:15 +01:00
commit 06ce13e226
420 changed files with 15131 additions and 5178 deletions

19
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- Pinned
- Security
- Specification
# Label to use when marking an issue as stale
staleLabel: Stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has been a
full year without activit. It will be closed if no further activity occurs.
Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been closed due to it having no activity.

View File

@ -53,10 +53,10 @@ Be sure to search for existing bugs before you create another one. Remember that
Reporting Security Issues
-----------------
There are a number of avenues for reporting security issues to the Jetty project available.
If the issue is directly related to Jetty itself then reporting to the Jetty developers is encouraged.
The most direct method is to mail [security@webtide.com](mailto:security@webtide.com).
Webtide is comprised of the active committers of the Jetty project is our preferred reporting method.
There are a number of avenues for reporting security issues to the Jetty project available.
If the issue is directly related to Jetty itself then reporting to the Jetty developers is encouraged.
The most direct method is to mail [security@webtide.com](mailto:security@webtide.com).
Webtide is comprised of the active committers of the Jetty project is our preferred reporting method.
We are flexible in how we work with reporters of security issues but we reserve the right to act in the interests of the Jetty project in all circumstances.
If the issue is related to Eclipse or its Jetty integration then we encourage you to reach out to [security@eclipse.org](mailto:security@eclipse.org).

4
Jenkinsfile vendored
View File

@ -40,11 +40,11 @@ pipeline {
}
}
stage("Build / Test - JDK12") {
stage("Build / Test - JDK13") {
agent { node { label 'linux' } }
steps {
timeout(time: 120, unit: 'MINUTES') {
mavenBuild("jdk12", "-Pmongodb install", "maven3", true)
mavenBuild("jdk13", "-Pmongodb install", "maven3", true)
warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']]
junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml'
}

View File

@ -23,7 +23,7 @@ Documentation
Project documentation is available on the Jetty Eclipse website.
- [http://www.eclipse.org/jetty/documentation](http://www.eclipse.org/jetty/documentation)
- [https://www.eclipse.org/jetty/documentation](https://www.eclipse.org/jetty/documentation)
Building
========
@ -40,9 +40,9 @@ The first build may take a longer than expected as Maven downloads all the depen
The build tests do a lot of stress testing, and on some machines it is necessary to set the file descriptor limit to greater than 2048 for the tests to all pass successfully.
It is possible to bypass tests by building with `mvn -Dmaven.test.skip=true install` but note that this will not produce some of the test jars that are leveraged in other places in the build.
It is possible to bypass tests by building with `mvn clean install -DskipTests`.
Professional Services
---------------------
Expert advice and production support are available through [Webtide.com](http://webtide.com).
Expert advice and production support are available through [Webtide.com](https://webtide.com).

View File

@ -1,13 +1,14 @@
jetty-10.0.0-SNAPSHOT
jetty-10.0.0-alpha0 - 11 July 2019
jetty-10.0.0-alpha0 - 11 July
2019
+ 113 Add support for NCSA Extended Log File Format
+ 114 Bring back overlay deployer
+ 132 ClientConnector abstraction
+ 207 Support javax.websocket version 1.1
+ 215 Add Conscrypt for native ALPN/TLS/SSL
+ 300 Implement Deflater / Inflater Object Pool
+ 482 [jetty-osgi] The CCL while parsing the xml files should be set to a
+ 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
+ 632 JMX tests rely on fixed port
@ -79,7 +80,7 @@ jetty-10.0.0-alpha0 - 11 July 2019
+ 2095 Remove FastCGI multiplexing
+ 2103 Server should open connectors early in start sequence
+ 2108 Update licence headers and plugin for 2018
+ 2140 Infinispan and hazelcast changes to scavenge zombie expired sessions.
+ 2140 Infinispan and hazelcast changes to scavenge zombie expired sessions
+ 2172 Support javax.websocket 1.1
+ 2175 Refactor WebSocket close handling
+ 2191 JPMS Support
@ -92,13 +93,12 @@ jetty-10.0.0-alpha0 - 11 July 2019
+ 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.
+ 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)
@ -115,7 +115,7 @@ jetty-10.0.0-alpha0 - 11 July 2019
+ 3197 Use jetty specific websocket API jar
+ 3213 MetaInfConfigurationTest tests disabled in jetty-10.0.x
+ 3216 Autobahn WebSocketServer failures in jetty 10
+ 3225 Response.sendError should not set reason.
+ 3225 Response.sendError should not set reason
+ 3246 javax-websocket-tests exception stacktraces
+ 3249 Update to apache jasper 9.0.14 for jetty-10
+ 3274 OSGi versions of java.base classes in
@ -123,7 +123,7 @@ jetty-10.0.0-alpha0 - 11 July 2019
+ 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.
+ 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
@ -157,7 +157,7 @@ jetty-10.0.0-alpha0 - 11 July 2019
+ 3648 javax.websocket client container incorrectly creates Server
SslContextFactory
+ 3661 JettyWebSocketServerContainer exposes websocket common classes
+ 3666 WebSocket - Handling sent 1009 close frame.
+ 3666 WebSocket - Handling sent 1009 close frame
+ 3696 Unwrap JavaxWebSocketClientContainer.connectToServer() exceptions
+ 3698 Missing WebSocket ServerContainer after server restart
+ 3700 stackoverflow in WebAppClassLoaderUrlStreamTest
@ -193,6 +193,98 @@ jetty-10.0.0-alpha0 - 11 July 2019
+ 3849 ClosedChannelException from jetty-test-webapp javax websocket chat
example
jetty-9.4.22.v20191022 - 22 October 2019
+ 2429 HttpClient backpressure improved
+ 3558 Error notifications can be received after a successful websocket
+ 3787 Jetty client sometimes returns EOFException instead of
SSLHandshakeException on certificate errors.
+ 3913 Clustered HttpSession IllegalStateException: Invalid for read
+ 3989 Inform custom ManagedSelector of dead selector via optional
onFailedSelect()
+ 4096 Thread in ReservedThreadExecutor does not exit when stopped
+ 4104 Frames are sent through ExtensionStack even if WebSocket Session is
closed
+ 4105 QueuedThreadPool increased thread usage and no idle thread decay
+ 4115 Drop HTTP/2 pseudo headers
+ 4121 QueuedThreadPool should support ThreadFactory behaviors
+ 4122 QueuedThreadPool should reset thread interrupted on failed run
+ 4128 OpenIdCredetials can't decode JWT ID token
+ 4132 Should be possible to use OIDC without metadata
+ 4141 ClassCastException with non-async Servlet + async Filter +
HttpServletRequestWrapper
+ 4142 Configurable HTTP/2 RateControl
+ 4144 Naked cast to Request should be avoided
+ 4156 IllegalStateException when forwarding to jsp with new session
+ 4158 Behaviour change in session handling in 9.4.21.v20190926
+ 4170 Client-side alias selection based on SSLEngine
+ 4174 ConcurrentModificationException when stopping jetty:run-war
+ 4176 Should not set header if sendError has been called
+ 4177 Configure HTTP proxy with SslContextFactory
+ 4179 Improve HttpChannel$SendCallback references for GC
+ 4183 Jetty considers bootstrap injected class to be a "server class"
+ 4188 Spin in HttpOutput.close
+ 4190 Jetty hangs after thread blocked in SharedBlockingCallback.block()
called by HttpOutput.close
+ 4191 Increase GzipHandler minGzipSize default size
+ 4193 InetAccessHandler - new includeConnectors/excludeConnectors not quite
correct anymore
+ 4201 Throw SSLHandshakeException in case of TLS handshake failures
+ 4203 Some Transfer-Encoding and Content-Length combinations do not result in
expected 400 Bad Request
+ 4204 Transfer-Encoding behavior does not follow RFC7230
+ 4208 Regression in Jetty 9.4.21: 304 response with Content-Length fails
+ 4209 Unused TLS connection is not closed in Java 11
+ 4217 SslConnection.DecryptedEnpoint.flush eternal busy loop
+ 4227 First authorization request produced by OIDC module fails due to
inclusion of sessionid
jetty-9.4.21.v20190926 - 26 September 2019
+ 97 Permanent UnavailableException thrown during servlet request handling
should cause servlet destroy
+ 137 Support OAuth
+ 155 No way to set keystore for JSR 356 websocket clients, needed for SSL
client authentication
+ 1036 Allow easy configuration of Scheduler-Threads and name them more
appropriate
+ 2815 HPack fields are opaque octets
+ 3040 Allow RFC6265 Cookies to include optional SameSite attribute
+ 3106 WebSocket connection stats and request stats
+ 3734 WebSocket suspend when input closed
+ 3747 Make Jetty Demo work with JPMS
+ 3806 Error Page handling Async race with ProxyServlet
+ 3913 Clustered HttpSession IllegalStateException: Invalid for read
+ 3936 Race condition when modifying session + sendRedirect()
+ 3956 Remove and warn on use of illegal HTTP/2 response headers
+ 3964 Improve efficiency of listeners
+ 3968 WebSocket sporadic ReadPendingException using suspend/resume
+ 3978 HTTP/2 fixes for robustly handling abnormal traffic and resource
exhaustion
+ 3983 JarFileResource incorrectly lists the contents of directories with
spaces
+ 3985 Improve lenient Cookie parsing
+ 3989 Inform custom ManagedSelector of dead selector via optional
onFailedSelect()
+ 4000 Add SameFileAliasChecker to help with FileSystem static file access
normalization on Mac and Windows
+ 4007 NullPointerException while trying to run jetty start.run on Windows
+ 4009 ServletContextHandler setSecurityHandler broke handler chain
+ 4020 Revert WebSocket ExtensionFactory change to interface
+ 4022 Servlet which is added by ServletRegistration can't be started
+ 4025 Provide more write-through behaviours for DefaultSessionCache
+ 4027 Ensure AbstractSessionDataStore cannot be used unless it is started
+ 4033 Ignore bad percent encodings in paths during
URIUtil.equalsIgnoreEncodings()
+ 4047 Gracefully stopped Jetty not flushing all response data
+ 4048 Multiple values in X-Forwarded-Port throw NumberFormatException
+ 4057 NullPointerException in o.e.j.h.HttpFields
+ 4064 NullPointerException initializing embedded servlet
+ 4075 Do not fail on servlet-mapping with url-pattern /On*
+ 4082 NullPointerExceptoin while Debug logging in client
+ 4084 Use of HttpConfiguration.setBlockingTimeout(long) in jetty.xml produces
warning on jetty-home startup
+ 4105 Cleanup of Idle thread count in QueuedThreadPool
+ 4113 HttpClient fails with JDK 13 and TLS 1.3
jetty-9.4.20.v20190813 - 13 August 2019
+ 300 Implement Deflater / Inflater Object Pool
+ 2061 WebSocket hangs in blockingWrite
@ -290,7 +382,7 @@ jetty-9.4.18.v20190429 - 29 April 2019
+ 3609 Fix infinispan start module dependencies
jetty-9.4.17.v20190418 - 18 April 2019
+ 2140 Infinispan and hazelcast changes to scavenge zombie expired sessions.
+ 2140 Infinispan and hazelcast changes to scavenge zombie expired sessions
+ 3464 Split SslContextFactory into Client and Server
+ 3549 Directory Listing on Windows reveals Resource Base path
+ 3555 DefaultHandler Reveals Base Resource Path of each Context
@ -368,6 +460,11 @@ jetty-9.4.15.v20190215 - 15 February 2019
+ 3350 Do not expect to be able to connect to https URLs with the HttpClient
created from a parameterless constructor
jetty-9.3.28.v20191105 - 05 November 2019
+ 3989 Inform custom ManagedSelector of dead selector via optional
onFailedSelect()
+ 4217 SslConnection.DecryptedEnpoint.flush eternal busy loop
jetty-9.3.27.v20190418 - 18 April 2019
+ 3549 Directory Listing on Windows reveals Resource Base path
+ 3555 DefaultHandler Reveals Base Resource Path of each Context
@ -380,6 +477,9 @@ jetty-9.3.26.v20190403 - 03 April 2019
ForwardedRequestCustomizer
+ 3319 Allow reverse sort for directory listed files
jetty-9.2.29.v20191105 - 05 November 2019
+ 4217 SslConnection.DecryptedEnpoint.flush eternal busy loop
jetty-9.2.28.v20190418 - 18 April 2019
+ 3549 Directory Listing on Windows reveals Resource Base path
+ 3555 DefaultHandler Reveals Base Resource Path of each Context

View File

@ -27,6 +27,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
@ -53,6 +54,7 @@ public class ProxyServerTest extends AbstractEmbeddedTest
server.stop();
}
@Tag("external")
@Test
public void testGetProxiedRFC() throws Exception
{

View File

@ -1,5 +1,5 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
org.eclipse.jetty.LEVEL=WARN
org.eclipse.jetty.LEVEL=INFO
org.eclipse.jetty.embedded.JettyDistribution.LEVEL=DEBUG
#org.eclipse.jetty.STACKS=true
#org.eclipse.jetty.STACKS=false

View File

@ -55,13 +55,13 @@ public class JDK9ClientALPNProcessor implements ALPNProcessor.Client
ALPNClientConnection alpn = (ALPNClientConnection)connection;
SSLParameters sslParameters = sslEngine.getSSLParameters();
List<String> protocols = alpn.getProtocols();
sslParameters.setApplicationProtocols(protocols.toArray(new String[protocols.size()]));
sslParameters.setApplicationProtocols(protocols.toArray(new String[0]));
sslEngine.setSSLParameters(sslParameters);
((DecryptedEndPoint)connection.getEndPoint()).getSslConnection()
.addHandshakeListener(new ALPNListener(alpn));
}
private final class ALPNListener implements SslHandshakeListener
private static final class ALPNListener implements SslHandshakeListener
{
private final ALPNClientConnection alpnConnection;

View File

@ -55,7 +55,7 @@ public class JDK9ServerALPNProcessor implements ALPNProcessor.Server, SslHandsha
sslEngine.setHandshakeApplicationProtocolSelector(new ALPNCallback((ALPNServerConnection)connection));
}
private final class ALPNCallback implements BiFunction<SSLEngine, List<String>, String>, SslHandshakeListener
private static final class ALPNCallback implements BiFunction<SSLEngine, List<String>, String>, SslHandshakeListener
{
private final ALPNServerConnection alpnConnection;
@ -68,10 +68,19 @@ public class JDK9ServerALPNProcessor implements ALPNProcessor.Server, SslHandsha
@Override
public String apply(SSLEngine engine, List<String> protocols)
{
if (LOG.isDebugEnabled())
LOG.debug("apply {} {}", alpnConnection, protocols);
alpnConnection.select(protocols);
return alpnConnection.getProtocol();
try
{
if (LOG.isDebugEnabled())
LOG.debug("apply {} {}", alpnConnection, protocols);
alpnConnection.select(protocols);
return alpnConnection.getProtocol();
}
catch (Throwable x)
{
// Cannot negotiate the protocol, return null to have
// JSSE send Alert.NO_APPLICATION_PROTOCOL to the client.
return null;
}
}
@Override

View File

@ -22,8 +22,13 @@ import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.servlet.http.HttpServletRequest;
@ -38,12 +43,16 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
public class JDK9ALPNTest
{
@ -85,10 +94,10 @@ public class JDK9ALPNTest
@Test
public void testClientNotSupportingALPNServerSpeaksDefaultProtocol() throws Exception
{
startServer(new AbstractHandler.ErrorDispatchHandler()
startServer(new AbstractHandler()
{
@Override
protected void doNonErrorHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
{
baseRequest.setHandled(true);
}
@ -127,10 +136,10 @@ public class JDK9ALPNTest
@Test
public void testClientSupportingALPNServerSpeaksNegotiatedProtocol() throws Exception
{
startServer(new AbstractHandler.ErrorDispatchHandler()
startServer(new AbstractHandler()
{
@Override
protected void doNonErrorHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
{
baseRequest.setHandled(true);
}
@ -168,4 +177,57 @@ public class JDK9ALPNTest
}
}
}
@Test
public void testClientSupportingALPNCannotNegotiateProtocol() throws Exception
{
startServer(new AbstractHandler() {
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
jettyRequest.setHandled(true);
}
});
SslContextFactory sslContextFactory = new SslContextFactory.Client(true);
sslContextFactory.start();
String host = "localhost";
int port = connector.getLocalPort();
try (SocketChannel client = SocketChannel.open(new InetSocketAddress(host, port)))
{
client.socket().setSoTimeout(5000);
SSLEngine sslEngine = sslContextFactory.newSSLEngine(host, port);
sslEngine.setUseClientMode(true);
SSLParameters sslParameters = sslEngine.getSSLParameters();
sslParameters.setApplicationProtocols(new String[]{"unknown/1.0"});
sslEngine.setSSLParameters(sslParameters);
sslEngine.beginHandshake();
assertSame(SSLEngineResult.HandshakeStatus.NEED_WRAP, sslEngine.getHandshakeStatus());
ByteBuffer sslBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize());
SSLEngineResult result = sslEngine.wrap(BufferUtil.EMPTY_BUFFER, sslBuffer);
assertSame(SSLEngineResult.Status.OK, result.getStatus());
sslBuffer.flip();
client.write(sslBuffer);
assertSame(SSLEngineResult.HandshakeStatus.NEED_UNWRAP, sslEngine.getHandshakeStatus());
sslBuffer.clear();
int read = client.read(sslBuffer);
assertThat(read, greaterThan(0));
sslBuffer.flip();
// TLS frame layout: record_type, major_version, minor_version, hi_length, lo_length
int recordTypeAlert = 21;
assertEquals(recordTypeAlert, sslBuffer.get(0) & 0xFF);
// Alert record layout: alert_level, alert_code
int alertLevelFatal = 2;
assertEquals(alertLevelFatal, sslBuffer.get(5) & 0xFF);
int alertCodeNoApplicationProtocol = 120;
assertEquals(alertCodeNoApplicationProtocol, sslBuffer.get(6) & 0xFF);
}
}
}

View File

@ -1,4 +1,4 @@
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[depend]
alpn-impl/alpn-9
alpn-impl/alpn-11

View File

@ -1,4 +1,4 @@
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[depend]
alpn-impl/alpn-9
alpn-impl/alpn-11

View File

@ -27,7 +27,6 @@ import java.security.CodeSource;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -615,9 +614,9 @@ public class AntWebAppContext extends WebAppContext
TaskLog.logWithTimestamp("Stopping web application " + this);
Thread.currentThread().sleep(500L);
super.doStop();
//remove all filters, servlets and listeners. They will be recreated
//either via application of a context xml file or web.xml or annotation or servlet api
setEventListeners(new EventListener[0]);
// remove all filters and servlets. They will be recreated
// either via application of a context xml file or web.xml or annotation or servlet api.
// Event listeners are reset in ContextHandler.doStop()
getServletHandler().setFilters(new FilterHolder[0]);
getServletHandler().setFilterMappings(new FilterMapping[0]);
getServletHandler().setServlets(new ServletHolder[0]);

View File

@ -284,6 +284,11 @@
<artifactId>jetty-security</artifactId>
<version>10.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-openid</artifactId>
<version>10.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>

View File

@ -111,7 +111,6 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-client</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>

View File

@ -5,3 +5,4 @@ Adds the Jetty HTTP client to the server classpath.
[lib]
lib/jetty-client-${jetty.version}.jar
lib/jetty-alpn-client-${jetty.version}.jar

View File

@ -26,14 +26,13 @@ module org.eclipse.jetty.client
exports org.eclipse.jetty.client.proxy;
exports org.eclipse.jetty.client.util;
requires org.eclipse.jetty.alpn.client;
requires org.eclipse.jetty.http;
requires org.eclipse.jetty.io;
requires org.eclipse.jetty.util;
// Only required if using SPNEGO.
requires static java.security.jgss;
// Only required if using the dynamic transport.
requires static org.eclipse.jetty.alpn.client;
// Only required if using JMX.
requires static org.eclipse.jetty.jmx;
}

View File

@ -497,27 +497,6 @@ public class HttpClient extends ContainerLifeCycle
return uri;
}
/**
* Returns a {@link Destination} for the given scheme, host and port.
* Applications may use {@link Destination}s to create {@link Connection}s
* that will be outside HttpClient's pooling mechanism, to explicitly
* control the connection lifecycle (in particular their termination with
* {@link Connection#close()}).
*
* @param scheme the destination scheme
* @param host the destination host
* @param port the destination port
* @return the destination
* @see #getDestinations()
* @deprecated use {@link #resolveDestination(Request)} instead
*/
@Deprecated
public Destination getDestination(String scheme, String host, int port)
{
Origin origin = createOrigin(scheme, host, port);
return resolveDestination(new HttpDestination.Key(origin, null));
}
public Destination resolveDestination(Request request)
{
Origin origin = createOrigin(request.getScheme(), request.getHost(), request.getPort());
@ -544,25 +523,15 @@ public class HttpClient extends ContainerLifeCycle
HttpDestination resolveDestination(HttpDestination.Key key)
{
HttpDestination destination = destinations.get(key);
if (destination == null)
return destinations.computeIfAbsent(key, k ->
{
destination = getTransport().newHttpDestination(key);
HttpDestination destination = getTransport().newHttpDestination(k);
// Start the destination before it's published to other threads.
addManaged(destination);
HttpDestination existing = destinations.putIfAbsent(key, destination);
if (existing != null)
{
removeBean(destination);
destination = existing;
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Created {}", destination);
}
}
return destination;
if (LOG.isDebugEnabled())
LOG.debug("Created {}", destination);
return destination;
});
}
protected boolean removeDestination(HttpDestination destination)
@ -1137,9 +1106,11 @@ public class HttpClient extends ContainerLifeCycle
return HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme);
}
protected ClientConnectionFactory newSslClientConnectionFactory(ClientConnectionFactory connectionFactory)
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{
return new SslClientConnectionFactory(getSslContextFactory(), getByteBufferPool(), getExecutor(), connectionFactory);
if (sslContextFactory == null)
sslContextFactory = getSslContextFactory();
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory);
}
private class ContentDecoderFactorySet implements Set<ContentDecoder.Factory>

View File

@ -53,6 +53,7 @@ import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.Sweeper;
@ -108,12 +109,12 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
{
connectionFactory = proxy.newClientConnectionFactory(connectionFactory);
if (proxy.isSecure())
connectionFactory = newSslClientConnectionFactory(connectionFactory);
connectionFactory = newSslClientConnectionFactory(proxy.getSslContextFactory(), connectionFactory);
}
else
{
if (isSecure())
connectionFactory = newSslClientConnectionFactory(connectionFactory);
connectionFactory = newSslClientConnectionFactory(null, connectionFactory);
}
return connectionFactory;
}
@ -149,9 +150,9 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
return new BlockingArrayQueue<>(client.getMaxRequestsQueuedPerDestination());
}
protected ClientConnectionFactory newSslClientConnectionFactory(ClientConnectionFactory connectionFactory)
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{
return client.newSslClientConnectionFactory(connectionFactory);
return client.newSslClientConnectionFactory(sslContextFactory, connectionFactory);
}
public boolean isSecure()

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;
import java.util.Map;
@ -34,10 +35,12 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
public class HttpProxy extends ProxyConfiguration.Proxy
{
@ -50,12 +53,27 @@ public class HttpProxy extends ProxyConfiguration.Proxy
public HttpProxy(Origin.Address address, boolean secure)
{
this(address, secure, new HttpDestination.Protocol(List.of("http/1.1"), false));
this(address, secure, null, new HttpDestination.Protocol(List.of("http/1.1"), false));
}
public HttpProxy(Origin.Address address, boolean secure, HttpDestination.Protocol protocol)
{
super(address, secure, Objects.requireNonNull(protocol));
this(address, secure, null, Objects.requireNonNull(protocol));
}
public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory)
{
this(address, true, sslContextFactory, new HttpDestination.Protocol(List.of("http/1.1"), false));
}
public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory, HttpDestination.Protocol protocol)
{
this(address, true, sslContextFactory, Objects.requireNonNull(protocol));
}
private HttpProxy(Origin.Address address, boolean secure, SslContextFactory.Client sslContextFactory, HttpDestination.Protocol protocol)
{
super(address, secure, sslContextFactory, Objects.requireNonNull(protocol));
}
@Override
@ -206,7 +224,12 @@ public class HttpProxy extends ProxyConfiguration.Proxy
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
ClientConnectionFactory connectionFactory = this.connectionFactory;
if (destination.isSecure())
connectionFactory = destination.newSslClientConnectionFactory(connectionFactory);
{
// Don't want to do DNS resolution here.
InetSocketAddress address = InetSocketAddress.createUnresolved(destination.getHost(), destination.getPort());
context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory);
}
var oldConnection = endPoint.getConnection();
var newConnection = connectionFactory.newConnection(endPoint, context);
endPoint.upgrade(newConnection);

View File

@ -23,9 +23,14 @@ import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.LongConsumer;
import java.util.function.LongUnaryOperator;
import java.util.stream.Collectors;
import org.eclipse.jetty.client.api.Response;
@ -35,7 +40,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IteratingNestedCallback;
import org.eclipse.jetty.util.MathUtils;
import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -71,9 +76,11 @@ public abstract class HttpReceiver
private final AtomicReference<ResponseState> responseState = new AtomicReference<>(ResponseState.IDLE);
private final HttpChannel channel;
private List<Response.AsyncContentListener> contentListeners;
private ContentDecoder decoder;
private ContentListeners contentListeners;
private Decoder decoder;
private Throwable failure;
private long demand;
private boolean stalled;
protected HttpReceiver(HttpChannel channel)
{
@ -85,6 +92,55 @@ public abstract class HttpReceiver
return channel;
}
void demand(long n)
{
if (n <= 0)
throw new IllegalArgumentException("Invalid demand " + n);
boolean resume = false;
synchronized (this)
{
demand = MathUtils.cappedAdd(demand, n);
if (stalled)
{
stalled = false;
resume = true;
}
if (LOG.isDebugEnabled())
LOG.debug("Response demand={}/{}, resume={}", n, demand, resume);
}
if (resume)
{
if (decoder != null)
decoder.resume();
else
receive();
}
}
protected long demand()
{
return demand(LongUnaryOperator.identity());
}
private long demand(LongUnaryOperator operator)
{
synchronized (this)
{
return demand = operator.applyAsLong(demand);
}
}
protected boolean hasDemandOrStall()
{
synchronized (this)
{
stalled = demand <= 0;
return !stalled;
}
}
protected HttpExchange getHttpExchange()
{
return channel.getHttpExchange();
@ -100,6 +156,10 @@ public abstract class HttpReceiver
return responseState.get() == ResponseState.FAILURE;
}
protected void receive()
{
}
/**
* Method to be invoked when the response status code is available.
* <p>
@ -116,13 +176,12 @@ public abstract class HttpReceiver
if (!updateResponseState(ResponseState.IDLE, ResponseState.TRANSIENT))
return false;
final HttpConversation conversation = exchange.getConversation();
final HttpResponse response = exchange.getResponse();
HttpConversation conversation = exchange.getConversation();
HttpResponse response = exchange.getResponse();
// Probe the protocol handlers
final HttpDestination destination = getHttpDestination();
final HttpClient client = destination.getHttpClient();
final ProtocolHandler protocolHandler = client.findProtocolHandler(exchange.getRequest(), response);
HttpDestination destination = getHttpDestination();
HttpClient client = destination.getHttpClient();
ProtocolHandler protocolHandler = client.findProtocolHandler(exchange.getRequest(), response);
Response.Listener handlerListener = null;
if (protocolHandler != null)
{
@ -241,23 +300,17 @@ public abstract class HttpReceiver
*/
protected boolean responseHeaders(HttpExchange exchange)
{
out:
while (true)
{
ResponseState current = responseState.get();
switch (current)
if (current == ResponseState.BEGIN || current == ResponseState.HEADER)
{
case BEGIN:
case HEADER:
{
if (updateResponseState(current, ResponseState.TRANSIENT))
break out;
if (updateResponseState(current, ResponseState.TRANSIENT))
break;
}
default:
{
return false;
}
}
else
{
return false;
}
}
@ -267,29 +320,35 @@ public abstract class HttpReceiver
ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
List<Response.ResponseListener> responseListeners = exchange.getConversation().getResponseListeners();
notifier.notifyHeaders(responseListeners, response);
contentListeners = responseListeners.stream()
.filter(Response.AsyncContentListener.class::isInstance)
.map(Response.AsyncContentListener.class::cast)
.collect(Collectors.toList());
contentListeners = new ContentListeners(responseListeners);
contentListeners.notifyBeforeContent(response);
List<String> contentEncodings = response.getHeaders().getCSV(HttpHeader.CONTENT_ENCODING.asString(), false);
if (contentEncodings != null && !contentEncodings.isEmpty())
if (!contentListeners.isEmpty())
{
for (ContentDecoder.Factory factory : getHttpDestination().getHttpClient().getContentDecoderFactories())
List<String> contentEncodings = response.getHeaders().getCSV(HttpHeader.CONTENT_ENCODING.asString(), false);
if (contentEncodings != null && !contentEncodings.isEmpty())
{
for (String encoding : contentEncodings)
for (ContentDecoder.Factory factory : getHttpDestination().getHttpClient().getContentDecoderFactories())
{
if (factory.getEncoding().equalsIgnoreCase(encoding))
for (String encoding : contentEncodings)
{
this.decoder = factory.newContentDecoder();
break;
if (factory.getEncoding().equalsIgnoreCase(encoding))
{
decoder = new Decoder(response, factory.newContentDecoder());
break;
}
}
}
}
}
if (updateResponseState(ResponseState.TRANSIENT, ResponseState.HEADERS))
return true;
{
boolean hasDemand = hasDemandOrStall();
if (LOG.isDebugEnabled())
LOG.debug("Response headers {}, hasDemand={}", response, hasDemand);
return hasDemand;
}
terminateResponse(exchange);
return false;
@ -307,45 +366,83 @@ public abstract class HttpReceiver
*/
protected boolean responseContent(HttpExchange exchange, ByteBuffer buffer, Callback callback)
{
out:
while (true)
{
ResponseState current = responseState.get();
switch (current)
if (current == ResponseState.HEADERS || current == ResponseState.CONTENT)
{
case HEADERS:
case CONTENT:
{
if (updateResponseState(current, ResponseState.TRANSIENT))
break out;
if (updateResponseState(current, ResponseState.TRANSIENT))
break;
}
default:
{
callback.failed(new IllegalStateException("Invalid response state " + current));
return false;
}
}
else
{
callback.failed(new IllegalStateException("Invalid response state " + current));
return false;
}
}
HttpResponse response = exchange.getResponse();
if (LOG.isDebugEnabled())
LOG.debug("Response content {}{}{}", response, System.lineSeparator(), BufferUtil.toDetailString(buffer));
ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
ContentDecoder decoder = this.decoder;
if (decoder == null)
boolean proceed = true;
if (demand() <= 0)
{
notifier.notifyContent(response, buffer, callback, contentListeners);
callback.failed(new IllegalStateException("No demand for response content"));
proceed = false;
}
else
HttpResponse response = exchange.getResponse();
if (proceed)
{
new Decoder(notifier, response, decoder, buffer, callback).iterate();
if (LOG.isDebugEnabled())
LOG.debug("Response content {}{}{}", response, System.lineSeparator(), BufferUtil.toDetailString(buffer));
ContentListeners listeners = this.contentListeners;
if (listeners != null)
{
if (listeners.isEmpty())
{
callback.succeeded();
}
else
{
Decoder decoder = this.decoder;
if (decoder == null)
{
listeners.notifyContent(response, buffer, callback);
}
else
{
try
{
proceed = decoder.decode(buffer, callback);
}
catch (Throwable x)
{
callback.failed(x);
proceed = false;
}
}
}
}
else
{
// May happen in case of concurrent abort.
proceed = false;
}
}
if (updateResponseState(ResponseState.TRANSIENT, ResponseState.CONTENT))
return true;
{
if (proceed)
{
boolean hasDemand = hasDemandOrStall();
if (LOG.isDebugEnabled())
LOG.debug("Response content {}, hasDemand={}", response, hasDemand);
return hasDemand;
}
else
{
return false;
}
}
terminateResponse(exchange);
return false;
@ -386,8 +483,7 @@ public abstract class HttpReceiver
// Mark atomically the response as terminated, with
// respect to concurrency between request and response.
Result result = exchange.terminateResponse();
terminateResponse(exchange, result);
terminateResponse(exchange);
return true;
}
@ -410,6 +506,9 @@ public abstract class HttpReceiver
if (exchange == null)
return false;
if (LOG.isDebugEnabled())
LOG.debug("Response failure " + exchange.getResponse(), failure);
// Mark atomically the response as completed, with respect
// to concurrency between response success and response failure.
if (exchange.responseComplete(failure))
@ -448,7 +547,7 @@ public abstract class HttpReceiver
}
/**
* Resets this {@link HttpReceiver} state.
* Resets the state of this HttpReceiver.
* <p>
* Subclasses should override (but remember to call {@code super}) to reset their own state.
* <p>
@ -456,13 +555,11 @@ public abstract class HttpReceiver
*/
protected void reset()
{
contentListeners = null;
destroyDecoder(decoder);
decoder = null;
cleanup();
}
/**
* Disposes this {@link HttpReceiver} state.
* Disposes the state of this HttpReceiver.
* <p>
* Subclasses should override (but remember to call {@code super}) to dispose their own state.
* <p>
@ -470,41 +567,32 @@ public abstract class HttpReceiver
*/
protected void dispose()
{
destroyDecoder(decoder);
decoder = null;
cleanup();
}
private static void destroyDecoder(ContentDecoder decoder)
private void cleanup()
{
if (decoder instanceof Destroyable)
{
((Destroyable)decoder).destroy();
}
contentListeners = null;
if (decoder != null)
decoder.destroy();
decoder = null;
demand = 0;
stalled = false;
}
public boolean abort(HttpExchange exchange, Throwable failure)
{
// Update the state to avoid more response processing.
boolean terminate;
out:
while (true)
{
ResponseState current = responseState.get();
switch (current)
if (current == ResponseState.FAILURE)
return false;
if (updateResponseState(current, ResponseState.FAILURE))
{
case FAILURE:
{
return false;
}
default:
{
if (updateResponseState(current, ResponseState.FAILURE))
{
terminate = current != ResponseState.TRANSIENT;
break out;
}
break;
}
terminate = current != ResponseState.TRANSIENT;
break;
}
}
@ -514,17 +602,19 @@ public abstract class HttpReceiver
HttpResponse response = exchange.getResponse();
if (LOG.isDebugEnabled())
LOG.debug("Response failure {} {} on {}: {}", response, exchange, getHttpChannel(), failure);
LOG.debug("Response abort {} {} on {}: {}", response, exchange, getHttpChannel(), failure);
List<Response.ResponseListener> listeners = exchange.getConversation().getResponseListeners();
ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
notifier.notifyFailure(listeners, response, failure);
// We want to deliver the "complete" event as last,
// so we emit it here only if no event handlers are
// executing, otherwise they will emit it.
if (terminate)
{
// Mark atomically the response as terminated, with
// respect to concurrency between request and response.
Result result = exchange.terminateResponse();
terminateResponse(exchange, result);
terminateResponse(exchange);
return true;
}
else
@ -591,46 +681,163 @@ public abstract class HttpReceiver
FAILURE
}
private class Decoder extends IteratingNestedCallback
/**
* <p>Wraps a list of content listeners, notifies them about content events and
* tracks individual listener demand to produce a global demand for content.</p>
*/
private class ContentListeners
{
private final ResponseNotifier notifier;
private final HttpResponse response;
private final ContentDecoder decoder;
private final ByteBuffer buffer;
private ByteBuffer decoded;
private final Map<Object, Long> demands = new ConcurrentHashMap<>();
private final LongConsumer demand = HttpReceiver.this::demand;
private final List<Response.DemandedContentListener> listeners;
public Decoder(ResponseNotifier notifier, HttpResponse response, ContentDecoder decoder, ByteBuffer buffer, Callback callback)
private ContentListeners(List<Response.ResponseListener> responseListeners)
{
super(callback);
this.notifier = notifier;
this.response = response;
this.decoder = decoder;
this.buffer = buffer;
listeners = responseListeners.stream()
.filter(Response.DemandedContentListener.class::isInstance)
.map(Response.DemandedContentListener.class::cast)
.collect(Collectors.toList());
}
@Override
protected Action process()
private boolean isEmpty()
{
return listeners.isEmpty();
}
private void notifyBeforeContent(HttpResponse response)
{
if (isEmpty())
{
// If no listeners, we want to proceed and consume any content.
demand.accept(1);
}
else
{
ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
notifier.notifyBeforeContent(response, this::demand, listeners);
}
}
private void notifyContent(HttpResponse response, ByteBuffer buffer, Callback callback)
{
HttpReceiver.this.demand(d -> d - 1);
ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
notifier.notifyContent(response, this::demand, buffer, callback, listeners);
}
private void demand(Object context, long value)
{
if (listeners.size() > 1)
accept(context, value);
else
demand.accept(value);
}
private void accept(Object context, long value)
{
// Increment the demand for the given listener.
demands.merge(context, value, MathUtils::cappedAdd);
// Check if we have demand from all listeners.
if (demands.size() == listeners.size())
{
long minDemand = Long.MAX_VALUE;
for (Long demand : demands.values())
{
if (demand < minDemand)
minDemand = demand;
}
if (minDemand > 0)
{
// We are going to demand for minDemand content
// chunks, so decrement the listener's demand by
// minDemand and remove those that have no demand left.
Iterator<Map.Entry<Object, Long>> iterator = demands.entrySet().iterator();
while (iterator.hasNext())
{
Map.Entry<Object, Long> entry = iterator.next();
long newValue = entry.getValue() - minDemand;
if (newValue == 0)
iterator.remove();
else
entry.setValue(newValue);
}
// Demand more content chunks for all the listeners.
demand.accept(minDemand);
}
}
}
}
/**
* <p>Implements the decoding of content, producing decoded buffers only if there is demand for content.</p>
*/
private class Decoder implements Destroyable
{
private final HttpResponse response;
private final ContentDecoder decoder;
private ByteBuffer encoded;
private Callback callback;
private Decoder(HttpResponse response, ContentDecoder decoder)
{
this.response = response;
this.decoder = Objects.requireNonNull(decoder);
}
private boolean decode(ByteBuffer encoded, Callback callback)
{
this.encoded = encoded;
this.callback = callback;
return decode();
}
private boolean decode()
{
while (true)
{
decoded = decoder.decode(buffer);
if (decoded.hasRemaining())
break;
if (!buffer.hasRemaining())
return Action.SUCCEEDED;
}
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded));
ByteBuffer buffer;
while (true)
{
buffer = decoder.decode(encoded);
if (buffer.hasRemaining())
break;
if (!encoded.hasRemaining())
{
callback.succeeded();
encoded = null;
callback = null;
return true;
}
}
ByteBuffer decoded = buffer;
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded));
notifier.notifyContent(response, decoded, this, contentListeners);
return Action.SCHEDULED;
contentListeners.notifyContent(response, decoded, Callback.from(() -> decoder.release(decoded), callback::failed));
boolean hasDemand = hasDemandOrStall();
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded {}, hasDemand={}", response, hasDemand);
if (!hasDemand)
return false;
}
}
private void resume()
{
if (LOG.isDebugEnabled())
LOG.debug("Response content resuming decoding {}", response);
if (decode())
receive();
}
@Override
public void succeeded()
public void destroy()
{
decoder.release(decoded);
super.succeeded();
if (decoder instanceof Destroyable)
((Destroyable)decoder).destroy();
}
}
}

View File

@ -41,6 +41,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.LongConsumer;
import java.util.function.Supplier;
import org.eclipse.jetty.client.api.ContentProvider;
@ -308,7 +309,7 @@ public class HttpRequest implements Request
@Override
public List<HttpCookie> getCookies()
{
return cookies != null ? cookies : Collections.<HttpCookie>emptyList();
return cookies != null ? cookies : Collections.emptyList();
}
@Override
@ -332,7 +333,7 @@ public class HttpRequest implements Request
@Override
public Map<String, Object> getAttributes()
{
return attributes != null ? attributes : Collections.<String, Object>emptyMap();
return attributes != null ? attributes : Collections.emptyMap();
}
@Override
@ -348,7 +349,7 @@ public class HttpRequest implements Request
// This method is invoked often in a request/response conversation,
// so we avoid allocation if there is no need to filter.
if (type == null || requestListeners == null)
return requestListeners != null ? (List<T>)requestListeners : Collections.<T>emptyList();
return requestListeners != null ? (List<T>)requestListeners : Collections.emptyList();
ArrayList<T> result = new ArrayList<>();
for (RequestListener listener : requestListeners)
@ -509,15 +510,16 @@ public class HttpRequest implements Request
@Override
public Request onResponseContent(final Response.ContentListener listener)
{
this.responseListeners.add(new Response.AsyncContentListener()
this.responseListeners.add(new Response.DemandedContentListener()
{
@Override
public void onContent(Response response, ByteBuffer content, Callback callback)
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback)
{
try
{
listener.onContent(response, content);
callback.succeeded();
demand.accept(1);
}
catch (Throwable x)
{
@ -531,12 +533,36 @@ public class HttpRequest implements Request
@Override
public Request onResponseContentAsync(final Response.AsyncContentListener listener)
{
this.responseListeners.add(new Response.AsyncContentListener()
this.responseListeners.add(new Response.DemandedContentListener()
{
@Override
public void onContent(Response response, ByteBuffer content, Callback callback)
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback)
{
listener.onContent(response, content, callback);
listener.onContent(response, content, Callback.from(() ->
{
callback.succeeded();
demand.accept(1);
}, callback::failed));
}
});
return this;
}
@Override
public Request onResponseContentDemanded(Response.DemandedContentListener listener)
{
this.responseListeners.add(new Response.DemandedContentListener()
{
@Override
public void onBeforeContent(Response response, LongConsumer demand)
{
listener.onBeforeContent(response, demand);
}
@Override
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback)
{
listener.onContent(response, demand, content, callback);
}
});
return this;
@ -897,6 +923,6 @@ public class HttpRequest implements Request
@Override
public String toString()
{
return String.format("%s[%s %s %s]@%x", this.getClass().getSimpleName(), getMethod(), getPath(), getVersion(), hashCode());
return String.format("%s[%s %s %s]@%x", getClass().getSimpleName(), getMethod(), getPath(), getVersion(), hashCode());
}
}

View File

@ -344,6 +344,9 @@ public abstract class HttpSender implements AsyncContentProvider.Listener
if (exchange == null)
return;
if (LOG.isDebugEnabled())
LOG.debug("Request failure " + exchange.getRequest(), failure);
// Mark atomically the request as completed, with respect
// to concurrency between request success and request failure.
if (exchange.requestComplete(failure))
@ -559,7 +562,7 @@ public abstract class HttpSender implements AsyncContentProvider.Listener
Request request = exchange.getRequest();
if (LOG.isDebugEnabled())
LOG.debug("Request failure {} {} on {}: {}", request, exchange, getHttpChannel(), failure);
LOG.debug("Request abort {} {} on {}: {}", request, exchange, getHttpChannel(), failure);
HttpDestination destination = getHttpChannel().getHttpDestination();
destination.getRequestNotifier().notifyFailure(request, failure);

View File

@ -26,6 +26,7 @@ import java.util.Set;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
* The configuration of the forward proxy to use with {@link org.eclipse.jetty.client.HttpClient}.
@ -64,12 +65,14 @@ public class ProxyConfiguration
private final Set<String> excluded = new HashSet<>();
private final Origin.Address address;
private final boolean secure;
private final SslContextFactory.Client sslContextFactory;
private final HttpDestination.Protocol protocol;
protected Proxy(Origin.Address address, boolean secure, HttpDestination.Protocol protocol)
protected Proxy(Origin.Address address, boolean secure, SslContextFactory.Client sslContextFactory, HttpDestination.Protocol protocol)
{
this.address = address;
this.secure = secure;
this.sslContextFactory = sslContextFactory;
this.protocol = protocol;
}
@ -89,6 +92,17 @@ public class ProxyConfiguration
return secure;
}
/**
* @return the optional SslContextFactory to use when connecting to proxies
*/
public SslContextFactory.Client getSslContextFactory()
{
return sslContextFactory;
}
/**
* @return the protocol spoken by this proxy
*/
public HttpDestination.Protocol getProtocol()
{
return protocol;

View File

@ -21,6 +21,8 @@ package org.eclipse.jetty.client;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.List;
import java.util.function.LongConsumer;
import java.util.function.ObjLongConsumer;
import java.util.stream.Collectors;
import org.eclipse.jetty.client.api.ContentResponse;
@ -103,36 +105,54 @@ public class ResponseNotifier
}
}
public void notifyContent(List<Response.ResponseListener> listeners, Response response, ByteBuffer buffer, Callback callback)
public void notifyBeforeContent(Response response, ObjLongConsumer<Object> demand, List<Response.DemandedContentListener> contentListeners)
{
List<Response.AsyncContentListener> contentListeners = listeners.stream()
.filter(Response.AsyncContentListener.class::isInstance)
.map(Response.AsyncContentListener.class::cast)
.collect(Collectors.toList());
notifyContent(response, buffer, callback, contentListeners);
for (Response.DemandedContentListener listener : contentListeners)
{
notifyBeforeContent(listener, response, d -> demand.accept(listener, d));
}
}
public void notifyContent(Response response, ByteBuffer buffer, Callback callback, List<Response.AsyncContentListener> contentListeners)
private void notifyBeforeContent(Response.DemandedContentListener listener, Response response, LongConsumer demand)
{
if (contentListeners.isEmpty())
try
{
listener.onBeforeContent(response, demand);
}
catch (Throwable x)
{
LOG.info("Exception while notifying listener " + listener, x);
}
}
public void notifyContent(Response response, ObjLongConsumer<Object> demand, ByteBuffer buffer, Callback callback, List<Response.DemandedContentListener> contentListeners)
{
int count = contentListeners.size();
if (count == 0)
{
callback.succeeded();
demand.accept(null, 1);
}
else if (count == 1)
{
Response.DemandedContentListener listener = contentListeners.get(0);
notifyContent(listener, response, d -> demand.accept(listener, d), buffer.slice(), callback);
}
else
{
CountingCallback counter = new CountingCallback(callback, contentListeners.size());
for (Response.AsyncContentListener listener : contentListeners)
callback = new CountingCallback(callback, count);
for (Response.DemandedContentListener listener : contentListeners)
{
notifyContent(listener, response, buffer.slice(), counter);
notifyContent(listener, response, d -> demand.accept(listener, d), buffer.slice(), callback);
}
}
}
private void notifyContent(Response.AsyncContentListener listener, Response response, ByteBuffer buffer, Callback callback)
private void notifyContent(Response.DemandedContentListener listener, Response response, LongConsumer demand, ByteBuffer buffer, Callback callback)
{
try
{
listener.onContent(response, buffer, callback);
listener.onContent(response, demand, buffer, callback);
}
catch (Throwable x)
{
@ -236,7 +256,15 @@ public class ResponseNotifier
{
byte[] content = ((ContentResponse)response).getContent();
if (content != null && content.length > 0)
notifyContent(listeners, response, ByteBuffer.wrap(content), Callback.NOOP);
{
List<Response.DemandedContentListener> contentListeners = listeners.stream()
.filter(Response.DemandedContentListener.class::isInstance)
.map(Response.DemandedContentListener.class::cast)
.collect(Collectors.toList());
ObjLongConsumer<Object> demand = (context, value) -> {};
notifyBeforeContent(response, demand, contentListeners);
notifyContent(response, demand, ByteBuffer.wrap(content), Callback.NOOP, contentListeners);
}
}
}

View File

@ -45,7 +45,7 @@ public class Socks4Proxy extends ProxyConfiguration.Proxy
public Socks4Proxy(Origin.Address address, boolean secure)
{
super(address, secure, null);
super(address, secure, null, null);
}
@Override
@ -197,7 +197,7 @@ public class Socks4Proxy extends ProxyConfiguration.Proxy
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
ClientConnectionFactory connectionFactory = this.connectionFactory;
if (destination.isSecure())
connectionFactory = destination.newSslClientConnectionFactory(connectionFactory);
connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory);
org.eclipse.jetty.io.Connection newConnection = connectionFactory.newConnection(getEndPoint(), context);
getEndPoint().upgrade(newConnection);
if (LOG.isDebugEnabled())

View File

@ -370,6 +370,12 @@ public interface Request
*/
Request onResponseContentAsync(Response.AsyncContentListener listener);
/**
* @param listener an asynchronous listener for response content events
* @return this request object
*/
Request onResponseContentDemanded(Response.DemandedContentListener listener);
/**
* @param listener a listener for response success event
* @return this request object

View File

@ -21,6 +21,8 @@ package org.eclipse.jetty.client.api;
import java.nio.ByteBuffer;
import java.util.EventListener;
import java.util.List;
import java.util.concurrent.Flow;
import java.util.function.LongConsumer;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpField;
@ -109,7 +111,7 @@ public interface Response
public interface HeaderListener extends ResponseListener
{
/**
* Callback method invoked when a response header has been received,
* Callback method invoked when a response header has been received and parsed,
* returning whether the header should be processed or not.
*
* @param response the response containing the response line data and the headers so far
@ -125,7 +127,7 @@ public interface Response
public interface HeadersListener extends ResponseListener
{
/**
* Callback method invoked when the response headers have been received and parsed.
* Callback method invoked when all the response headers have been received and parsed.
*
* @param response the response containing the response line data and the headers
*/
@ -133,14 +135,16 @@ public interface Response
}
/**
* Listener for the response content events.
* Synchronous listener for the response content events.
*
* @see AsyncContentListener
*/
public interface ContentListener extends ResponseListener
{
/**
* Callback method invoked when the response content has been received.
* This method may be invoked multiple times, and the {@code content} buffer must be consumed
* before returning from this method.
* Callback method invoked when the response content has been received, parsed and there is demand.
* This method may be invoked multiple times, and the {@code content} buffer
* must be consumed (or copied) before returning from this method.
*
* @param response the response containing the response line data and the headers
* @param content the content bytes received
@ -148,18 +152,60 @@ public interface Response
public void onContent(Response response, ByteBuffer content);
}
/**
* Asynchronous listener for the response content events.
*
* @see DemandedContentListener
*/
public interface AsyncContentListener extends ResponseListener
{
/**
* Callback method invoked asynchronously when the response content has been received.
* Callback method invoked when the response content has been received, parsed and there is demand.
* The {@code callback} object should be succeeded to signal that the
* {@code content} buffer has been consumed and to demand more content.
*
* @param response the response containing the response line data and the headers
* @param content the content bytes received
* @param callback the callback to call when the content is consumed.
* @param callback the callback to call when the content is consumed and to demand more content
*/
public void onContent(Response response, ByteBuffer content, Callback callback);
}
/**
* Asynchronous listener for the response content events.
*/
public interface DemandedContentListener extends ResponseListener
{
/**
* Callback method invoked before response content events.
* The {@code demand} object should be used to demand content, otherwise
* the demand remains at zero (no demand) and
* {@link #onContent(Response, LongConsumer, ByteBuffer, Callback)} will
* not be invoked even if content has been received and parsed.
*
* @param response the response containing the response line data and the headers
* @param demand the object that allows to demand content buffers
*/
public default void onBeforeContent(Response response, LongConsumer demand)
{
demand.accept(1);
}
/**
* Callback method invoked when the response content has been received.
* The {@code callback} object should be succeeded to signal that the
* {@code content} buffer has been consumed.
* The {@code demand} object should be used to demand more content,
* similarly to {@link Flow.Subscription#request(long)}.
*
* @param response the response containing the response line data and the headers
* @param demand the object that allows to demand content buffers
* @param content the content bytes received
* @param callback the callback to call when the content is consumed
*/
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback);
}
/**
* Listener for the response succeeded event.
*/
@ -212,7 +258,7 @@ public interface Response
/**
* Listener for all response events.
*/
public interface Listener extends BeginListener, HeaderListener, HeadersListener, ContentListener, AsyncContentListener, SuccessListener, FailureListener, CompleteListener
public interface Listener extends BeginListener, HeaderListener, HeadersListener, ContentListener, AsyncContentListener, DemandedContentListener, SuccessListener, FailureListener, CompleteListener
{
/**
* An empty implementation of {@link Listener}
@ -254,6 +300,16 @@ public interface Response
}
}
@Override
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback)
{
onContent(response, content, Callback.from(() ->
{
callback.succeeded();
demand.accept(1);
}, callback::failed));
}
@Override
public void onSuccess(Response response)
{

View File

@ -33,6 +33,7 @@ import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.ProcessorUtils;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
@ManagedObject("The HTTP/1.1 client transport")
@ -40,6 +41,9 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
{
public static final HttpDestination.Protocol HTTP11 = new HttpDestination.Protocol(List.of("http/1.1"), false);
private int headerCacheSize = 1024;
private boolean headerCacheCaseSensitive;
public HttpClientTransportOverHTTP()
{
this(Math.max(1, ProcessorUtils.availableProcessors() / 2));
@ -75,7 +79,7 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY);
@SuppressWarnings("unchecked")
Promise<Connection> promise = (Promise<Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
org.eclipse.jetty.io.Connection connection = newHttpConnection(endPoint, destination, promise);
var connection = newHttpConnection(endPoint, destination, promise);
if (LOG.isDebugEnabled())
LOG.debug("Created {}", connection);
return customize(connection, context);
@ -85,4 +89,26 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
{
return new HttpConnectionOverHTTP(endPoint, destination, promise);
}
@ManagedAttribute("The maximum allowed size in bytes for an HTTP header field cache")
public int getHeaderCacheSize()
{
return headerCacheSize;
}
public void setHeaderCacheSize(int headerCacheSize)
{
this.headerCacheSize = headerCacheSize;
}
@ManagedAttribute("Whether the header field cache is case sensitive")
public boolean isHeaderCacheCaseSensitive()
{
return headerCacheCaseSensitive;
}
public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
this.headerCacheCaseSensitive = headerCacheCaseSensitive;
}
}

View File

@ -155,17 +155,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
@Override
public void onFillable()
{
HttpExchange exchange = channel.getHttpExchange();
if (exchange != null)
{
channel.receive();
}
else
{
// If there is no exchange, then could be either a remote close,
// or garbage bytes; in both cases we close the connection
close();
}
channel.receive();
}
@Override

View File

@ -22,6 +22,7 @@ import java.io.EOFException;
import java.nio.ByteBuffer;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpReceiver;
import org.eclipse.jetty.client.HttpResponse;
@ -34,20 +35,29 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.CompletableCallback;
import org.eclipse.jetty.util.Callback;
public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.ResponseHandler
{
private final HttpParser parser;
private ByteBuffer buffer;
private RetainableByteBuffer networkBuffer;
private boolean shutdown;
private boolean complete;
public HttpReceiverOverHTTP(HttpChannelOverHTTP channel)
{
super(channel);
parser = new HttpParser(this, -1, channel.getHttpDestination().getHttpClient().getHttpCompliance());
HttpClient httpClient = channel.getHttpDestination().getHttpClient();
parser = new HttpParser(this, -1, httpClient.getHttpCompliance());
HttpClientTransport transport = httpClient.getTransport();
if (transport instanceof HttpClientTransportOverHTTP)
{
HttpClientTransportOverHTTP httpTransport = (HttpClientTransportOverHTTP)transport;
parser.setHeaderCacheSize(httpTransport.getHeaderCacheSize());
parser.setHeaderCacheCaseSensitive(httpTransport.isHeaderCacheCaseSensitive());
}
}
@Override
@ -63,41 +73,66 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
protected ByteBuffer getResponseBuffer()
{
return buffer;
return networkBuffer == null ? null : networkBuffer.getBuffer();
}
@Override
public void receive()
{
if (buffer == null)
acquireBuffer();
if (networkBuffer == null)
acquireNetworkBuffer();
process();
}
private void acquireBuffer()
private void acquireNetworkBuffer()
{
HttpClient client = getHttpDestination().getHttpClient();
ByteBufferPool bufferPool = client.getByteBufferPool();
buffer = bufferPool.acquire(client.getResponseBufferSize(), true);
networkBuffer = newNetworkBuffer();
if (LOG.isDebugEnabled())
LOG.debug("Acquired {}", networkBuffer);
}
private void releaseBuffer()
private void reacquireNetworkBuffer()
{
if (buffer == null)
RetainableByteBuffer currentBuffer = networkBuffer;
if (currentBuffer == null)
throw new IllegalStateException();
if (BufferUtil.hasContent(buffer))
if (currentBuffer.hasRemaining())
throw new IllegalStateException();
currentBuffer.release();
networkBuffer = newNetworkBuffer();
if (LOG.isDebugEnabled())
LOG.debug("Reacquired {} <- {}", currentBuffer, networkBuffer);
}
private RetainableByteBuffer newNetworkBuffer()
{
HttpClient client = getHttpDestination().getHttpClient();
ByteBufferPool bufferPool = client.getByteBufferPool();
bufferPool.release(buffer);
buffer = null;
return new RetainableByteBuffer(bufferPool, client.getResponseBufferSize(), true);
}
private void releaseNetworkBuffer()
{
if (networkBuffer == null)
throw new IllegalStateException();
if (networkBuffer.hasRemaining())
throw new IllegalStateException();
networkBuffer.release();
if (LOG.isDebugEnabled())
LOG.debug("Released {}", networkBuffer);
networkBuffer = null;
}
protected ByteBuffer onUpgradeFrom()
{
if (BufferUtil.hasContent(buffer))
if (networkBuffer.hasRemaining())
{
ByteBuffer upgradeBuffer = ByteBuffer.allocate(buffer.remaining());
upgradeBuffer.put(buffer).flip();
ByteBuffer upgradeBuffer = BufferUtil.allocate(networkBuffer.remaining());
BufferUtil.clearToFill(upgradeBuffer);
BufferUtil.put(networkBuffer.getBuffer(), upgradeBuffer);
BufferUtil.flipToFlush(upgradeBuffer, 0);
return upgradeBuffer;
}
return null;
@ -111,39 +146,42 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
EndPoint endPoint = connection.getEndPoint();
while (true)
{
boolean upgraded = connection != endPoint.getConnection();
// Always parse even empty buffers to advance the parser.
boolean stopProcessing = parse();
// Connection may be closed or upgraded in a parser callback.
boolean upgraded = connection != endPoint.getConnection();
if (connection.isClosed() || upgraded)
{
if (LOG.isDebugEnabled())
LOG.debug("{} {}", connection, upgraded ? "upgraded" : "closed");
releaseBuffer();
releaseNetworkBuffer();
return;
}
if (parse())
if (stopProcessing)
return;
int read = endPoint.fill(buffer);
if (networkBuffer.getReferences() > 1)
reacquireNetworkBuffer();
int read = endPoint.fill(networkBuffer.getBuffer());
if (LOG.isDebugEnabled())
LOG.debug("Read {} bytes {} from {}", read, BufferUtil.toDetailString(buffer), endPoint);
LOG.debug("Read {} bytes in {} from {}", read, networkBuffer, endPoint);
if (read > 0)
{
connection.addBytesIn(read);
if (parse())
return;
}
else if (read == 0)
{
releaseBuffer();
releaseNetworkBuffer();
fillInterested();
return;
}
else
{
releaseBuffer();
releaseNetworkBuffer();
shutdown();
return;
}
@ -153,9 +191,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
{
if (LOG.isDebugEnabled())
LOG.debug(x);
BufferUtil.clear(buffer);
if (buffer != null)
releaseBuffer();
networkBuffer.clear();
releaseNetworkBuffer();
failAndClose(x);
}
}
@ -169,20 +206,20 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
{
while (true)
{
boolean handle = parser.parseNext(buffer);
boolean handle = parser.parseNext(networkBuffer.getBuffer());
boolean complete = this.complete;
this.complete = false;
if (LOG.isDebugEnabled())
LOG.debug("Parsed {}, remaining {} {}", handle, buffer.remaining(), parser);
LOG.debug("Parsed {}, remaining {} {}", handle, networkBuffer.remaining(), parser);
if (handle)
return true;
if (!buffer.hasRemaining())
if (networkBuffer.isEmpty())
return false;
if (complete)
{
if (LOG.isDebugEnabled())
LOG.debug("Discarding unexpected content after response: {}", BufferUtil.toDetailString(buffer));
BufferUtil.clear(buffer);
LOG.debug("Discarding unexpected content after response: {}", networkBuffer);
networkBuffer.clear();
return false;
}
}
@ -215,32 +252,18 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
}
@Override
public int getHeaderCacheSize()
{
// TODO get from configuration
return 4096;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
// TODO get from configuration
return false;
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return false;
return;
String method = exchange.getRequest().getMethod();
parser.setHeadResponse(HttpMethod.HEAD.is(method) ||
(HttpMethod.CONNECT.is(method) && status == HttpStatus.OK_200));
exchange.getResponse().version(version).status(status).reason(reason);
return !responseBegin(exchange);
responseBegin(exchange);
}
@Override
@ -276,26 +299,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
if (exchange == null)
return false;
CompletableCallback callback = new CompletableCallback()
{
@Override
public void resume()
{
if (LOG.isDebugEnabled())
LOG.debug("Content consumed asynchronously, resuming processing");
process();
}
@Override
public void abort(Throwable x)
{
failAndClose(x);
}
};
// Do not short circuit these calls.
boolean proceed = responseContent(exchange, buffer, callback);
boolean async = callback.tryComplete();
return !proceed || async;
networkBuffer.retain();
return !responseContent(exchange, buffer, Callback.from(networkBuffer::release, this::failAndClose));
}
@Override

View File

@ -288,7 +288,6 @@ public class HttpSenderOverHTTP extends HttpSender
public void failed(Throwable x)
{
release();
callback.failed(x);
super.failed(x);
}
@ -299,6 +298,13 @@ public class HttpSenderOverHTTP extends HttpSender
callback.succeeded();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
super.onCompleteFailure(cause);
callback.failed(cause);
}
private void release()
{
ByteBufferPool bufferPool = httpClient.getByteBufferPool();

View File

@ -26,10 +26,10 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
public class EmptyServerHandler extends AbstractHandler.ErrorDispatchHandler
public class EmptyServerHandler extends AbstractHandler
{
@Override
protected final void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
public final void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
jettyRequest.setHandled(true);
service(target, jettyRequest, request, response);

View File

@ -37,7 +37,6 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
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;
@ -48,7 +47,6 @@ import static org.junit.jupiter.api.Assertions.fail;
* This test class runs tests to make sure that hostname verification (http://www.ietf.org/rfc/rfc2818.txt
* section 3.1) is configurable in SslContextFactory and works as expected.
*/
@Disabled
public class HostnameVerificationTest
{
private SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client();
@ -100,18 +98,15 @@ public class HostnameVerificationTest
{
client.stop();
server.stop();
server.join();
}
/**
* This test is supposed to verify that hostname verification works as described in:
* http://www.ietf.org/rfc/rfc2818.txt section 3.1. It uses a certificate with a common name different to localhost
* and sends a request to localhost. This should fail with an SSLHandshakeException.
*
* @throws Exception on test failure
*/
@Test
public void simpleGetWithHostnameVerificationEnabledTest() throws Exception
public void simpleGetWithHostnameVerificationEnabledTest()
{
clientSslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
String uri = "https://localhost:" + connector.getLocalPort() + "/";
@ -119,8 +114,16 @@ public class HostnameVerificationTest
ExecutionException x = assertThrows(ExecutionException.class, () -> client.GET(uri));
Throwable cause = x.getCause();
assertThat(cause, Matchers.instanceOf(SSLHandshakeException.class));
Throwable root = cause.getCause().getCause();
assertThat(root, Matchers.instanceOf(CertificateException.class));
// Search for the CertificateException.
Throwable certificateException = cause.getCause();
while (certificateException != null)
{
if (certificateException instanceof CertificateException)
break;
certificateException = certificateException.getCause();
}
assertThat(certificateException, Matchers.instanceOf(CertificateException.class));
}
/**

View File

@ -116,7 +116,7 @@ public class HttpClientCustomProxyTest
{
private CAFEBABEProxy(Origin.Address address, boolean secure)
{
super(address, secure, null);
super(address, secure, null, null);
}
@Override

View File

@ -24,7 +24,7 @@ import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
@ -44,7 +44,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class HttpClientGZIPTest extends AbstractHttpClientServerTest
@ -217,16 +217,11 @@ public class HttpClientGZIPTest extends AbstractHttpClientServerTest
}
});
final CountDownLatch latch = new CountDownLatch(1);
client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
.send(result ->
{
if (result.isFailed())
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertThrows(ExecutionException.class, () ->
client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
.timeout(5, TimeUnit.SECONDS)
.send());
}
@ParameterizedTest

View File

@ -19,18 +19,25 @@
package org.eclipse.jetty.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocket;
@ -39,17 +46,26 @@ import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.io.ssl.SslHandshakeListener;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnJre;
@ -68,7 +84,6 @@ public class HttpClientTLSTest
private Server server;
private ServerConnector connector;
private HttpClient client;
private SSLSocket sslSocket;
private void startServer(SslContextFactory.Server sslContextFactory, Handler handler) throws Exception
{
@ -424,16 +439,16 @@ public class HttpClientTLSTest
String host = "localhost";
int port = connector.getLocalPort();
Socket socket = new Socket(host, port);
sslSocket = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket, host, port, true);
Socket socket1 = new Socket(host, port);
SSLSocket sslSocket1 = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket1, host, port, true);
CountDownLatch handshakeLatch1 = new CountDownLatch(1);
AtomicReference<byte[]> session1 = new AtomicReference<>();
sslSocket.addHandshakeCompletedListener(event ->
sslSocket1.addHandshakeCompletedListener(event ->
{
session1.set(event.getSession().getId());
handshakeLatch1.countDown();
});
sslSocket.startHandshake();
sslSocket1.startHandshake();
assertTrue(handshakeLatch1.await(5, TimeUnit.SECONDS));
// In TLS 1.3 the server sends a NewSessionTicket post-handshake message
@ -441,29 +456,29 @@ public class HttpClientTLSTest
assertThrows(SocketTimeoutException.class, () ->
{
sslSocket.setSoTimeout(1000);
sslSocket.getInputStream().read();
sslSocket1.setSoTimeout(1000);
sslSocket1.getInputStream().read();
});
// The client closes abruptly.
socket.close();
socket1.close();
// Try again and compare the session ids.
socket = new Socket(host, port);
sslSocket = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket, host, port, true);
Socket socket2 = new Socket(host, port);
SSLSocket sslSocket2 = (SSLSocket)clientTLSFactory.getSslContext().getSocketFactory().createSocket(socket2, host, port, true);
CountDownLatch handshakeLatch2 = new CountDownLatch(1);
AtomicReference<byte[]> session2 = new AtomicReference<>();
sslSocket.addHandshakeCompletedListener(event ->
sslSocket2.addHandshakeCompletedListener(event ->
{
session2.set(event.getSession().getId());
handshakeLatch2.countDown();
});
sslSocket.startHandshake();
sslSocket2.startHandshake();
assertTrue(handshakeLatch2.await(5, TimeUnit.SECONDS));
assertArrayEquals(session1.get(), session2.get());
sslSocket.close();
sslSocket2.close();
}
@Test
@ -482,10 +497,10 @@ public class HttpClientTLSTest
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector))
{
@Override
protected ClientConnectionFactory newSslClientConnectionFactory(ClientConnectionFactory connectionFactory)
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{
SslClientConnectionFactory ssl = (SslClientConnectionFactory)super.newSslClientConnectionFactory(connectionFactory);
ssl.setAllowMissingCloseMessage(false);
SslClientConnectionFactory ssl = (SslClientConnectionFactory)super.newSslClientConnectionFactory(sslContextFactory, connectionFactory);
ssl.setRequireCloseMessage(true);
return ssl;
}
};
@ -512,19 +527,19 @@ public class HttpClientTLSTest
break;
}
// If the response is Content-Length delimited, allowing the
// missing TLS Close Message is fine because the application
// will see a EOFException anyway.
// If the response is connection delimited, allowing the
// missing TLS Close Message is bad because the application
// will see a successful response with truncated content.
// If the response is Content-Length delimited, the lack of
// the TLS Close Message is fine because the application
// will see a EOFException anyway: the Content-Length and
// the actual content bytes count won't match.
// If the response is connection delimited, the lack of the
// TLS Close Message is bad because the application will
// see a successful response, but with truncated content.
// Verify that by not allowing the missing
// TLS Close Message we get a response failure.
// Verify that by requiring the TLS Close Message we get
// a response failure.
byte[] half = new byte[8];
String response = "HTTP/1.1 200 OK\r\n" +
// "Content-Length: " + (half.length * 2) + "\r\n" +
"Connection: close\r\n" +
"\r\n";
OutputStream output = sslSocket.getOutputStream();
@ -564,4 +579,368 @@ public class HttpClientTLSTest
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testNeverUsedConnectionThenServerIdleTimeout() throws Exception
{
long idleTimeout = 2000;
SslContextFactory.Server serverTLSFactory = createServerSslContextFactory();
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
AtomicLong serverBytes = new AtomicLong();
SslConnectionFactory ssl = new SslConnectionFactory(serverTLSFactory, http.getProtocol())
{
@Override
protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine)
{
return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
{
@Override
protected int networkFill(ByteBuffer input) throws IOException
{
int n = super.networkFill(input);
if (n > 0)
serverBytes.addAndGet(n);
return n;
}
};
}
};
connector = new ServerConnector(server, 1, 1, ssl, http);
connector.setIdleTimeout(idleTimeout);
server.addConnector(connector);
server.setHandler(new EmptyServerHandler());
server.start();
SslContextFactory.Client clientTLSFactory = createClientSslContextFactory();
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSelectors(1);
clientConnector.setSslContextFactory(clientTLSFactory);
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
AtomicLong clientBytes = new AtomicLong();
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector))
{
@Override
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{
if (sslContextFactory == null)
sslContextFactory = getSslContextFactory();
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory)
{
@Override
protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine)
{
return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
{
@Override
protected int networkFill(ByteBuffer input) throws IOException
{
int n = super.networkFill(input);
if (n > 0)
clientBytes.addAndGet(n);
return n;
}
};
}
};
}
};
client.setExecutor(clientThreads);
client.start();
// Create a connection but don't use it.
Origin origin = new Origin(HttpScheme.HTTPS.asString(), "localhost", connector.getLocalPort());
HttpDestination destination = client.resolveDestination(new HttpDestination.Key(origin, null));
DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
// Trigger the creation of a new connection, but don't use it.
connectionPool.tryCreate(-1);
// Verify that the connection has been created.
while (true)
{
Thread.sleep(50);
if (connectionPool.getConnectionCount() == 1)
break;
}
// Wait for the server to idle timeout the connection.
Thread.sleep(idleTimeout + idleTimeout / 2);
// The connection should be gone from the connection pool.
assertEquals(0, connectionPool.getConnectionCount(), connectionPool.dump());
assertEquals(0, serverBytes.get());
assertEquals(0, clientBytes.get());
}
@Test
public void testNeverUsedConnectionThenClientIdleTimeout() throws Exception
{
SslContextFactory.Server serverTLSFactory = createServerSslContextFactory();
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
AtomicLong serverBytes = new AtomicLong();
SslConnectionFactory ssl = new SslConnectionFactory(serverTLSFactory, http.getProtocol())
{
@Override
protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine)
{
return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
{
@Override
protected int networkFill(ByteBuffer input) throws IOException
{
int n = super.networkFill(input);
if (n > 0)
serverBytes.addAndGet(n);
return n;
}
};
}
};
connector = new ServerConnector(server, 1, 1, ssl, http);
server.addConnector(connector);
server.setHandler(new EmptyServerHandler());
server.start();
long idleTimeout = 2000;
SslContextFactory.Client clientTLSFactory = createClientSslContextFactory();
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSelectors(1);
clientConnector.setSslContextFactory(clientTLSFactory);
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
AtomicLong clientBytes = new AtomicLong();
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector))
{
@Override
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{
if (sslContextFactory == null)
sslContextFactory = getSslContextFactory();
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory)
{
@Override
protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine)
{
return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
{
@Override
protected int networkFill(ByteBuffer input) throws IOException
{
int n = super.networkFill(input);
if (n > 0)
clientBytes.addAndGet(n);
return n;
}
};
}
};
}
};
client.setIdleTimeout(idleTimeout);
client.setExecutor(clientThreads);
client.start();
// Create a connection but don't use it.
Origin origin = new Origin(HttpScheme.HTTPS.asString(), "localhost", connector.getLocalPort());
HttpDestination destination = client.resolveDestination(new HttpDestination.Key(origin, null));
DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
// Trigger the creation of a new connection, but don't use it.
connectionPool.tryCreate(-1);
// Verify that the connection has been created.
while (true)
{
Thread.sleep(50);
if (connectionPool.getConnectionCount() == 1)
break;
}
// Wait for the client to idle timeout the connection.
Thread.sleep(idleTimeout + idleTimeout / 2);
// The connection should be gone from the connection pool.
assertEquals(0, connectionPool.getConnectionCount(), connectionPool.dump());
assertEquals(0, serverBytes.get());
assertEquals(0, clientBytes.get());
}
@Test
public void testSSLEngineClosedDuringHandshake() throws Exception
{
SslContextFactory.Server serverTLSFactory = createServerSslContextFactory();
startServer(serverTLSFactory, new EmptyServerHandler());
SslContextFactory.Client clientTLSFactory = createClientSslContextFactory();
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSelectors(1);
clientConnector.setSslContextFactory(clientTLSFactory);
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector))
{
@Override
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{
if (sslContextFactory == null)
sslContextFactory = getSslContextFactory();
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory)
{
@Override
protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine)
{
return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
{
@Override
protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuffer output) throws SSLException
{
sslEngine.closeOutbound();
return super.wrap(sslEngine, input, output);
}
};
}
};
}
};
client.setExecutor(clientThreads);
client.start();
ExecutionException failure = assertThrows(ExecutionException.class, () -> client.newRequest("localhost", connector.getLocalPort())
.scheme(HttpScheme.HTTPS.asString())
.send());
Throwable cause = failure.getCause();
assertThat(cause, Matchers.instanceOf(SSLHandshakeException.class));
}
@Test
public void testTLSLargeFragments() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
SslContextFactory.Server serverTLSFactory = createServerSslContextFactory();
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
SslConnectionFactory ssl = new SslConnectionFactory(serverTLSFactory, http.getProtocol())
{
@Override
protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, SSLEngine engine)
{
return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
{
@Override
protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException
{
int inputBytes = input.remaining();
SSLEngineResult result = super.unwrap(sslEngine, input, output);
if (inputBytes == 5)
serverLatch.countDown();
return result;
}
};
}
};
connector = new ServerConnector(server, 1, 1, ssl, http);
server.addConnector(connector);
server.setHandler(new EmptyServerHandler());
server.start();
long idleTimeout = 2000;
CountDownLatch clientLatch = new CountDownLatch(1);
SslContextFactory.Client clientTLSFactory = createClientSslContextFactory();
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSelectors(1);
clientConnector.setSslContextFactory(clientTLSFactory);
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector))
{
@Override
protected ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
{
if (sslContextFactory == null)
sslContextFactory = getSslContextFactory();
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory)
{
@Override
protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine)
{
return new SslConnection(byteBufferPool, executor, endPoint, engine, isDirectBuffersForEncryption(), isDirectBuffersForDecryption())
{
@Override
protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuffer output) throws SSLException
{
try
{
clientLatch.countDown();
assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
return super.wrap(sslEngine, input, output);
}
catch (InterruptedException x)
{
throw new SSLException(x);
}
}
};
}
};
}
};
client.setIdleTimeout(idleTimeout);
client.setExecutor(clientThreads);
client.start();
String host = "localhost";
int port = connector.getLocalPort();
CountDownLatch responseLatch = new CountDownLatch(1);
client.newRequest(host, port)
.scheme(HttpScheme.HTTPS.asString())
.send(result ->
{
assertTrue(result.isSucceeded());
assertEquals(HttpStatus.OK_200, result.getResponse().getStatus());
responseLatch.countDown();
});
// Wait for the TLS buffers to be acquired by the client, then the
// HTTP request will be paused waiting for the TLS buffer to be expanded.
assertTrue(clientLatch.await(5, TimeUnit.SECONDS));
// Send the large frame bytes that will enlarge the TLS buffers.
try (Socket socket = new Socket(host, port))
{
OutputStream output = socket.getOutputStream();
byte[] largeFrameBytes = new byte[5];
largeFrameBytes[0] = 22; // Type = handshake
largeFrameBytes[1] = 3; // Major TLS version
largeFrameBytes[2] = 3; // Minor TLS version
// Frame length is 0x7FFF == 32767, i.e. a "large fragment".
// Maximum allowed by RFC 8446 is 16384, but SSLEngine supports up to 33093.
largeFrameBytes[3] = 0x7F; // Length hi byte
largeFrameBytes[4] = (byte)0xFF; // Length lo byte
output.write(largeFrameBytes);
output.flush();
// Just close the connection now, the large frame
// length was enough to trigger the buffer expansion.
}
// The HTTP request will resume and be forced to handle the TLS buffer expansion.
assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
}
}

View File

@ -50,6 +50,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.LongConsumer;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
@ -533,10 +534,10 @@ public class HttpClientTest extends AbstractHttpClientServerTest
@ArgumentsSource(ScenarioProvider.class)
public void test_ExchangeIsComplete_OnlyWhenBothRequestAndResponseAreComplete(Scenario scenario) throws Exception
{
start(scenario, new AbstractHandler.ErrorDispatchHandler()
start(scenario, new AbstractHandler()
{
@Override
protected void doNonErrorHandle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setContentLength(0);
@ -1117,6 +1118,13 @@ public class HttpClientTest extends AbstractHttpClientServerTest
counter.incrementAndGet();
}
@Override
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback)
{
// Should not be invoked
counter.incrementAndGet();
}
@Override
public void onSuccess(Response response)
{

View File

@ -39,7 +39,6 @@ public class HttpClientJMXTest
String name = "foo";
HttpClient httpClient = new HttpClient();
httpClient.setName(name);
httpClient.start();
try
{
@ -47,6 +46,7 @@ public class HttpClientJMXTest
MBeanContainer mbeanContainer = new MBeanContainer(mbeanServer);
// Adding MBeanContainer as a bean will trigger the registration of MBeans.
httpClient.addBean(mbeanContainer);
httpClient.start();
String domain = HttpClient.class.getPackage().getName();
ObjectName pattern = new ObjectName(domain + ":type=" + HttpClient.class.getSimpleName().toLowerCase(Locale.ENGLISH) + ",*");

View File

@ -143,6 +143,7 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements
_scanner.setRecursive(_recursive);
_scanner.setFilenameFilter(_filenameFilter);
_scanner.setReportDirs(true);
_scanner.setScanDepth(1); //consider direct dir children of monitored dir
_scanner.addListener(_scannerListener);
addBean(_scanner);

View File

@ -81,6 +81,11 @@ public class WebAppProvider extends ScanningAppProvider
String lowername = name.toLowerCase(Locale.ENGLISH);
File file = new File(dir, name);
Resource r = Resource.newResource(file);
if (getMonitoredResources().contains(r) && r.isDirectory())
{
return false;
}
// ignore hidden files
if (lowername.startsWith("."))

View File

@ -84,7 +84,7 @@
<Set name="responseHeaderSize">8192</Set>
<Set name="sendServerVersion">true</Set>
<Set name="sendDateHeader">false</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
<!-- Uncomment to enable handling of X-Forwarded- style headers
<Call name="addCustomizer">

View File

@ -121,7 +121,7 @@
<destFileName>test-spec.war</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.eclipse.jetty</groupId>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-proxy-webapp</artifactId>
<version>${project.version}</version>
<type>war</type>
@ -416,7 +416,7 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-proxy-webapp</artifactId>
<type>war</type>
<version>${project.version}</version>

View File

@ -100,8 +100,8 @@
<GITDOCURL>https://github.com/eclipse/jetty.project/tree/jetty-10.0.x-doc-refactor/jetty-documentation/src/main/asciidoc</GITDOCURL>
<DISTGUIDE>../distribution-guide/index.html</DISTGUIDE>
<EMBEDGUIDE>../embedded-guide/index.html</EMBEDGUIDE>
<QUICKGUIDE>../quickstart-guideindex.html</QUICKGUIDE>
<CONTRIBGUIDE>../contribution-guideindex.html</CONTRIBGUIDE>
<QUICKGUIDE>../quickstart-guide/index.html</QUICKGUIDE>
<CONTRIBGUIDE>../contribution-guide/index.html</CONTRIBGUIDE>
<MVNCENTRAL>http://central.maven.org/maven2</MVNCENTRAL>
<VERSION>${project.version}</VERSION>
<TIMESTAMP>${maven.build.timestamp}</TIMESTAMP>

View File

@ -139,7 +139,7 @@ Here is an example, setting the context attribute in code (although you can also
----
WebAppContext context = new WebAppContext();
context.setAttribute("org.eclipse.jetty.containerInitializerOrder",
"org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer, com.acme.Foo.MySCI, *");
"org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer, com.acme.Foo.MySCI, *");
----
In this example, we ensure that the `WebSocketServerContainerInitializer` is the very first `ServletContainerInitializer` that is called, followed by MySCI and then any other `ServletContainerInitializer` instances that were discovered but not yet called.

View File

@ -33,7 +33,6 @@ The Denial of Service (DoS) filter limits exposure to request flooding, whether
The DoS filter keeps track of the number of requests from a connection per second.
If the requests exceed the limit, Jetty rejects, delays, or throttles the request, and sends a warning message.
The filter works on the assumption that the attacker might be written in simple blocking style, so by suspending requests you are hopefully consuming the attacker's resources.
The DoS filter is related to the QoS filter, using Continuations to prioritize requests and avoid thread starvation.
[[dos-filter-using]]
==== Using the DoS Filter

View File

@ -31,7 +31,6 @@
The Jetty `GzipHandler` is a compression handler that you can apply to any dynamic resource (servlet).
It fixes many of the bugs in commonly available compression filters: it works with asynchronous servlets; it handles all ways to set content length.
It has been tested with Jetty continuations and suspending requests.
Some user-agents might be excluded from compression to avoid common browser bugs (yes, this means IE!).
The `GzipHandler` can be added to the entire server by enabling the `gzip.mod` module.

View File

@ -26,11 +26,21 @@ The requirements for running HTTP/2 are JDK 8 or greater, and typically also ALP
A server deployed over TLS (SSL) normally advertises the HTTP/2 protocol via the TLS extension Application Layer Protocol Negotiation link:#alpn[(ALPN)].
____
[IMPORTANT]
[NOTE]
To use HTTP/2 in Jetty via a TLS connector you need to add the link:#alpn-starting[ALPN boot jar] in the boot classpath.
This is done automatically when using the Jetty distribution's start.jar link:#startup-modules[module system], but must be configured directly otherwise.
____
[[http2-security-update]]
==== Jetty HTTP/2 Security Update
In mid-2019, there were a link:#security-reports[number of CVEs] were issued warning against vulnerable HTTP/2 implementations. These CVEs (CVE-2019-9511 thru CVE-2019-9518) generally centered around attackers manipulating and flooding HTTP/2 servers and creating a denial of service (DOS). These vulnerabilities were patched with Jetty 9.4.21.
As a result of these CVEs, Jetty introduced a new, configurable denial of service (DOS) protection feature in Jetty 9.4.22.
Jettys HTTP/2 implementation now features a new Rate Control parameter, `jetty.http2.rateControl.maxEventsPerSecond`, that defaults to 20 events per second, per connection for all pings, bad frames, settings frames, priority changes etc.
[[http2-modules]]
==== Jetty HTTP/2 Sub Projects

View File

@ -19,8 +19,8 @@
[[serving-aliased-files]]
=== Aliased Files and Symbolic links
Web applications will often server static content from the file system provided by the operating system running underneath the JVM.
However because file systems often implement multiple aliased names for the same file, then security constraints and other servlet URI space mappings my inadvertently be bypassed by aliases.
Web applications will often serve static content from the file system provided by the operating system running underneath the JVM.
However, because file systems often implement multiple aliased names for the same file, then security constraints and other servlet URI space mappings may inadvertently be bypassed by aliases.
A key example of this is case insensitivity and 8.3 filenames implemented by the Windows file system.
If a file within a web application called `/mysecretfile.txt` is protected by a security constraint on the URI `/mysecretfile.txt`, then a request to `/MySecretFile.TXT` will not match the URI constraint because URIs are case sensitive, but the Windows file system will report that a file does exist at that name and it will be served despite the security constraint.

View File

@ -262,3 +262,16 @@ You might need to escape the slash "\|" to use this on some environments.
maven.repo.uri=[url]::
The url to use to download Maven dependencies.
Default is https://repo1.maven.org/maven2/.
==== Shaded Start.jar
If you have a need for a shaded version of `start.jar` (such as for Gradle), you can achieve this via a Maven dependency.
[source, xml, subs="{sub-order}"]
....
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-start</artifactId>
<version>{VERSION}</version>
<classifier>shaded</classifier>
</dependency>
....

View File

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

View File

@ -1,129 +0,0 @@
//
// ========================================================================
// 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.
// ========================================================================
//
[[continuations-intro]]
=== Introduction
Continuations are a mechanism to implement Asynchronous servlets similar to asynchronous features in Servlet 3.0, but provides a simpler and portable interface.
==== Why Asynchronous Servlets ?
===== Not Asynchronous IO
The concept of Asynchronous Servlets is often confused with Asynchronous IO or the use of NIO.
However, Asynchronous Servlets are not primarily motivated by asynchronous IO, since:
* HTTP Requests are mostly small and arrive in a single packet. Servlets rarely block on requests.
* Many responses are small and fit within the server buffers, so servlets often do not block writing responses.
* Even if we could expose asynchronous IO in a servlet, it is a hard paradigm to program. For example what would an application do if it read 2 bytes of a 3 byte UTF-8 character?
It would have to buffer and wait for more bytes.
This is best done by the container rather than the application.
===== Asynchronous Waiting
The main use-case for asynchronous servlets is waiting for non-IO events or resources.
Many web applications need to wait at some stage during the processing of a HTTP request, for example:
* Waiting for a resource to be available before processing the request (e.g., thread, JDBC Connection).
* Waiting for an application event in an AJAX Comet application (e.g., chat message, price change).
* Waiting for a response from a remote service (e.g., RESTful or SOAP call to a web service).
The servlet API (pre 2.5) supports only a synchronous call style, so that any waiting that a servlet needs to do must be with blocking.
Unfortunately this means that the thread allocated to the request must be held during that wait along with all its resources: kernel thread, stack memory and often pooled buffers, character converters, EE authentication context, etc.
It is wasteful of system resources to hold these resources while waiting. Significantly better scalability and quality of service can be achieved if waiting is done asynchronously.
==== Asynchronous Servlet Examples
===== AJAX Comet Server Push
Web 2.0 applications can use the http://en.wikipedia.org/wiki/Comet_(programming)[comet] technique (aka AJAX Push, Server Push, Long Polling) to dynamically update a web page without refreshing the entire page.
Consider a stock portfolio web application. Each browser will send a long poll request to the server asking for any of the user's stock prices that have changed. The server will receive the long poll requests from all its clients, but will not immediately respond.
Instead the server waits until a stock price changes, at which time it will send a response to each of the clients with that stock in their portfolio.
The clients that receive the long poll response will immediately send another long poll request so they may obtain future price changes.
Thus the server will typically hold a long poll request for every connected user, so if the servlet is not asynchronous, there would need more than 1000 threads available to handle 1000 simultaneous users.
1000 threads can consume over 256MB of memory; that would be better used for the application rather than idly waiting for a price to change.
If the servlet is asynchronous, then the number of threads needed is governed by the time to generate each response and the frequency of price changes.
If every user receives a price every 10 seconds and the response takes 10ms to generate, then 1000 users can be serviced with just 1 thread, and the 256MB of stack be freed for other purposes.
For more on comet see the http://cometd.org/[cometd] project that works asynchronously with Jetty.
===== Asynchronous RESTful Web Service
Consider a web application that accesses a remote web service (e.g., SOAP service or RESTful service).
Typically a remote web service can take hundreds of milliseconds to produce a response -- eBay's RESTful web service frequently takes 350ms to respond with a list of auctions matching a given keyword -- while only a few 10s of milliseconds of CPU time are needed to locally process a request and generate a response.
To handle 1000 requests per second, which each perform a 200ms web service call, a webapp would needs 1000*(200+20)/1000 = 220 threads and 110MB of stack memory.
It would also be vulnerable to thread starvation if bursts occurred or the web service became slower. If handled asynchronously, the web application would not need to hold a thread while waiting for web service response.
Even if the asynchronous mechanism cost 10ms (which it doesn't), then this webapp would need 1000*(20+10)/1000 = 30 threads and 15MB of stack memory.
This is a 86% reduction in the resources required and 95MB more memory would be available for the application.
Furthermore, if multiple web services request are required, the asynchronous approach allows these to be made in parallel rather than serially, without allocating additional threads.
For an example of Jetty's solution, see the https://webtide.com/async-rest-jetty-9/[Asynchronous REST example]
===== Quality of Service (e.g., JDBC Connection Pool)
Consider a web application handling on average 400 requests per second, with each request interacting with the database for 50ms.
To handle this load, 400*50/1000 = 20 JDBC connections are need on average.
However, requests do not come at an even rate and there are often bursts and pauses.
To protect a database from bursts, often a JDBC connection pool is applied to limit the simultaneous requests made on the database.
So for this application, it would be reasonable to apply a JDBC pool of 30 connections, to provide for a 50% margin.
If momentarily the request rate doubled, then the 30 connections would only be able to handle 600 requests per second, and 200 requests per second would join those waiting on the JDBC Connection pool.
Then if the servlet container had a thread pool with 200 threads, that would be entirely consumed by threads waiting for JDBC connections in 1 second of this request rate.
After 1s, the web application would be unable to process any requests at all because no threads would be available.
Even requests that do not use the database would be blocked due to thread starvation.
To double the thread pool would require an additional 100MB of stack memory and would only give the application another 1s of grace under load!
This thread starvation situation can also occur if the database runs slowly or is momentarily unavailable.
Thread starvation is a very frequently reported problem, and causes the entire web service to lock up and become unresponsive.
If the web container was able to suspend the requests waiting for a JDBC connection without threads, then thread starvation would not occur, as only 30 threads would be consumed by requests accessing the database and the other 470 threads would be available to process the request that do not access the database.
For an example of Jetty's solution, see the Quality of Service Filter.
==== Servlet Threading Model
The scalability issues of Java servlets are caused mainly by the server threading model:
===== Thread per connection
The traditional IO model of Java associated a thread with every TCP/IP connection.
If you have a few very active threads, this model can scale to a very high number of requests per second.
However, the traffic profile typical of many web applications is many persistent HTTP connections that are mostly idle while users read pages or search for the next link to click. With such profiles, the thread-per-connection model can have problems scaling to the thousands of threads required to support thousands of users on large scale deployments.
===== Thread per request
The Java NIO libraries support asynchronous IO, so that threads no longer need to be allocated to every connection.
When the connection is idle (between requests), then the connection is added to an NIO select set, which allows one thread to scan many connections for activity.
Only when IO is detected on a connection is a thread allocated to it.
However, the servlet 2.5 API model still requires a thread to be allocated for the duration of the request handling.
This thread-per-request model allows much greater scaling of connections (users) at the expense of a small reduction to maximum requests per second due to extra scheduling latency.
===== Asynchronous Request handling
The Jetty Continuation (and the servlet 3.0 asynchronous) API introduce a change in the servlet API that allows a request to be dispatched multiple times to a servlet.
If the servlet does not have the resources required on a dispatch, then the request is suspended (or put into asynchronous mode), so that the servlet may return from the dispatch without a response being sent.
When the waited-for resources become available, the request is re-dispatched to the servlet, with a new thread, and a response is generated.

View File

@ -1,126 +0,0 @@
//
// ========================================================================
// 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.
// ========================================================================
//
[[continuations-patterns]]
=== Common Continuation Patterns
==== Suspend Resume Pattern
The suspend/resume style is used when a servlet and/or filter is used to generate the response after an asynchronous wait that is terminated by an asynchronous handler.
Typically a request attribute is used to pass results and to indicate if the request has already been suspended.
[source, java, subs="{sub-order}"]
----
void doGet(HttpServletRequest request, HttpServletResponse response)
{
// if we need to get asynchronous results
Object results = request.getAttribute("results");
if (results==null)
{
final Continuation continuation = ContinuationSupport.getContinuation(request);
// if this is not a timeout
if (continuation.isExpired())
{
sendMyTimeoutResponse(response);
return;
}
// suspend the request
continuation.suspend(); // always suspend before registration
// register with async service. The code here will depend on the
// the service used (see Jetty HttpClient for example)
myAsyncHandler.register(new MyHandler()
{
public void onMyEvent(Object result)
{
continuation.setAttribute("results",results);
continuation.resume();
}
});
return; // or continuation.undispatch();
}
// Send the results
sendMyResultResponse(response,results);
}
----
This style is very good when the response needs the facilities of the servlet container (e.g., it uses a web framework) or if one event may resume many requests so the container's thread pool can be used to handle each of them.
==== Suspend Continue Pattern
The suspend/complete style is used when an asynchronous handler is used to generate the response:
[source, java, subs="{sub-order}"]
----
void doGet(HttpServletRequest request, HttpServletResponse response)
{
final Continuation continuation = ContinuationSupport.getContinuation(request);
// if this is not a timeout
if (continuation.isExpired())
{
sendMyTimeoutResponse(request,response);
return;
}
// suspend the request
continuation.suspend(); // response may be wrapped.
// register with async service. The code here will depend on the
// the service used (see Jetty HttpClient for example)
myAsyncHandler.register(new MyHandler()
{
public void onMyEvent(Object result)
{
sendMyResultResponse(continuation.getServletResponse(),results);
continuation.complete();
}
});
}
----
This style is very good when the response does not need the facilities of the servlet container (e.g., it does not use a web framework) and if an event will resume only one continuation.
If many responses are to be sent (e.g., a chat room), then writing one response may block and cause a DOS on the other responses.
==== Examples
* The https://github.com/eclipse/jetty.project/blob/jetty-8/test-jetty-webapp/src/main/java/com/acme/ChatServlet.java[ChatServlet example] shows how the suspend/resume style can be used to directly code a chat room (See similar https://github.com/eclipse/jetty.project/blob/master/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/ChatServlet.java[example] using Async Servlets).
The same principles are applied to frameworks like http://cometd.org/[cometd] which provide an richer environment for such applications, based on Continuations.
* The link:{JDURL}/org/eclipse/jetty/servlets/QoSFilter.html[QoSFilter] uses suspend/resume style to limit the number of requests simultaneously within the filter.
This can be used to protect a JDBC connection pool or other limited resource from too many simultaneous requests.
+
If too many requests are received, the extra requests wait for a short time on a semaphore, before being suspended.
As requests within the filter return, they use a priority queue to resume the suspended requests.
This allows your authenticated or priority users to get a better share of your server's resources when the machine is under load.
+
* The link:{JDURL}/org/eclipse/jetty/servlets/DoSFilter.html[DosFilter] is similar to the QoSFilter, but protects a web application from a denial of service attack, as much as is possible from within a web application.
+
If too many requests are detected coming from one source, then those requests are suspended and a warning generated.
This works on the assumption that the attacker may be written in simple blocking style, so by suspending you are hopefully consuming their resources. True protection from DOS can only be achieved by network devices (or eugenics :)).
+
* The link:{JDURL}/org/eclipse/jetty/proxy/ProxyServlet.html[ProxyServlet] uses the suspend/complete style and the Jetty asynchronous HTTP client to implement a scalable Proxy server (or transparent proxy).

View File

@ -1,116 +0,0 @@
//
// ========================================================================
// 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.
// ========================================================================
//
[[continuations-using]]
=== Using Continuations
Asynchronous servlets were originally introduced with Jetty 6 Continuations, which were a Jetty specific mechanism.
From Jetty 7 onwards, the Continuations API has been extended to be a general purpose API that will work asynchronously on any servlet-3.0 container, as well as on Jetty 6, 7, or 8.
Continuations will also work in blocking mode with any servlet 2.5 container.
==== Obtaining a Continuation
The link:{JDURL}/org/eclipse/jetty/continuation/ContinuationSupport.html[ContinuationSupport] factory class can be used to obtain a continuation instance associated with a request:
`Continuation continuation = ContinuationSupport.getContinuation(request);`
==== Suspending a Request
To suspend a request, the suspend method can be called on the continuation:
[source, java, subs="{sub-order}"]
----
void doGet(HttpServletRequest request, HttpServletResponse response)
{
...
// optionally:
// continuation.setTimeout(long);
continuation.suspend();
...
}
----
The lifecycle of the request will be extended beyond the return to the container from the `Servlet.service(...)` method and `Filter.doFilter(...)` calls. When these dispatch methods return, the suspended request will not yet be committed and a response will not yet be sent to the HTTP client.
Once the request has been suspended, the continuation should be registered with an asynchronous service so that it may be used by an asynchronous callback when the waited-for event happens.
The request will be suspended until either `continuation.resume()` or `continuation.complete()` is called. If neither is called then the continuation will timeout.
The timeout should be set before the suspend, by a call to `continuation.setTimeout(long)` if no timeout is set, then the default period is used.
If no timeout listeners resume or complete the continuation, then the continuation is resumed with `continuation.isExpired()` true.
Suspension is analogous to the servlet 3.0 `request.startAsync()` method. Unlike jetty 6 continuations, an exception is not thrown by suspend and the method should return normally.
This allows the registration of the continuation to occur after suspension and avoids the need for a mutex.
If an exception is desirable (to bypass code that is unaware of continuations and may try to commit the response), then `continuation.undispatch()` may be called to exit the current thread from the current dispatch by throwing a `ContinuationThrowable`.
==== Resuming a Request
Once an asynchronous event has occurred, the continuation can be resumed:
[source, java, subs="{sub-order}"]
----
void myAsyncCallback(Object results)
{
continuation.setAttribute("results",results);
continuation.resume();
}
----
When a continuation is resumed, the request is re-dispatched to the servlet container, almost as if the request had been received again.
However during the re-dispatch, the `continuation.isInitial()` method returns false and any attributes set by the asynchronous handler are available.
Continuation resume is analogous to Servlet 3.0 `AsyncContext.dispatch()`.
==== Completing a Request
As an alternative to resuming a request, an asynchronous handler may write the response itself. After writing the response, the handler must indicate the request handling is complete by calling the complete method:
[source, java, subs="{sub-order}"]
----
void myAsyncCallback(Object results)
{
writeResults(continuation.getServletResponse(),results);
continuation.complete();
}
----
After complete is called, the container schedules the response to be committed and flushed. Continuation complete is analogous to Servlet 3.0 `AsyncContext.complete()`.
==== Continuation Listeners
An application may monitor the status of a continuation by using a ContinuationListener:
[source, java, subs="{sub-order}"]
----
void doGet(HttpServletRequest request, HttpServletResponse response)
{
...
Continuation continuation = ContinuationSupport.getContinuation(request);
continuation.addContinuationListener(new ContinuationListener()
{
public void onTimeout(Continuation continuation) { ... }
public void onComplete(Continuation continuation) { ... }
});
continuation.suspend();
...
}
----
Continuation listeners are analogous to Servlet 3.0 AsyncListeners.

View File

@ -213,7 +213,7 @@ Below is the relevant section taken from link:{GITBROWSEURL}/jetty-server/src/ma
<Set name="responseHeaderSize"><Property name="jetty.httpConfig.responseHeaderSize" deprecated="jetty.response.header.size" default="8192" /></Set>
<Set name="sendServerVersion"><Property name="jetty.httpConfig.sendServerVersion" deprecated="jetty.send.server.version" default="true" /></Set>
<Set name="sendDateHeader"><Property name="jetty.httpConfig.sendDateHeader" deprecated="jetty.send.date.header" default="false" /></Set>
<Set name="headerCacheSize"><Property name="jetty.httpConfig.headerCacheSize" default="4096" /></Set>
<Set name="headerCacheSize"><Property name="jetty.httpConfig.headerCacheSize" default="1024" /></Set>
<Set name="delayDispatchUntilContent"><Property name="jetty.httpConfig.delayDispatchUntilContent" deprecated="jetty.delayDispatchUntilContent" default="true"/></Set>
<Set name="maxErrorDispatches"><Property name="jetty.httpConfig.maxErrorDispatches" default="10"/></Set>
<Set name="persistentConnectionsEnabled"><Property name="jetty.httpConfig.persistentConnectionsEnabled" default="true"/></Set>

View File

@ -26,7 +26,6 @@ include::handlers/chapter.adoc[]
include::websockets/intro/chapter.adoc[]
include::websockets/jetty/chapter.adoc[]
include::ant/chapter.adoc[]
include::continuations/chapter.adoc[]
include::frameworks/chapter.adoc[]
include::architecture/chapter.adoc[]
include::platforms/chapter.adoc[]

View File

@ -28,9 +28,24 @@ If you would like to report a security issue please follow these link:#security-
|=======================================================================
|yyyy/mm/dd |ID |Exploitable |Severity |Affects |Fixed Version |Comment
|2019/08/13 |CVE-2019-9518 |Med |Med |< = 9.4.20 |9.4.21
|https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9518[Some HTTP/2 implementations are vulnerable to a flood of empty frames, potentially leading to a denial of service.]
|2019/08/13 |CVE-2019-9516 |Med |Med |< = 9.4.20 |9.4.21
|https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9516[Some HTTP/2 implementations are vulnerable to a header leak, potentially leading to a denial of service.]
|2019/08/13 |CVE-2019-9515 |Med |Med |< = 9.4.20 |9.4.21
|https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9515[Some HTTP/2 implementations are vulnerable to a settings flood, potentially leading to a denial of service when an attacker sent a stream of SETTINGS frames to the peer.]
|2019/08/13 |CVE-2019-9514 |Med |Med |< = 9.4.20 |9.4.21
|https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9514[Some HTTP/2 implementations are vulnerable to a reset flood, potentially leading to a denial of service.]
|2019/08/13 |CVE-2019-9512 |Low |Low |< = 9.4.20 |9.4.21
|https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9512[Some HTTP/2 implementations are vulnerable to ping floods which could lead to a denial of service.]
|2019/08/13 |CVE-2019-9511 |Low |Low |< = 9.4.20 |9.4.21
|https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9511[Some HTTP/2 implementations are vulnerable to window size manipulation and stream prioritization manipulation which could lead to a denial of service.]
|2019/04/11 |CVE-2019-10247 |Med |Med |< = 9.4.16 |9.2.28, 9.3.27, 9.4.17
|https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-10247[If no webapp was mounted to the root namespace and a 404 was encountered, an HTML page would be generated displaying the fully qualified base resource location for each context.]

View File

@ -107,18 +107,18 @@ To enable Websocket, you need to enable the `websocket` link:#enabling-modules[m
Once this module is enabled for your Jetty base, it will apply to all webapps deployed to that base. If you want to be more selective about which webapps use Websocket, then you can:
Disable JSR-356 for a particular webapp:::
You can disable jsr-356 for a particular webapp by setting the link:#context_attributes[context attribute] `org.eclipse.jetty.websocket.jsr356` to `false`.
Disable Websocket for a particular webapp:::
You can disable jsr-356 for a particular webapp by setting the link:#context_attributes[context attribute] `org.eclipse.jetty.websocket.javax` to `false`.
This will mean that websockets are not available to your webapp, however deployment time scanning for websocket-related classes such as endpoints will still occur.
This can be a significant impost if your webapp contains a lot of classes and/or jar files.
To completely disable websockets and avoid all setup costs associated with it for a particular webapp, use instead the context attribute `org.eclipse.jetty.containerInitializerExclusionPattern`, described next, which allows you to exclude the websocket ServletContainerInitializer that causes the scanning.
Completely disable jsr-356 for a particular webapp:::
Set the `org.eclipse.jetty.containerInitializerExclusionPattern` link:#context_attributes[context attribute] to include `org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer`.
Completely disable Websocket for a particular webapp:::
Set the `org.eclipse.jetty.containerInitializerExclusionPattern` link:#context_attributes[context attribute] to include `org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer`.
Here's an example of doing this in code, although you can do the link:#intro-jetty-configuration-webapps[same in xml]:
+
[source, java, subs="{sub-order}"]
----
WebAppContext context = new WebAppContext();
context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern",
"org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer|com.acme.*");
context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern",
"org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer|com.acme.*");
----

View File

@ -115,10 +115,54 @@ catch (IOException e)
How to send a Text message in 2 parts, using the partial message support in RemoteEndpoint.
This will block until each part of the message is sent, possibly throwing an IOException if unable to send the partial message.
[[websocket-async-send]]
==== Async Send Message
There are also four (4) async send message methods available:
* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendBytes(java.nio.ByteBuffer,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendBytes(ByteBuffer message, WriteCallback callback)`]
* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendPartialBytes(java.nio.ByteBuffer,boolean,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendPartialBytes(ByteBuffer message, boolean isLast, WriteCallback callback)`]
* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendString(java.lang.String,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendString(String message, WriteCallback callback)`]
* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendPartialString(java.lang.String,boolean,org.eclipse.jetty.websocket.api.WriteCallback)[`RemoteEndpoint.sendPartialString(String message, boolean isLast, WriteCallback callback)`]
All these async send methods use `WriteCallback`, which allows you to be notified when the write either succeeds or fails.
[source, java, subs="{sub-order}"]
----
WriteCallback callback = new WriteCallback()
{
@Override
public void writeSuccess()
{
// Notification that the write has succeeded.
}
@Override
public void writeFailed(Throwable x)
{
// Notification that the write has failed.
t.printStackTrace();
}
};
----
The async send methods can be used in a similar way to the blocking send methods, however the method will return before the message is transmitted, and you are notified of the final result of the message transmission through the `WriteCallback`.
The static `WriteCallback.NOOP` can be used to do nothing on success / failure of the callback.
[source, java, subs="{sub-order}"]
----
RemoteEndpoint remote = session.getRemote();
// Async Send of a BINARY message to remote endpoint
ByteBuffer message = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
remote.sendBytes(message, callback);
----
[[pingpong]]
==== Send Ping / Pong Control Frame
You can also send Ping and Pong control frames using the RemoteEndpoint.
You can also send Ping and Pong control frames using the `RemoteEndpoint`.
[source, java, subs="{sub-order}"]
----
@ -138,7 +182,7 @@ catch (IOException e)
----
How to send a Ping control frame, with a payload of `"You There?"` (arriving at Remote Endpoint as a byte array payload).
This will block until the message is sent, possibly throwing an IOException if unable to send the ping frame.
This will block until the message is sent, possibly throwing an `IOException` if unable to send the ping frame.
[source, java, subs="{sub-order}"]
----
@ -158,143 +202,23 @@ catch (IOException e)
----
How to send a Pong control frame, with a payload of `"Yup I'm here"` (arriving at Remote Endpoint as a byte array payload).
This will block until the message is sent, possibly throwing an IOException if unable to send the pong frame.
This will block until the message is sent, possibly throwing an `IOException` if unable to send the pong frame.
To be correct in your usage of Pong frames, you should return the same byte array data that you received in the Ping frame.
[[async]]
==== Async Send Message
However there are also 2 Async send message methods available:
* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendBytesByFuture(java.nio.ByteBuffer)[`RemoteEndpoint.sendBytesByFuture(ByteBuffer message)`]
* link:{JDURL}/org/eclipse/jetty/websocket/api/RemoteEndpoint.html#sendStringByFuture(java.lang.String)[`RemoteEndpoint.sendStringByFuture(String message)`]
Both return a `Future<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.
You can also asynchronously send Ping and Pong frames using the `WriteCallback`, this will return before the Ping/Pong is
transmitted and notify you of the result in `WriteCallback` `writeSuccess()` or `writeFailed()`.
[source, java, subs="{sub-order}"]
----
RemoteEndpoint remote = session.getRemote();
// Async Send of a BINARY message to remote endpoint
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
remote.sendBytesByFuture(buf);
String pingData = "You There?";
ByteBuffer pingPayload = ByteBuffer.wrap(data.getBytes());
String pongData = "Yup, I'm here";
ByteBuffer pongPayload = ByteBuffer.wrap(data.getBytes());
remote.sendPing(pingPayload, WriteCallback.NOOP);
remote.sendPong(pongPayload, WriteCallback.NOOP);
----
How to send a simple Binary message using the RemoteEndpoint.
The message will be enqueued for outgoing write, but you will not know if it succeeded or failed.
[source, java, subs="{sub-order}"]
----
RemoteEndpoint remote = session.getRemote();
// Async Send of a BINARY message to remote endpoint
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
try
{
Future<Void> fut = remote.sendBytesByFuture(buf);
// wait for completion (forever)
fut.get();
}
catch (ExecutionException | InterruptedException e)
{
// Send failed
e.printStackTrace();
}
----
How to send a simple Binary message using the RemoteEndpoint, tracking the `Future<Void>` to know if the send succeeded or failed.
[source, java, subs="{sub-order}"]
----
RemoteEndpoint remote = session.getRemote();
// Async Send of a BINARY message to remote endpoint
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
Future<Void> fut = null;
try
{
fut = remote.sendBytesByFuture(buf);
// wait for completion (timeout)
fut.get(2,TimeUnit.SECONDS);
}
catch (ExecutionException | InterruptedException e)
{
// Send failed
e.printStackTrace();
}
catch (TimeoutException e)
{
// timeout
e.printStackTrace();
if (fut != null)
{
// cancel the message
fut.cancel(true);
}
}
----
How to send a simple Binary message using the RemoteEndpoint, tracking the `Future<Void>` and waiting only prescribed amount of time for the send to complete, cancelling the message if the timeout occurs.
[source, java, subs="{sub-order}"]
----
RemoteEndpoint remote = session.getRemote();
// Async Send of a TEXT message to remote endpoint
remote.sendStringByFuture("Hello World");
----
How to send a simple Text message using the RemoteEndpoint.
The message will be enqueued for outgoing write, but you will not know if it succeeded or failed.
[source, java, subs="{sub-order}"]
----
RemoteEndpoint remote = session.getRemote();
// Async Send of a TEXT message to remote endpoint
try
{
Future<Void> fut = remote.sendStringByFuture("Hello World");
// wait for completion (forever)
fut.get();
}
catch (ExecutionException | InterruptedException e)
{
// Send failed
e.printStackTrace();
}
----
How to send a simple Binary message using the RemoteEndpoint, tracking the `Future<Void>` to know if the send succeeded or failed.
[source, java, subs="{sub-order}"]
----
RemoteEndpoint remote = session.getRemote();
// Async Send of a TEXT message to remote endpoint
Future<Void> fut = null;
try
{
fut = remote.sendStringByFuture("Hello World");
// wait for completion (timeout)
fut.get(2,TimeUnit.SECONDS);
}
catch (ExecutionException | InterruptedException e)
{
// Send failed
e.printStackTrace();
}
catch (TimeoutException e)
{
// timeout
e.printStackTrace();
if (fut != null)
{
// cancel the message
fut.cancel(true);
}
}
----
How to send a simple Binary message using the RemoteEndpoint, tracking the `Future<Void>` and waiting only prescribed amount of time for the send to complete, cancelling the message if the timeout occurs.

View File

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

View File

@ -19,7 +19,7 @@
[[jetty-websocket-server-api]]
=== Jetty WebSocket Server API
Jetty provides the ability to wire up WebSocket endpoints to Servlet Path Specs via the use of a WebSocketServlet bridge servlet.
Jetty provides the ability to wire up WebSocket endpoints to Servlet Path Specs via the use of a `JettyWebSocketServlet` bridge servlet.
Internally, Jetty manages the HTTP Upgrade to WebSocket and migration from a HTTP Connection to a WebSocket Connection.
@ -27,7 +27,7 @@ This will only work when running within the Jetty Container (unlike past Jetty t
==== The Jetty WebSocketServlet
To wire up your WebSocket to a specific path via the WebSocketServlet, you will need to extend org.eclipse.jetty.websocket.servlet.WebSocketServlet and specify what WebSocket object should be created with incoming Upgrade requests.
To wire up your WebSocket to a specific path via the `JettyWebSocketServlet`, you will need to extend `org.eclipse.jetty.websocket.servlet.JettyWebSocketServlet` and specify what `WebSocket` object should be created with incoming Upgrade requests.
[source, java, subs="{sub-order}"]
----
@ -36,8 +36,8 @@ include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclips
This example will create a Servlet mapped via the http://docs.oracle.com/javaee/6/api/javax/servlet/annotation/WebServlet.html[@WebServlet] annotation to the Servlet path spec of `"/echo"` (or you can do this manually in the `WEB-INF/web.xml` of your web application) which will create MyEchoSocket instances when encountering HTTP Upgrade requests.
The link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServlet.html#configure(org.eclipse.jetty.websocket.servlet.WebSocketServletFactory)[`WebSocketServlet.configure(WebSocketServletFactory factory)`] is where you put your specific configuration for your WebSocket.
In the example we specify a 10 second idle timeout and register MyEchoSocket with the default WebSocketCreator the WebSocket class we want to be created on Upgrade.
The link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServlet.html#configure(org.eclipse.jetty.websocket.servlet.JettyWebSocketServletFactory)[`JettyWebSocketServlet.configure(JettyWebSocketServletFactory factory)`] is where you put your specific configuration for your WebSocket.
In the example we specify a 10 second idle timeout and register MyEchoSocket with the default JettyWebSocketCreator the WebSocket class we want to be created on Upgrade.
____
[NOTE]
@ -46,21 +46,21 @@ when configuring websockets. Be sure the websocket configuration is
lower than your firewall or router.
____
==== Using the WebSocketCreator
==== Using the JettyWebSocketCreator
All WebSocket's are created via whatever link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketCreator.html[WebSocketCreator] you have registered with the link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.html[WebSocketServletFactory].
All WebSocket's are created via whatever link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketCreator.html[JettyWebSocketCreator] you have registered with the link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServletFactory.html[JettyWebSocketServletFactory].
By default, the WebSocketServletFactory is a simple WebSocketCreator capable of creating a single WebSocket object.
Use link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.html#register(java.lang.Class)[`WebSocketCreator.register(Class<?> websocket)`] to tell the WebSocketServletFactory which class it should instantiate (make sure it has a default constructor).
By default, the `JettyWebSocketServletFactory` is a simple `JettyWebSocketCreator` capable of creating a single WebSocket object.
Use link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServletFactory.html#register(java.lang.Class)[`JettyWebSocketCreator.register(Class<?> websocket)`] to tell the `JettyWebSocketServletFactory` which class it should instantiate (make sure it has a default constructor).
If you have a more complicated creation scenario, you might want to provide your own WebSocketCreator that bases the WebSocket it creates off of information present in the UpgradeRequest object.
If you have a more complicated creation scenario, you might want to provide your own `JettyWebSocketCreator` that bases the WebSocket it creates off of information present in the `UpgradeRequest` object.
[source, java, subs="{sub-order}"]
----
include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java[]
----
Here we show a WebSocketCreator that will utilize the http://tools.ietf.org/html/rfc6455#section-1.9[WebSocket subprotocol] information from request to determine what WebSocket type should be
Here we show a `JettyWebSocketCreator` that will utilize the http://tools.ietf.org/html/rfc6455#section-1.9[WebSocket subprotocol] information from request to determine what WebSocket type should be
created.
[source, java, subs="{sub-order}"]
@ -68,9 +68,9 @@ created.
include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java[]
----
When you want a custom WebSocketCreator, use link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.html#setCreator(org.eclipse.jetty.websocket.servlet.WebSocketCreator)[`WebSocketServletFactory.setCreator(WebSocketCreator creator)`] and the WebSocketServletFactory will use your creator for all incoming Upgrade requests on this servlet.
When you want a custom `JettyWebSocketCreator`, use link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketServletFactory.html#setCreator(org.eclipse.jetty.websocket.servlet.JettyWebSocketCreator)[`JettyWebSocketServletFactory.setCreator(JettyWebSocketCreator creator)`] and the `JettyWebSocketServletFactory` will use your creator for all incoming Upgrade requests on this servlet.
Other uses for a WebSocketCreator:
Other uses for a `JettyWebSocketCreator`:
* Controlling the selection of WebSocket subprotocol
* Performing any WebSocket origin you deem important.
@ -78,4 +78,4 @@ Other uses for a WebSocketCreator:
* Obtaining the Servlet HttpSession object (if it exists)
* Specifying a response status code and reason
If you don't want to accept the upgrade, simply return null from the link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketCreator.html#createWebSocket(org.eclipse.jetty.websocket.api.UpgradeRequest, org.eclipse.jetty.websocket.api.UpgradeResponse)[`WebSocketCreator.createWebSocket(UpgradeRequest req, UpgradeResponse resp)`] method.
If you don't want to accept the upgrade, simply return null from the link:{JDURL}/org/eclipse/jetty/websocket/servlet/JettyWebSocketCreator.html#createWebSocket(org.eclipse.jetty.websocket.api.UpgradeRequest,org.eclipse.jetty.websocket.api.UpgradeResponse)[`JettyWebSocketCreator.createWebSocket(UpgradeRequest req, UpgradeResponse resp)`] method.

View File

@ -81,6 +81,11 @@ public class HttpChannelOverFCGI extends HttpChannel
return sender.isFailed() || receiver.isFailed();
}
void receive()
{
connection.process();
}
@Override
public void send(HttpExchange exchange)
{

View File

@ -49,8 +49,9 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.CompletableCallback;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -68,7 +69,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
private final Flusher flusher;
private final Delegate delegate;
private final ClientParser parser;
private ByteBuffer buffer;
private RetainableByteBuffer networkBuffer;
public HttpConnectionOverFCGI(EndPoint endPoint, HttpDestination destination, Promise<Connection> promise)
{
@ -114,69 +115,80 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
@Override
public void onFillable()
{
buffer = acquireBuffer();
process(buffer);
networkBuffer = newNetworkBuffer();
process();
}
private ByteBuffer acquireBuffer()
private void reacquireNetworkBuffer()
{
if (networkBuffer == null)
throw new IllegalStateException();
if (networkBuffer.hasRemaining())
throw new IllegalStateException();
networkBuffer.release();
networkBuffer = newNetworkBuffer();
if (LOG.isDebugEnabled())
LOG.debug("Reacquired {}", networkBuffer);
}
private RetainableByteBuffer newNetworkBuffer()
{
HttpClient client = destination.getHttpClient();
ByteBufferPool bufferPool = client.getByteBufferPool();
return bufferPool.acquire(client.getResponseBufferSize(), true);
// TODO: configure directness.
return new RetainableByteBuffer(bufferPool, client.getResponseBufferSize(), true);
}
private void releaseBuffer(ByteBuffer buffer)
private void releaseNetworkBuffer()
{
@SuppressWarnings("ReferenceEquality")
boolean isCurrentBuffer = (this.buffer == buffer);
assert (isCurrentBuffer);
HttpClient client = destination.getHttpClient();
ByteBufferPool bufferPool = client.getByteBufferPool();
bufferPool.release(buffer);
this.buffer = null;
if (networkBuffer == null)
throw new IllegalStateException();
if (networkBuffer.hasRemaining())
throw new IllegalStateException();
networkBuffer.release();
if (LOG.isDebugEnabled())
LOG.debug("Released {}", networkBuffer);
this.networkBuffer = null;
}
private void process(ByteBuffer buffer)
void process()
{
try
{
EndPoint endPoint = getEndPoint();
boolean looping = false;
while (true)
{
if (!looping && parse(buffer))
if (parse(networkBuffer.getBuffer()))
return;
int read = endPoint.fill(buffer);
if (networkBuffer.getReferences() > 1)
reacquireNetworkBuffer();
// The networkBuffer may have been reacquired.
int read = endPoint.fill(networkBuffer.getBuffer());
if (LOG.isDebugEnabled())
LOG.debug("Read {} bytes from {}", read, endPoint);
if (read > 0)
if (read == 0)
{
if (parse(buffer))
return;
}
else if (read == 0)
{
releaseBuffer(buffer);
releaseNetworkBuffer();
fillInterested();
return;
}
else
else if (read < 0)
{
releaseBuffer(buffer);
releaseNetworkBuffer();
shutdown();
return;
}
looping = true;
}
}
catch (Exception x)
{
if (LOG.isDebugEnabled())
LOG.debug(x);
releaseBuffer(buffer);
networkBuffer.clear();
releaseNetworkBuffer();
close(x);
}
}
@ -404,13 +416,13 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
HttpChannelOverFCGI channel = activeChannels.get(request);
if (channel != null)
channel.responseHeaders();
else
noChannel(request);
return !channel.responseHeaders();
noChannel(request);
return false;
}
@Override
@ -423,26 +435,8 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
HttpChannelOverFCGI channel = activeChannels.get(request);
if (channel != null)
{
CompletableCallback callback = new CompletableCallback()
{
@Override
public void resume()
{
if (LOG.isDebugEnabled())
LOG.debug("Content consumed asynchronously, resuming processing");
process(HttpConnectionOverFCGI.this.buffer);
}
@Override
public void abort(Throwable x)
{
close(x);
}
};
// Do not short circuit these calls.
boolean proceed = channel.content(buffer, callback);
boolean async = callback.tryComplete();
return !proceed || async;
networkBuffer.retain();
return !channel.content(buffer, Callback.from(networkBuffer::release, HttpConnectionOverFCGI.this::close));
}
else
{

View File

@ -33,6 +33,12 @@ public class HttpReceiverOverFCGI extends HttpReceiver
super(channel);
}
@Override
protected HttpChannelOverFCGI getHttpChannel()
{
return (HttpChannelOverFCGI)super.getHttpChannel();
}
@Override
protected boolean responseBegin(HttpExchange exchange)
{
@ -68,4 +74,10 @@ public class HttpReceiverOverFCGI extends HttpReceiver
{
return super.responseFailure(failure);
}
@Override
protected void receive()
{
getHttpChannel().receive();
}
}

View File

@ -80,9 +80,9 @@ public class ClientParser extends Parser
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
listener.onHeaders(request);
return listener.onHeaders(request);
}
@Override

View File

@ -135,7 +135,11 @@ public abstract class Parser
{
public void onHeader(int request, HttpField field);
public void onHeaders(int request);
/**
* @param request the request id
* @return true to signal to the parser to stop parsing, false to continue parsing
*/
public boolean onHeaders(int request);
/**
* @param request the request id
@ -158,8 +162,9 @@ public abstract class Parser
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
return false;
}
@Override

View File

@ -82,7 +82,7 @@ public class ResponseContentParser extends StreamContentParser
parsers.remove(request);
}
private class ResponseParser implements HttpParser.ResponseHandler
private static class ResponseParser implements HttpParser.ResponseHandler
{
private final HttpFields fields = new HttpFields();
private ClientParser.Listener listener;
@ -90,6 +90,7 @@ public class ResponseContentParser extends StreamContentParser
private final FCGIHttpParser httpParser;
private State state = State.HEADERS;
private boolean seenResponseCode;
private boolean stalled;
private ResponseParser(ClientParser.Listener listener, int request)
{
@ -111,7 +112,11 @@ public class ResponseContentParser extends StreamContentParser
case HEADERS:
{
if (httpParser.parseNext(buffer))
{
state = State.CONTENT_MODE;
if (stalled)
return true;
}
remaining = buffer.remaining();
break;
}
@ -151,21 +156,7 @@ public class ResponseContentParser extends StreamContentParser
}
@Override
public int getHeaderCacheSize()
{
// TODO: configure this
return 4096;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
// TODO get from configuration
return false;
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
// The HTTP request line does not exist in FCGI responses
throw new IllegalStateException();
@ -247,16 +238,17 @@ public class ResponseContentParser extends StreamContentParser
}
}
private void notifyHeaders()
private boolean notifyHeaders()
{
try
{
listener.onHeaders(request);
return listener.onHeaders(request);
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Exception while invoking listener " + listener, x);
return false;
}
}
@ -269,8 +261,10 @@ public class ResponseContentParser extends StreamContentParser
notifyBegin(200, "OK");
notifyHeaders(fields);
}
notifyHeaders();
// Return from HTTP parsing so that we can parse the content.
// Remember whether we have demand.
stalled = notifyHeaders();
// Always return from HTTP parsing so that we
// can parse the content with the FCGI parser.
return true;
}

View File

@ -109,10 +109,11 @@ public class ClientGeneratorTest
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
assertEquals(id, request);
params.set(params.get() * primes[4]);
return false;
}
});

View File

@ -91,10 +91,11 @@ public class ClientParserTest
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
assertEquals(id, request);
params.set(params.get() * primes[2]);
return false;
}
});

View File

@ -50,12 +50,6 @@ public class HttpTransportOverFCGI implements HttpTransport
this.request = request;
}
@Override
public boolean isOptimizedForDirectBuffers()
{
return false;
}
@Override
public void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback)
{

View File

@ -144,7 +144,7 @@ public class ServerFCGIConnection extends AbstractConnection
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
HttpChannelOverFCGI channel = channels.get(request);
if (LOG.isDebugEnabled())
@ -154,6 +154,7 @@ public class ServerFCGIConnection extends AbstractConnection
channel.onRequest();
channel.dispatch();
}
return false;
}
@Override

View File

@ -723,6 +723,11 @@
<artifactId>jetty-alpn-java-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-openid</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-server</artifactId>

View File

@ -6,7 +6,7 @@
<!-- ================================================================ -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.setuid.SetUIDListener">
<Set name="startServerAsPrivileged"><Property name="jetty.setuid.startServerAsPrivileged" default="false"/></Set>

View File

@ -5,7 +5,7 @@
<!-- Mixin the Start FileNoticeLifeCycleListener -->
<!-- =============================================================== -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.util.component.FileNoticeLifeCycleListener">
<Arg><Property name="jetty.state" default="./jetty.state"/></Arg>

View File

@ -34,7 +34,7 @@ import static java.util.EnumSet.noneOf;
*/
public class CookieCompliance implements ComplianceViolation.Mode
{
enum Violation implements ComplianceViolation
public enum Violation implements ComplianceViolation
{
COMMA_NOT_VALID_OCTET("https://tools.ietf.org/html/rfc6265#section-4.1.1", "Comma not valid as cookie-octet or separator"),
RESERVED_NAMES_NOT_DOLLAR_PREFIXED("https://tools.ietf.org/html/rfc6265#section-4.1.1", "Reserved names no longer use '$' prefix");
@ -57,13 +57,13 @@ public class CookieCompliance implements ComplianceViolation.Mode
@Override
public String getURL()
{
return null;
return url;
}
@Override
public String getDescription()
{
return null;
return description;
}
}
@ -87,9 +87,8 @@ public class CookieCompliance implements ComplianceViolation.Mode
private CookieCompliance(String name, Set<Violation> violations)
{
Objects.nonNull(violations);
_name = name;
_violations = unmodifiableSet(copyOf(violations));
_violations = unmodifiableSet(copyOf(Objects.requireNonNull(violations)));
}
@Override

View File

@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
// TODO consider replacing this with java.net.HttpCookie
// TODO consider replacing this with java.net.HttpCookie (once it supports RFC6265)
public class HttpCookie
{
private static final String __COOKIE_DELIM = "\",;\\ \t";
@ -33,14 +33,14 @@ public class HttpCookie
/**
*If this string is found within the comment parsed with {@link #isHttpOnlyInComment(String)} the check will return true
**/
private static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
public static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
/**
*These strings are used by {@link #getSameSiteFromComment(String)} to check for a SameSite specifier in the comment
**/
private static final String SAME_SITE_COMMENT = "__SAME_SITE_";
private static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__";
private static final String SAME_SITE_LAX_COMMENT = SAME_SITE_COMMENT + "LAX__";
private static final String SAME_SITE_STRICT_COMMENT = SAME_SITE_COMMENT + "STRICT__";
public static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__";
public static final String SAME_SITE_LAX_COMMENT = SAME_SITE_COMMENT + "LAX__";
public static final String SAME_SITE_STRICT_COMMENT = SAME_SITE_COMMENT + "STRICT__";
public enum SameSite
{
@ -428,17 +428,17 @@ public class HttpCookie
{
if (comment != null)
{
if (comment.contains(SAME_SITE_NONE_COMMENT))
if (comment.contains(SAME_SITE_STRICT_COMMENT))
{
return SameSite.NONE;
return SameSite.STRICT;
}
if (comment.contains(SAME_SITE_LAX_COMMENT))
{
return SameSite.LAX;
}
if (comment.contains(SAME_SITE_STRICT_COMMENT))
if (comment.contains(SAME_SITE_NONE_COMMENT))
{
return SameSite.STRICT;
return SameSite.NONE;
}
}
@ -462,6 +462,44 @@ public class HttpCookie
return strippedComment.length() == 0 ? null : strippedComment;
}
public static String getCommentWithAttributes(String comment, boolean httpOnly, SameSite sameSite)
{
if (comment == null && sameSite == null)
return null;
StringBuilder builder = new StringBuilder();
if (StringUtil.isNotBlank(comment))
{
comment = getCommentWithoutAttributes(comment);
if (StringUtil.isNotBlank(comment))
builder.append(comment);
}
if (httpOnly)
builder.append(HTTP_ONLY_COMMENT);
if (sameSite != null)
{
switch (sameSite)
{
case NONE:
builder.append(SAME_SITE_NONE_COMMENT);
break;
case STRICT:
builder.append(SAME_SITE_STRICT_COMMENT);
break;
case LAX:
builder.append(SAME_SITE_LAX_COMMENT);
break;
default:
throw new IllegalArgumentException(sameSite.toString());
}
}
if (builder.length() == 0)
return null;
return builder.toString();
}
public static class SetCookieHttpField extends HttpField
{
final HttpCookie _cookie;

View File

@ -69,6 +69,11 @@ public class HttpField
return _name;
}
public String getLowerCaseName()
{
return _header != null ? _header.lowerCaseName() : StringUtil.asciiToLowerCase(_name);
}
public String getValue()
{
return _value;

View File

@ -78,7 +78,7 @@ public class HttpGenerator
FLUSH, // The buffers previously generated should be flushed
CONTINUE, // Continue generating the message
SHUTDOWN_OUT, // Need EOF to be signaled
DONE // Message generation complete
DONE // The current phase of generation is complete
}
// other statics
@ -686,17 +686,24 @@ public class HttpGenerator
_endOfContent = EndOfContent.NO_CONTENT;
// But it is an error if there actually is content
if (_contentPrepared > 0 || contentLength > 0)
if (_contentPrepared > 0)
throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response");
if (contentLengthField)
{
if (_contentPrepared == 0 && last)
if (response != null && response.getStatus() == HttpStatus.NOT_MODIFIED_304)
putContentLength(header, contentLength);
else if (contentLength > 0)
{
// TODO discard content for backward compatibility with 9.3 releases
// TODO review if it is still needed in 9.4 or can we just throw.
content.clear();
contentLength = 0;
if (_contentPrepared == 0 && last)
{
// TODO discard content for backward compatibility with 9.3 releases
// TODO review if it is still needed in 9.4 or can we just throw.
content.clear();
}
else
throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response");
}
else
throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Content for no content response");
}
}
// Else if we are HTTP/1.1 and the content length is unknown and we are either persistent

View File

@ -150,6 +150,7 @@ public enum HttpHeader
}
private final String _string;
private final String _lowerCase;
private final byte[] _bytes;
private final byte[] _bytesColonSpace;
private final ByteBuffer _buffer;
@ -157,11 +158,17 @@ public enum HttpHeader
HttpHeader(String s)
{
_string = s;
_lowerCase = StringUtil.asciiToLowerCase(s);
_bytes = StringUtil.getBytes(s);
_bytesColonSpace = StringUtil.getBytes(s + ": ");
_buffer = ByteBuffer.wrap(_bytes);
}
public String lowerCaseName()
{
return _lowerCase;
}
public ByteBuffer toBuffer()
{
return _buffer.asReadOnlyBuffer();

View File

@ -24,7 +24,6 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import org.eclipse.jetty.http.HttpCompliance.Violation;
import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.ArrayTernaryTrie;
import org.eclipse.jetty.util.ArrayTrie;
@ -35,6 +34,8 @@ import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import static org.eclipse.jetty.http.HttpCompliance.RFC7230;
import static org.eclipse.jetty.http.HttpCompliance.Violation;
import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME;
import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS;
import static org.eclipse.jetty.http.HttpCompliance.Violation.NO_COLON_AFTER_FIELD_NAME;
@ -136,6 +137,7 @@ public class HttpParser
CHUNK_SIZE,
CHUNK_PARAMS,
CHUNK,
CONTENT_END,
TRAILER,
END,
CLOSE, // The associated stream/endpoint should be closed
@ -160,7 +162,6 @@ public class HttpParser
private int _headerBytes;
private boolean _host;
private boolean _headerComplete;
private volatile State _state = State.START;
private volatile FieldState _fieldState = FieldState.FIELD;
private volatile boolean _eof;
@ -170,6 +171,7 @@ public class HttpParser
private Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune?
private EndOfContent _endOfContent;
private boolean _hasContentLength;
private boolean _hasTransferEncoding;
private long _contentLength = -1;
private long _contentPosition;
private int _chunkLength;
@ -178,9 +180,10 @@ public class HttpParser
private boolean _cr;
private ByteBuffer _contentChunk;
private Trie<HttpField> _fieldCache;
private int _length;
private final StringBuilder _string = new StringBuilder();
private int _headerCacheSize = 1024;
private boolean _headerCacheCaseSensitive;
static
{
@ -237,7 +240,7 @@ public class HttpParser
private static HttpCompliance compliance()
{
return HttpCompliance.RFC7230;
return RFC7230;
}
public HttpParser(RequestHandler handler)
@ -290,6 +293,26 @@ public class HttpParser
return _handler;
}
public int getHeaderCacheSize()
{
return _headerCacheSize;
}
public void setHeaderCacheSize(int headerCacheSize)
{
_headerCacheSize = headerCacheSize;
}
public boolean isHeaderCacheCaseSensitive()
{
return _headerCacheCaseSensitive;
}
public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
_headerCacheCaseSensitive = headerCacheCaseSensitive;
}
protected void checkViolation(Violation violation) throws BadMessageException
{
if (violation.isAllowedBy(_complianceMode))
@ -528,16 +551,19 @@ public class HttpParser
{
boolean handleHeader = _handler.headerComplete();
_headerComplete = true;
boolean handleContent = _handler.contentComplete();
boolean handleMessage = _handler.messageComplete();
return handleHeader || handleContent || handleMessage;
if (handleHeader)
return true;
setState(State.CONTENT_END);
return handleContentMessage();
}
private boolean handleContentMessage()
{
boolean handleContent = _handler.contentComplete();
boolean handleMessage = _handler.messageComplete();
return handleContent || handleMessage;
if (handleContent)
return true;
setState(State.END);
return _handler.messageComplete();
}
/* Parse a request or response line
@ -707,7 +733,7 @@ public class HttpParser
case LF:
setState(State.HEADER);
handle |= _responseHandler.startResponse(_version, _responseStatus, null);
_responseHandler.startResponse(_version, _responseStatus, null);
break;
default:
@ -725,10 +751,11 @@ public class HttpParser
case LF:
// HTTP/0.9
checkViolation(Violation.HTTP_0_9);
handle = _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.END);
_requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.CONTENT);
_endOfContent = EndOfContent.NO_CONTENT;
BufferUtil.clear(buffer);
handle |= handleHeaderContentMessage();
handle = handleHeaderContentMessage();
break;
case ALPHA:
@ -804,16 +831,17 @@ public class HttpParser
if (_responseHandler != null)
{
setState(State.HEADER);
handle |= _responseHandler.startResponse(_version, _responseStatus, null);
_responseHandler.startResponse(_version, _responseStatus, null);
}
else
{
// HTTP/0.9
checkViolation(Violation.HTTP_0_9);
handle = _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.END);
_requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.CONTENT);
_endOfContent = EndOfContent.NO_CONTENT;
BufferUtil.clear(buffer);
handle |= handleHeaderContentMessage();
handle = handleHeaderContentMessage();
}
break;
@ -834,15 +862,13 @@ public class HttpParser
checkVersion();
// Should we try to cache header fields?
if (_fieldCache == null && _version.getVersion() >= HttpVersion.HTTP_1_1.getVersion() && _handler.getHeaderCacheSize() > 0)
{
int headerCache = _handler.getHeaderCacheSize();
int headerCache = getHeaderCacheSize();
if (_fieldCache == null && _version.getVersion() >= HttpVersion.HTTP_1_1.getVersion() && headerCache > 0)
_fieldCache = new ArrayTernaryTrie<>(headerCache);
}
setState(State.HEADER);
handle |= _requestHandler.startRequest(_methodString, _uri.toString(), _version);
_requestHandler.startRequest(_methodString, _uri.toString(), _version);
continue;
case ALPHA:
@ -864,7 +890,7 @@ public class HttpParser
case LF:
String reason = takeString();
setState(State.HEADER);
handle |= _responseHandler.startResponse(_version, _responseStatus, reason);
_responseHandler.startResponse(_version, _responseStatus, reason);
continue;
case ALPHA:
@ -916,6 +942,9 @@ public class HttpParser
switch (_header)
{
case CONTENT_LENGTH:
if (_hasTransferEncoding)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH);
if (_hasContentLength)
{
checkViolation(MULTIPLE_CONTENT_LENGTHS);
@ -924,9 +953,6 @@ public class HttpParser
}
_hasContentLength = true;
if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH);
if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
{
_contentLength = convertContentLength(_valueString);
@ -938,9 +964,15 @@ public class HttpParser
break;
case TRANSFER_ENCODING:
_hasTransferEncoding = true;
if (_hasContentLength)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH);
// we encountered another Transfer-Encoding header, but chunked was already set
if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
if (HttpHeaderValue.CHUNKED.is(_valueString))
{
_endOfContent = EndOfContent.CHUNKED_CONTENT;
@ -949,15 +981,26 @@ public class HttpParser
else
{
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;
_contentLength = -1;
if (HttpHeaderValue.CHUNKED.is(values.get(i)))
{
if (chunked != -1)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, multiple chunked tokens");
chunked = i;
// declared chunked
_endOfContent = EndOfContent.CHUNKED_CONTENT;
_contentLength = -1;
}
// we have a non-chunked token after a declared chunked token
else if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
{
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
}
}
else if (values.stream().anyMatch(HttpHeaderValue.CHUNKED::is))
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad chunking");
}
break;
case HOST:
@ -1098,6 +1141,17 @@ public class HttpParser
return _handler.messageComplete();
}
// We found Transfer-Encoding headers, but none declared the 'chunked' token
if (_hasTransferEncoding && _endOfContent != EndOfContent.CHUNKED_CONTENT)
{
if (_responseHandler == null || _endOfContent != EndOfContent.EOF_CONTENT)
{
// Transfer-Encoding chunked not specified
// https://tools.ietf.org/html/rfc7230#section-3.3.1
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Transfer-Encoding, chunked not last");
}
}
// Was there a required host header?
if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null)
{
@ -1140,11 +1194,6 @@ public class HttpParser
_headerComplete = true;
return handle;
}
case NO_CONTENT:
{
setState(State.END);
return handleHeaderContentMessage();
}
default:
{
setState(State.CONTENT);
@ -1190,7 +1239,7 @@ public class HttpParser
}
}
if (v != null && _handler.isHeaderCacheCaseSensitive())
if (v != null && isHeaderCacheCaseSensitive())
{
String ev = BufferUtil.toString(buffer, buffer.position() + n.length() + 1, v.length(), StandardCharsets.ISO_8859_1);
if (!v.equals(ev))
@ -1443,8 +1492,16 @@ public class HttpParser
// Handle HEAD response
if (_responseStatus > 0 && _headResponse)
{
setState(State.END);
return handleContentMessage();
if (_state != State.CONTENT_END)
{
setState(State.CONTENT_END);
return handleContentMessage();
}
else
{
setState(State.END);
return _handler.messageComplete();
}
}
else
{
@ -1463,11 +1520,18 @@ public class HttpParser
// handle end states
if (_state == State.END)
{
// eat white space
while (buffer.remaining() > 0 && buffer.get(buffer.position()) <= HttpTokens.SPACE)
// Eat CR or LF white space, but not SP.
int whiteSpace = 0;
while (buffer.remaining() > 0)
{
byte b = buffer.get(buffer.position());
if (b != HttpTokens.CARRIAGE_RETURN && b != HttpTokens.LINE_FEED)
break;
buffer.get();
++whiteSpace;
}
if (debugEnabled && whiteSpace > 0)
LOG.debug("Discarded {} CR or LF characters", whiteSpace);
}
else if (isClose() || isClosed())
{
@ -1475,18 +1539,13 @@ public class HttpParser
}
// Handle EOF
if (_eof && !buffer.hasRemaining())
if (isAtEOF() && !buffer.hasRemaining())
{
switch (_state)
{
case CLOSED:
break;
case START:
setState(State.CLOSED);
_handler.earlyEOF();
break;
case END:
case CLOSE:
setState(State.CLOSED);
@ -1497,13 +1556,18 @@ public class HttpParser
if (_fieldState == FieldState.FIELD)
{
// Be forgiving of missing last CRLF
setState(State.CONTENT_END);
boolean handle = handleContentMessage();
if (handle && _state == State.CONTENT_END)
return true;
setState(State.CLOSED);
return handleContentMessage();
return handle;
}
setState(State.CLOSED);
_handler.earlyEOF();
break;
case START:
case CONTENT:
case CHUNKED_CONTENT:
case CHUNK_SIZE:
@ -1549,18 +1613,28 @@ public class HttpParser
protected boolean parseContent(ByteBuffer buffer)
{
int remaining = buffer.remaining();
if (remaining == 0 && _state == State.CONTENT)
if (remaining == 0)
{
long content = _contentLength - _contentPosition;
if (content == 0)
switch (_state)
{
setState(State.END);
return handleContentMessage();
case CONTENT:
long content = _contentLength - _contentPosition;
if (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
{
setState(State.CONTENT_END);
return handleContentMessage();
}
break;
case CONTENT_END:
setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
return _handler.messageComplete();
default:
// No bytes to parse, return immediately.
return false;
}
}
// Handle _content
byte ch;
// Handle content.
while (_state.ordinal() < State.TRAILER.ordinal() && remaining > 0)
{
switch (_state)
@ -1576,9 +1650,9 @@ public class HttpParser
case CONTENT:
{
long content = _contentLength - _contentPosition;
if (content == 0)
if (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
{
setState(State.END);
setState(State.CONTENT_END);
return handleContentMessage();
}
else
@ -1601,7 +1675,7 @@ public class HttpParser
if (_contentPosition == _contentLength)
{
setState(State.END);
setState(State.CONTENT_END);
return handleContentMessage();
}
}
@ -1726,10 +1800,10 @@ public class HttpParser
break;
}
case CLOSED:
case CONTENT_END:
{
BufferUtil.clear(buffer);
return false;
setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
return _handler.messageComplete();
}
default:
@ -1779,6 +1853,7 @@ public class HttpParser
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
_contentLength = -1;
_hasContentLength = false;
_hasTransferEncoding = false;
_contentPosition = 0;
_responseStatus = 0;
_contentChunk = null;
@ -1812,8 +1887,8 @@ public class HttpParser
return String.format("%s{s=%s,%d of %d}",
getClass().getSimpleName(),
_state,
_contentPosition,
_contentLength);
getContentRead(),
getContentLength());
}
/* Event Handler interface
@ -1863,13 +1938,6 @@ public class HttpParser
default void badMessage(BadMessageException failure)
{
}
/**
* @return the size in bytes of the per parser header cache
*/
int getHeaderCacheSize();
boolean isHeaderCacheCaseSensitive();
}
public interface RequestHandler extends HttpHandler
@ -1880,9 +1948,8 @@ public class HttpParser
* @param method The method
* @param uri The raw bytes of the URI. These are copied into a ByteBuffer that will not be changed until this parser is reset and reused.
* @param version the http version in use
* @return true if handling parsing should return.
*/
boolean startRequest(String method, String uri, HttpVersion version);
void startRequest(String method, String uri, HttpVersion version);
}
public interface ResponseHandler extends HttpHandler
@ -1893,9 +1960,8 @@ public class HttpParser
* @param version the http version in use
* @param status the response status
* @param reason the response reason phrase
* @return true if handling parsing should return
*/
boolean startResponse(HttpVersion version, int status, String reason);
void startResponse(HttpVersion version, int status, String reason);
}
@SuppressWarnings("serial")

View File

@ -233,7 +233,7 @@ public class ServletPathSpec extends PathSpec
{
this.group = PathSpecGroup.EXACT;
this.prefix = servletPathSpec;
if (servletPathSpec.endsWith("*") )
if (servletPathSpec.endsWith("*"))
{
LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification",
servletPathSpec);

View File

@ -18,17 +18,25 @@
package org.eclipse.jetty.http;
import java.util.stream.Stream;
import org.hamcrest.Matchers;
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 org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class HttpCookieTest
{
@ -93,7 +101,6 @@ public class HttpCookieTest
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", httpCookie.getRFC6265SetCookie());
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.NONE);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=None", httpCookie.getRFC6265SetCookie());
@ -102,8 +109,11 @@ public class HttpCookieTest
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.STRICT);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Strict", httpCookie.getRFC6265SetCookie());
}
String[] badNameExamples = {
public static Stream<String> rfc6265BadNameSource()
{
return Stream.of(
"\"name\"",
"name\t",
"na me",
@ -113,25 +123,32 @@ public class HttpCookieTest
"{name}",
"[name]",
"\""
};
);
}
for (String badNameExample : badNameExamples)
{
try
@ParameterizedTest
@MethodSource("rfc6265BadNameSource")
public void testSetRFC6265CookieBadName(String badNameExample)
{
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() ->
{
httpCookie = new HttpCookie(badNameExample, "value", null, "/", 1, true, true, null, -1);
HttpCookie httpCookie = new HttpCookie(badNameExample, "value", null, "/", 1, true, true, null, -1);
httpCookie.getRFC6265SetCookie();
fail(badNameExample);
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad name: [" + badNameExample + "]", ex.getMessage(),
allOf(containsString("RFC6265"), containsString("RFC2616")));
}
}
});
// make sure that exception mentions just how mad of a name it truly is
assertThat("message", ex.getMessage(),
allOf(
// violation of Cookie spec
containsString("RFC6265"),
// violation of HTTP spec
containsString("RFC2616")
));
}
String[] badValueExamples = {
public static Stream<String> rfc6265BadValueSource()
{
return Stream.of(
"va\tlue",
"\t",
"value\u0000",
@ -143,39 +160,44 @@ public class HttpCookieTest
"val\\ue",
"val\"ue",
"\""
};
);
}
for (String badValueExample : badValueExamples)
{
try
@ParameterizedTest
@MethodSource("rfc6265BadValueSource")
public void testSetRFC6265CookieBadValue(String badValueExample)
{
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() ->
{
httpCookie = new HttpCookie("name", badValueExample, null, "/", 1, true, true, null, -1);
HttpCookie httpCookie = new HttpCookie("name", badValueExample, null, "/", 1, true, true, null, -1);
httpCookie.getRFC6265SetCookie();
fail();
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad value [" + badValueExample + "]", ex.getMessage(), Matchers.containsString("RFC6265"));
}
}
});
assertThat("message", ex.getMessage(), containsString("RFC6265"));
}
String[] goodNameExamples = {
public static Stream<String> rfc6265GoodNameSource()
{
return Stream.of(
"name",
"n.a.m.e",
"na-me",
"+name",
"na*me",
"na$me",
"#name"
};
"#name");
}
for (String goodNameExample : goodNameExamples)
{
httpCookie = new HttpCookie(goodNameExample, "value", null, "/", 1, true, true, null, -1);
// should not throw an exception
}
@ParameterizedTest
@MethodSource("rfc6265GoodNameSource")
public void testSetRFC6265CookieGoodName(String goodNameExample)
{
new HttpCookie(goodNameExample, "value", null, "/", 1, true, true, null, -1);
// should not throw an exception
}
public static Stream<String> rfc6265GoodValueSource()
{
String[] goodValueExamples = {
"value",
"",
@ -185,37 +207,150 @@ public class HttpCookieTest
"val/ue",
"v.a.l.u.e"
};
return Stream.of(goodValueExamples);
}
for (String goodValueExample : goodValueExamples)
@ParameterizedTest
@MethodSource("rfc6265GoodValueSource")
public void testSetRFC6265CookieGoodValue(String goodValueExample)
{
new HttpCookie("name", goodValueExample, null, "/", 1, true, true, null, -1);
// should not throw an exception
}
@ParameterizedTest
@ValueSource(strings = {
"__HTTP_ONLY__",
"__HTTP_ONLY__comment",
"comment__HTTP_ONLY__"
})
public void testIsHttpOnlyInCommentTrue(String comment)
{
assertTrue(HttpCookie.isHttpOnlyInComment(comment), "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"comment",
"",
"__",
"__HTTP__ONLY__",
"__http_only__",
"HTTP_ONLY",
"__HTTP__comment__ONLY__"
})
public void testIsHttpOnlyInCommentFalse(String comment)
{
assertFalse(HttpCookie.isHttpOnlyInComment(comment), "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"__SAME_SITE_NONE__",
"__SAME_SITE_NONE____SAME_SITE_NONE__"
})
public void testGetSameSiteFromCommentNONE(String comment)
{
assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.NONE, "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"__SAME_SITE_LAX__",
"__SAME_SITE_LAX____SAME_SITE_NONE__",
"__SAME_SITE_NONE____SAME_SITE_LAX__",
"__SAME_SITE_LAX____SAME_SITE_NONE__"
})
public void testGetSameSiteFromCommentLAX(String comment)
{
assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.LAX, "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"__SAME_SITE_STRICT__",
"__SAME_SITE_NONE____SAME_SITE_STRICT____SAME_SITE_LAX__",
"__SAME_SITE_STRICT____SAME_SITE_LAX____SAME_SITE_NONE__",
"__SAME_SITE_STRICT____SAME_SITE_STRICT__"
})
public void testGetSameSiteFromCommentSTRICT(String comment)
{
assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.STRICT, "Comment \"" + comment + "\"");
}
/**
* A comment that does not have a declared SamesSite attribute defined
*/
@ParameterizedTest
@ValueSource(strings = {
"__HTTP_ONLY__",
"comment",
// not jetty attributes
"SameSite=None",
"SameSite=Lax",
"SameSite=Strict",
// incomplete jetty attributes
"SAME_SITE_NONE",
"SAME_SITE_LAX",
"SAME_SITE_STRICT",
})
public void testGetSameSiteFromCommentUndefined(String comment)
{
assertNull(HttpCookie.getSameSiteFromComment(comment), "Comment \"" + comment + "\"");
}
public static Stream<Arguments> getCommentWithoutAttributesSource()
{
return Stream.of(
// normal - only attribute comment
Arguments.of("__SAME_SITE_LAX__", null),
// normal - no attribute comment
Arguments.of("comment", "comment"),
// mixed - attributes at end
Arguments.of("comment__SAME_SITE_NONE__", "comment"),
Arguments.of("comment__HTTP_ONLY____SAME_SITE_NONE__", "comment"),
// mixed - attributes at start
Arguments.of("__SAME_SITE_NONE__comment", "comment"),
Arguments.of("__HTTP_ONLY____SAME_SITE_NONE__comment", "comment"),
// mixed - attributes at start and end
Arguments.of("__SAME_SITE_NONE__comment__HTTP_ONLY__", "comment"),
Arguments.of("__HTTP_ONLY__comment__SAME_SITE_NONE__", "comment")
);
}
@ParameterizedTest
@MethodSource("getCommentWithoutAttributesSource")
public void testGetCommentWithoutAttributes(String rawComment, String expectedComment)
{
String actualComment = HttpCookie.getCommentWithoutAttributes(rawComment);
if (expectedComment == null)
{
httpCookie = new HttpCookie("name", goodValueExample, null, "/", 1, true, true, null, -1);
// should not throw an exception
assertNull(actualComment);
}
else
{
assertEquals(actualComment, expectedComment);
}
}
@Test
public void testGetHttpOnlyFromComment()
public void testGetCommentWithAttributes()
{
assertTrue(HttpCookie.isHttpOnlyInComment("__HTTP_ONLY__"));
assertTrue(HttpCookie.isHttpOnlyInComment("__HTTP_ONLY__comment"));
assertFalse(HttpCookie.isHttpOnlyInComment("comment"));
}
assertThat(HttpCookie.getCommentWithAttributes(null, false, null), nullValue());
assertThat(HttpCookie.getCommentWithAttributes("", false, null), nullValue());
assertThat(HttpCookie.getCommentWithAttributes("hello", false, null), is("hello"));
@Test
public void testGetSameSiteFromComment()
{
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_NONE__"), HttpCookie.SameSite.NONE);
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_LAX__"), HttpCookie.SameSite.LAX);
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_STRICT__"), HttpCookie.SameSite.STRICT);
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_NONE____SAME_SITE_STRICT__"), HttpCookie.SameSite.NONE);
assertNull(HttpCookie.getSameSiteFromComment("comment"));
}
assertThat(HttpCookie.getCommentWithAttributes(null, true, HttpCookie.SameSite.STRICT),
is("__HTTP_ONLY____SAME_SITE_STRICT__"));
assertThat(HttpCookie.getCommentWithAttributes("", true, HttpCookie.SameSite.NONE),
is("__HTTP_ONLY____SAME_SITE_NONE__"));
assertThat(HttpCookie.getCommentWithAttributes("hello", true, HttpCookie.SameSite.LAX),
is("hello__HTTP_ONLY____SAME_SITE_LAX__"));
@Test
public void getCommentWithoutAttributes()
{
assertEquals(HttpCookie.getCommentWithoutAttributes("comment__SAME_SITE_NONE__"), "comment");
assertEquals(HttpCookie.getCommentWithoutAttributes("comment__HTTP_ONLY____SAME_SITE_NONE__"), "comment");
assertNull(HttpCookie.getCommentWithoutAttributes("__SAME_SITE_LAX__"));
assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", false, null), nullValue());
assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", true, HttpCookie.SameSite.NONE),
is("__HTTP_ONLY____SAME_SITE_NONE__"));
assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__hello", true, HttpCookie.SameSite.LAX),
is("hello__HTTP_ONLY____SAME_SITE_LAX__"));
}
}

View File

@ -32,6 +32,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpGeneratorServerHTTPTest
@ -70,7 +71,7 @@ public class HttpGeneratorServerHTTPTest
assertEquals("OK??Test", _reason);
if (_content == null)
assertTrue(run.result._body == null, msg);
assertNull(run.result._body, msg);
else
assertThat(msg, run.result._contentLength, either(equalTo(_content.length())).or(equalTo(-1)));
}
@ -248,10 +249,9 @@ public class HttpGeneratorServerHTTPTest
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
_reason = reason;
return false;
}
@Override
@ -259,18 +259,6 @@ public class HttpGeneratorServerHTTPTest
{
throw failure;
}
@Override
public int getHeaderCacheSize()
{
return 4096;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
return false;
}
}
public static final String CONTENT = "The quick brown fox jumped over the lazy dog.\nNow is the time for all good men to come to the aid of the party\nThe moon is blue to a fish in love.\n";

View File

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

View File

@ -73,7 +73,7 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
final HTTP2ClientConnection connection = new HTTP2ClientConnection(client, byteBufferPool, executor, endPoint,
parser, session, client.getInputBufferSize(), promise, listener);
connection.addListener(connectionListener);
connection.addEventListener(connectionListener);
return customize(connection, context);
}

View File

@ -0,0 +1,386 @@
//
// ========================================================================
// 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.http2.client;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.ISession;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Promise;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class DataDemandTest extends AbstractTest
{
@Test
public void testExplicitDemand() throws Exception
{
int length = FlowControlStrategy.DEFAULT_WINDOW_SIZE - 1;
AtomicReference<Stream> serverStreamRef = new AtomicReference<>();
Queue<DataFrame> serverQueue = new ConcurrentLinkedQueue<>();
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
serverStreamRef.set(stream);
return new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
// Don't demand and don't complete callbacks.
serverQueue.offer(frame);
}
};
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("POST", new HttpFields());
FuturePromise<Stream> promise = new FuturePromise<>();
Queue<DataFrame> clientQueue = new ConcurrentLinkedQueue<>();
client.newStream(new HeadersFrame(post, null, false), promise, new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
clientQueue.offer(frame);
}
});
Stream clientStream = promise.get(5, TimeUnit.SECONDS);
// Send a single frame larger than the default frame size,
// so that it will be split on the server in multiple frames.
clientStream.data(new DataFrame(clientStream.getId(), ByteBuffer.allocate(length), true), Callback.NOOP);
// The server should receive only 1 DATA frame because it does explicit demand.
// Wait a bit more to be sure it only receives 1 DATA frame.
Thread.sleep(1000);
assertEquals(1, serverQueue.size());
Stream serverStream = serverStreamRef.get();
assertNotNull(serverStream);
// Demand more DATA frames.
int count = 2;
serverStream.demand(count);
Thread.sleep(1000);
// The server should have received `count` more DATA frames.
assertEquals(1 + count, serverQueue.size());
// Demand all the rest.
serverStream.demand(Long.MAX_VALUE);
int loops = 0;
while (true)
{
if (++loops > 100)
fail();
Thread.sleep(100);
long sum = serverQueue.stream()
.mapToLong(frame -> frame.getData().remaining())
.sum();
if (sum == length)
break;
}
// Even if demanded, the flow control window should not have
// decreased because the callbacks have not been completed.
int recvWindow = ((ISession)serverStream.getSession()).updateRecvWindow(0);
assertEquals(FlowControlStrategy.DEFAULT_WINDOW_SIZE - length, recvWindow);
// Send a large DATA frame to the client.
serverStream.data(new DataFrame(serverStream.getId(), ByteBuffer.allocate(length), true), Callback.NOOP);
// The client should receive only 1 DATA frame because it does explicit demand.
// Wait a bit more to be sure it only receives 1 DATA frame.
Thread.sleep(1000);
assertEquals(1, clientQueue.size());
// Demand more DATA frames.
clientStream.demand(count);
Thread.sleep(1000);
// The client should have received `count` more DATA frames.
assertEquals(1 + count, clientQueue.size());
// Demand all the rest.
clientStream.demand(Long.MAX_VALUE);
loops = 0;
while (true)
{
if (++loops > 100)
fail();
Thread.sleep(100);
long sum = clientQueue.stream()
.mapToLong(frame -> frame.getData().remaining())
.sum();
if (sum == length)
break;
}
// Both the client and server streams should be gone now.
assertNull(clientStream.getSession().getStream(clientStream.getId()));
assertNull(serverStream.getSession().getStream(serverStream.getId()));
}
@Test
public void testOnBeforeData() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, false), Callback.from(() -> sendData(stream), x -> {}));
return null;
}
private void sendData(Stream stream)
{
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1024 * 1024), true), Callback.NOOP);
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("GET", new HttpFields());
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch responseLatch = new CountDownLatch(1);
CountDownLatch beforeDataLatch = new CountDownLatch(1);
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, true), promise, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
assertEquals(HttpStatus.OK_200, response.getStatus());
responseLatch.countDown();
}
@Override
public void onBeforeData(Stream stream)
{
beforeDataLatch.countDown();
// Don't demand.
}
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
callback.succeeded();
if (frame.isEndStream())
latch.countDown();
}
});
Stream clientStream = promise.get(5, TimeUnit.SECONDS);
assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
assertTrue(beforeDataLatch.await(5, TimeUnit.SECONDS));
// Should not receive DATA frames until demanded.
assertFalse(latch.await(1, TimeUnit.SECONDS));
// Now demand the first DATA frame.
clientStream.demand(1);
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testDemandFromOnHeaders() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, false), Callback.from(() -> sendData(stream), x -> {}));
return null;
}
private void sendData(Stream stream)
{
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1024 * 1024), true), Callback.NOOP);
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("GET", new HttpFields());
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, true), new Promise.Adapter<>(), new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
stream.demand(1);
}
@Override
public void onBeforeData(Stream stream)
{
// Do not demand from here, we have already demanded in onHeaders().
}
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
callback.succeeded();
if (frame.isEndStream())
latch.countDown();
}
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testOnBeforeDataDoesNotReenter() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, false), Callback.from(() -> sendData(stream), x -> {}));
return null;
}
private void sendData(Stream stream)
{
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1024 * 1024), true), Callback.NOOP);
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("GET", new HttpFields());
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, true), new Promise.Adapter<>(), new Stream.Listener.Adapter()
{
private boolean inBeforeData;
@Override
public void onBeforeData(Stream stream)
{
inBeforeData = true;
stream.demand(1);
inBeforeData = false;
}
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
assertFalse(inBeforeData);
callback.succeeded();
if (frame.isEndStream())
latch.countDown();
}
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testSynchronousDemandDoesNotStackOverflow() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
return new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
callback.succeeded();
stream.demand(1);
if (frame.isEndStream())
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP);
}
}
};
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("POST", new HttpFields());
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, false), promise, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
if (frame.isEndStream())
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
assertEquals(HttpStatus.OK_200, response.getStatus());
latch.countDown();
}
}
});
Stream clientStream = promise.get(5, TimeUnit.SECONDS);
// Generate a lot of small DATA frames and write them in a single
// write so that the server will continuously be notified and demand,
// which will test that it won't throw StackOverflowError.
MappedByteBufferPool byteBufferPool = new MappedByteBufferPool();
Generator generator = new Generator(byteBufferPool);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
for (int i = 512; i >= 0; --i)
generator.data(lease, new DataFrame(clientStream.getId(), ByteBuffer.allocate(1), i == 0), 1);
// Since this is a naked write, we need to wait that the
// client finishes writing the SETTINGS reply to the server
// during connection initialization, or we risk a WritePendingException.
Thread.sleep(1000);
((HTTP2Session)clientStream.getSession()).getEndPoint().write(Callback.NOOP, lease.getByteBuffers().toArray(new ByteBuffer[0]));
assertTrue(latch.await(15, TimeUnit.SECONDS));
}
}

View File

@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@ -44,7 +45,9 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.parser.RateControl;
import org.eclipse.jetty.http2.parser.ServerParser;
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
@ -58,8 +61,12 @@ import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.Promise;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HTTP2Test extends AbstractTest
@ -792,6 +799,100 @@ public class HTTP2Test extends AbstractTest
assertTrue(goAwayLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testClientInvalidHeader() throws Exception
{
start(new EmptyHttpServlet());
// A bad header in the request should fail on the client.
Session session = newClient(new Session.Listener.Adapter());
HttpFields requestFields = new HttpFields();
requestFields.put(":custom", "special");
MetaData.Request metaData = newRequest("GET", requestFields);
HeadersFrame request = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
session.newStream(request, promise, new Stream.Listener.Adapter());
ExecutionException x = assertThrows(ExecutionException.class, () -> promise.get(5, TimeUnit.SECONDS));
assertThat(x.getCause(), instanceOf(HpackException.StreamException.class));
}
@Test
public void testServerInvalidHeader() throws Exception
{
start(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
{
response.setHeader(":custom", "special");
}
});
// Good request with bad header in the response.
Session session = newClient(new Session.Listener.Adapter());
MetaData.Request metaData = newRequest("GET", new HttpFields());
HeadersFrame request = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch resetLatch = new CountDownLatch(1);
session.newStream(request, promise, new Stream.Listener.Adapter()
{
@Override
public void onReset(Stream stream, ResetFrame frame)
{
resetLatch.countDown();
}
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
assertNotNull(stream);
assertTrue(resetLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testServerInvalidHeaderFlushed() throws Exception
{
CountDownLatch serverFailure = new CountDownLatch(1);
start(new EmptyHttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setHeader(":custom", "special");
try
{
response.flushBuffer();
}
catch (IOException x)
{
assertThat(x.getCause(), instanceOf(HpackException.StreamException.class));
serverFailure.countDown();
throw x;
}
}
});
// Good request with bad header in the response.
Session session = newClient(new Session.Listener.Adapter());
MetaData.Request metaData = newRequest("GET", "/flush", new HttpFields());
HeadersFrame request = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch resetLatch = new CountDownLatch(1);
session.newStream(request, promise, new Stream.Listener.Adapter()
{
@Override
public void onReset(Stream stream, ResetFrame frame)
{
// Cannot receive a 500 because we force the flush on the server, so
// the response is committed even if the server was not able to write it.
resetLatch.countDown();
}
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
assertNotNull(stream);
assertTrue(serverFailure.await(5, TimeUnit.SECONDS));
assertTrue(resetLatch.await(5, TimeUnit.SECONDS));
}
private static void sleep(long time)
{
try

View File

@ -37,6 +37,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
@ -298,7 +299,34 @@ public class TrailersTest extends AbstractTest
}
@Test
public void testRequestTrailerInvalidHpack() throws Exception
public void testRequestTrailerInvalidHpackSent() throws Exception
{
start(new EmptyHttpServlet());
Session session = newClient(new Session.Listener.Adapter());
MetaData.Request request = newRequest("POST", new HttpFields());
HeadersFrame requestFrame = new HeadersFrame(request, null, false);
FuturePromise<Stream> promise = new FuturePromise<>();
session.newStream(requestFrame, promise, new Stream.Listener.Adapter());
Stream stream = promise.get(5, TimeUnit.SECONDS);
ByteBuffer data = ByteBuffer.wrap(StringUtil.getUtf8Bytes("hello"));
Callback.Completable completable = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), data, false), completable);
CountDownLatch failureLatch = new CountDownLatch(1);
completable.thenRun(() ->
{
// Invalid trailer: cannot contain pseudo headers.
HttpFields trailerFields = new HttpFields();
trailerFields.put(HttpHeader.C_METHOD, "GET");
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailer, null, true);
stream.headers(trailerFrame, Callback.from(Callback.NOOP::succeeded, x -> failureLatch.countDown()));
});
assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testRequestTrailerInvalidHpackReceived() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
start(new HttpServlet()
@ -344,6 +372,8 @@ public class TrailersTest extends AbstractTest
stream.data(new DataFrame(stream.getId(), data, false), completable);
completable.thenRun(() ->
{
// Disable checks for invalid headers.
((HTTP2Session)session).getGenerator().setValidateHpackEncoding(false);
// Invalid trailer: cannot contain pseudo headers.
HttpFields trailerFields = new HttpFields();
trailerFields.put(HttpHeader.C_METHOD, "GET");

View File

@ -56,6 +56,8 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
private final ISession session;
private final int bufferSize;
private final ExecutionStrategy strategy;
private boolean useInputDirectByteBuffers;
private boolean useOutputDirectByteBuffers;
public HTTP2Connection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, Parser parser, ISession session, int bufferSize)
{
@ -99,6 +101,26 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
producer.setInputBuffer(buffer);
}
public boolean isUseInputDirectByteBuffers()
{
return useInputDirectByteBuffers;
}
public void setUseInputDirectByteBuffers(boolean useInputDirectByteBuffers)
{
this.useInputDirectByteBuffers = useInputDirectByteBuffers;
}
public boolean isUseOutputDirectByteBuffers()
{
return useOutputDirectByteBuffers;
}
public void setUseOutputDirectByteBuffers(boolean useOutputDirectByteBuffers)
{
this.useOutputDirectByteBuffers = useOutputDirectByteBuffers;
}
@Override
public void onOpen()
{
@ -309,7 +331,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
if (currentBuffer == null)
throw new IllegalStateException();
if (currentBuffer.getBuffer().hasRemaining())
if (currentBuffer.hasRemaining())
throw new IllegalStateException();
currentBuffer.release();
@ -389,7 +411,7 @@ public class HTTP2Connection extends AbstractConnection implements WriteFlusher.
{
private NetworkBuffer()
{
super(byteBufferPool, bufferSize, false);
super(byteBufferPool, bufferSize, isUseInputDirectByteBuffers());
}
private void put(ByteBuffer source)

View File

@ -30,6 +30,7 @@ import java.util.Set;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.util.Callback;
@ -207,6 +208,13 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
}
}
}
catch (HpackException.StreamException failure)
{
if (LOG.isDebugEnabled())
LOG.debug("Failure generating " + entry, failure);
entry.failed(failure);
pending.remove();
}
catch (Throwable failure)
{
// Failure to generate the entry is catastrophic.
@ -397,7 +405,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
return 0;
}
protected abstract boolean generate(ByteBufferPool.Lease lease);
protected abstract boolean generate(ByteBufferPool.Lease lease) throws HpackException;
public abstract long onFlushed(long bytes) throws IOException;

View File

@ -52,6 +52,7 @@ import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EndPoint;
@ -62,7 +63,6 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CountingCallback;
import org.eclipse.jetty.util.MathUtils;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.Retainable;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
@ -1236,7 +1236,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
}
@Override
protected boolean generate(ByteBufferPool.Lease lease)
protected boolean generate(ByteBufferPool.Lease lease) throws HpackException
{
frameBytes = generator.control(lease, frame);
beforeSend();
@ -1482,7 +1482,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
}
}
private class DataCallback extends Callback.Nested implements Retainable
private class DataCallback extends Callback.Nested
{
private final IStream stream;
private final int flowControlLength;
@ -1494,14 +1494,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
this.flowControlLength = flowControlLength;
}
@Override
public void retain()
{
Callback callback = getCallback();
if (callback instanceof Retainable)
((Retainable)callback).retain();
}
@Override
public void succeeded()
{

View File

@ -21,6 +21,8 @@ package org.eclipse.jetty.http2;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.WritePendingException;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@ -42,16 +44,20 @@ import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.io.IdleTimeout;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.MathUtils;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.Scheduler;
public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpable
{
private static final Logger LOG = Log.getLogger(HTTP2Stream.class);
private final AutoLock lock = new AutoLock();
private final Queue<DataEntry> dataQueue = new ArrayDeque<>();
private final AtomicReference<Object> attachment = new AtomicReference<>();
private final AtomicReference<ConcurrentMap<String, Object>> attributes = new AtomicReference<>();
private final AtomicReference<CloseState> closeState = new AtomicReference<>(CloseState.NOT_CLOSED);
@ -67,6 +73,9 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
private Listener listener;
private boolean remoteReset;
private long dataLength;
private long dataDemand;
private boolean dataInitial;
private boolean dataProcess;
public HTTP2Stream(Scheduler scheduler, ISession session, int streamId, MetaData.Request request, boolean local)
{
@ -76,6 +85,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
this.request = request;
this.local = local;
this.dataLength = Long.MIN_VALUE;
this.dataInitial = true;
}
@Override
@ -343,10 +353,88 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
}
}
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
session.removeStream(this);
boolean initial;
boolean proceed = false;
DataEntry entry = new DataEntry(frame, callback);
try (AutoLock l = lock.lock())
{
dataQueue.offer(entry);
initial = dataInitial;
if (initial)
{
dataInitial = false;
// Fake that we are processing data so we return
// from onBeforeData() before calling onData().
dataProcess = true;
}
else if (!dataProcess)
{
dataProcess = proceed = dataDemand > 0;
}
}
if (LOG.isDebugEnabled())
LOG.debug("{} data processing of {} for {}", initial ? "Starting" : proceed ? "Proceeding" : "Stalling", frame, this);
if (initial)
{
notifyBeforeData(this);
try (AutoLock l = lock.lock())
{
dataProcess = proceed = dataDemand > 0;
}
}
if (proceed)
processData();
}
notifyData(this, frame, callback);
@Override
public void demand(long n)
{
if (n <= 0)
throw new IllegalArgumentException("Invalid demand " + n);
long demand;
boolean proceed = false;
try (AutoLock l = lock.lock())
{
demand = dataDemand = MathUtils.cappedAdd(dataDemand, n);
if (!dataProcess)
dataProcess = proceed = !dataQueue.isEmpty();
}
if (LOG.isDebugEnabled())
LOG.debug("Demand {}/{}, {} data processing for {}", n, demand, proceed ? "proceeding" : "stalling", this);
if (proceed)
processData();
}
private void processData()
{
while (true)
{
DataEntry dataEntry;
try (AutoLock l = lock.lock())
{
if (dataQueue.isEmpty() || dataDemand == 0)
{
if (LOG.isDebugEnabled())
LOG.debug("Stalling data processing for {}", this);
dataProcess = false;
return;
}
--dataDemand;
dataEntry = dataQueue.poll();
}
DataFrame frame = dataEntry.frame;
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
session.removeStream(this);
notifyDataDemanded(this, frame, dataEntry.callback);
}
}
private long demand()
{
try (AutoLock l = lock.lock())
{
return dataDemand;
}
}
private void onReset(ResetFrame frame, Callback callback)
@ -573,14 +661,34 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
}
}
private void notifyData(Stream stream, DataFrame frame, Callback callback)
private void notifyBeforeData(Stream stream)
{
Listener listener = this.listener;
if (listener != null)
{
try
{
listener.onData(stream, frame, callback);
listener.onBeforeData(stream);
}
catch (Throwable x)
{
LOG.info("Failure while notifying listener " + listener, x);
}
}
else
{
stream.demand(1);
}
}
private void notifyDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
Listener listener = this.listener;
if (listener != null)
{
try
{
listener.onDataDemanded(stream, frame, callback);
}
catch (Throwable x)
{
@ -591,6 +699,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
else
{
callback.succeeded();
stream.demand(1);
}
}
@ -682,16 +791,29 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
@Override
public String toString()
{
return String.format("%s@%x#%d{sendWindow=%s,recvWindow=%s,reset=%b/%b,%s,age=%d,attachment=%s}",
return String.format("%s@%x#%d{sendWindow=%s,recvWindow=%s,demand=%d,reset=%b/%b,%s,age=%d,attachment=%s}",
getClass().getSimpleName(),
hashCode(),
getId(),
sendWindow,
recvWindow,
demand(),
localReset,
remoteReset,
closeState,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - timeStamp),
attachment);
}
private static class DataEntry
{
private final DataFrame frame;
private final Callback callback;
private DataEntry(DataFrame frame, Callback callback)
{
this.frame = frame;
this.callback = callback;
}
}
}

View File

@ -493,12 +493,6 @@ public abstract class HTTP2StreamEndPoint implements EndPoint
LOG.debug("onClose {}", this);
}
@Override
public boolean isOptimizedForDirectBuffers()
{
return true;
}
@Override
public void upgrade(Connection newConnection)
{

View File

@ -29,7 +29,7 @@ import org.eclipse.jetty.util.Promise;
* <p>A {@link Stream} represents a bidirectional exchange of data on top of a {@link Session}.</p>
* <p>Differently from socket streams, where the input and output streams are permanently associated
* with the socket (and hence with the connection that the socket represents), there can be multiple
* HTTP/2 streams present concurrent for an HTTP/2 session.</p>
* HTTP/2 streams present concurrently for an HTTP/2 session.</p>
* <p>A {@link Stream} maps to an HTTP request/response cycle, and after the request/response cycle is
* completed, the stream is closed and removed from the session.</p>
* <p>Like {@link Session}, {@link Stream} is the active part and by calling its API applications
@ -129,9 +129,25 @@ public interface Stream
*/
public void setIdleTimeout(long idleTimeout);
/**
* <p>Demands {@code n} more {@code DATA} frames for this stream.</p>
*
* @param n the increment of the demand, must be greater than zero
* @see Listener#onDataDemanded(Stream, DataFrame, Callback)
*/
public void demand(long n);
/**
* <p>A {@link Stream.Listener} is the passive counterpart of a {@link Stream} and receives
* events happening on an HTTP/2 stream.</p>
* <p>HTTP/2 data is flow controlled - this means that only a finite number of data events
* are delivered, until the flow control window is exhausted.</p>
* <p>Applications control the delivery of data events by requesting them via
* {@link Stream#demand(long)}; the first event is always delivered, while subsequent
* events must be explicitly demanded.</p>
* <p>Applications control the HTTP/2 flow control by completing the callback associated
* with data events - this allows the implementation to recycle the data buffer and
* eventually to enlarge the flow control window so that the sender can send more data.</p>
*
* @see Stream
*/
@ -164,15 +180,42 @@ public interface Stream
*/
public Listener onPush(Stream stream, PushPromiseFrame frame);
/**
* <p>Callback method invoked before notifying the first DATA frame.</p>
* <p>The default implementation initializes the demand for DATA frames.</p>
*
* @param stream the stream
*/
public default void onBeforeData(Stream stream)
{
stream.demand(1);
}
/**
* <p>Callback method invoked when a DATA frame has been received.</p>
*
* @param stream the stream
* @param frame the DATA frame received
* @param callback the callback to complete when the bytes of the DATA frame have been consumed
* @see #onDataDemanded(Stream, DataFrame, Callback)
*/
public void onData(Stream stream, DataFrame frame, Callback callback);
/**
* <p>Callback method invoked when a DATA frame has been demanded.</p>
* <p>Implementations of this method must arrange to call (within the
* method or otherwise asynchronously) {@link #demand(long)}.</p>
*
* @param stream the stream
* @param frame the DATA frame received
* @param callback the callback to complete when the bytes of the DATA frame have been consumed
*/
public default void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
onData(stream, frame, callback);
stream.demand(1);
}
/**
* <p>Callback method invoked when a RST_STREAM frame has been received for this stream.</p>
*

View File

@ -20,8 +20,11 @@ package org.eclipse.jetty.http2.generator;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
public abstract class FrameGenerator
@ -33,7 +36,7 @@ public abstract class FrameGenerator
this.headerGenerator = headerGenerator;
}
public abstract int generate(ByteBufferPool.Lease lease, Frame frame);
public abstract int generate(ByteBufferPool.Lease lease, Frame frame) throws HpackException;
protected ByteBuffer generateHeader(ByteBufferPool.Lease lease, FrameType frameType, int length, int flags, int streamId)
{
@ -44,4 +47,24 @@ public abstract class FrameGenerator
{
return headerGenerator.getMaxFrameSize();
}
public boolean isUseDirectByteBuffers()
{
return headerGenerator.isUseDirectByteBuffers();
}
protected ByteBuffer encode(HpackEncoder encoder, ByteBufferPool.Lease lease, MetaData metaData, int maxFrameSize) throws HpackException
{
ByteBuffer hpacked = lease.acquire(maxFrameSize, isUseDirectByteBuffers());
try
{
encoder.encode(hpacked, metaData);
return hpacked;
}
catch (HpackException x)
{
lease.release(hpacked);
throw x;
}
}
}

View File

@ -22,6 +22,7 @@ import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
public class Generator
@ -38,10 +39,15 @@ public class Generator
}
public Generator(ByteBufferPool byteBufferPool, int maxDynamicTableSize, int maxHeaderBlockFragment)
{
this(byteBufferPool, true, maxDynamicTableSize, maxHeaderBlockFragment);
}
public Generator(ByteBufferPool byteBufferPool, boolean useDirectByteBuffers, int maxDynamicTableSize, int maxHeaderBlockFragment)
{
this.byteBufferPool = byteBufferPool;
headerGenerator = new HeaderGenerator();
headerGenerator = new HeaderGenerator(useDirectByteBuffers);
hpackEncoder = new HpackEncoder(maxDynamicTableSize);
this.generators = new FrameGenerator[FrameType.values().length];
@ -65,6 +71,11 @@ public class Generator
return byteBufferPool;
}
public void setValidateHpackEncoding(boolean validateEncoding)
{
hpackEncoder.setValidateEncoding(validateEncoding);
}
public void setHeaderTableSize(int headerTableSize)
{
hpackEncoder.setRemoteMaxDynamicTableSize(headerTableSize);
@ -75,7 +86,7 @@ public class Generator
headerGenerator.setMaxFrameSize(maxFrameSize);
}
public int control(ByteBufferPool.Lease lease, Frame frame)
public int control(ByteBufferPool.Lease lease, Frame frame) throws HpackException
{
return generators[frame.getType().getType()].generate(lease, frame);
}

View File

@ -27,10 +27,26 @@ import org.eclipse.jetty.io.ByteBufferPool;
public class HeaderGenerator
{
private int maxFrameSize = Frame.DEFAULT_MAX_LENGTH;
private final boolean useDirectByteBuffers;
public HeaderGenerator()
{
this(true);
}
public HeaderGenerator(boolean useDirectByteBuffers)
{
this.useDirectByteBuffers = useDirectByteBuffers;
}
public boolean isUseDirectByteBuffers()
{
return useDirectByteBuffers;
}
public ByteBuffer generate(ByteBufferPool.Lease lease, FrameType frameType, int capacity, int length, int flags, int streamId)
{
ByteBuffer header = lease.acquire(capacity, true);
ByteBuffer header = lease.acquire(capacity, isUseDirectByteBuffers());
header.put((byte)((length & 0x00_FF_00_00) >>> 16));
header.put((byte)((length & 0x00_00_FF_00) >>> 8));
header.put((byte)((length & 0x00_00_00_FF)));

View File

@ -27,6 +27,7 @@ import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
@ -50,13 +51,13 @@ public class HeadersGenerator extends FrameGenerator
}
@Override
public int generate(ByteBufferPool.Lease lease, Frame frame)
public int generate(ByteBufferPool.Lease lease, Frame frame) throws HpackException
{
HeadersFrame headersFrame = (HeadersFrame)frame;
return generateHeaders(lease, headersFrame.getStreamId(), headersFrame.getMetaData(), headersFrame.getPriority(), headersFrame.isEndStream());
}
public int generateHeaders(ByteBufferPool.Lease lease, int streamId, MetaData metaData, PriorityFrame priority, boolean endStream)
public int generateHeaders(ByteBufferPool.Lease lease, int streamId, MetaData metaData, PriorityFrame priority, boolean endStream) throws HpackException
{
if (streamId < 0)
throw new IllegalArgumentException("Invalid stream id: " + streamId);
@ -66,10 +67,7 @@ public class HeadersGenerator extends FrameGenerator
if (priority != null)
flags = Flags.PRIORITY;
int maxFrameSize = getMaxFrameSize();
ByteBuffer hpacked = lease.acquire(maxFrameSize, false);
BufferUtil.clearToFill(hpacked);
encoder.encode(hpacked, metaData);
ByteBuffer hpacked = encode(encoder, lease, metaData, getMaxFrameSize());
int hpackedLength = hpacked.position();
BufferUtil.flipToFlush(hpacked, 0);

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
@ -40,13 +41,13 @@ public class PushPromiseGenerator extends FrameGenerator
}
@Override
public int generate(ByteBufferPool.Lease lease, Frame frame)
public int generate(ByteBufferPool.Lease lease, Frame frame) throws HpackException
{
PushPromiseFrame pushPromiseFrame = (PushPromiseFrame)frame;
return generatePushPromise(lease, pushPromiseFrame.getStreamId(), pushPromiseFrame.getPromisedStreamId(), pushPromiseFrame.getMetaData());
}
public int generatePushPromise(ByteBufferPool.Lease lease, int streamId, int promisedStreamId, MetaData metaData)
public int generatePushPromise(ByteBufferPool.Lease lease, int streamId, int promisedStreamId, MetaData metaData) throws HpackException
{
if (streamId < 0)
throw new IllegalArgumentException("Invalid stream id: " + streamId);
@ -58,9 +59,7 @@ public class PushPromiseGenerator extends FrameGenerator
int extraSpace = 4;
maxFrameSize -= extraSpace;
ByteBuffer hpacked = lease.acquire(maxFrameSize, false);
BufferUtil.clearToFill(hpacked);
encoder.encode(hpacked, metaData);
ByteBuffer hpacked = encode(encoder, lease, metaData, maxFrameSize);
int hpackedLength = hpacked.position();
BufferUtil.flipToFlush(hpacked, 0);

Some files were not shown because too many files have changed in this diff Show More