diff --git a/VERSION.txt b/VERSION.txt index 2a5f5a996b1..9a808978213 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -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 diff --git a/documentation/jetty-documentation/pom.xml b/documentation/jetty-documentation/pom.xml index 0bf84544f74..18e19cdb775 100644 --- a/documentation/jetty-documentation/pom.xml +++ b/documentation/jetty-documentation/pom.xml @@ -47,8 +47,7 @@ ${project.basedir} ${settings.localRepository} ../programming-guide/index.html - http://www.eclipse.org/jetty/javadoc/jetty-10 - http://download.eclipse.org/jetty/stable-9/xref + https://www.eclipse.org/jetty/javadoc/jetty-10 ${basedir}/.. https://github.com/eclipse/jetty.project/tree/master https://github.com/eclipse/jetty.project/tree/jetty-10.0.x-doc-refactor/jetty-documentation/src/main/asciidoc diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/index.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/index.adoc index b18d235724e..aaf01516489 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/index.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/index.adoc @@ -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 diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/keystore/keystore-create.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/keystore/keystore-create.adoc index 80b341c38bf..8f9df6085d2 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/keystore/keystore-create.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/keystore/keystore-create.adoc @@ -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. diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/chapter.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/chapter.adoc index fd169616644..68ab3f14f5f 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/chapter.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/chapter.adoc @@ -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 diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-http2.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-http2.adoc index ea4db2adad0..de7c2aa4add 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-http2.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-http2.adoc @@ -143,6 +143,3 @@ microservice3 <--> jetty : HTTP/2 microservice2 <--> microservice3 : HTTP/2 microservice1 <--> microservice3 : HTTP/2 ---- - -include::protocols-http2s.adoc[] -include::protocols-http2c.adoc[] diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-websocket.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-websocket.adoc index f9063b4bc19..adec0056638 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-websocket.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/protocols/protocols-websocket.adoc @@ -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. diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc index 9f6614337b2..e21231941ce 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc @@ -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 diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/index.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/index.adoc index 7c28883bfc6..b7d0222f9b2 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/index.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/index.adoc @@ -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[] diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/maven/jetty-jspc-maven-plugin.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/maven/jetty-jspc-maven-plugin.adoc index 987c4216555..20eb169a3bd 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/maven/jetty-jspc-maven-plugin.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/maven/jetty-jspc-maven-plugin.adoc @@ -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. diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration.adoc new file mode 100644 index 00000000000..51d527a433d --- /dev/null +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/migration/migration.adoc @@ -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(); + } + }); + } +} +---- +|=== \ No newline at end of file diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java index 451760ee51a..67b35f05b0a 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java @@ -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[] diff --git a/jetty-bom/pom.xml b/jetty-bom/pom.xml index 88baa92b7b8..c02fa050ea1 100644 --- a/jetty-bom/pom.xml +++ b/jetty-bom/pom.xml @@ -105,6 +105,11 @@ jetty-client 10.0.3-SNAPSHOT + + org.eclipse.jetty + jetty-cdi + 10.0.3-SNAPSHOT + org.eclipse.jetty jetty-deploy diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java index cdac10161a8..478ec63b023 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java @@ -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); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java index 15b200cc04a..c45f3664e65 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java @@ -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) { diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java index 9c1f8ae80c2..cfe8eb37d40 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java @@ -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: diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java index 656c25e9036..0eb0d557ddd 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java @@ -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. /foo/%2e%2e/bar + * Allow ambiguous path segments e.g. /foo/%2e%2e/bar */ 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. /foo/b%2fr + * Allow ambiguous path separator within a URI segment e.g. /foo/b%2fr */ 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. /foo/..;/bar + * Allow ambiguous path parameters within a URI segment e.g. /foo/..;/bar */ - 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 /foo/x%2f%2e%2e%/bar provided to applications as /foo/x/../bar + */ + 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 violations) + { + return new UriCompliance("CUSTOM" + __custom.getAndIncrement(), violations); + } + /** * Create compliance set from string. *

@@ -151,22 +179,23 @@ public final class UriCompliance implements ComplianceViolation.Mode */ public static UriCompliance from(String spec) { - Set sections; + Set 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 _allowed; - private UriCompliance(String name, Set violations) + public UriCompliance(String name, Set violations) { Objects.requireNonNull(violations); _name = name; diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java index a31995cbd24..7e944e38f55 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java @@ -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"))); } } diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java index 74b8bdf0a2f..cba850eac9a 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/SmallThreadPoolLoadTest.java @@ -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) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java index fd672e39912..9bd94727f72 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java @@ -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 _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; + } } } diff --git a/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java b/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java index 50d9fd01031..0353f10929c 100644 --- a/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java +++ b/jetty-jspc-maven-plugin/src/main/java/org/eclipse/jetty/jspc/plugin/JspcMojo.java @@ -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); diff --git a/jetty-maven-plugin/src/it/it-parent-pom/pom.xml b/jetty-maven-plugin/src/it/it-parent-pom/pom.xml index 443f71b8ebb..72cac69f64b 100644 --- a/jetty-maven-plugin/src/it/it-parent-pom/pom.xml +++ b/jetty-maven-plugin/src/it/it-parent-pom/pom.xml @@ -17,7 +17,7 @@ commons-io commons-io - 2.6 + 2.7 org.eclipse.jetty.toolchain diff --git a/jetty-runner/pom.xml b/jetty-runner/pom.xml index 753e6c4a5a8..5589cc85ba7 100644 --- a/jetty-runner/pom.xml +++ b/jetty-runner/pom.xml @@ -50,6 +50,17 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + org.apache.maven.plugins maven-invoker-plugin @@ -65,8 +76,12 @@ + ${it.debug} + true ${maven.dependency.plugin.version} + ${maven.surefire.version} + ${hamcrest.version} clean @@ -149,5 +164,18 @@ jetty-slf4j-impl runtime + + org.eclipse.jetty.demos + demo-simple-webapp + ${project.version} + war + test + + + org.eclipse.jetty + jetty-client + ${project.version} + test + diff --git a/jetty-runner/src/it/demo-simple-webapp-runner-with-path/invoker.properties b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/invoker.properties new file mode 100644 index 00000000000..fd18ebccf10 --- /dev/null +++ b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/invoker.properties @@ -0,0 +1 @@ +invoker.goals = test diff --git a/jetty-runner/src/it/demo-simple-webapp-runner-with-path/pom.xml b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/pom.xml new file mode 100644 index 00000000000..44b419977fb --- /dev/null +++ b/jetty-runner/src/it/demo-simple-webapp-runner-with-path/pom.xml @@ -0,0 +1,139 @@ + + + + 4.0.0 + + org.eclipse.jetty.its + jetty-runner-it-test-demo-simple-webapp + 1.0.0-SNAPSHOT + war + + + UTF-8 + + + + + + org.eclipse.jetty + jetty-runner + @project.version@ + + + org.eclipse.jetty + jetty-runner + @project.version@ + tests + test-jar + test + + + org.eclipse.jetty.demos + demo-simple-webapp + @project.version@ + war + + + org.eclipse.jetty + jetty-client + @project.version@ + test + + + org.hamcrest + hamcrest-core + @hamcrest.version@ + test + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + @maven.dependency.plugin.version@ + + + copy-jetty-runner + generate-resources + + copy + + + + + org.eclipse.jetty + jetty-runner + @project.version@ + jar + false + ${project.build.directory}/ + jetty-runner.jar + + + org.eclipse.jetty.demos + demo-simple-webapp + @project.version@ + war + false + ${project.build.directory} + demo-simple-webapp.war + + + false + true + + + + + + org.codehaus.mojo + exec-maven-plugin + @maven.exec.plugin.version@ + + + + exec + + generate-test-resources + + ${project.build.directory}/jetty-runner.log + true + ${java.home}/bin/java + + -jar + ${project.build.directory}/jetty-runner.jar + --out + ${project.build.directory}/jetty-runner.out + --port + 0 + --path + french-chocolate-rocks + --server-uri-file + ${project.build.directory}/server-uri.txt + ${project.build.directory}/demo-simple-webapp.war + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + @maven.surefire.version@ + + + IntegrationTest*.java + + + + + org.eclipse.jetty:jetty-runner + + + + + + diff --git a/jetty-runner/src/it/demo-simple-webapp-runner/invoker.properties b/jetty-runner/src/it/demo-simple-webapp-runner/invoker.properties new file mode 100644 index 00000000000..fd18ebccf10 --- /dev/null +++ b/jetty-runner/src/it/demo-simple-webapp-runner/invoker.properties @@ -0,0 +1 @@ +invoker.goals = test diff --git a/jetty-runner/src/it/demo-simple-webapp-runner/pom.xml b/jetty-runner/src/it/demo-simple-webapp-runner/pom.xml new file mode 100644 index 00000000000..df7e6d8e0dc --- /dev/null +++ b/jetty-runner/src/it/demo-simple-webapp-runner/pom.xml @@ -0,0 +1,139 @@ + + + + 4.0.0 + + org.eclipse.jetty.its + jetty-runner-it-test-demo-simple-webapp + 1.0.0-SNAPSHOT + war + + + UTF-8 + + + + + + org.eclipse.jetty + jetty-runner + @project.version@ + + + org.eclipse.jetty + jetty-runner + @project.version@ + tests + test-jar + test + + + org.eclipse.jetty.demos + demo-simple-webapp + @project.version@ + war + + + org.eclipse.jetty + jetty-client + @project.version@ + test + + + org.hamcrest + hamcrest-core + @hamcrest.version@ + test + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + @maven.dependency.plugin.version@ + + + copy-jetty-runner + generate-resources + + copy + + + + + org.eclipse.jetty + jetty-runner + @project.version@ + jar + false + ${project.build.directory}/ + jetty-runner.jar + + + org.eclipse.jetty.demos + demo-simple-webapp + @project.version@ + war + false + ${project.build.directory} + demo-simple-webapp.war + + + false + true + + + + + + org.codehaus.mojo + exec-maven-plugin + @maven.exec.plugin.version@ + + + + exec + + generate-test-resources + + ${project.build.directory}/jetty-runner.log + true + ${java.home}/bin/java + + + + -jar + ${project.build.directory}/jetty-runner.jar + --out + ${project.build.directory}/jetty-runner.out + --port + 0 + --server-uri-file + ${project.build.directory}/server-uri.txt + ${project.build.directory}/demo-simple-webapp.war + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + @maven.surefire.version@ + + + IntegrationTest*.java + + + + + org.eclipse.jetty:jetty-runner + + + + + + diff --git a/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java b/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java index ebaf15d51f8..abc00a97639 100644 --- a/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java +++ b/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java @@ -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 _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(); } diff --git a/jetty-runner/src/test/java/org/eclipse/jetty/maven/jettyrunner/it/IntegrationTestJettyRunner.java b/jetty-runner/src/test/java/org/eclipse/jetty/maven/jettyrunner/it/IntegrationTestJettyRunner.java new file mode 100644 index 00000000000..e505e78cbc1 --- /dev/null +++ b/jetty-runner/src/test/java/org/eclipse/jetty/maven/jettyrunner/it/IntegrationTestJettyRunner.java @@ -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 lines = Files.readAllLines(portTxt); + return lines.get(0); + } + } + + throw new Exception("cannot find started Jetty"); + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java index 72202727e9f..c8484963f6b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java @@ -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(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java index 33db3e00871..9cfd28e557c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java @@ -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 _ignorePathMap; private String[] _ignorePaths; + private BiPredicate _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 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); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index 824b1b21676..728214a7572 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -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; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 7baeffa8442..65a182b24fd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -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; } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 27bf9d2e3b7..95866fb9910 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -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())) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java index 95fb38c85a3..78274594816 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java @@ -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 *

* 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. + *

*

* 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. + *

*

* 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 _methods = new IncludeExclude<>(); private final IncludeExclude _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 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 _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 _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 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); } }; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java new file mode 100644 index 00000000000..e6b6e0b36ee --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/FileBufferedResponseHandler.java @@ -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; + +/** + *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * 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. + *

+ */ +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(); + } + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/FileBufferedResponseHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/FileBufferedResponseHandlerTest.java new file mode 100644 index 00000000000..c6bb92f8b3f --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/FileBufferedResponseHandlerTest.java @@ -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 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 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; + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index 4963430c818..4d1c0d29d4c 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -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)); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 28a736d5b72..2661e609e23 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -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() diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java b/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java index 985da7b50cb..74a35389b4b 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/BaseBuilder.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.start.fileinits.TestFileInitializer; import org.eclipse.jetty.start.fileinits.UriFileInitializer; /** - * Build a start configuration in ${jetty.base}, 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 ${jetty.base} + * 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 files = new ArrayList(); + List files = new ArrayList<>(); AtomicReference 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 filter = new DirectoryStream.Filter() + DirectoryStream.Filter 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 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 files) throws IOException + private boolean processFileResources(List files) { if ((files == null) || (files.isEmpty())) { diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java index 17bc231e8d5..37074630866 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Modules.java @@ -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 Set 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); diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java index 8516c0573d4..46b629b14e5 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java @@ -52,14 +52,8 @@ import org.eclipse.jetty.util.ManifestUtils; public class StartArgs { public static final String VERSION; - public static final Set ALL_PARTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - "java", - "opts", - "path", - "main", - "args"))); - public static final Set ARG_PARTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( - "args"))); + public static final Set ALL_PARTS = Set.of("java", "opts", "path", "main", "args"); + public static final Set ARG_PARTS = Set.of("args"); static { @@ -126,12 +120,12 @@ public class StartArgs /** * List of enabled modules */ - private List modules = new ArrayList<>(); + private final List modules = new ArrayList<>(); /** * List of modules to skip [files] section validation */ - private Set skipFileValidationModules = new HashSet<>(); + private final Set 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 files = new ArrayList<>(); + private final List 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 xmls = new ArrayList<>(); + private final List xmls = new ArrayList<>(); /** * List of all active [jpms] sections for enabled modules */ - private Set jmodAdds = new LinkedHashSet<>(); - private Map> jmodPatch = new LinkedHashMap<>(); - private Map> jmodOpens = new LinkedHashMap<>(); - private Map> jmodExports = new LinkedHashMap<>(); - private Map> jmodReads = new LinkedHashMap<>(); + private final Set jmodAdds = new LinkedHashSet<>(); + private final Map> jmodPatch = new LinkedHashMap<>(); + private final Map> jmodOpens = new LinkedHashMap<>(); + private final Map> jmodExports = new LinkedHashMap<>(); + private final Map> jmodReads = new LinkedHashMap<>(); /** * JVM arguments, found via command line and in all active [exec] sections from enabled modules */ - private List jvmArgs = new ArrayList<>(); + private final List jvmArgs = new ArrayList<>(); /** * List of all xml references found directly on command line or start.ini */ - private List xmlRefs = new ArrayList<>(); + private final List xmlRefs = new ArrayList<>(); /** * List of all property references found directly on command line or start.ini */ - private List propertyFileRefs = new ArrayList<>(); + private final List propertyFileRefs = new ArrayList<>(); /** * List of all property files */ - private List propertyFiles = new ArrayList<>(); + private final List propertyFiles = new ArrayList<>(); - private Props properties = new Props(); - private Map systemPropertySource = new HashMap<>(); - private List rawLibs = new ArrayList<>(); + private final Props properties = new Props(); + private final Map systemPropertySource = new HashMap<>(); + private final List rawLibs = new ArrayList<>(); // jetty.base - build out commands /** * --add-module=[module,[module]] */ - private List startModules = new ArrayList<>(); + private final List startModules = new ArrayList<>(); // module inspection commands /** diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java index 0292223d1e1..227906aa53c 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java @@ -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); } } diff --git a/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java b/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java index bf11dc2e4dc..cad559725e3 100644 --- a/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java +++ b/jetty-websocket/websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/internal/messages/MessageInputStream.java @@ -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 diff --git a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java index 7a4b4232ba3..0346be14eb3 100644 --- a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java +++ b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnTests.java @@ -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 { diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java index 21c6e28a533..99cfd8bb2e6 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java @@ -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) diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java index e0bbe141404..3aa44344cc2 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/AvailableDecoders.java @@ -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 +public class AvailableDecoders implements Iterable, Closeable { private final List registeredDecoders = new ArrayList<>(); private final EndpointConfig config; @@ -211,4 +212,10 @@ public class AvailableDecoders implements Iterable { return registeredDecoders.stream(); } + + @Override + public void close() + { + registeredDecoders.forEach(RegisteredDecoder::destroyInstance); + } } diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java index 3608d7796b4..b2ba440a86c 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/decoders/RegisteredDecoder.java @@ -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 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() { diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java index 45ba8718fa2..96b4fc10b20 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/AvailableEncoders.java @@ -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> +public class AvailableEncoders implements Predicate>, 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> { return registeredEncoders.stream().anyMatch(registered -> registered.isType(type)); } + + @Override + public void close() + { + registeredEncoders.forEach(RegisteredEncoder::destroyInstance); + } } diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java index e15ce8c4a44..c2c108c6e6b 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/encoders/RegisteredEncoder.java @@ -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 encoder; public final Class 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() { diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java index 72acf34cc6d..d8b410d89a2 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderLifeCycleTest.java @@ -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 clazz) throws Exception diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java index a0d0c38a795..9714a3d59a0 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTest.java @@ -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 keys = new ArrayList<>(); - for (String key : pathParams.keySet()) - { - keys.add(key); - } + List 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 keys = new ArrayList<>(); - for (String key : pathParams.keySet()) - { - keys.add(key); - } + List 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); diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java index 15c8773939b..9291affd1f8 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/TextStreamTest.java @@ -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 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 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 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 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 receivedFrames = fuzzer.getOutputFrames(); + fuzzer.expectMessage(receivedFrames, OpCode.TEXT, ByteBuffer.wrap(expectedData)); + fuzzer.expect(List.of(CloseStatus.toFrame(CloseStatus.NORMAL))); } } diff --git a/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java b/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java index ba204f54521..436b1abb689 100644 --- a/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java +++ b/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/MessageInputStreamTest.java @@ -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 { diff --git a/pom.xml b/pom.xml index b6e7682738d..577ffd2c9e6 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ 3.6.0 3.0.0-M1 3.0.0-M1 + 3.0.0 false @@ -768,8 +769,7 @@ asciidoctor-diagram - http://www.eclipse.org/jetty/javadoc/${project.version} - http://download.eclipse.org/jetty/stable-9/xref + https://www.eclipse.org/jetty/javadoc/jetty-10 ${basedir}/.. https://github.com/eclipse/jetty.project/tree/jetty-9.4.x https://github.com/eclipse/jetty.project/tree/jetty-10.0.x-doc-refactor/jetty-documentation/src/main/asciidoc @@ -813,7 +813,7 @@ org.codehaus.mojo exec-maven-plugin - 3.0.0 + ${maven.exec.plugin.version} org.eclipse.m2e diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 0924c38af36..97d940c3ea6 100644 --- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -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"))); + } + } } diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java index f6d3de2a54d..b0ea1757b79 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java @@ -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 Assumptions.assumeTrue(scenario.transport == HTTP || scenario.transport == H2C); } - @Disabled @ParameterizedTest @ArgumentsSource(TransportProvider.class) public void testConnectionStatistics(Transport transport) throws Exception diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java index 41a04b19882..84c8f0cb1af 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/CustomRequestLogTest.java @@ -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 _entries = new BlockingArrayQueue<>(); - BlockingQueue requestTimes = new BlockingArrayQueue<>(); - ServerConnector _serverConnector; - URI _serverURI; + private final BlockingQueue _entries = new BlockingArrayQueue<>(); + private final BlockingQueue 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 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 e = NetworkInterface.getNetworkInterfaces(); while (e.hasMoreElements()) { - NetworkInterface n = (NetworkInterface)e.nextElement(); + NetworkInterface n = e.nextElement(); if (n.isLoopback()) { - Enumeration ee = n.getInetAddresses(); + Enumeration 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()); } } } diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java index c71b048cef3..5261ba53b48 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/GzipWithSendErrorTest.java @@ -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)); diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputInterceptorTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputInterceptorTest.java new file mode 100644 index 00000000000..30824c514ca --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputInterceptorTest.java @@ -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); + } + } +} diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ConcurrencyTest.java similarity index 92% rename from tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java rename to tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ConcurrencyTest.java index a49cfda1c1e..93798a411d8 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ConcurrencyTest.java @@ -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());