Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-6106-WebSocketCDI

This commit is contained in:
Lachlan Roberts 2021-04-27 16:15:52 +10:00
commit a0cca858a7
61 changed files with 2608 additions and 472 deletions

View File

@ -27,13 +27,13 @@ jetty-10.0.2 - 26 March 2021
+ 6037 Review logging modules for j.u.l.
+ 6050 Websocket: NotUtf8Exception after upgrade 9.4.35 -> 9.4.36 or newer
+ 6063 Allow override of hazelcast version when using module
+ 6072 jetty server high CPU when client send data length > 17408
+ 6072 jetty server high CPU when client send data length > 17408 - Resolves CVE-2021-28165
+ 6076 Embedded Jetty throws null pointer exception
+ 6082 SslConnection compacting
+ 6085 Jetty keeps Sessions in use after "Duplicate valid session cookies"
Message
+ 6101 Normalise ambiguous URIs
+ 6102 Exclude webapps directory from deployment scan
+ 6101 Normalize ambiguous URIs - Resolves CVE-2021-28164
+ 6102 Exclude webapps directory from deployment scan - Resolves CVE-2021-28163
jetty-10.0.1 - 19 February 2021
+ 1673 jetty-demo/etc/keystore should not be distributed
@ -133,8 +133,22 @@ jetty-10.0.0.beta3 - 21 October 2020
+ 5475 Update to spifly 1.3.2 and asm 9
+ 5480 NPE from WebInfConfiguration.deconfigure during WebAppContext shutdown
jetty-9.4.39.v20210325 - 25 March 2021
+ 6034 SslContextFactory may select a wildcard certificate during SNI
selection when a more specific SSL certificate is present
+ 6050 Websocket: NotUtf8Exception after upgrade to 9.4.36 or newer
+ 6052 Cleanup TypeUtil and ModuleLocation to allow jetty-client/hybrid to
work on Android
+ 6063 Allow override of hazelcast version when using module
+ 6072 jetty server high CPU when client send data length > 17408 - Resolves CVE-2021-28165
+ 6085 Jetty keeps Sessions in use after "Duplicate valid session cookies"
Message
+ 6101 Normalize ambiguous URIs - Resolves CVE-2021-28164
+ 6102 Exclude webapps directory from deployment scan - Resolves CVE-2021-28163
jetty-9.4.38.v20210224 - 24 February 2021
+ 4275 Path Normalization/Traversal - Context Matching
+ 5963 Improve QuotedQualityCSV for CVE-2020-27223
+ 5977 Cache-Control header set by a filter is override by the value from
DefaultServlet configuration
+ 5994 QueuedThreadPool "free" threads
@ -158,7 +172,7 @@ jetty-9.4.37.v20210219 - 19 February 2021
+ 5979 Configurable gzip Etag extension
jetty-9.4.36.v20210114 - 14 January 2021
+ 5310 Jetty Http2 client discards the response fames when there is GOAWAY and
+ 5310 Jetty Http2 client discards the response frames when there is GOAWAY and
sends RST_STREAM
+ 5499 Improve temporary buffer usage for WebSocket PerMessageDeflate
+ 5633 Allow to configure HttpClient request authority
@ -420,7 +434,6 @@ jetty-9.4.31.v20200723 - 23 July 2020
+ 5057 `javax.servlet.include.context_path` attribute on root context. should
be empty string, but is `"/"`
+ 5064 NotSerializableException for OpenIdConfiguration
+ 5069 HttpClientTimeoutTests can occasionally fail due to unreachable network
jetty-9.4.30.v20200611 - 11 June 2020
+ 4776 Incorrect path matching for WebSocket using PathMappings
@ -723,10 +736,8 @@ jetty-9.4.20.v20190813 - 13 August 2019
+ 3648 javax.websocket client container incorrectly creates Server
SslContextFactory
+ 3698 Missing WebSocket ServerContainer after server restart
+ 3700 stackoverflow in WebAppClassLoaderUrlStreamTest
+ 3708 Swap various java.lang.String replace() methods for better performant
ones
+ 3731 Add testing of CDI behaviors
+ 3736 NPE from WebAppClassLoader during CDI
+ 3746 ClassCastException in WriteFlusher.java - IdleState cannot be cast to
FailedState
@ -928,7 +939,6 @@ jetty-9.2.27.v20190403 - 03 April 2019
jetty-9.4.14.v20181114 - 14 November 2018
+ 3097 Duplicated programmatic Servlet Listeners causing duplicate calls
+ 3103 HttpClientLoadTest reports a leak in byte buffer
+ 3104 Align jetty-schemas version within apache-jsp module as well
jetty-9.4.13.v20181111 - 11 November 2018
@ -992,8 +1002,6 @@ jetty-9.4.12.v20180830 - 30 August 2018
Runtimes
+ 2075 Deprecating MultiException
+ 2135 Android 8.1 needs direct buffers for SSL/TLS to work
+ 2233 JDK9 Test failure:
org.eclipse.jetty.server.ThreadStarvationTest.testWriteStarvation[https/ssl/tls]
+ 2342 File Descriptor Leak: Conscrypt: "Too many open files"
+ 2349 HTTP/2 max streams enforcement
+ 2398 MultiPartFormInputStream parsing should default to UTF-8, but allowed
@ -1003,9 +1011,6 @@ jetty-9.4.12.v20180830 - 30 August 2018
+ 2530 Client waits forever for cancelled large uploads
+ 2560 Review PathResource exception handling
+ 2565 HashLoginService silently ignores file:/ config paths from 9.3.x
+ 2592 Failing test on Windows:
ServerTimeoutsTest.testAsyncWriteIdleTimeoutFires[transport: HTTP]
+ 2597 Failing tests on windows UnixSocketTest
+ 2631 IllegalArgumentException: Buffering capacity exceeded, from HttpClient
HEAD Requests to resources referencing large body contents
+ 2648 LdapLoginModule fails with forceBinding=true under Java 9
@ -1067,7 +1072,6 @@ jetty-9.4.12.v20180830 - 30 August 2018
hot redeploy on Windows
+ 2836 Sequential HTTPS requests may not reuse the same connection
+ 2844 Clean up webdefault.xml and DefaultServlet doc
+ 2846 add unit test for ldap module
+ 2847 Wrap Connection.Listener invocations in try/catch
+ 2860 Leakage of HttpDestinations in HttpClient
+ 2871 Server reads -1 after client resets HTTP/2 stream
@ -1426,7 +1430,6 @@ jetty-9.4.7.v20170914 - 14 September 2017
+ 1759 HTTP/2: producer can block in onReset
+ 1766 JettyClientContainerProvider does not actually use common objects
correctly
+ 1789 PropertyUserStoreTest failures in Windows
+ 1790 HTTP/2: 100% CPU usage seen during close/shutdown of endpoint
+ 1792 Accept ISO-8859-1 characters in response reason
+ 1794 Config properties typos in session-store-cache.mod
@ -1439,8 +1442,6 @@ jetty-9.4.7.v20170914 - 14 September 2017
+ 1809 NPE: StandardDescriptorProcessor.visitSecurityConstraint() with null/no
security manager
+ 1814 Move JavaVersion to jetty-util for future Java 9 support requirements
+ 1816 HttpClientTest.testClientCannotValidateServerCertificate() hangs with
JDK 9
+ 475546 ClosedChannelException when connection to HTTPS over HTTP proxy with
CONNECT
@ -1662,11 +1663,8 @@ jetty-9.4.3.v20170317 - 17 March 2017
jetty-9.3.17.v20170317 - 17 March 2017
+ 329 Javadoc for HttpTester and ServletTester needs to reference limited HTTP
version scope
+ 609 websocket ClientCloseTest testServerNoCloseHandshake is failing
+ 1015 Ensure jetty-distribution excludes git / temp files
+ 1047 ReadPendingException and then thread death
+ 1049 test-jetty-osgi test exits/crashes the surefire forked JVM
+ 1282 ByteArrayEndPointTest.testIdle() failure
+ 1296 Introduce HTTP parser "content complete" event
+ 1326 Jetty shutdown command got NullPointerException (http2 module added to
start)
@ -1686,7 +1684,6 @@ jetty-9.3.17.v20170317 - 17 March 2017
+ 1390 HashLoginService and "this.web-inf.url" property are incompatible
+ 1394 Default OS Locale/Encoding/Charset can cause test failures
+ 1396 Set-Cookie produced by Jetty is invalid for RFC6265 and Chrome
+ 1399 SlowClientTest is failing on CI
+ 1401 HttpOutput.recycle() does not clear the write listener
jetty-9.4.2.v20170220 - 20 February 2017
@ -1790,9 +1787,6 @@ jetty-9.3.16.v20170120 - 20 January 2017
+ 1229 ClassLoader constraint issue when using NativeWebSocketConfiguration
with WEB-INF/lib/jetty-http.jar present
+ 1234 onBadMessage called from with handled message
+ 1259 HostnameVerificationTest.simpleGetWithHostnameVerificationEnabledTest
is broken
+ 1261 Intermittent H2C test failure AsyncIOServletTest.testAsyncReadEarlyEOF
+ 1262 BufferUtil.isMappedBuffer() uses reflection on private JDK fields
+ 1265 JAXB not available in JDK 9
+ 1267 Request.getRemoteUser can throw undeclared IllegalStateException via
@ -1806,7 +1800,6 @@ jetty-9.3.16.v20170120 - 20 January 2017
+ 1275 Get rid of Mockito
+ 1276 Remove org.eclipse.jetty.websocket.server.WebSocketServerFactory from
SPI
+ 1277 http2 alpn test error
jetty-9.2.21.v20170120 - 20 January 2017
+ 592 Support no-value Host header in HttpParser
@ -1842,7 +1835,6 @@ jetty-9.3.15.v20161220 - 20 December 2016
+ 1099 PushCacheFilter pushes POST requests
+ 1108 Please improve logging in SslContextFactory when there are no approved
cipher suites
+ 1114 Add testcase for WSUF for stop/start of the Server
+ 1118 Filter.destroy() conflicts with ContainerLifeCycle.destroy() in
WebSocketUpgradeFilter
+ 1123 Broken lifecycle for WebSocket's mappings

View File

@ -47,8 +47,7 @@
<project.basedir>${project.basedir}</project.basedir>
<maven.local.repo>${settings.localRepository}</maven.local.repo>
<prog_guide>../programming-guide/index.html</prog_guide>
<JDURL>http://www.eclipse.org/jetty/javadoc/jetty-10</JDURL>
<JXURL>http://download.eclipse.org/jetty/stable-9/xref</JXURL>
<JDURL>https://www.eclipse.org/jetty/javadoc/jetty-10</JDURL>
<SRCDIR>${basedir}/..</SRCDIR>
<GITBROWSEURL>https://github.com/eclipse/jetty.project/tree/master</GITBROWSEURL>
<GITDOCURL>https://github.com/eclipse/jetty.project/tree/jetty-10.0.x-doc-refactor/jetty-documentation/src/main/asciidoc</GITDOCURL>

View File

@ -11,9 +11,8 @@
// ========================================================================
//
:doctitle: Eclipse Jetty: Operations Guide
:doctitle: link:https://eclipse.org/jetty[Eclipse Jetty]: Operations Guide
:toc-title: Operations Guide
:breadcrumb: Home:../index.html | Operations Guide:./index.html
:idprefix: og-
:docinfo: private-head

View File

@ -22,24 +22,26 @@ The following command creates a KeyStore file containing a private key and a sel
----
keytool
-genkeypair <1>
-validity 90 <2>
-keyalg RSA <3>
-keysize 2048 <4>
-keystore /path/to/keystore.p12 <5>
-storetype pkcs12 <6>
-dname "CN=domain.com, OU=Unit, O=Company, L=City, S=State, C=Country" <7>
-ext san=dns:www.domain.com,dns:domain.org <8>
-v <9>
-alias mykey <2>
-validity 90 <3>
-keyalg RSA <4>
-keysize 2048 <5>
-keystore /path/to/keystore.p12 <6>
-storetype pkcs12 <7>
-dname "CN=domain.com, OU=Unit, O=Company, L=City, S=State, C=Country" <8>
-ext san=dns:www.domain.com,dns:domain.org <9>
-v <10>
----
<1> the command to generate a key and certificate pair
<2> specifies the number of days after which the certificate expires
<3> the algorithm _must_ be RSA (the DSA algorithm does not work for web sites)
<4> indicates the strength of the key
<5> the keyStore file
<6> the keyStore type, stick with the standard PKCS12
<7> the distinguished name (more below) -- customize it with your values for CN, OU, O, L, S and C
<8> the extension with the subject alternative names (more below)
<9> verbose output
<2> the alias name of the key and certificate pair
<3> specifies the number of days after which the certificate expires
<4> the algorithm _must_ be RSA (the DSA algorithm does not work for web sites)
<5> indicates the strength of the key
<6> the KeyStore file
<7> the KeyStore type, stick with the standard PKCS12
<8> the distinguished name (more below) -- customize it with your values for CN, OU, O, L, S and C
<9> the extension with the subject alternative names (more below)
<10> verbose output
The command prompts for the KeyStore password that you must choose to protect the access to the KeyStore.
@ -56,3 +58,13 @@ In the example above, `san=dns:www.domain.com,dns:domain.org` specifies `www.dom
In rare cases, you may want to specify IP addresses, rather than domains, in the SAN extension.
The syntax in such case is `san=ip:127.0.0.1,ip:[::1]`, which specifies as subject alternative names IPv4 `127.0.0.1` and IPv6 `[::1]`.
====
[[og-keystore-create-many]]
===== KeyStores with Multiple Entries
A single KeyStore may contain multiple key/certificate pairs.
This is useful when you need to support multiple domains on the same Jetty server (typically accomplished using xref:og-deploy-virtual-hosts[virtual hosts]).
You can create multiple key/certificate pairs as detailed in the xref:og-keystore-create[previous section], provided that you assign each one to a different alias.
Compliant TLS clients will send the xref:og-protocols-ssl-sni[TLS SNI extension] when creating new connections, and Jetty will automatically choose the right certificate by matching the SNI name sent by the client with the CN or SAN of certificates present in the KeyStore.

View File

@ -34,8 +34,11 @@ $ java -jar $JETTY_HOME/start.jar --list-modules=connector
include::protocols-http.adoc[]
include::protocols-https.adoc[]
include::protocols-http2.adoc[]
include::protocols-http2s.adoc[]
include::protocols-http2c.adoc[]
include::protocols-ssl.adoc[]
include::protocols-proxy.adoc[]
include::protocols-websocket.adoc[]
// TODO: old_docs/connectors/*.adoc

View File

@ -143,6 +143,3 @@ microservice3 <--> jetty : HTTP/2
microservice2 <--> microservice3 : HTTP/2
microservice1 <--> microservice3 : HTTP/2
----
include::protocols-http2s.adoc[]
include::protocols-http2c.adoc[]

View File

@ -34,7 +34,7 @@ Allows a single stream of an HTTP/2 connection to be upgraded to WebSocket.
This allows one TCP connection to be shared by both protocols and extends HTTP/2's more efficient use of the network to WebSockets.
[[og-protocols-websocket-configure]]
==== Configuring WebSocket
===== Configuring WebSocket
Jetty provides two WebSocket implementations: one based on the Java WebSocket APIs defined by JSR 356, provided by module `websocket-javax`, and one based on Jetty specific WebSocket APIs, provided by module `websocket-jetty`.
The Jetty `websocket` module enables both implementations, but each implementation can be enabled independently.
@ -69,7 +69,7 @@ $ java -jar $JETTY_HOME/start.jar --add-modules=http,https,http2c,http2,websocke
----
[[og-protocols-websocket-disable]]
==== Selectively Disabling WebSocket
===== Selectively Disabling WebSocket
Enabling the WebSocket Jetty modules comes with a startup cost because Jetty must perform two steps:
@ -104,7 +104,7 @@ For a specific web application, you can disable step 2 for Java WebSocket suppor
Furthermore, for a specific web application, you can disable step 1 (and therefore also step 2) as described in the xref:og-annotations[annotations processing section].
[[og-protocols-websocket-webapp-client]]
==== Using WebSocket Client in WebApps
===== Using WebSocket Client in WebApps
Web applications may need to use a WebSocket client to communicate with third party WebSocket services.

View File

@ -84,6 +84,17 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPCli
Stopping `HttpClient` makes sure that the memory it holds (for example, authentication credentials, cookies, etc.) is released, and that the thread pool and scheduler are properly stopped allowing all threads used by `HttpClient` to exit.
[NOTE]
====
You cannot call `HttpClient.stop()` from one of its own threads, as it would cause a deadlock.
It is recommended that you stop `HttpClient` from an unrelated thread, or from a newly allocated thread, for example:
[source,java,indent=0]
----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=stopFromOtherThread]
----
====
[[pg-client-http-arch]]
==== HttpClient Architecture

View File

@ -11,9 +11,8 @@
// ========================================================================
//
:doctitle: Eclipse Jetty: Programming Guide
:doctitle: link:https://eclipse.org/jetty[Eclipse Jetty]: Programming Guide
:toc-title: Jetty Programming Guide
:breadcrumb: Home:../index.html | Programming Guide:./index.html
:idprefix: pg-
:docinfo: private-head
@ -25,3 +24,4 @@ include::server/server.adoc[]
include::maven/maven.adoc[]
include::arch.adoc[]
include::troubleshooting.adoc[]
include::migration/migration.adoc[]

View File

@ -90,6 +90,16 @@ keepSources::
Default value: false
+
If true, the generated .java files are not deleted at the end of processing.
scanAllDirectories::
Default value: true
+
Determines if dirs on the classpath should be scanned as well as jars.
If true, this allows scanning for tlds of dependent projects that
are in the reactor as unassembled jars.
scanManifest::
Default value: true
+
Determines if the manifest of JAR files found on the classpath should be scanned.
sourceVersion::
Introduced in Jetty 9.3.6.
Java version of jsp source files.

View File

@ -0,0 +1,135 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
[appendix]
[[pg-migration]]
== Migration Guides
[[pg-migration-94-to-10]]
=== Migrating from Jetty 9.4.x to Jetty 10.0.x
[[pg-migration-94-to-10-java-version]]
==== Required Java Version Changes
[cols="1,1", options="header"]
|===
| Jetty 9.4.x | Jetty 10.0.x
| Java 8 | Java 11
|===
[[pg-migration-94-to-10-websocket]]
==== WebSocket Migration Guide
Migrating from Jetty 9.4.x to Jetty 10.0.x requires changes in the coordinates of the Maven artifact dependencies for WebSocket. Some of these classes have also changed name and package. This is not a comprehensive list of changes but should cover the most common changes encountered during migration.
[[pg-migration-94-to-10-websocket-maven-artifact-changes]]
===== Maven Artifacts Changes
[cols="1a,1a", options="header"]
|===
| Jetty 9.4.x | Jetty 10.0.x
| `org.eclipse.jetty.websocket:**websocket-api**`
| `org.eclipse.jetty.websocket:**websocket-jetty-api**`
| `org.eclipse.jetty.websocket:**websocket-server**`
| `org.eclipse.jetty.websocket:**websocket-jetty-server**`
| `org.eclipse.jetty.websocket:**websocket-client**`
| `org.eclipse.jetty.websocket:**websocket-jetty-client**`
| `org.eclipse.jetty.websocket:**javax-websocket-server-impl**`
| `org.eclipse.jetty.websocket:**websocket-javax-server**`
| `org.eclipse.jetty.websocket:**javax-websocket-client-impl**`
| `org.eclipse.jetty.websocket:**websocket-javax-client**`
|===
[[pg-migration-94-to-10-websocket-class-name-changes]]
===== Class Names Changes
[cols="1a,1a", options="header"]
|===
| Jetty 9.4.x | Jetty 10.0.x
| `org.eclipse.jetty.websocket.**server.NativeWebSocketServletContainerInitializer**`
| `org.eclipse.jetty.websocket.**server.config.JettyWebSocketServletContainerInitializer**`
| `org.eclipse.jetty.websocket.**jsr356.server.deploy.WebSocketServerContainerInitializer**`
| `org.eclipse.jetty.websocket.**javax.server.config.JavaxWebSocketServletContainerInitializer**`
| `org.eclipse.jetty.websocket.**servlet.WebSocketCreator**`
| `org.eclipse.jetty.websocket.**server.JettyWebSocketCreator**`
| `org.eclipse.jetty.websocket.**servlet.ServletUpgradeRequest**`
| `org.eclipse.jetty.websocket.**server.JettyServerUpgradeRequest**`
| `org.eclipse.jetty.websocket.**servlet.ServletUpgradeResponse**`
| `org.eclipse.jetty.websocket.**server.JettyServerUpgradeResponse**`
| `org.eclipse.jetty.websocket.**servlet.WebSocketServlet**`
| `org.eclipse.jetty.websocket.**server.JettyWebSocketServlet**`
| `org.eclipse.jetty.websocket.**servlet.WebSocketServletFactory**`
| `org.eclipse.jetty.websocket.**server.JettyWebSocketServletFactory**`
|===
[[pg-migration-94-to-10-websocket-example-code]]
===== Example Code
[cols="1a,1a", options="header"]
|===
| Jetty 9.4.x
| Jetty 10.0.x
|
[source,java]
----
public class ExampleWebSocketServlet extends WebSocketServlet
{
@Override
public void configure(WebSocketServletFactory factory)
{
factory.setCreator(new WebSocketCreator()
{
@Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
return new ExampleEndpoint();
}
});
}
}
----
|
[source,java]
----
public class ExampleWebSocketServlet extends JettyWebSocketServlet
{
@Override
public void configure(JettyWebSocketServletFactory factory)
{
factory.setCreator(new JettyWebSocketCreator()
{
@Override
public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp)
{
return new ExampleEndpoint();
}
});
}
}
----
|===

View File

@ -66,6 +66,7 @@ import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.HttpCookieStore;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import static java.lang.System.Logger.Level.INFO;
@ -97,6 +98,17 @@ public class HTTPClientDocs
// end::stop[]
}
public void stopFromOtherThread() throws Exception
{
HttpClient httpClient = new HttpClient();
httpClient.start();
// tag::stopFromOtherThread[]
// Stop HttpClient from a new thread.
// Use LifeCycle.stop(...) to rethrow checked exceptions as unchecked.
new Thread(() -> LifeCycle.stop(httpClient)).start();
// end::stopFromOtherThread[]
}
public void tlsExplicit() throws Exception
{
// tag::tlsExplicit[]

View File

@ -105,6 +105,11 @@
<artifactId>jetty-client</artifactId>
<version>10.0.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-cdi</artifactId>
<version>10.0.3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-deploy</artifactId>

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.client;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.io.ClientConnector;
@ -30,7 +31,7 @@ public abstract class AbstractConnectorHttpClientTransport extends AbstractHttpC
protected AbstractConnectorHttpClientTransport(ClientConnector connector)
{
this.connector = connector;
this.connector = Objects.requireNonNull(connector);
addBean(connector);
}

View File

@ -19,6 +19,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -86,7 +87,12 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans
*/
public HttpClientTransportDynamic()
{
this(new ClientConnector(), HttpClientConnectionFactory.HTTP11);
this(HttpClientConnectionFactory.HTTP11);
}
public HttpClientTransportDynamic(ClientConnectionFactory.Info... factoryInfos)
{
this(findClientConnector(factoryInfos), factoryInfos);
}
/**
@ -98,7 +104,6 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans
public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFactory.Info... factoryInfos)
{
super(connector);
addBean(connector);
if (factoryInfos.length == 0)
factoryInfos = new Info[]{HttpClientConnectionFactory.HTTP11};
this.factoryInfos = Arrays.asList(factoryInfos);
@ -112,6 +117,15 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans
new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination, 1));
}
private static ClientConnector findClientConnector(ClientConnectionFactory.Info[] infos)
{
return Arrays.stream(infos)
.map(info -> info.getBean(ClientConnector.class))
.filter(Objects::nonNull)
.findFirst()
.orElse(new ClientConnector());
}
@Override
public Origin newOrigin(HttpRequest request)
{

View File

@ -314,8 +314,9 @@ public class HttpStatus
switch (status)
{
case NO_CONTENT_204:
case NOT_MODIFIED_304:
case RESET_CONTENT_205:
case PARTIAL_CONTENT_206:
case NOT_MODIFIED_304:
return true;
default:

View File

@ -25,7 +25,9 @@ import org.slf4j.LoggerFactory;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import static java.util.EnumSet.allOf;
import static java.util.EnumSet.complementOf;
import static java.util.EnumSet.noneOf;
import static java.util.EnumSet.of;
/**
* URI compliance modes for Jetty request handling.
@ -37,23 +39,29 @@ public final class UriCompliance implements ComplianceViolation.Mode
protected static final Logger LOG = LoggerFactory.getLogger(UriCompliance.class);
/**
* These are URI compliance violations, which may be allowed by the compliance mode. Currently all these
* violations are for additional criteria in excess of the strict requirements of rfc3986.
* These are URI compliance "violations", which may be allowed by the compliance mode. These are actual
* violations of the RFC, as they represent additional requirements in excess of the strict compliance of rfc3986.
* A compliance mode that contains one or more of these Violations, allows request to violate the corresponding
* additional requirement.
*/
public enum Violation implements ComplianceViolation
{
/**
* Ambiguous path segments e.g. <code>/foo/%2e%2e/bar</code>
* Allow ambiguous path segments e.g. <code>/foo/%2e%2e/bar</code>
*/
AMBIGUOUS_PATH_SEGMENT("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path segment"),
/**
* Ambiguous path separator within a URI segment e.g. <code>/foo/b%2fr</code>
* Allow ambiguous path separator within a URI segment e.g. <code>/foo/b%2fr</code>
*/
AMBIGUOUS_PATH_SEPARATOR("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path separator"),
/**
* Ambiguous path parameters within a URI segment e.g. <code>/foo/..;/bar</code>
* Allow ambiguous path parameters within a URI segment e.g. <code>/foo/..;/bar</code>
*/
AMBIGUOUS_PATH_PARAMETER("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path parameter");
AMBIGUOUS_PATH_PARAMETER("https://tools.ietf.org/html/rfc3986#section-3.3", "Ambiguous URI path parameter"),
/**
* Allow Non canonical ambiguous paths. eg <code>/foo/x%2f%2e%2e%/bar</code> provided to applications as <code>/foo/x/../bar</code>
*/
NON_CANONICAL_AMBIGUOUS_PATHS("https://tools.ietf.org/html/rfc3986#section-3.3", "Non canonical ambiguous paths");
private final String _url;
private final String _description;
@ -84,19 +92,28 @@ public final class UriCompliance implements ComplianceViolation.Mode
}
/**
* The default compliance mode that extends RFC3986 compliance with additional violations to avoid ambiguous URIs
* The default compliance mode that extends RFC3986 compliance with additional violations to avoid most ambiguous URIs.
* This mode does allow {@link Violation#AMBIGUOUS_PATH_SEPARATOR}, but disallows
* {@link Violation#AMBIGUOUS_PATH_PARAMETER} and {@link Violation#AMBIGUOUS_PATH_SEGMENT}.
* Ambiguous paths are not allowed by {@link Violation#NON_CANONICAL_AMBIGUOUS_PATHS}.
*/
public static final UriCompliance DEFAULT = new UriCompliance("DEFAULT", noneOf(Violation.class));
public static final UriCompliance DEFAULT = new UriCompliance("DEFAULT", of(Violation.AMBIGUOUS_PATH_SEPARATOR));
/**
* LEGACY compliance mode that disallows only ambiguous path parameters as per Jetty-9.4
* LEGACY compliance mode that models Jetty-9.4 behavior by allowing {@link Violation#AMBIGUOUS_PATH_SEGMENT} and {@link Violation#AMBIGUOUS_PATH_SEPARATOR}
*/
public static final UriCompliance LEGACY = new UriCompliance("LEGACY", EnumSet.of(Violation.AMBIGUOUS_PATH_SEGMENT, Violation.AMBIGUOUS_PATH_SEPARATOR));
public static final UriCompliance LEGACY = new UriCompliance("LEGACY", of(Violation.AMBIGUOUS_PATH_SEGMENT, Violation.AMBIGUOUS_PATH_SEPARATOR));
/**
* Compliance mode that exactly follows RFC3986, including allowing all additional ambiguous URI Violations
* Compliance mode that exactly follows RFC3986, including allowing all additional ambiguous URI Violations,
* except {@link Violation#NON_CANONICAL_AMBIGUOUS_PATHS}, thus ambiguous paths are canonicalized for safety.
*/
public static final UriCompliance RFC3986 = new UriCompliance("RFC3986", allOf(Violation.class));
public static final UriCompliance RFC3986 = new UriCompliance("RFC3986", complementOf(of(Violation.NON_CANONICAL_AMBIGUOUS_PATHS)));
/**
* Compliance mode that allows all URI Violations, including allowing ambiguous paths in non canonicalized form.
*/
public static final UriCompliance UNSAFE = new UriCompliance("UNSAFE", allOf(Violation.class));
/**
* @deprecated equivalent to DEFAULT
@ -125,6 +142,17 @@ public final class UriCompliance implements ComplianceViolation.Mode
return null;
}
/**
* Create compliance set from a set of allowed Violations.
*
* @param violations A string of violations to allow:
* @return the compliance from the string spec
*/
public static UriCompliance from(Set<Violation> violations)
{
return new UriCompliance("CUSTOM" + __custom.getAndIncrement(), violations);
}
/**
* Create compliance set from string.
* <p>
@ -151,22 +179,23 @@ public final class UriCompliance implements ComplianceViolation.Mode
*/
public static UriCompliance from(String spec)
{
Set<Violation> sections;
Set<Violation> violations;
String[] elements = spec.split("\\s*,\\s*");
switch (elements[0])
{
case "0":
sections = noneOf(Violation.class);
violations = noneOf(Violation.class);
break;
case "*":
sections = allOf(Violation.class);
violations = allOf(Violation.class);
break;
default:
{
UriCompliance mode = UriCompliance.valueOf(elements[0]);
sections = (mode == null) ? noneOf(Violation.class) : copyOf(mode.getAllowed());
violations = (mode == null) ? noneOf(Violation.class) : copyOf(mode.getAllowed());
break;
}
}
@ -178,12 +207,12 @@ public final class UriCompliance implements ComplianceViolation.Mode
element = element.substring(1);
Violation section = Violation.valueOf(element);
if (exclude)
sections.remove(section);
violations.remove(section);
else
sections.add(section);
violations.add(section);
}
UriCompliance compliance = new UriCompliance("CUSTOM" + __custom.getAndIncrement(), sections);
UriCompliance compliance = new UriCompliance("CUSTOM" + __custom.getAndIncrement(), violations);
if (LOG.isDebugEnabled())
LOG.debug("UriCompliance from {}->{}", spec, compliance);
return compliance;
@ -192,7 +221,7 @@ public final class UriCompliance implements ComplianceViolation.Mode
private final String _name;
private final Set<Violation> _allowed;
private UriCompliance(String name, Set<Violation> violations)
public UriCompliance(String name, Set<Violation> violations)
{
Objects.requireNonNull(violations);
_name = name;

View File

@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class SyntaxTest
@ -61,17 +62,11 @@ public class SyntaxTest
for (String token : tokens)
{
try
{
Syntax.requireValidRFC2616Token(token, "Test Based");
fail("RFC2616 Token [" + token + "] Should have thrown " + IllegalArgumentException.class.getName());
}
catch (IllegalArgumentException e)
{
assertThat("Testing Bad RFC2616 Token [" + token + "]", e.getMessage(),
Throwable e = assertThrows(IllegalArgumentException.class,
() -> Syntax.requireValidRFC2616Token(token, "Test Based"));
assertThat("Testing Bad RFC2616 Token [" + token + "]", e.getMessage(),
allOf(containsString("Test Based"),
containsString("RFC2616")));
}
containsString("RFC2616")));
}
}

View File

@ -44,7 +44,6 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.Scheduler;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -52,7 +51,6 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Disabled
public class SmallThreadPoolLoadTest extends AbstractTest
{
private final Logger logger = LoggerFactory.getLogger(SmallThreadPoolLoadTest.class);
@ -83,8 +81,8 @@ public class SmallThreadPoolLoadTest extends AbstractTest
boolean result = IntStream.range(0, 16).parallel()
.mapToObj(i -> IntStream.range(0, runs)
.mapToObj(j -> run(session, iterations))
.reduce(true, (acc, res) -> acc && res))
.reduce(true, (acc, res) -> acc && res);
.reduce(true, Boolean::logicalAnd))
.reduce(true, Boolean::logicalAnd);
assertTrue(result);
}
@ -94,10 +92,10 @@ public class SmallThreadPoolLoadTest extends AbstractTest
try
{
CountDownLatch latch = new CountDownLatch(iterations);
int factor = (logger.isDebugEnabled() ? 25 : 1) * 100;
long factor = (logger.isDebugEnabled() ? 25 : 1) * 100;
// Dumps the state of the client if the test takes too long.
final Thread testThread = Thread.currentThread();
Thread testThread = Thread.currentThread();
Scheduler.Task task = client.getScheduler().schedule(() ->
{
logger.warn("Interrupting test, it is taking too long{}Server:{}{}{}Client:{}{}",
@ -123,7 +121,7 @@ public class SmallThreadPoolLoadTest extends AbstractTest
logger.info("{} requests in {} ms, {}/{} success/failure, {} req/s",
iterations, elapsed,
successes, iterations - successes,
elapsed > 0 ? iterations * 1000 / elapsed : -1);
elapsed > 0 ? iterations * 1000L / elapsed : -1);
return true;
}
catch (Exception x)

View File

@ -42,6 +42,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
static final Logger LOG = LoggerFactory.getLogger(ByteArrayEndPoint.class);
static final InetAddress NOIP;
static final InetSocketAddress NOIPPORT;
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 1024;
static
{
@ -67,6 +68,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
private final AutoLock _lock = new AutoLock();
private final Condition _hasOutput = _lock.newCondition();
private final Queue<ByteBuffer> _inQ = new ArrayDeque<>();
private final int _outputSize;
private ByteBuffer _out;
private boolean _growOutput;
@ -113,7 +115,8 @@ public class ByteArrayEndPoint extends AbstractEndPoint
super(timer);
if (BufferUtil.hasContent(input))
addInput(input);
_out = output == null ? BufferUtil.allocate(1024) : output;
_outputSize = (output == null) ? 1024 : output.capacity();
_out = output == null ? BufferUtil.allocate(_outputSize) : output;
setIdleTimeout(idleTimeoutMs);
onOpen();
}
@ -290,7 +293,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
try (AutoLock lock = _lock.lock())
{
b = _out;
_out = BufferUtil.allocate(b.capacity());
_out = BufferUtil.allocate(_outputSize);
}
getWriteFlusher().completeWrite();
return b;
@ -316,7 +319,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint
return null;
}
b = _out;
_out = BufferUtil.allocate(b.capacity());
_out = BufferUtil.allocate(_outputSize);
}
getWriteFlusher().completeWrite();
return b;
@ -424,9 +427,14 @@ public class ByteArrayEndPoint extends AbstractEndPoint
BufferUtil.compact(_out);
if (b.remaining() > BufferUtil.space(_out))
{
ByteBuffer n = BufferUtil.allocate(_out.capacity() + b.remaining() * 2);
BufferUtil.append(n, _out);
_out = n;
// Don't grow larger than MAX_BUFFER_SIZE to avoid memory issues.
if (_out.capacity() < MAX_BUFFER_SIZE)
{
long newBufferCapacity = Math.min((long)(_out.capacity() + b.remaining() * 1.5), MAX_BUFFER_SIZE);
ByteBuffer n = BufferUtil.allocate(Math.toIntExact(newBufferCapacity));
BufferUtil.append(n, _out);
_out = n;
}
}
}

View File

@ -83,6 +83,7 @@ public class JspcMojo extends AbstractMojo
{
private boolean scanAll;
private boolean scanManifest;
public void setClassLoader(ClassLoader loader)
{
@ -99,6 +100,16 @@ public class JspcMojo extends AbstractMojo
return this.scanAll;
}
public void setScanManifest(boolean scanManifest)
{
this.scanManifest = scanManifest;
}
public boolean getScanManifest()
{
return this.scanManifest;
}
@Override
protected TldScanner newTldScanner(JspCServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal)
{
@ -106,6 +117,7 @@ public class JspcMojo extends AbstractMojo
{
StandardJarScanner jarScanner = new StandardJarScanner();
jarScanner.setScanAllDirectories(getScanAllDirectories());
jarScanner.setScanManifest(getScanManifest());
context.setAttribute(JarScanner.class.getName(), jarScanner);
}
@ -243,6 +255,13 @@ public class JspcMojo extends AbstractMojo
@Parameter(defaultValue = "true")
private boolean scanAllDirectories;
/**
* Determines if the manifest of JAR files found on the classpath should be scanned.
* True by default.
*/
@Parameter(defaultValue = "true")
private boolean scanManifest;
@Override
public void execute() throws MojoExecutionException, MojoFailureException
{
@ -319,6 +338,7 @@ public class JspcMojo extends AbstractMojo
jspc.setOutputDir(generatedClasses);
jspc.setClassLoader(fakeWebAppClassLoader);
jspc.setScanAllDirectories(scanAllDirectories);
jspc.setScanManifest(scanManifest);
jspc.setCompile(true);
if (sourceVersion != null)
jspc.setCompilerSourceVM(sourceVersion);

View File

@ -17,7 +17,7 @@
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>

View File

@ -50,6 +50,17 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-invoker-plugin</artifactId>
@ -65,8 +76,12 @@
</execution>
</executions>
<configuration>
<debug>${it.debug}</debug>
<addTestClassPath>true</addTestClassPath>
<scriptVariables>
<maven.dependency.plugin.version>${maven.dependency.plugin.version}</maven.dependency.plugin.version>
<maven.surefire.version>${maven.surefire.version}</maven.surefire.version>
<hamcrest.version>${hamcrest.version}</hamcrest.version>
</scriptVariables>
<goals>
<goal>clean</goal>
@ -149,5 +164,18 @@
<artifactId>jetty-slf4j-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.demos</groupId>
<artifactId>demo-simple-webapp</artifactId>
<version>${project.version}</version>
<type>war</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1 @@
invoker.goals = test

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.its</groupId>
<artifactId>jetty-runner-it-test-demo-simple-webapp</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-runner</artifactId>
<version>@project.version@</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-runner</artifactId>
<version>@project.version@</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.demos</groupId>
<artifactId>demo-simple-webapp</artifactId>
<version>@project.version@</version>
<type>war</type>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>@project.version@</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>@hamcrest.version@</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>@maven.dependency.plugin.version@</version>
<executions>
<execution>
<id>copy-jetty-runner</id>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-runner</artifactId>
<version>@project.version@</version>
<type>jar</type>
<overWrite>false</overWrite>
<outputDirectory>${project.build.directory}/</outputDirectory>
<destFileName>jetty-runner.jar</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.eclipse.jetty.demos</groupId>
<artifactId>demo-simple-webapp</artifactId>
<version>@project.version@</version>
<type>war</type>
<overWrite>false</overWrite>
<outputDirectory>${project.build.directory}</outputDirectory>
<destFileName>demo-simple-webapp.war</destFileName>
</artifactItem>
</artifactItems>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>@maven.exec.plugin.version@</version>
<executions>
<execution>
<goals>
<goal>exec</goal>
</goals>
<phase>generate-test-resources</phase>
<configuration>
<outputFile>${project.build.directory}/jetty-runner.log</outputFile>
<async>true</async>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>-jar</argument>
<argument>${project.build.directory}/jetty-runner.jar</argument>
<argument>--out</argument>
<argument>${project.build.directory}/jetty-runner.out</argument>
<argument>--port</argument>
<argument>0</argument>
<argument>--path</argument>
<argument>french-chocolate-rocks</argument>
<argument>--server-uri-file</argument>
<argument>${project.build.directory}/server-uri.txt</argument>
<argument>${project.build.directory}/demo-simple-webapp.war</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>@maven.surefire.version@</version>
<configuration>
<includes>
<include>IntegrationTest*.java</include>
</includes>
<systemPropertyVariables>
</systemPropertyVariables>
<dependenciesToScan>
<dependency>org.eclipse.jetty:jetty-runner</dependency>
</dependenciesToScan>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1 @@
invoker.goals = test

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.its</groupId>
<artifactId>jetty-runner-it-test-demo-simple-webapp</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-runner</artifactId>
<version>@project.version@</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-runner</artifactId>
<version>@project.version@</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.demos</groupId>
<artifactId>demo-simple-webapp</artifactId>
<version>@project.version@</version>
<type>war</type>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>@project.version@</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>@hamcrest.version@</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>@maven.dependency.plugin.version@</version>
<executions>
<execution>
<id>copy-jetty-runner</id>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-runner</artifactId>
<version>@project.version@</version>
<type>jar</type>
<overWrite>false</overWrite>
<outputDirectory>${project.build.directory}/</outputDirectory>
<destFileName>jetty-runner.jar</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.eclipse.jetty.demos</groupId>
<artifactId>demo-simple-webapp</artifactId>
<version>@project.version@</version>
<type>war</type>
<overWrite>false</overWrite>
<outputDirectory>${project.build.directory}</outputDirectory>
<destFileName>demo-simple-webapp.war</destFileName>
</artifactItem>
</artifactItems>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>@maven.exec.plugin.version@</version>
<executions>
<execution>
<goals>
<goal>exec</goal>
</goals>
<phase>generate-test-resources</phase>
<configuration>
<outputFile>${project.build.directory}/jetty-runner.log</outputFile>
<async>true</async>
<executable>${java.home}/bin/java</executable>
<arguments>
<!-- <argument>-Xdebug</argument>-->
<!-- <argument>-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000</argument>-->
<argument>-jar</argument>
<argument>${project.build.directory}/jetty-runner.jar</argument>
<argument>--out</argument>
<argument>${project.build.directory}/jetty-runner.out</argument>
<argument>--port</argument>
<argument>0</argument>
<argument>--server-uri-file</argument>
<argument>${project.build.directory}/server-uri.txt</argument>
<argument>${project.build.directory}/demo-simple-webapp.war</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>@maven.surefire.version@</version>
<configuration>
<includes>
<include>IntegrationTest*.java</include>
</includes>
<systemPropertyVariables>
</systemPropertyVariables>
<dependenciesToScan>
<dependency>org.eclipse.jetty:jetty-runner</dependency>
</dependenciesToScan>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -19,6 +19,9 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -78,7 +81,9 @@ public class Runner
org.eclipse.jetty.plus.webapp.EnvConfiguration.class.getCanonicalName(),
org.eclipse.jetty.plus.webapp.PlusConfiguration.class.getCanonicalName(),
org.eclipse.jetty.annotations.AnnotationConfiguration.class.getCanonicalName(),
org.eclipse.jetty.webapp.JettyWebXmlConfiguration.class.getCanonicalName()
org.eclipse.jetty.webapp.JettyWebXmlConfiguration.class.getCanonicalName(),
org.eclipse.jetty.webapp.WebAppConfiguration.class.getCanonicalName(),
org.eclipse.jetty.webapp.JspConfiguration.class.getCanonicalName()
};
public static final String CONTAINER_INCLUDE_JAR_PATTERN = ".*/jetty-runner-[^/]*\\.jar$";
public static final String DEFAULT_CONTEXT_PATH = "/";
@ -92,6 +97,7 @@ public class Runner
protected ArrayList<String> _configFiles;
protected boolean _enableStats = false;
protected String _statsPropFile;
protected String _serverUriFile;
/**
* Classpath
@ -164,6 +170,7 @@ public class Runner
System.err.println(" --out file - info/warn/debug log filename (with optional 'yyyy_mm_dd' wildcard");
System.err.println(" --host name|ip - interface to listen on (default is all interfaces)");
System.err.println(" --port n - port to listen on (default 8080)");
System.err.println(" --server-uri-file path - file to write a single line with server base URI");
System.err.println(" --stop-port n - port to listen for stop command (or -DSTOP.PORT=n)");
System.err.println(" --stop-key n - security string for stop command (required if --stop-port is present) (or -DSTOP.KEY=n)");
System.err.println(" [--jar file]*n - each tuple specifies an extra jar to be added to the classloader");
@ -293,6 +300,9 @@ public class Runner
_statsPropFile = args[++i];
_statsPropFile = ("unsecure".equalsIgnoreCase(_statsPropFile) ? null : _statsPropFile);
break;
case "--server-uri-file":
_serverUriFile = args[++i];
break;
default:
// process system property type argument so users can use in second args part
if (args[i].startsWith("-D"))
@ -310,7 +320,7 @@ public class Runner
}
}
// process contexts
// process contexts
if (!runnerServerInitialized) // log handlers not registered, server maybe not created, etc
{
@ -334,7 +344,7 @@ public class Runner
}
//check that everything got configured, and if not, make the handlers
HandlerCollection handlers = (HandlerCollection)_server.getChildHandlerByClass(HandlerCollection.class);
HandlerCollection handlers = _server.getChildHandlerByClass(HandlerCollection.class);
if (handlers == null)
{
handlers = new HandlerList();
@ -342,7 +352,7 @@ public class Runner
}
//check if contexts already configured
_contexts = (ContextHandlerCollection)handlers.getChildHandlerByClass(ContextHandlerCollection.class);
_contexts = handlers.getChildHandlerByClass(ContextHandlerCollection.class);
if (_contexts == null)
{
_contexts = new ContextHandlerCollection();
@ -519,6 +529,13 @@ public class Runner
public void run() throws Exception
{
_server.start();
if (_serverUriFile != null)
{
Path fileWithPort = Paths.get(_serverUriFile);
Files.deleteIfExists(fileWithPort);
String serverUri = _server.getURI().toString();
Files.writeString(fileWithPort, serverUri);
}
_server.join();
}

View File

@ -0,0 +1,66 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.maven.jettyrunner.it;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.hamcrest.MatcherAssert.assertThat;
public class IntegrationTestJettyRunner
{
@Test
public void testGet() throws Exception
{
String serverUri = findServerUri();
HttpClient httpClient = new HttpClient();
try
{
httpClient.start();
ContentResponse response = httpClient.newRequest(serverUri).send();
String res = response.getContentAsString();
assertThat(res, Matchers.containsString("Hello World!"));
}
finally
{
httpClient.stop();
}
}
private String findServerUri() throws Exception
{
long now = System.currentTimeMillis();
while (System.currentTimeMillis() - now < MINUTES.toMillis(2))
{
Path portTxt = Paths.get("target", "server-uri.txt");
if (Files.exists(portTxt))
{
List<String> lines = Files.readAllLines(portTxt);
return lines.get(0);
}
}
throw new Exception("cannot find started Jetty");
}
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.server;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
@ -302,12 +303,16 @@ class AsyncContentProducer implements ContentProducer
// In case the _rawContent was set by consumeAll(), check the httpChannel
// to see if it has a more precise error. Otherwise, the exact same
// special content will be returned by the httpChannel.
HttpInput.Content refreshedRawContent = produceRawContent();
if (refreshedRawContent != null)
_rawContent = refreshedRawContent;
// special content will be returned by the httpChannel; do not do that
// if the _error flag was set, meaning the current error is definitive.
if (!_error)
{
HttpInput.Content refreshedRawContent = produceRawContent();
if (refreshedRawContent != null)
_rawContent = refreshedRawContent;
_error = _rawContent.getError() != null;
}
_error = _rawContent.getError() != null;
if (LOG.isDebugEnabled())
LOG.debug("raw content is special (with error = {}), returning it {}", _error, this);
return _rawContent;
@ -317,7 +322,9 @@ class AsyncContentProducer implements ContentProducer
{
if (LOG.isDebugEnabled())
LOG.debug("using interceptor to transform raw content {}", this);
_transformedContent = _interceptor.readFrom(_rawContent);
_transformedContent = intercept();
if (_error)
return _rawContent;
}
else
{
@ -369,6 +376,26 @@ class AsyncContentProducer implements ContentProducer
return _transformedContent;
}
private HttpInput.Content intercept()
{
try
{
return _interceptor.readFrom(_rawContent);
}
catch (Throwable x)
{
IOException failure = new IOException("Bad content", x);
failCurrentContent(failure);
// Set the _error flag to mark the error as definitive, i.e.:
// do not try to produce new raw content to get a fresher error.
_error = true;
Response response = _httpChannel.getResponse();
if (response.isCommitted())
_httpChannel.abort(failure);
return null;
}
}
private HttpInput.Content produceRawContent()
{
HttpInput.Content content = _httpChannel.produceContent();

View File

@ -22,6 +22,7 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -279,6 +280,7 @@ public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
private final String _formatString;
private transient PathMappings<String> _ignorePathMap;
private String[] _ignorePaths;
private BiPredicate<Request, Response> _filter;
public CustomRequestLog()
{
@ -311,6 +313,16 @@ public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
}
}
/**
* This allows you to set a custom filter to decide whether to log a request or omit it from the request log.
* This filter is evaluated after path filtering is applied from {@link #setIgnorePaths(String[])}.
* @param filter - a BiPredicate which returns true if this request should be logged.
*/
public void setFilter(BiPredicate<Request, Response> filter)
{
_filter = filter;
}
@ManagedAttribute("The RequestLogWriter")
public RequestLog.Writer getWriter()
{
@ -325,11 +337,14 @@ public class CustomRequestLog extends ContainerLifeCycle implements RequestLog
@Override
public void log(Request request, Response response)
{
if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null)
return;
if (_filter != null && !_filter.test(request, response))
return;
try
{
if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null)
return;
StringBuilder sb = _buffers.get();
sb.setLength(0);

View File

@ -263,9 +263,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
{
// Fill the request buffer (if needed).
int filled = fillRequestBuffer();
if (filled > 0)
bytesIn.add(filled);
else if (filled == -1 && getEndPoint().isOutputShutdown())
if (filled < 0 && getEndPoint().isOutputShutdown())
close();
// Parse the request buffer.
@ -300,6 +298,14 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
}
}
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("{} caught exception {}", this, _channel.getState(), x);
BufferUtil.clear(_requestBuffer);
releaseRequestBuffer();
getEndPoint().close(x);
}
finally
{
setCurrentConnection(last);
@ -333,10 +339,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
private int fillRequestBuffer()
{
if (_contentBufferReferences.get() > 0)
{
LOG.warn("{} fill with unconsumed content!", this);
return 0;
}
throw new IllegalStateException("fill with unconsumed content on " + this);
if (BufferUtil.isEmpty(_requestBuffer))
{
@ -352,8 +355,9 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
if (filled == 0) // Do a retry on fill 0 (optimization for SSL connections)
filled = getEndPoint().fill(_requestBuffer);
// tell parser
if (filled < 0)
if (filled > 0)
bytesIn.add(filled);
else if (filled < 0)
_parser.atEOF();
if (LOG.isDebugEnabled())
@ -363,7 +367,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
}
catch (IOException e)
{
LOG.debug("Unable to fill from endpoint {}", getEndPoint(), e);
if (LOG.isDebugEnabled())
LOG.debug("Unable to fill from endpoint {}", getEndPoint(), e);
_parser.atEOF();
return -1;
}

View File

@ -23,7 +23,6 @@ import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import javax.servlet.RequestDispatcher;
@ -627,6 +626,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
catch (Throwable t)
{
onWriteComplete(true, t);
throw t;
}
}
}

View File

@ -67,7 +67,6 @@ import javax.servlet.http.WebConnection;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField;
import org.eclipse.jetty.http.HttpField;
@ -1692,10 +1691,11 @@ public class Request implements HttpServletRequest
_httpFields = request.getFields();
final HttpURI uri = request.getURI();
UriCompliance compliance = null;
boolean ambiguous = uri.isAmbiguous();
if (ambiguous)
{
UriCompliance compliance = _channel == null || _channel.getHttpConfiguration() == null ? null : _channel.getHttpConfiguration().getUriCompliance();
compliance = _channel == null || _channel.getHttpConfiguration() == null ? null : _channel.getHttpConfiguration().getUriCompliance();
if (uri.hasAmbiguousSegment() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT)))
throw new BadMessageException("Ambiguous segment in URI");
if (uri.hasAmbiguousSeparator() && (compliance == null || !compliance.allows(UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR)))
@ -1746,9 +1746,9 @@ public class Request implements HttpServletRequest
path = (encoded.length() == 1) ? "/" : _uri.getDecodedPath();
// Strictly speaking if a URI is legal and encodes ambiguous segments, then they should be
// reflected in the decoded string version. However, it can be ambiguous to provide a decoded path as
// a string, so we normalize again. If an application wishes to see ambiguous URIs, then they can look
// at the encoded form of the URI
if (ambiguous)
// a string, so we normalize again. If an application wishes to see ambiguous URIs, then they must
// set the {@link UriCompliance.Violation#NON_CANONICAL_AMBIGUOUS_PATHS} compliance.
if (ambiguous && (compliance == null || !compliance.allows(UriCompliance.Violation.NON_CANONICAL_AMBIGUOUS_PATHS)))
path = URIUtil.canonicalPath(path);
}
else if ("*".equals(encoded) || HttpMethod.CONNECT.is(getMethod()))

View File

@ -15,14 +15,15 @@ package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.HttpChannel;
@ -39,16 +40,17 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Buffered Response Handler
* <p>
* A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
* mechanism to buffer the entire response content until the output is closed.
* This allows the commit to be delayed until the response is complete and thus
* headers and response status can be changed while writing the body.
* </p>
* <p>
* Note that the decision to buffer is influenced by the headers and status at the
* first write, and thus subsequent changes to those headers will not influence the
* decision to buffer or not.
* </p>
* <p>
* Note also that there are no memory limits to the size of the buffer, thus
* this handler can represent an unbounded memory commitment if the content
@ -57,7 +59,7 @@ import org.slf4j.LoggerFactory;
*/
public class BufferedResponseHandler extends HandlerWrapper
{
static final Logger LOG = LoggerFactory.getLogger(BufferedResponseHandler.class);
private static final Logger LOG = LoggerFactory.getLogger(BufferedResponseHandler.class);
private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
@ -65,10 +67,7 @@ public class BufferedResponseHandler extends HandlerWrapper
public BufferedResponseHandler()
{
// include only GET requests
_methods.include(HttpMethod.GET.asString());
// Exclude images, aduio and video from buffering
for (String type : MimeTypes.getKnownMimeTypes())
{
if (type.startsWith("image/") ||
@ -76,7 +75,9 @@ public class BufferedResponseHandler extends HandlerWrapper
type.startsWith("video/"))
_mimeTypes.exclude(type);
}
LOG.debug("{} mime types {}", this, _mimeTypes);
if (LOG.isDebugEnabled())
LOG.debug("{} mime types {}", this, _mimeTypes);
}
public IncludeExclude<String> getMethodIncludeExclude()
@ -94,66 +95,6 @@ public class BufferedResponseHandler extends HandlerWrapper
return _mimeTypes;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
final ServletContext context = baseRequest.getServletContext();
final String path = baseRequest.getPathInContext();
LOG.debug("{} handle {} in {}", this, baseRequest, context);
HttpOutput out = baseRequest.getResponse().getHttpOutput();
// Are we already being gzipped?
HttpOutput.Interceptor interceptor = out.getInterceptor();
while (interceptor != null)
{
if (interceptor instanceof BufferedInterceptor)
{
LOG.debug("{} already intercepting {}", this, request);
_handler.handle(target, baseRequest, request, response);
return;
}
interceptor = interceptor.getNextInterceptor();
}
// If not a supported method - no Vary because no matter what client, this URI is always excluded
if (!_methods.test(baseRequest.getMethod()))
{
LOG.debug("{} excluded by method {}", this, request);
_handler.handle(target, baseRequest, request, response);
return;
}
// If not a supported URI- no Vary because no matter what client, this URI is always excluded
// Use pathInfo because this is be
if (!isPathBufferable(path))
{
LOG.debug("{} excluded by path {}", this, request);
_handler.handle(target, baseRequest, request, response);
return;
}
// If the mime type is known from the path, then apply mime type filtering
String mimeType = context == null ? MimeTypes.getDefaultMimeByExtension(path) : context.getMimeType(path);
if (mimeType != null)
{
mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
if (!isMimeTypeBufferable(mimeType))
{
LOG.debug("{} excluded by path suffix mime type {}", this, request);
// handle normally without setting vary header
_handler.handle(target, baseRequest, request, response);
return;
}
}
// install interceptor and handle
out.setInterceptor(new BufferedInterceptor(baseRequest.getHttpChannel(), out.getInterceptor()));
if (_handler != null)
_handler.handle(target, baseRequest, request, response);
}
protected boolean isMimeTypeBufferable(String mimetype)
{
return _mimeTypes.test(mimetype);
@ -167,116 +108,197 @@ public class BufferedResponseHandler extends HandlerWrapper
return _paths.test(requestURI);
}
private class BufferedInterceptor implements HttpOutput.Interceptor
protected boolean shouldBuffer(HttpChannel channel, boolean last)
{
final Interceptor _next;
final HttpChannel _channel;
final Queue<ByteBuffer> _buffers = new ConcurrentLinkedQueue<>();
Boolean _aggregating;
ByteBuffer _aggregate;
if (last)
return false;
public BufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
Response response = channel.getResponse();
int status = response.getStatus();
if (HttpStatus.hasNoBody(status) || HttpStatus.isRedirection(status))
return false;
String ct = response.getContentType();
if (ct == null)
return true;
ct = MimeTypes.getContentTypeWithoutCharset(ct);
return isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
final ServletContext context = baseRequest.getServletContext();
final String path = baseRequest.getPathInContext();
if (LOG.isDebugEnabled())
LOG.debug("{} handle {} in {}", this, baseRequest, context);
// Are we already buffering?
HttpOutput out = baseRequest.getResponse().getHttpOutput();
HttpOutput.Interceptor interceptor = out.getInterceptor();
while (interceptor != null)
{
if (interceptor instanceof BufferedInterceptor)
{
if (LOG.isDebugEnabled())
LOG.debug("{} already intercepting {}", this, request);
_handler.handle(target, baseRequest, request, response);
return;
}
interceptor = interceptor.getNextInterceptor();
}
// If not a supported method this URI is always excluded.
if (!_methods.test(baseRequest.getMethod()))
{
if (LOG.isDebugEnabled())
LOG.debug("{} excluded by method {}", this, request);
_handler.handle(target, baseRequest, request, response);
return;
}
// If not a supported path this URI is always excluded.
if (!isPathBufferable(path))
{
if (LOG.isDebugEnabled())
LOG.debug("{} excluded by path {}", this, request);
_handler.handle(target, baseRequest, request, response);
return;
}
// If the mime type is known from the path then apply mime type filtering.
String mimeType = context == null ? MimeTypes.getDefaultMimeByExtension(path) : context.getMimeType(path);
if (mimeType != null)
{
mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
if (!isMimeTypeBufferable(mimeType))
{
if (LOG.isDebugEnabled())
LOG.debug("{} excluded by path suffix mime type {}", this, request);
// handle normally without setting vary header
_handler.handle(target, baseRequest, request, response);
return;
}
}
// Install buffered interceptor and handle.
out.setInterceptor(newBufferedInterceptor(baseRequest.getHttpChannel(), out.getInterceptor()));
if (_handler != null)
_handler.handle(target, baseRequest, request, response);
}
protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
return new ArrayBufferedInterceptor(httpChannel, interceptor);
}
/**
* An {@link HttpOutput.Interceptor} which is created by {@link #newBufferedInterceptor(HttpChannel, Interceptor)}
* and is used by the implementation to buffer outgoing content.
*/
protected interface BufferedInterceptor extends HttpOutput.Interceptor
{
}
private class ArrayBufferedInterceptor implements BufferedInterceptor
{
private final Interceptor _next;
private final HttpChannel _channel;
private final Queue<ByteBuffer> _buffers = new ArrayDeque<>();
private Boolean _aggregating;
private ByteBuffer _aggregate;
public ArrayBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
_next = interceptor;
_channel = httpChannel;
}
@Override
public void resetBuffer()
{
_buffers.clear();
_aggregating = null;
_aggregate = null;
}
;
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
// if we are not committed, have to decide if we should aggregate or not
if (_aggregating == null)
{
Response response = _channel.getResponse();
int sc = response.getStatus();
if (sc > 0 && (sc < 200 || sc == 204 || sc == 205 || sc >= 300))
_aggregating = Boolean.FALSE; // No body
else
{
String ct = response.getContentType();
if (ct == null)
_aggregating = Boolean.TRUE;
else
{
ct = MimeTypes.getContentTypeWithoutCharset(ct);
_aggregating = isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
}
}
}
// If we are not aggregating, then handle normally
if (!_aggregating.booleanValue())
{
getNextInterceptor().write(content, last, callback);
return;
}
// If last
if (last)
{
// Add the current content to the buffer list without a copy
if (BufferUtil.length(content) > 0)
_buffers.add(content);
if (LOG.isDebugEnabled())
LOG.debug("{} committing {}", this, _buffers.size());
commit(_buffers, callback);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("{} aggregating", this);
// Aggregate the content into buffer chain
while (BufferUtil.hasContent(content))
{
// Do we need a new aggregate buffer
if (BufferUtil.space(_aggregate) == 0)
{
int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(), BufferUtil.length(content));
_aggregate = BufferUtil.allocate(size); // TODO use a buffer pool
_buffers.add(_aggregate);
}
BufferUtil.append(_aggregate, content);
}
callback.succeeded();
}
}
@Override
public Interceptor getNextInterceptor()
{
return _next;
}
protected void commit(Queue<ByteBuffer> buffers, Callback callback)
@Override
public void resetBuffer()
{
// If only 1 buffer
if (_buffers.size() == 0)
getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
else if (_buffers.size() == 1)
// just flush it with the last callback
getNextInterceptor().write(_buffers.remove(), true, callback);
_buffers.clear();
_aggregating = null;
_aggregate = null;
BufferedInterceptor.super.resetBuffer();
}
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
// If we are not committed, have to decide if we should aggregate or not.
if (_aggregating == null)
_aggregating = shouldBuffer(_channel, last);
// If we are not aggregating, then handle normally.
if (!_aggregating)
{
getNextInterceptor().write(content, last, callback);
return;
}
if (last)
{
// Add the current content to the buffer list without a copy.
if (BufferUtil.length(content) > 0)
_buffers.offer(content);
if (LOG.isDebugEnabled())
LOG.debug("{} committing {}", this, _buffers.size());
commit(callback);
}
else
{
// Create an iterating callback to do the writing
if (LOG.isDebugEnabled())
LOG.debug("{} aggregating", this);
// Aggregate the content into buffer chain.
while (BufferUtil.hasContent(content))
{
// Do we need a new aggregate buffer.
if (BufferUtil.space(_aggregate) == 0)
{
// TODO: use a buffer pool always allocating with outputBufferSize to avoid polluting the ByteBufferPool.
int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(), BufferUtil.length(content));
_aggregate = BufferUtil.allocate(size);
_buffers.offer(_aggregate);
}
BufferUtil.append(_aggregate, content);
}
callback.succeeded();
}
}
private void commit(Callback callback)
{
if (_buffers.size() == 0)
{
getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
}
else if (_buffers.size() == 1)
{
getNextInterceptor().write(_buffers.poll(), true, callback);
}
else
{
// Create an iterating callback to do the writing.
IteratingCallback icb = new IteratingCallback()
{
@Override
protected Action process() throws Exception
protected Action process()
{
ByteBuffer buffer = _buffers.poll();
if (buffer == null)
@ -289,14 +311,14 @@ public class BufferedResponseHandler extends HandlerWrapper
@Override
protected void onCompleteSuccess()
{
// Signal last callback
// Signal last callback.
callback.succeeded();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
// Signal last callback
// Signal last callback.
callback.failed(cause);
}
};

View File

@ -0,0 +1,232 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server.handler;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpOutput.Interceptor;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.IteratingCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
* mechanism to buffer the entire response content until the output is closed.
* This allows the commit to be delayed until the response is complete and thus
* headers and response status can be changed while writing the body.
* </p>
* <p>
* Note that the decision to buffer is influenced by the headers and status at the
* first write, and thus subsequent changes to those headers will not influence the
* decision to buffer or not.
* </p>
* <p>
* Note also that there are no memory limits to the size of the buffer, thus
* this handler can represent an unbounded memory commitment if the content
* generated can also be unbounded.
* </p>
*/
public class FileBufferedResponseHandler extends BufferedResponseHandler
{
private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandler.class);
private Path _tempDir = new File(System.getProperty("java.io.tmpdir")).toPath();
public Path getTempDir()
{
return _tempDir;
}
public void setTempDir(Path tempDir)
{
_tempDir = Objects.requireNonNull(tempDir);
}
@Override
protected BufferedInterceptor newBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
return new FileBufferedInterceptor(httpChannel, interceptor);
}
private class FileBufferedInterceptor implements BufferedResponseHandler.BufferedInterceptor
{
private static final int MAX_MAPPED_BUFFER_SIZE = Integer.MAX_VALUE / 2;
private final Interceptor _next;
private final HttpChannel _channel;
private Boolean _aggregating;
private Path _filePath;
private OutputStream _fileOutputStream;
public FileBufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
_next = interceptor;
_channel = httpChannel;
}
@Override
public Interceptor getNextInterceptor()
{
return _next;
}
@Override
public void resetBuffer()
{
dispose();
BufferedInterceptor.super.resetBuffer();
}
private void dispose()
{
IO.close(_fileOutputStream);
_fileOutputStream = null;
_aggregating = null;
if (_filePath != null)
{
try
{
Files.delete(_filePath);
}
catch (Throwable t)
{
LOG.warn("Could not delete file {}", _filePath, t);
}
_filePath = null;
}
}
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("{} write last={} {}", this, last, BufferUtil.toDetailString(content));
// If we are not committed, must decide if we should aggregate or not.
if (_aggregating == null)
_aggregating = shouldBuffer(_channel, last);
// If we are not aggregating, then handle normally.
if (!_aggregating)
{
getNextInterceptor().write(content, last, callback);
return;
}
if (LOG.isDebugEnabled())
LOG.debug("{} aggregating", this);
try
{
if (BufferUtil.hasContent(content))
aggregate(content);
}
catch (Throwable t)
{
dispose();
callback.failed(t);
return;
}
if (last)
commit(callback);
else
callback.succeeded();
}
private void aggregate(ByteBuffer content) throws IOException
{
if (_fileOutputStream == null)
{
// Create a new OutputStream to a file.
_filePath = Files.createTempFile(_tempDir, "BufferedResponse", "");
_fileOutputStream = Files.newOutputStream(_filePath, StandardOpenOption.WRITE);
}
BufferUtil.writeTo(content, _fileOutputStream);
}
private void commit(Callback callback)
{
if (_fileOutputStream == null)
{
// We have no content to write, signal next interceptor that we are finished.
getNextInterceptor().write(BufferUtil.EMPTY_BUFFER, true, callback);
return;
}
try
{
_fileOutputStream.close();
_fileOutputStream = null;
}
catch (Throwable t)
{
dispose();
callback.failed(t);
return;
}
// Create an iterating callback to do the writing
IteratingCallback icb = new IteratingCallback()
{
private final long fileLength = _filePath.toFile().length();
private long _pos = 0;
private boolean _last = false;
@Override
protected Action process() throws Exception
{
if (_last)
return Action.SUCCEEDED;
long len = Math.min(MAX_MAPPED_BUFFER_SIZE, fileLength - _pos);
_last = (_pos + len == fileLength);
ByteBuffer buffer = BufferUtil.toMappedBuffer(_filePath, _pos, len);
getNextInterceptor().write(buffer, _last, this);
_pos += len;
return Action.SCHEDULED;
}
@Override
protected void onCompleteSuccess()
{
dispose();
callback.succeeded();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
dispose();
callback.failed(cause);
}
};
icb.iterate();
}
}
}

View File

@ -0,0 +1,609 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.FileBufferedResponseHandler;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FileBufferedResponseHandlerTest
{
private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandlerTest.class);
private Server _server;
private LocalConnector _localConnector;
private ServerConnector _serverConnector;
private Path _testDir;
private FileBufferedResponseHandler _bufferedHandler;
@BeforeEach
public void before() throws Exception
{
_testDir = MavenTestingUtils.getTargetTestingPath(FileBufferedResponseHandlerTest.class.getName());
FS.ensureDirExists(_testDir);
_server = new Server();
HttpConfiguration config = new HttpConfiguration();
config.setOutputBufferSize(1024);
config.setOutputAggregationSize(256);
_localConnector = new LocalConnector(_server, new HttpConnectionFactory(config));
_localConnector.setIdleTimeout(Duration.ofMinutes(1).toMillis());
_server.addConnector(_localConnector);
_serverConnector = new ServerConnector(_server, new HttpConnectionFactory(config));
_server.addConnector(_serverConnector);
_bufferedHandler = new FileBufferedResponseHandler();
_bufferedHandler.setTempDir(_testDir);
_bufferedHandler.getPathIncludeExclude().include("/include/*");
_bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
_bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
_server.setHandler(_bufferedHandler);
FS.ensureEmpty(_testDir);
}
@AfterEach
public void after() throws Exception
{
_server.stop();
}
@Test
public void testPathNotIncluded() throws Exception
{
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was committed after the first write and we never created a file to buffer the response into.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: true"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testIncludedByPath() throws Exception
{
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was not committed after the first write and a file was created to buffer the response.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: false"));
assertThat(responseContent, containsString("NumFiles: 1"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testExcludedByPath() throws Exception
{
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was committed after the first write and we never created a file to buffer the response into.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: true"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testExcludedByMime() throws Exception
{
String excludedMimeType = "text/excluded";
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setContentType(excludedMimeType);
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was committed after the first write and we never created a file to buffer the response into.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("Committed: true"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testFlushed() throws Exception
{
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(1024);
PrintWriter writer = response.getWriter();
writer.println("a string smaller than the buffer size");
writer.println("NumFilesBeforeFlush: " + getNumFiles());
writer.flush();
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The response was not committed after the buffer was flushed and a file was created to buffer the response.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("NumFilesBeforeFlush: 0"));
assertThat(responseContent, containsString("Committed: false"));
assertThat(responseContent, containsString("NumFiles: 1"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testClosed() throws Exception
{
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(10);
PrintWriter writer = response.getWriter();
writer.println("a string larger than the buffer size");
writer.println("NumFiles: " + getNumFiles());
writer.close();
writer.println("writtenAfterClose");
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The content written after close was not sent.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, not(containsString("writtenAfterClose")));
assertThat(responseContent, containsString("NumFiles: 1"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testBufferSizeBig() throws Exception
{
int bufferSize = 4096;
String largeContent = generateContent(bufferSize - 64);
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(bufferSize);
PrintWriter writer = response.getWriter();
writer.println(largeContent);
writer.println("Committed: " + response.isCommitted());
writer.println("NumFiles: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The content written was not buffered as a file as it was less than the buffer size.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, not(containsString("writtenAfterClose")));
assertThat(responseContent, containsString("Committed: false"));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testFlushEmpty() throws Exception
{
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(1024);
PrintWriter writer = response.getWriter();
writer.flush();
int numFiles = getNumFiles();
writer.println("NumFiles: " + numFiles);
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// The flush should not create the file unless there is content to write.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, containsString("NumFiles: 0"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testReset() throws Exception
{
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
response.setBufferSize(8);
PrintWriter writer = response.getWriter();
writer.println("THIS WILL BE RESET");
writer.flush();
writer.println("THIS WILL BE RESET");
int numFilesBeforeReset = getNumFiles();
response.resetBuffer();
int numFilesAfterReset = getNumFiles();
writer.println("NumFilesBeforeReset: " + numFilesBeforeReset);
writer.println("NumFilesAfterReset: " + numFilesAfterReset);
writer.println("a string larger than the buffer size");
writer.println("NumFilesAfterWrite: " + getNumFiles());
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent();
// Resetting the response buffer will delete the file.
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(responseContent, not(containsString("THIS WILL BE RESET")));
assertThat(responseContent, containsString("NumFilesBeforeReset: 1"));
assertThat(responseContent, containsString("NumFilesAfterReset: 0"));
assertThat(responseContent, containsString("NumFilesAfterWrite: 1"));
assertThat(getNumFiles(), is(0));
}
@Test
public void testFileLargerThanMaxInteger() throws Exception
{
long fileSize = Integer.MAX_VALUE + 1234L;
byte[] bytes = randomBytes(1024 * 1024);
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
ServletOutputStream outputStream = response.getOutputStream();
long written = 0;
while (written < fileSize)
{
int length = Math.toIntExact(Math.min(bytes.length, fileSize - written));
outputStream.write(bytes, 0, length);
written += length;
}
outputStream.flush();
response.setHeader("NumFiles", Integer.toString(getNumFiles()));
response.setHeader("FileSize", Long.toString(getFileSize()));
}
});
_server.start();
AtomicLong received = new AtomicLong();
HttpTester.Response response = new HttpTester.Response()
{
@Override
public boolean content(ByteBuffer ref)
{
// Verify the content is what was sent.
while (ref.hasRemaining())
{
byte byteFromBuffer = ref.get();
long totalReceived = received.getAndIncrement();
int bytesIndex = (int)(totalReceived % bytes.length);
byte byteFromArray = bytes[bytesIndex];
if (byteFromBuffer != byteFromArray)
{
LOG.warn("Mismatch at index {} received bytes {}, {}!={}", bytesIndex, totalReceived, byteFromBuffer, byteFromArray, new IllegalStateException());
return true;
}
}
return false;
}
};
try (Socket socket = new Socket("localhost", _serverConnector.getLocalPort()))
{
OutputStream output = socket.getOutputStream();
String request = "GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
HttpTester.Input input = HttpTester.from(socket.getInputStream());
HttpTester.parseResponse(input, response);
}
assertTrue(response.isComplete());
assertThat(response.get("NumFiles"), is("1"));
assertThat(response.get("FileSize"), is(Long.toString(fileSize)));
assertThat(received.get(), is(fileSize));
assertThat(getNumFiles(), is(0));
}
@Test
public void testNextInterceptorFailed() throws Exception
{
AbstractHandler failingInterceptorHandler = new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
HttpOutput httpOutput = baseRequest.getResponse().getHttpOutput();
HttpOutput.Interceptor nextInterceptor = httpOutput.getInterceptor();
httpOutput.setInterceptor(new HttpOutput.Interceptor()
{
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
callback.failed(new Throwable("intentionally throwing from interceptor"));
}
@Override
public HttpOutput.Interceptor getNextInterceptor()
{
return nextInterceptor;
}
});
}
};
_server.setHandler(new HandlerCollection(failingInterceptorHandler, _server.getHandler()));
CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
byte[] chunk1 = "this content will ".getBytes();
byte[] chunk2 = "be buffered in a file".getBytes();
response.setContentLength(chunk1.length + chunk2.length);
ServletOutputStream outputStream = response.getOutputStream();
// Write chunk1 and then flush so it is written to the file.
outputStream.write(chunk1);
outputStream.flush();
assertThat(getNumFiles(), is(1));
try
{
// ContentLength is set so it knows this is the last write.
// This will cause the file to be written to the next interceptor which will fail.
outputStream.write(chunk2);
}
catch (Throwable t)
{
errorFuture.complete(t);
throw t;
}
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// Response was aborted.
assertThat(response.getStatus(), is(0));
// We failed because of the next interceptor.
Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
assertThat(error.getMessage(), containsString("intentionally throwing from interceptor"));
// All files were deleted.
assertThat(getNumFiles(), is(0));
}
@Test
public void testFileWriteFailed() throws Exception
{
// Set the temp directory to an empty directory so that the file cannot be created.
File tempDir = MavenTestingUtils.getTargetTestingDir(getClass().getSimpleName());
FS.ensureDeleted(tempDir);
_bufferedHandler.setTempDir(tempDir.toPath());
CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
_bufferedHandler.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
baseRequest.setHandled(true);
ServletOutputStream outputStream = response.getOutputStream();
byte[] content = "this content will be buffered in a file".getBytes();
try
{
// Write the content and flush it to the file.
// This should throw as it cannot create the file to aggregate into.
outputStream.write(content);
outputStream.flush();
}
catch (Throwable t)
{
errorFuture.complete(t);
throw t;
}
}
});
_server.start();
String rawResponse = _localConnector.getResponse("GET /include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// Response was aborted.
assertThat(response.getStatus(), is(0));
// We failed because cannot create the file.
Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
assertThat(error, instanceOf(NoSuchFileException.class));
// No files were created.
assertThat(getNumFiles(), is(0));
}
private int getNumFiles()
{
File[] files = _testDir.toFile().listFiles();
if (files == null)
return 0;
return files.length;
}
private long getFileSize()
{
File[] files = _testDir.toFile().listFiles();
assertNotNull(files);
assertThat(files.length, is(1));
return files[0].length();
}
private static String generateContent(int size)
{
Random random = new Random();
StringBuilder stringBuilder = new StringBuilder(size);
for (int i = 0; i < size; i++)
{
stringBuilder.append((char)Math.abs(random.nextInt(0x7F)));
}
return stringBuilder.toString();
}
@SuppressWarnings("SameParameterValue")
private byte[] randomBytes(int size)
{
byte[] data = new byte[size];
new Random().nextBytes(data);
return data;
}
}

View File

@ -32,6 +32,8 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -40,6 +42,7 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.logging.StacklessLogging;
@ -47,6 +50,7 @@ import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -59,9 +63,11 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpConnectionTest
@ -1353,6 +1359,68 @@ public class HttpConnectionTest
}
}
@Test
public void testBytesIn() throws Exception
{
String chunk1 = "0123456789ABCDEF";
String chunk2 = IntStream.range(0, 64).mapToObj(i -> chunk1).collect(Collectors.joining());
long dataLength = chunk1.length() + chunk2.length();
server.stop();
server.setHandler(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
jettyRequest.setHandled(true);
IO.copy(request.getInputStream(), IO.getNullStream());
HttpConnection connection = HttpConnection.getCurrentConnection();
long bytesIn = connection.getBytesIn();
assertThat(bytesIn, greaterThan(dataLength));
}
});
server.start();
LocalEndPoint localEndPoint = connector.executeRequest("" +
"POST / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Content-Length: " + dataLength + "\r\n" +
"\r\n" +
chunk1);
// Wait for the server to block on the read().
Thread.sleep(500);
// Send more content.
localEndPoint.addInput(chunk2);
HttpTester.Response response = HttpTester.parseResponse(localEndPoint.getResponse());
assertEquals(response.getStatus(), HttpStatus.OK_200);
localEndPoint.close();
localEndPoint = connector.executeRequest("" +
"POST / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
Integer.toHexString(chunk1.length()) + "\r\n" +
chunk1 + "\r\n");
// Wait for the server to block on the read().
Thread.sleep(500);
// Send more content.
localEndPoint.addInput("" +
Integer.toHexString(chunk2.length()) + "\r\n" +
chunk2 + "\r\n" +
"0\r\n" +
"\r\n");
response = HttpTester.parseResponse(localEndPoint.getResponse());
assertEquals(response.getStatus(), HttpStatus.OK_200);
localEndPoint.close();
}
private int checkContains(String s, int offset, String c)
{
assertThat(s.substring(offset), Matchers.containsString(c));

View File

@ -28,6 +28,7 @@ import java.security.Principal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
@ -1733,12 +1734,45 @@ public class RequestTest
"Host: whatever\r\n" +
"\r\n";
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.DEFAULT);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(new UriCompliance("Test", EnumSet.noneOf(UriCompliance.Violation.class)));
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 400"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.LEGACY);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
assertThat(_connector.getResponse(request), startsWith("HTTP/1.1 200"));
}
@Test
public void testAmbiguousPaths() throws Exception
{
_handler._checker = (request, response) ->
{
response.getOutputStream().println("servletPath=" + request.getServletPath());
response.getOutputStream().println("pathInfo=" + request.getPathInfo());
return true;
};
String request = "GET /unnormal/.././path/ambiguous%2f%2e%2e/%2e;/info HTTP/1.0\r\n" +
"Host: whatever\r\n" +
"\r\n";
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from(EnumSet.of(
UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR,
UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT,
UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER)));
assertThat(_connector.getResponse(request), Matchers.allOf(
startsWith("HTTP/1.1 200"),
containsString("pathInfo=/path/info")));
_connector.getBean(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.from(EnumSet.of(
UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR,
UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT,
UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER,
UriCompliance.Violation.NON_CANONICAL_AMBIGUOUS_PATHS)));
assertThat(_connector.getResponse(request), Matchers.allOf(
startsWith("HTTP/1.1 200"),
containsString("pathInfo=/path/ambiguous/.././info")));
}
@Test
public void testPushBuilder()

View File

@ -39,7 +39,7 @@ import org.eclipse.jetty.start.fileinits.TestFileInitializer;
import org.eclipse.jetty.start.fileinits.UriFileInitializer;
/**
* Build a start configuration in <code>${jetty.base}</code>, including
* Build a start configuration in {@code ${jetty.base}}, including
* ini files, directories, and libs. Also handles License management.
*/
public class BaseBuilder
@ -47,7 +47,7 @@ public class BaseBuilder
public static interface Config
{
/**
* Add a module to the start environment in <code>${jetty.base}</code>
* Add a module to the start environment in {@code ${jetty.base}}
*
* @param module the module to add
* @param props The properties to substitute into a template
@ -163,7 +163,7 @@ public class BaseBuilder
}
// generate the files
List<FileArg> files = new ArrayList<FileArg>();
List<FileArg> files = new ArrayList<>();
AtomicReference<BaseBuilder.Config> builder = new AtomicReference<>();
AtomicBoolean modified = new AtomicBoolean();
@ -184,18 +184,21 @@ public class BaseBuilder
if (Files.exists(startd))
{
// Copy start.d files into start.ini
DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<Path>()
DirectoryStream.Filter<Path> filter = new DirectoryStream.Filter<>()
{
PathMatcher iniMatcher = PathMatchers.getMatcher("glob:**/start.d/*.ini");
private final PathMatcher iniMatcher = PathMatchers.getMatcher("glob:**/start.d/*.ini");
@Override
public boolean accept(Path entry) throws IOException
public boolean accept(Path entry)
{
return iniMatcher.matches(entry);
}
};
List<Path> paths = new ArrayList<>();
for (Path path : Files.newDirectoryStream(startd, filter))
{
paths.add(path);
}
paths.sort(new NaturalSort.Paths());
// Read config from start.d
@ -212,12 +215,16 @@ public class BaseBuilder
try (FileWriter out = new FileWriter(startini.toFile(), true))
{
for (String line : startLines)
{
out.append(line).append(System.lineSeparator());
}
}
// delete start.d files
for (Path path : paths)
{
Files.delete(path);
}
Files.delete(startd);
}
}
@ -264,56 +271,66 @@ public class BaseBuilder
StartLog.warn("Use of both %s and %s is deprecated", getBaseHome().toShortForm(startd), getBaseHome().toShortForm(startini));
builder.set(useStartD ? new StartDirBuilder(this) : new StartIniBuilder(this));
newlyAdded.stream().map(modules::get).forEach(module ->
{
String ini = null;
try
{
if (module.isSkipFilesValidation())
{
StartLog.debug("Skipping [files] validation on %s", module.getName());
}
else
{
// if (explicitly added and ini file modified)
if (startArgs.getStartModules().contains(module.getName()))
{
ini = builder.get().addModule(module, startArgs.getProperties());
if (ini != null)
modified.set(true);
}
for (String file : module.getFiles())
{
files.add(new FileArg(module, startArgs.getProperties().expand(file)));
}
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
if (module.isDynamic())
// Collect the filesystem operations to perform,
// only for those modules that are enabled.
newlyAdded.stream()
.map(modules::get)
.filter(Module::isEnabled)
.forEach(module ->
{
for (String s : module.getEnableSources())
String ini = null;
try
{
StartLog.info("%-15s %s", module.getName(), s);
if (module.isSkipFilesValidation())
{
StartLog.debug("Skipping [files] validation on %s", module.getName());
}
else
{
// if (explicitly added and ini file modified)
if (startArgs.getStartModules().contains(module.getName()))
{
ini = builder.get().addModule(module, startArgs.getProperties());
if (ini != null)
modified.set(true);
}
for (String file : module.getFiles())
{
files.add(new FileArg(module, startArgs.getProperties().expand(file)));
}
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
if (module.isDynamic())
{
for (String s : module.getEnableSources())
{
StartLog.info("%-15s %s", module.getName(), s);
}
}
else if (module.isTransitive())
{
if (module.hasIniTemplate())
{
StartLog.info("%-15s transitively enabled, ini template available with --add-module=%s",
module.getName(),
module.getName());
}
else
{
StartLog.info("%-15s transitively enabled", module.getName());
}
}
}
else if (module.isTransitive())
{
if (module.hasIniTemplate())
StartLog.info("%-15s transitively enabled, ini template available with --add-module=%s",
module.getName(),
module.getName());
else
StartLog.info("%-15s transitively enabled", module.getName());
}
else
{
StartLog.info("%-15s initialized in %s", module.getName(), ini);
}
});
{
StartLog.info("%-15s initialized in %s", module.getName(), ini);
}
});
files.addAll(startArgs.getFiles());
if (!files.isEmpty() && processFileResources(files))
@ -370,7 +387,7 @@ public class BaseBuilder
* @param files the list of {@link FileArg}s to process
* @return true if base directory modified, false if left untouched
*/
private boolean processFileResources(List<FileArg> files) throws IOException
private boolean processFileResources(List<FileArg> files)
{
if ((files == null) || (files.isEmpty()))
{

View File

@ -18,7 +18,6 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@ -552,7 +551,7 @@ public class Modules implements Iterable<Module>
Set<Module> providers = _provided.get(name);
StartLog.debug("Providers of [%s] are %s", name, providers);
if (providers == null || providers.isEmpty())
return Collections.emptySet();
return Set.of();
providers = new HashSet<>(providers);

View File

@ -52,14 +52,8 @@ import org.eclipse.jetty.util.ManifestUtils;
public class StartArgs
{
public static final String VERSION;
public static final Set<String> ALL_PARTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"java",
"opts",
"path",
"main",
"args")));
public static final Set<String> ARG_PARTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
"args")));
public static final Set<String> ALL_PARTS = Set.of("java", "opts", "path", "main", "args");
public static final Set<String> ARG_PARTS = Set.of("args");
static
{
@ -126,12 +120,12 @@ public class StartArgs
/**
* List of enabled modules
*/
private List<String> modules = new ArrayList<>();
private final List<String> modules = new ArrayList<>();
/**
* List of modules to skip [files] section validation
*/
private Set<String> skipFileValidationModules = new HashSet<>();
private final Set<String> skipFileValidationModules = new HashSet<>();
/**
* Map of enabled modules to the source of where that activation occurred
@ -141,56 +135,56 @@ public class StartArgs
/**
* List of all active [files] sections from enabled modules
*/
private List<FileArg> files = new ArrayList<>();
private final List<FileArg> files = new ArrayList<>();
/**
* List of all active [lib] sections from enabled modules
*/
private Classpath classpath;
private final Classpath classpath;
/**
* List of all active [xml] sections from enabled modules
*/
private List<Path> xmls = new ArrayList<>();
private final List<Path> xmls = new ArrayList<>();
/**
* List of all active [jpms] sections for enabled modules
*/
private Set<String> jmodAdds = new LinkedHashSet<>();
private Map<String, Set<String>> jmodPatch = new LinkedHashMap<>();
private Map<String, Set<String>> jmodOpens = new LinkedHashMap<>();
private Map<String, Set<String>> jmodExports = new LinkedHashMap<>();
private Map<String, Set<String>> jmodReads = new LinkedHashMap<>();
private final Set<String> jmodAdds = new LinkedHashSet<>();
private final Map<String, Set<String>> jmodPatch = new LinkedHashMap<>();
private final Map<String, Set<String>> jmodOpens = new LinkedHashMap<>();
private final Map<String, Set<String>> jmodExports = new LinkedHashMap<>();
private final Map<String, Set<String>> jmodReads = new LinkedHashMap<>();
/**
* JVM arguments, found via command line and in all active [exec] sections from enabled modules
*/
private List<String> jvmArgs = new ArrayList<>();
private final List<String> jvmArgs = new ArrayList<>();
/**
* List of all xml references found directly on command line or start.ini
*/
private List<String> xmlRefs = new ArrayList<>();
private final List<String> xmlRefs = new ArrayList<>();
/**
* List of all property references found directly on command line or start.ini
*/
private List<String> propertyFileRefs = new ArrayList<>();
private final List<String> propertyFileRefs = new ArrayList<>();
/**
* List of all property files
*/
private List<Path> propertyFiles = new ArrayList<>();
private final List<Path> propertyFiles = new ArrayList<>();
private Props properties = new Props();
private Map<String, String> systemPropertySource = new HashMap<>();
private List<String> rawLibs = new ArrayList<>();
private final Props properties = new Props();
private final Map<String, String> systemPropertySource = new HashMap<>();
private final List<String> rawLibs = new ArrayList<>();
// jetty.base - build out commands
/**
* --add-module=[module,[module]]
*/
private List<String> startModules = new ArrayList<>();
private final List<String> startModules = new ArrayList<>();
// module inspection commands
/**

View File

@ -25,6 +25,7 @@ import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
@ -1032,9 +1033,14 @@ public class BufferUtil
public static ByteBuffer toMappedBuffer(File file) throws IOException
{
try (FileChannel channel = FileChannel.open(file.toPath(), StandardOpenOption.READ))
return toMappedBuffer(file.toPath(), 0, file.length());
}
public static ByteBuffer toMappedBuffer(Path filePath, long pos, long len) throws IOException
{
try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ))
{
return channel.map(MapMode.READ_ONLY, 0, file.length());
return channel.map(MapMode.READ_ONLY, pos, len);
}
}

View File

@ -95,7 +95,9 @@ public class MessageInputStream extends InputStream implements MessageSink
@Override
public int read(final byte[] b, final int off, final int len) throws IOException
{
return read(ByteBuffer.wrap(b, off, len).flip());
ByteBuffer buffer = ByteBuffer.wrap(b, off, len).slice();
BufferUtil.clear(buffer);
return read(buffer);
}
public int read(ByteBuffer buffer) throws IOException

View File

@ -51,7 +51,7 @@ import org.testcontainers.utility.MountableFile;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Disabled
@Disabled("Disable this test so it doesn't run locally as it takes 1h+ to run.")
@Testcontainers
public class AutobahnTests
{

View File

@ -266,6 +266,10 @@ public class JavaxWebSocketFrameHandler implements FrameHandler
{
notifyOnClose(closeStatus, callback);
container.notifySessionListeners((listener) -> listener.onJavaxWebSocketSessionClosed(session));
// Close AvailableEncoders and AvailableDecoders to call destroy() on any instances of Encoder/Encoder created.
session.getDecoders().close();
session.getEncoders().close();
}
private void notifyOnClose(CloseStatus closeStatus, Callback callback)

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.websocket.javax.common.decoders;
import java.io.Closeable;
import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
@ -30,7 +31,7 @@ import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException;
import org.eclipse.jetty.websocket.core.internal.util.ReflectUtils;
public class AvailableDecoders implements Iterable<RegisteredDecoder>
public class AvailableDecoders implements Iterable<RegisteredDecoder>, Closeable
{
private final List<RegisteredDecoder> registeredDecoders = new ArrayList<>();
private final EndpointConfig config;
@ -211,4 +212,10 @@ public class AvailableDecoders implements Iterable<RegisteredDecoder>
{
return registeredDecoders.stream();
}
@Override
public void close()
{
registeredDecoders.forEach(RegisteredDecoder::destroyInstance);
}
}

View File

@ -19,10 +19,12 @@ import javax.websocket.EndpointConfig;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.javax.common.InitException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RegisteredDecoder
{
private final WebSocketComponents components;
private static final Logger LOG = LoggerFactory.getLogger(RegisteredDecoder.class);
// The user supplied Decoder class
public final Class<? extends Decoder> decoder;
@ -31,6 +33,7 @@ public class RegisteredDecoder
public final Class<?> objectType;
public final boolean primitive;
public final EndpointConfig config;
private final WebSocketComponents components;
private Decoder instance;
@ -78,6 +81,23 @@ public class RegisteredDecoder
return (T)instance;
}
public void destroyInstance()
{
if (instance != null)
{
try
{
instance.destroy();
}
catch (Throwable t)
{
LOG.warn("Error destroying Decoder", t);
}
instance = null;
}
}
@Override
public String toString()
{

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.websocket.javax.common.encoders;
import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
@ -29,9 +30,12 @@ import org.eclipse.jetty.websocket.core.exception.InvalidSignatureException;
import org.eclipse.jetty.websocket.core.exception.InvalidWebSocketException;
import org.eclipse.jetty.websocket.core.internal.util.ReflectUtils;
import org.eclipse.jetty.websocket.javax.common.InitException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AvailableEncoders implements Predicate<Class<?>>
public class AvailableEncoders implements Predicate<Class<?>>, Closeable
{
private static final Logger LOG = LoggerFactory.getLogger(AvailableEncoders.class);
private final EndpointConfig config;
private final WebSocketComponents components;
@ -241,4 +245,10 @@ public class AvailableEncoders implements Predicate<Class<?>>
{
return registeredEncoders.stream().anyMatch(registered -> registered.isType(type));
}
@Override
public void close()
{
registeredEncoders.forEach(RegisteredEncoder::destroyInstance);
}
}

View File

@ -15,8 +15,13 @@ package org.eclipse.jetty.websocket.javax.common.encoders;
import javax.websocket.Encoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RegisteredEncoder
{
private static final Logger LOG = LoggerFactory.getLogger(RegisteredEncoder.class);
public final Class<? extends Encoder> encoder;
public final Class<? extends Encoder> interfaceType;
public final Class<?> objectType;
@ -46,6 +51,23 @@ public class RegisteredEncoder
return objectType.isAssignableFrom(type);
}
public void destroyInstance()
{
if (instance != null)
{
try
{
instance.destroy();
}
catch (Throwable t)
{
LOG.warn("Error destroying Decoder", t);
}
instance = null;
}
}
@Override
public String toString()
{

View File

@ -37,7 +37,6 @@ import org.eclipse.jetty.websocket.javax.common.encoders.AvailableEncoders;
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.javax.tests.EchoSocket;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
@ -147,8 +146,6 @@ public class EncoderLifeCycleTest
}
}
// TODO: Encoder.destroy() is never called in Jetty 10.
@Disabled()
@ParameterizedTest
@ValueSource(classes = {StringHolder.class, StringHolderSubtype.class})
public void testEncoderLifeCycle(Class<? extends StringHolder> clazz) throws Exception

View File

@ -37,7 +37,6 @@ import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.javax.tests.Fuzzer;
import org.eclipse.jetty.websocket.javax.tests.LocalServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@ -62,11 +61,7 @@ public class SessionTest
else
{
ret.append('[').append(pathParams.size()).append(']');
List<String> keys = new ArrayList<>();
for (String key : pathParams.keySet())
{
keys.add(key);
}
List<String> keys = new ArrayList<>(pathParams.keySet());
Collections.sort(keys);
for (String key : keys)
{
@ -126,11 +121,7 @@ public class SessionTest
else
{
ret.append('[').append(pathParams.size()).append(']');
List<String> keys = new ArrayList<>();
for (String key : pathParams.keySet())
{
keys.add(key);
}
List<String> keys = new ArrayList<>(pathParams.keySet());
Collections.sort(keys);
for (String key : keys)
{
@ -227,14 +218,13 @@ public class SessionTest
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/info/{a}/{b}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/info/{a}/{b}/{c}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/info/{a}/{b}/{c}/{d}/").build());
/*
endpointClass = SessionInfoEndpoint.class;
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/{b}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/{b}/{c}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass,"/einfo/{a}/{b}/{c}/{d}/").build());
*/
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/{b}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/{b}/{c}/").build());
container.addEndpoint(ServerEndpointConfig.Builder.create(endpointClass, "/einfo/{a}/{b}/{c}/{d}/").build());
}
private void assertResponse(String requestPath, String requestMessage,
@ -293,7 +283,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
@Disabled
public void testPathParamsEndpointEmpty(Case testCase) throws Exception
{
setup(testCase);
@ -303,7 +292,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
@Disabled
public void testPathParamsEndpointSingle(Case testCase) throws Exception
{
setup(testCase);
@ -313,7 +301,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
@Disabled
public void testPathParamsEndpointDouble(Case testCase) throws Exception
{
setup(testCase);
@ -323,7 +310,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
@Disabled
public void testPathParamsEndpointTriple(Case testCase) throws Exception
{
setup(testCase);
@ -363,7 +349,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
@Disabled
public void testRequestUriEndpointBasic(Case testCase) throws Exception
{
setup(testCase);
@ -373,7 +358,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
@Disabled
public void testRequestUriEndpointWithPathParam(Case testCase) throws Exception
{
setup(testCase);
@ -383,7 +367,6 @@ public class SessionTest
@ParameterizedTest(name = "{0}")
@MethodSource("data")
@Disabled
public void testRequestUriEndpointWithPathParamWithQuery(Case testCase) throws Exception
{
setup(testCase);

View File

@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.websocket.ClientEndpointConfig;
@ -50,7 +51,6 @@ import org.eclipse.jetty.websocket.javax.tests.WSEndpointTracker;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -116,33 +116,26 @@ public class TextStreamTest
send.add(CloseStatus.toFrame(CloseStatus.NORMAL));
ByteBuffer expectedMessage = DataUtils.copyOf(data);
List<Frame> expect = new ArrayList<>();
expect.add(new Frame(OpCode.TEXT).setPayload(expectedMessage));
expect.add(CloseStatus.toFrame(CloseStatus.NORMAL));
try (Fuzzer fuzzer = server.newNetworkFuzzer("/echo"))
{
fuzzer.sendBulk(send);
fuzzer.expect(expect);
BlockingQueue<Frame> receivedFrames = fuzzer.getOutputFrames();
fuzzer.expectMessage(receivedFrames, OpCode.TEXT, expectedMessage);
fuzzer.expect(List.of(CloseStatus.toFrame(CloseStatus.NORMAL)));
}
}
// TODO These tests incorrectly assumes no frame fragmentation.
// When message fragmentation is implemented in PartialStringMessageSink then update
// this test to check on the server side for no buffers larger than the maxTextMessageBufferSize.
@Disabled
@Test
public void testAtMaxDefaultMessageBufferSize() throws Exception
{
testEcho(container.getDefaultMaxTextMessageBufferSize());
}
@Disabled
@Test
public void testLargerThenMaxDefaultMessageBufferSize() throws Exception
{
int size = container.getDefaultMaxTextMessageBufferSize() + 16;
int maxTextMessageBufferSize = container.getDefaultMaxTextMessageBufferSize();
int size = maxTextMessageBufferSize + 16;
byte[] data = newData(size);
List<Frame> send = new ArrayList<>();
@ -153,19 +146,13 @@ public class TextStreamTest
byte[] expectedData = new byte[data.length];
System.arraycopy(data, 0, expectedData, 0, data.length);
// Frames expected are influenced by container.getDefaultMaxTextMessageBufferSize setting
ByteBuffer frame1 = ByteBuffer.wrap(expectedData, 0, container.getDefaultMaxTextMessageBufferSize());
ByteBuffer frame2 = ByteBuffer
.wrap(expectedData, container.getDefaultMaxTextMessageBufferSize(), size - container.getDefaultMaxTextMessageBufferSize());
List<Frame> expect = new ArrayList<>();
expect.add(new Frame(OpCode.TEXT).setPayload(frame1).setFin(false));
expect.add(new Frame(OpCode.CONTINUATION).setPayload(frame2).setFin(true));
expect.add(CloseStatus.toFrame(CloseStatus.NORMAL));
try (Fuzzer fuzzer = server.newNetworkFuzzer("/echo"))
{
fuzzer.sendBulk(send);
fuzzer.expect(expect);
BlockingQueue<Frame> receivedFrames = fuzzer.getOutputFrames();
fuzzer.expectMessage(receivedFrames, OpCode.TEXT, ByteBuffer.wrap(expectedData));
fuzzer.expect(List.of(CloseStatus.toFrame(CloseStatus.NORMAL)));
}
}

View File

@ -69,6 +69,36 @@ public class MessageInputStreamTest
});
}
@Test
public void testMultipleReadsIntoSingleByteArray() throws IOException
{
try (MessageInputStream stream = new MessageInputStream())
{
// Append a single message (simple, short)
Frame frame = new Frame(OpCode.TEXT);
frame.setPayload("Hello World");
frame.setFin(true);
stream.accept(frame, Callback.NOOP);
// Read entire message it from the stream.
byte[] bytes = new byte[100];
int read = stream.read(bytes, 0, 6);
assertThat(read, is(6));
read = stream.read(bytes, 6, 10);
assertThat(read, is(5));
read = stream.read(bytes, 11, 10);
assertThat(read, is(-1));
String message = new String(bytes, 0, 11, StandardCharsets.UTF_8);
// Test it
assertThat("Message", message, is("Hello World"));
}
}
@Test
public void testBlockOnRead() throws Exception
{

View File

@ -59,6 +59,7 @@
<maven.plugin-tools.version>3.6.0</maven.plugin-tools.version>
<maven.install.plugin.version>3.0.0-M1</maven.install.plugin.version>
<maven.deploy.plugin.version>3.0.0-M1</maven.deploy.plugin.version>
<maven.exec.plugin.version>3.0.0</maven.exec.plugin.version>
<!-- testing -->
<it.debug>false</it.debug>
@ -768,8 +769,7 @@
<require>asciidoctor-diagram</require>
</requires>
<attributes>
<JDURL>http://www.eclipse.org/jetty/javadoc/${project.version}</JDURL>
<JXURL>http://download.eclipse.org/jetty/stable-9/xref</JXURL>
<JDURL>https://www.eclipse.org/jetty/javadoc/jetty-10</JDURL>
<SRCDIR>${basedir}/..</SRCDIR>
<GITBROWSEURL>https://github.com/eclipse/jetty.project/tree/jetty-9.4.x</GITBROWSEURL>
<GITDOCURL>https://github.com/eclipse/jetty.project/tree/jetty-10.0.x-doc-refactor/jetty-documentation/src/main/asciidoc</GITDOCURL>
@ -813,7 +813,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<version>${maven.exec.plugin.version}</version>
</plugin>
<plugin>
<groupId>org.eclipse.m2e</groupId>

View File

@ -859,4 +859,54 @@ public class DistributionTests extends AbstractJettyHomeTest
}
}
}
@Test
public void testDefaultLoggingProviderNotActiveWhenExplicitProviderIsPresent() throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution1 = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
String[] args1 = {
"--approve-all-licenses",
"--add-modules=logging-logback,http"
};
try (JettyHomeTester.Run run1 = distribution1.start(args1))
{
assertTrue(run1.awaitFor(10, TimeUnit.SECONDS));
assertEquals(0, run1.getExitValue());
Path jettyBase = run1.getConfig().getJettyBase();
assertTrue(Files.exists(jettyBase.resolve("resources/logback.xml")));
// The jetty-logging.properties should be absent.
assertFalse(Files.exists(jettyBase.resolve("resources/jetty-logging.properties")));
}
JettyHomeTester distribution2 = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
// Try the modules in reverse order, since it may execute a different code path.
String[] args2 = {
"--approve-all-licenses",
"--add-modules=http,logging-logback"
};
try (JettyHomeTester.Run run2 = distribution2.start(args2))
{
assertTrue(run2.awaitFor(1000, TimeUnit.SECONDS));
assertEquals(0, run2.getExitValue());
Path jettyBase = run2.getConfig().getJettyBase();
assertTrue(Files.exists(jettyBase.resolve("resources/logback.xml")));
// The jetty-logging.properties should be absent.
assertFalse(Files.exists(jettyBase.resolve("resources/jetty-logging.properties")));
}
}
}

View File

@ -32,7 +32,6 @@ import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.IO;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
@ -50,7 +49,6 @@ public class ConnectionStatisticsTest extends AbstractTest<TransportScenario>
Assumptions.assumeTrue(scenario.transport == HTTP || scenario.transport == H2C);
}
@Disabled
@ParameterizedTest
@ArgumentsSource(TransportProvider.class)
public void testConnectionStatistics(Transport transport) throws Exception

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.test;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
@ -27,7 +26,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@ -53,6 +52,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.DateCache;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.junit.jupiter.api.AfterEach;
@ -66,22 +66,24 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
public class CustomRequestLogTest
{
CustomRequestLog _log;
Server _server;
LocalConnector _connector;
BlockingQueue<String> _entries = new BlockingArrayQueue<>();
BlockingQueue<Long> requestTimes = new BlockingArrayQueue<>();
ServerConnector _serverConnector;
URI _serverURI;
private final BlockingQueue<String> _entries = new BlockingArrayQueue<>();
private final BlockingQueue<Long> requestTimes = new BlockingArrayQueue<>();
private CustomRequestLog _log;
private Server _server;
private LocalConnector _connector;
private ServerConnector _serverConnector;
private URI _serverURI;
private static final long DELAY = 2000;
@BeforeEach
public void before() throws Exception
public void before()
{
_server = new Server();
_connector = new LocalConnector(_server);
@ -111,6 +113,7 @@ public class CustomRequestLogTest
_serverURI = new URI(String.format("http://%s:%d/", host, localPort));
}
@SuppressWarnings("SameParameterValue")
private static SecurityHandler getSecurityHandler(String username, String password, String realm)
{
HashLoginService loginService = new HashLoginService();
@ -142,6 +145,22 @@ public class CustomRequestLogTest
_server.stop();
}
@Test
public void testRequestFilter() throws Exception
{
AtomicReference<Boolean> logRequest = new AtomicReference<>();
testHandlerServerStart("RequestPath: %U");
_log.setFilter((request, response) -> logRequest.get());
logRequest.set(true);
_connector.getResponse("GET /path HTTP/1.0\n\n");
assertThat(_entries.poll(5, TimeUnit.SECONDS), is("RequestPath: /path"));
logRequest.set(false);
_connector.getResponse("GET /path HTTP/1.0\n\n");
assertNull(_entries.poll(1, TimeUnit.SECONDS));
}
@Test
public void testLogRemoteUser() throws Exception
{
@ -197,16 +216,16 @@ public class CustomRequestLogTest
"%{server}a|%{server}p|" +
"%{client}a|%{client}p");
Enumeration e = NetworkInterface.getNetworkInterfaces();
Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
while (e.hasMoreElements())
{
NetworkInterface n = (NetworkInterface)e.nextElement();
NetworkInterface n = e.nextElement();
if (n.isLoopback())
{
Enumeration ee = n.getInetAddresses();
Enumeration<InetAddress> ee = n.getInetAddresses();
while (ee.hasMoreElements())
{
InetAddress i = (InetAddress)ee.nextElement();
InetAddress i = ee.nextElement();
try (Socket client = newSocket(i.getHostAddress(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
@ -217,7 +236,7 @@ public class CustomRequestLogTest
os.write(request.getBytes(StandardCharsets.ISO_8859_1));
os.flush();
String[] log = _entries.poll(5, TimeUnit.SECONDS).split("\\|");
String[] log = Objects.requireNonNull(_entries.poll(5, TimeUnit.SECONDS)).split("\\|");
assertThat(log.length, is(8));
String localAddr = log[0];
@ -428,7 +447,7 @@ public class CustomRequestLogTest
_connector.getResponse("GET / HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
long requestTime = requestTimes.poll(5, TimeUnit.SECONDS);
long requestTime = getTimeRequestReceived();
DateCache dateCache = new DateCache(CustomRequestLog.DEFAULT_DATE_FORMAT, Locale.getDefault(), "GMT");
assertThat(log, is("RequestTime: [" + dateCache.format(requestTime) + "]"));
}
@ -442,7 +461,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET / HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
long requestTime = requestTimes.poll(5, TimeUnit.SECONDS);
assertNotNull(log);
long requestTime = getTimeRequestReceived();
DateCache dateCache1 = new DateCache("EEE MMM dd HH:mm:ss zzz yyyy", Locale.getDefault(), "GMT");
DateCache dateCache2 = new DateCache("EEE MMM dd HH:mm:ss zzz yyyy", Locale.getDefault(), "EST");
@ -461,7 +481,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /delay HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
long lowerBound = requestTimes.poll(5, TimeUnit.SECONDS);
assertNotNull(log);
long lowerBound = getTimeRequestReceived();
long upperBound = System.currentTimeMillis();
long measuredDuration = Long.parseLong(log);
@ -479,7 +500,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /delay HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
long lowerBound = requestTimes.poll(5, TimeUnit.SECONDS);
assertNotNull(log);
long lowerBound = getTimeRequestReceived();
long upperBound = System.currentTimeMillis();
long measuredDuration = Long.parseLong(log);
@ -497,7 +519,8 @@ public class CustomRequestLogTest
_connector.getResponse("GET /delay HTTP/1.0\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
long lowerBound = requestTimes.poll(5, TimeUnit.SECONDS);
assertNotNull(log);
long lowerBound = getTimeRequestReceived();
long upperBound = System.currentTimeMillis();
long measuredDuration = Long.parseLong(log);
@ -575,11 +598,6 @@ public class CustomRequestLogTest
fail(log);
}
protected Socket newSocket() throws Exception
{
return newSocket(_serverURI.getHost(), _serverURI.getPort());
}
protected Socket newSocket(String host, int port) throws Exception
{
Socket socket = new Socket(host, port);
@ -604,10 +622,17 @@ public class CustomRequestLogTest
}
}
private long getTimeRequestReceived() throws InterruptedException
{
Long requestTime = requestTimes.poll(5, TimeUnit.SECONDS);
assertNotNull(requestTime);
return requestTime;
}
private class TestServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(request));
@ -652,8 +677,7 @@ public class CustomRequestLogTest
if (request.getContentLength() > 0)
{
InputStream in = request.getInputStream();
while (in.read() > 0);
IO.readBytes(request.getInputStream());
}
}
}

View File

@ -251,7 +251,8 @@ public class GzipWithSendErrorTest
assertThat("Request Input Content Received", inputContentReceived.get(), is(0L));
assertThat("Request Input Content Received less then initial buffer", inputContentReceived.get(), lessThanOrEqualTo((long)sizeActuallySent));
assertThat("Request Connection BytesIn should have some minimal data", inputBytesIn.get(), greaterThanOrEqualTo(1024L));
assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo((long)sizeActuallySent));
long requestBytesSent = sizeActuallySent + 512; // Take into account headers and chunked metadata.
assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo(requestBytesSent));
// Now provide rest
content.offer(ByteBuffer.wrap(compressedRequest, sizeActuallySent, compressedRequest.length - sizeActuallySent));
@ -351,7 +352,8 @@ public class GzipWithSendErrorTest
assertThat("Request Input Content Received", inputContentReceived.get(), read ? greaterThan(0L) : is(0L));
assertThat("Request Input Content Received less then initial buffer", inputContentReceived.get(), lessThanOrEqualTo((long)sizeActuallySent));
assertThat("Request Connection BytesIn should have some minimal data", inputBytesIn.get(), greaterThanOrEqualTo(1024L));
assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo((long)sizeActuallySent));
long requestBytesSent = sizeActuallySent + 512; // Take into account headers and chunked metadata.
assertThat("Request Connection BytesIn read should not have read all of the data", inputBytesIn.get(), lessThanOrEqualTo(requestBytesSent));
// Now provide rest
content.offer(ByteBuffer.wrap(compressedRequest, sizeActuallySent, compressedRequest.length - sizeActuallySent));

View File

@ -0,0 +1,337 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.test;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.AsyncContext;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.AsyncRequestContent;
import org.eclipse.jetty.client.util.BytesRequestContent;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.HttpInput;
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.IO;
import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpInputInterceptorTest
{
private Server server;
private HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory();
private ServerConnector connector;
private HttpClient client;
private void start(Handler handler) throws Exception
{
server = new Server();
connector = new ServerConnector(server, 1, 1, httpConnectionFactory);
server.addConnector(connector);
server.setHandler(handler);
client = new HttpClient();
server.addBean(client);
server.start();
}
@AfterEach
public void dispose()
{
LifeCycle.stop(server);
}
@Test
public void testBlockingReadInterceptorThrows() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
start(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
jettyRequest.setHandled(true);
// Throw immediately from the interceptor.
jettyRequest.getHttpInput().addInterceptor(content ->
{
throw new RuntimeException();
});
assertThrows(IOException.class, () -> IO.readBytes(request.getInputStream()));
serverLatch.countDown();
response.setStatus(HttpStatus.NO_CONTENT_204);
}
});
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.body(new BytesRequestContent(new byte[1]))
.timeout(5, TimeUnit.SECONDS)
.send();
assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
}
@Test
public void testBlockingReadInterceptorConsumesHalfThenThrows() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
start(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
jettyRequest.setHandled(true);
// Consume some and then throw.
AtomicInteger readCount = new AtomicInteger();
jettyRequest.getHttpInput().addInterceptor(content ->
{
int reads = readCount.incrementAndGet();
if (reads == 1)
{
ByteBuffer buffer = content.getByteBuffer();
int half = buffer.remaining() / 2;
int limit = buffer.limit();
buffer.limit(buffer.position() + half);
ByteBuffer chunk = buffer.slice();
buffer.position(buffer.limit());
buffer.limit(limit);
return new HttpInput.Content(chunk);
}
throw new RuntimeException();
});
assertThrows(IOException.class, () -> IO.readBytes(request.getInputStream()));
serverLatch.countDown();
response.setStatus(HttpStatus.NO_CONTENT_204);
}
});
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.body(new BytesRequestContent(new byte[1024]))
.timeout(5, TimeUnit.SECONDS)
.send();
assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
}
@Test
public void testAvailableReadInterceptorThrows() throws Exception
{
CountDownLatch interceptorLatch = new CountDownLatch(1);
start(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
jettyRequest.setHandled(true);
// Throw immediately from the interceptor.
jettyRequest.getHttpInput().addInterceptor(content ->
{
interceptorLatch.countDown();
throw new RuntimeException();
});
int available = request.getInputStream().available();
assertEquals(0, available);
}
});
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.body(new BytesRequestContent(new byte[1]))
.timeout(5, TimeUnit.SECONDS)
.send();
assertTrue(interceptorLatch.await(5, TimeUnit.SECONDS));
assertEquals(HttpStatus.OK_200, response.getStatus());
}
@Test
public void testIsReadyReadInterceptorThrows() throws Exception
{
AsyncRequestContent asyncRequestContent = new AsyncRequestContent(ByteBuffer.wrap(new byte[1]));
CountDownLatch interceptorLatch = new CountDownLatch(1);
CountDownLatch readFailureLatch = new CountDownLatch(1);
start(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
jettyRequest.setHandled(true);
AtomicBoolean onDataAvailable = new AtomicBoolean();
jettyRequest.getHttpInput().addInterceptor(content ->
{
if (onDataAvailable.get())
{
interceptorLatch.countDown();
throw new RuntimeException();
}
else
{
return content;
}
});
AsyncContext asyncContext = request.startAsync();
ServletInputStream input = request.getInputStream();
input.setReadListener(new ReadListener()
{
@Override
public void onDataAvailable()
{
onDataAvailable.set(true);
// The input.setReadListener() call called the interceptor so there is content for read().
assertThat(input.isReady(), is(true));
assertDoesNotThrow(() -> assertEquals(0, input.read()));
// Make the client send more content so that the interceptor will be called again.
asyncRequestContent.offer(ByteBuffer.wrap(new byte[1]));
asyncRequestContent.close();
sleep(500); // Wait a little to make sure the content arrived by next isReady() call.
// The interceptor should throw, but isReady() should not.
assertThat(input.isReady(), is(true));
assertThrows(IOException.class, () -> assertEquals(0, input.read()));
readFailureLatch.countDown();
response.setStatus(HttpStatus.NO_CONTENT_204);
asyncContext.complete();
}
@Override
public void onAllDataRead()
{
}
@Override
public void onError(Throwable error)
{
error.printStackTrace();
}
});
}
});
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.body(asyncRequestContent)
.timeout(5, TimeUnit.SECONDS)
.send();
assertTrue(interceptorLatch.await(5, TimeUnit.SECONDS));
assertTrue(readFailureLatch.await(5, TimeUnit.SECONDS));
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
}
@Test
public void testSetReadListenerReadInterceptorThrows() throws Exception
{
RuntimeException failure = new RuntimeException();
CountDownLatch interceptorLatch = new CountDownLatch(1);
start(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
jettyRequest.setHandled(true);
// Throw immediately from the interceptor.
jettyRequest.getHttpInput().addInterceptor(content ->
{
interceptorLatch.countDown();
failure.addSuppressed(new Throwable());
throw failure;
});
AsyncContext asyncContext = request.startAsync();
ServletInputStream input = request.getInputStream();
input.setReadListener(new ReadListener()
{
@Override
public void onDataAvailable()
{
}
@Override
public void onAllDataRead()
{
}
@Override
public void onError(Throwable error)
{
assertSame(failure, error.getCause());
response.setStatus(HttpStatus.NO_CONTENT_204);
asyncContext.complete();
}
});
}
});
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.body(new BytesRequestContent(new byte[1]))
.timeout(5, TimeUnit.SECONDS)
.send();
assertTrue(interceptorLatch.await(5, TimeUnit.SECONDS));
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
}
private static void sleep(long time)
{
try
{
Thread.sleep(time);
}
catch (InterruptedException x)
{
throw new RuntimeException(x);
}
}
}

View File

@ -37,14 +37,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* SameNodeLoadTest
* ConcurrencyTest
*
* This test performs multiple concurrent requests for the same session on the same node.
* This test performs multiple concurrent requests from different clients
* for the same session on the same node.
*/
public class SameNodeLoadTest
public class ConcurrencyTest
{
@Test
@DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
public void testLoad() throws Exception
{
DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory();
@ -68,17 +68,18 @@ public class SameNodeLoadTest
{
String url = "http://localhost:" + port1 + contextPath + servletMapping;
//create session via first server
//create session upfront so the session id is established and
//can be shared to all clients
ContentResponse response1 = client.GET(url + "?action=init");
assertEquals(HttpServletResponse.SC_OK, response1.getStatus());
String sessionCookie = response1.getHeaders().get("Set-Cookie");
assertTrue(sessionCookie != null);
//simulate 10 clients making 100 requests each
//simulate 10 clients making 10 requests each for the same session
ExecutorService executor = Executors.newCachedThreadPool();
int clientsCount = 10;
CyclicBarrier barrier = new CyclicBarrier(clientsCount + 1);
int requestsCount = 100;
int requestsCount = 10;
Worker[] workers = new Worker[clientsCount];
for (int i = 0; i < clientsCount; ++i)
{
@ -96,7 +97,9 @@ public class SameNodeLoadTest
System.err.println("Elapsed ms:" + elapsed);
executor.shutdownNow();
// Perform one request to get the result
// Perform one request to get the result - the session
// should have counted all the requests by incrementing
// a counter in an attribute.
Request request = client.newRequest(url + "?action=result");
ContentResponse response2 = request.send();
assertEquals(HttpServletResponse.SC_OK, response2.getStatus());