From 1f78946a5c1676a06316384fd0e318fd8121df29 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Wed, 5 Jun 2024 20:40:26 -0400 Subject: [PATCH 01/18] Add response to debug log event in HttpSender#failRequest() (#11878) Signed-off-by: Gary Gregory --- .../java/org/eclipse/jetty/client/transport/HttpSender.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java index 7df73f3cd26..c60607572a7 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpSender.java @@ -208,7 +208,7 @@ public abstract class HttpSender return false; if (LOG.isDebugEnabled()) - LOG.debug("Request failure {}", exchange.getRequest(), failure); + LOG.debug("Request failure {}, response {}", exchange.getRequest(), exchange.getResponse(), failure); // Mark atomically the request as completed, with respect // to concurrency between request success and request failure. From 45562b012ad19c28c1eb6b8efeb9a7c9f59dfcec Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Tue, 11 Jun 2024 01:53:19 +0200 Subject: [PATCH 02/18] Issue #11893 fix maven helloworld example (#11896) --- .../maven-jetty/jetty-maven-helloworld.adoc | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc index 64a3b8f3c79..b03f8409793 100644 --- a/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc +++ b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc @@ -47,37 +47,46 @@ Use an editor to create the file `src/main/java/org/example/HelloWorld.java` wit ---- package org.example; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.ServletException; -import java.io.IOException; -import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; -public class HelloWorld extends AbstractHandler +class HelloWorldHandler extends Handler.Abstract.NonBlocking { - public void handle(String target, - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response) - throws IOException, ServletException + @Override + public boolean handle(Request request, Response response, Callback callback) { - response.setContentType("text/html;charset=utf-8"); - response.setStatus(HttpServletResponse.SC_OK); - baseRequest.setHandled(true); - response.getWriter().println("

Hello World

"); - } + response.setStatus(200); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html; charset=UTF-8"); - public static void main(String[] args) throws Exception - { - Server server = new Server(8080); - server.setHandler(new HelloWorld()); - - server.start(); - server.join(); + // Write a Hello World response. + Content.Sink.write(response, true, """ + + + + Jetty Hello World Handler + + +

Hello World

+ + + """, callback); + return true; } } + +Server server = new Server(); +Connector connector = new ServerConnector(server); +server.addConnector(connector); + +// Set the Hello World Handler. +server.setHandler(new HelloWorldHandler()); + +server.start(); +} ---- [[creating-embedded-pom-descriptor]] @@ -116,7 +125,6 @@ Use an editor to create the file `pom.xml` in the `JettyMavenHelloWorld` directo org.codehaus.mojo exec-maven-plugin - 1.1 java From bc24f87303d142f4cc28f765d5f06f0fe7f6cfd1 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 12 Jun 2024 01:11:03 +1000 Subject: [PATCH 03/18] Fix openid.mod files for Jetty 12 Signed-off-by: Lachlan Roberts --- .../jetty-openid-baseloginservice.xml | 0 .../src/main/config/modules/openid.mod | 2 +- jetty-ee10/jetty-ee10-home/pom.xml | 5 -- .../src/main/config/modules/ee10-openid.mod | 10 ---- .../src/main/config/etc/jetty-ee8-openid.xml | 51 ------------------ .../src/main/config/modules/ee8-openid.mod | 42 ++------------- .../jetty-ee8-openid-baseloginservice.xml | 10 ---- .../src/main/config/etc/jetty-ee9-openid.xml | 54 ------------------- .../src/main/config/modules/ee9-openid.mod | 42 +-------------- .../jetty-ee9-openid-baseloginservice.xml | 10 ---- 10 files changed, 6 insertions(+), 220 deletions(-) rename jetty-core/jetty-openid/src/main/config/{modules/openid => etc}/jetty-openid-baseloginservice.xml (100%) delete mode 100644 jetty-ee10/jetty-ee10-servlet/src/main/config/modules/ee10-openid.mod delete mode 100644 jetty-ee8/jetty-ee8-openid/src/main/config/etc/jetty-ee8-openid.xml delete mode 100644 jetty-ee8/jetty-ee8-openid/src/main/config/modules/openid/jetty-ee8-openid-baseloginservice.xml delete mode 100644 jetty-ee9/jetty-ee9-openid/src/main/config/etc/jetty-ee9-openid.xml delete mode 100644 jetty-ee9/jetty-ee9-openid/src/main/config/modules/openid/jetty-ee9-openid-baseloginservice.xml diff --git a/jetty-core/jetty-openid/src/main/config/modules/openid/jetty-openid-baseloginservice.xml b/jetty-core/jetty-openid/src/main/config/etc/jetty-openid-baseloginservice.xml similarity index 100% rename from jetty-core/jetty-openid/src/main/config/modules/openid/jetty-openid-baseloginservice.xml rename to jetty-core/jetty-openid/src/main/config/etc/jetty-openid-baseloginservice.xml diff --git a/jetty-core/jetty-openid/src/main/config/modules/openid.mod b/jetty-core/jetty-openid/src/main/config/modules/openid.mod index 8a1a8265462..d0ab743adec 100644 --- a/jetty-core/jetty-openid/src/main/config/modules/openid.mod +++ b/jetty-core/jetty-openid/src/main/config/modules/openid.mod @@ -12,7 +12,7 @@ lib/jetty-util-ajax-${jetty.version}.jar lib/jetty-openid-${jetty.version}.jar [files] -basehome:modules/openid/jetty-openid-baseloginservice.xml|etc/jetty-openid-baseloginservice.xml +basehome:etc/jetty-openid-baseloginservice.xml|etc/jetty-openid-baseloginservice.xml [xml] etc/jetty-openid-baseloginservice.xml diff --git a/jetty-ee10/jetty-ee10-home/pom.xml b/jetty-ee10/jetty-ee10-home/pom.xml index ff07af0abe3..4684c12ffcc 100644 --- a/jetty-ee10/jetty-ee10-home/pom.xml +++ b/jetty-ee10/jetty-ee10-home/pom.xml @@ -18,11 +18,6 @@ - - org.eclipse.jetty - jetty-openid - true - org.eclipse.jetty jetty-security diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/config/modules/ee10-openid.mod b/jetty-ee10/jetty-ee10-servlet/src/main/config/modules/ee10-openid.mod deleted file mode 100644 index bcddc284d2e..00000000000 --- a/jetty-ee10/jetty-ee10-servlet/src/main/config/modules/ee10-openid.mod +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/ - -[description] -Adds openid security for EE10. - -[environment] -ee10 - -[depend] -openid diff --git a/jetty-ee8/jetty-ee8-openid/src/main/config/etc/jetty-ee8-openid.xml b/jetty-ee8/jetty-ee8-openid/src/main/config/etc/jetty-ee8-openid.xml deleted file mode 100644 index 42c9c7ff14b..00000000000 --- a/jetty-ee8/jetty-ee8-openid/src/main/config/etc/jetty-ee8-openid.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod b/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod index 6cf9d3a83a8..782b970193b 100644 --- a/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod +++ b/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod @@ -3,46 +3,12 @@ [description] Adds OpenId Connect authentication to the server. +[environment] +ee8 + [depend] ee8-security openid -client [lib] -lib/jetty-ee8-openid-${jetty.version}.jar -lib/jetty-util-ajax-${jetty.version}.jar - -[files] -basehome:modules/openid/jetty-ee8-openid-baseloginservice.xml|etc/openid-baseloginservice.xml - -[xml] -etc/openid-baseloginservice.xml -etc/jetty-ee8-openid.xml - -[ini-template] -## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration") -# jetty.openid.provider=https://id.example.com/ - -## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible) -# jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization - -## The OpenID Identity Provider's token endpoint (optional if the metadata of the OP is accessible) -# jetty.openid.provider.tokenEndpoint=https://id.example.com/token - -## The Client Identifier -# jetty.openid.clientId=test1234 - -## The Client Secret -# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P - -## Additional Scopes to Request -# jetty.openid.scopes=email,profile - -## Whether to Authenticate users not found by base LoginService -# jetty.openid.authenticateNewUsers=false - -## True if all certificates should be trusted by the default SslContextFactory -# jetty.openid.sslContextFactory.trustAll=false - -## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic). -# jetty.openid.authenticationMethod=client_secret_post +lib/jetty-ee8-openid-${jetty.version}.jar \ No newline at end of file diff --git a/jetty-ee8/jetty-ee8-openid/src/main/config/modules/openid/jetty-ee8-openid-baseloginservice.xml b/jetty-ee8/jetty-ee8-openid/src/main/config/modules/openid/jetty-ee8-openid-baseloginservice.xml deleted file mode 100644 index 1773ebd46db..00000000000 --- a/jetty-ee8/jetty-ee8-openid/src/main/config/modules/openid/jetty-ee8-openid-baseloginservice.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/jetty-ee9/jetty-ee9-openid/src/main/config/etc/jetty-ee9-openid.xml b/jetty-ee9/jetty-ee9-openid/src/main/config/etc/jetty-ee9-openid.xml deleted file mode 100644 index ed1fdd3331d..00000000000 --- a/jetty-ee9/jetty-ee9-openid/src/main/config/etc/jetty-ee9-openid.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod b/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod index 1ff2509f2e5..417b783aa61 100644 --- a/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod +++ b/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod @@ -9,46 +9,6 @@ ee9 [depend] ee9-security openid -client [lib] -lib/jetty-ee9-openid-${jetty.version}.jar -lib/jetty-util-ajax-${jetty.version}.jar - -[files] -basehome:modules/openid/jetty-ee9-openid-baseloginservice.xml|etc/openid-baseloginservice.xml - -[xml] -etc/openid-baseloginservice.xml -etc/jetty-ee9-openid.xml - -[ini-template] -## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration") -# jetty.openid.provider=https://id.example.com/ - -## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible) -# jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization - -## The OpenID Identity Provider's token endpoint (optional if the metadata of the OP is accessible) -# jetty.openid.provider.tokenEndpoint=https://id.example.com/token - -## The Client Identifier -# jetty.openid.clientId=test1234 - -## The Client Secret -# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P - -## Additional Scopes to Request -# jetty.openid.scopes=email,profile - -## Whether to Authenticate users not found by base LoginService -# jetty.openid.authenticateNewUsers=false - -## True if all certificates should be trusted by the default SslContextFactory -# jetty.openid.sslContextFactory.trustAll=false - -## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic). -# jetty.openid.authenticationMethod=client_secret_post - -## Whether the user should be logged out after the idToken expires. -# jetty.openid.logoutWhenIdTokenIsExpired=false +lib/jetty-ee9-openid-${jetty.version}.jar \ No newline at end of file diff --git a/jetty-ee9/jetty-ee9-openid/src/main/config/modules/openid/jetty-ee9-openid-baseloginservice.xml b/jetty-ee9/jetty-ee9-openid/src/main/config/modules/openid/jetty-ee9-openid-baseloginservice.xml deleted file mode 100644 index 1773ebd46db..00000000000 --- a/jetty-ee9/jetty-ee9-openid/src/main/config/modules/openid/jetty-ee9-openid-baseloginservice.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file From 9d0a457e59d94fd8374c1466ff9613549a7e00c3 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Wed, 12 Jun 2024 03:20:14 +0200 Subject: [PATCH 04/18] Fix #11902 undeprecate addHiddenClasses(Attributes, String...) (#11903) --- .../src/main/java/org/eclipse/jetty/ee/WebAppClassLoading.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/jetty-core/jetty-ee/src/main/java/org/eclipse/jetty/ee/WebAppClassLoading.java b/jetty-core/jetty-ee/src/main/java/org/eclipse/jetty/ee/WebAppClassLoading.java index dcdac85af03..26dc37af71c 100644 --- a/jetty-core/jetty-ee/src/main/java/org/eclipse/jetty/ee/WebAppClassLoading.java +++ b/jetty-core/jetty-ee/src/main/java/org/eclipse/jetty/ee/WebAppClassLoading.java @@ -169,9 +169,7 @@ public class WebAppClassLoading * Add a hidden (server) Class pattern to use for all WebAppContexts of a given {@link Server}. * @param attributes The {@link Attributes} instance to add classes to * @param patterns the patterns to use - * @deprecated use {@link #addHiddenClasses(Server, String...)} instead */ - @Deprecated (since = "12.0.9", forRemoval = true) public static void addHiddenClasses(Attributes attributes, String... patterns) { if (patterns != null && patterns.length > 0) From 6278ad8f777b75fac4e9ea9e06f9eea122c8d405 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 12 Jun 2024 12:18:04 +1000 Subject: [PATCH 05/18] PR #11907 - add newlines to end of openid .mod files Signed-off-by: Lachlan Roberts --- jetty-core/jetty-openid/src/main/config/modules/openid.mod | 2 +- .../jetty-ee8-openid/src/main/config/modules/ee8-openid.mod | 2 +- .../jetty-ee9-openid/src/main/config/modules/ee9-openid.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jetty-core/jetty-openid/src/main/config/modules/openid.mod b/jetty-core/jetty-openid/src/main/config/modules/openid.mod index d0ab743adec..48d1b4e6ca4 100644 --- a/jetty-core/jetty-openid/src/main/config/modules/openid.mod +++ b/jetty-core/jetty-openid/src/main/config/modules/openid.mod @@ -47,4 +47,4 @@ etc/jetty-openid.xml # jetty.openid.authenticationMethod=client_secret_post ## Whether the user should be logged out after the idToken expires. -# jetty.openid.logoutWhenIdTokenIsExpired=false \ No newline at end of file +# jetty.openid.logoutWhenIdTokenIsExpired=false diff --git a/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod b/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod index 782b970193b..b68eb35ac3d 100644 --- a/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod +++ b/jetty-ee8/jetty-ee8-openid/src/main/config/modules/ee8-openid.mod @@ -11,4 +11,4 @@ ee8-security openid [lib] -lib/jetty-ee8-openid-${jetty.version}.jar \ No newline at end of file +lib/jetty-ee8-openid-${jetty.version}.jar diff --git a/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod b/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod index 417b783aa61..d86a075bc8b 100644 --- a/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod +++ b/jetty-ee9/jetty-ee9-openid/src/main/config/modules/ee9-openid.mod @@ -11,4 +11,4 @@ ee9-security openid [lib] -lib/jetty-ee9-openid-${jetty.version}.jar \ No newline at end of file +lib/jetty-ee9-openid-${jetty.version}.jar From b45fe4d7d246b7907fb4cf01559327dc529bb52e Mon Sep 17 00:00:00 2001 From: Jesse McConnell Date: Wed, 12 Jun 2024 09:39:05 -0500 Subject: [PATCH 06/18] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75b76b2ad71..053e5427cc5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Eclipse Jetty is a lightweight, highly scalable, Java-based web server and Servl Jetty's goal is to support web protocols (HTTP/1, HTTP/2, HTTP/3, WebSocket, etc.) in a high volume low latency way that provides maximum performance while retaining the ease of use and compatibility with years of Servlet development. Jetty is a modern fully asynchronous web server that has a long history as a component oriented technology, and can be easily embedded into applications while still offering a solid traditional distribution for webapp deployment. -- https://eclipse.dev/jetty/ +- https://jetty.org - https://projects.eclipse.org/projects/rt.jetty ## Webapp Example @@ -70,4 +70,4 @@ The documentation is divided into three guides, based on use case: # Commercial Support -Expert advice and production support of Jetty are provided by [Webtide](https://webtide.com). \ No newline at end of file +Expert advice and production support of Jetty are provided by [Webtide](https://webtide.com). From 8e6ab939f53006e49de54679ee8c01191b1c53c7 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 12 Jun 2024 17:29:16 +0200 Subject: [PATCH 07/18] Fixes #11892 - mtls not working with http/3. (#11900) The client certificate is now exposed in QuicheConnection, so that it can be returned by QuicStreamEndPoint.getSslSessionData(). Not much else is exposed by Quiche, so not much else that we can provide to applications, for example no TLS session id, no cipher suite, etc. Fixed --enable-native-access command line option to run tests, as the foreign dependency is in the class-path. Signed-off-by: Simone Bordet --- .../jetty/quic/common/QuicSession.java | 29 +++++++++ .../jetty/quic/common/QuicStreamEndPoint.java | 10 ++++ .../jetty/quic/quiche/QuicheConnection.java | 2 + .../foreign/ForeignQuicheConnection.java | 3 +- .../quic/quiche/jna/JnaQuicheConnection.java | 3 + .../jetty-ee10-test-client-transports/pom.xml | 2 +- .../test/client/transport/AbstractTest.java | 8 +++ .../client/transport/NeedClientAuthTest.java | 60 +++++++++++++++++++ 8 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/NeedClientAuthTest.java diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java index c853fd1aec8..8f998fa8743 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java @@ -13,10 +13,14 @@ package org.eclipse.jetty.quic.common; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; import java.util.EventListener; @@ -423,6 +427,31 @@ public abstract class QuicSession extends ContainerLifeCycle } } + /** + *

Returns the peer certificates chain.

+ *

Due to current Quiche C API limitations (that the Rust version does not have), + * only the last certificate in the chain is returned. + * This may change in the future when the C APIs are aligned to the Rust APIs.

+ * + * @return the peer certificates chain (currently only the last certificate in the chain) + */ + public X509Certificate[] getPeerCertificates() + { + try + { + byte[] encoded = quicheConnection.getPeerCertificate(); + if (encoded == null) + return null; + CertificateFactory factory = CertificateFactory.getInstance("X509"); + X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(encoded)); + return new X509Certificate[]{certificate}; + } + catch (CertificateException x) + { + return null; + } + } + @Override public void dump(Appendable out, String indent) throws IOException { diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java index e7e6f8da59a..b8a2a731a63 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.quic.common; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; +import java.security.cert.X509Certificate; import java.util.List; import java.util.stream.IntStream; @@ -221,6 +222,15 @@ public class QuicStreamEndPoint extends AbstractEndPoint return session; } + @Override + public SslSessionData getSslSessionData() + { + X509Certificate[] peerCertificates = getQuicSession().getPeerCertificates(); + if (peerCertificates == null) + return null; + return SslSessionData.from(null, null, null, peerCertificates); + } + public void onWritable() { if (LOG.isDebugEnabled()) diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java index b426242cbcb..439c25707d0 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java @@ -152,6 +152,8 @@ public abstract class QuicheConnection public abstract CloseInfo getLocalCloseInfo(); + public abstract byte[] getPeerCertificate(); + public static class CloseInfo { private final long error; diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java index 9e67935e59c..2327aa66494 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java @@ -518,6 +518,7 @@ public class ForeignQuicheConnection extends QuicheConnection } } + @Override public byte[] getPeerCertificate() { try (AutoLock ignore = lock.lock()) @@ -532,7 +533,7 @@ public class ForeignQuicheConnection extends QuicheConnection quiche_h.quiche_conn_peer_cert(quicheConn, outSegment, outLenSegment); long outLen = outLenSegment.get(NativeHelper.C_LONG, 0L); - if (outLen == 0L) + if (outLen <= 0L) return null; byte[] out = new byte[(int)outLen]; // dereference outSegment pointer diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java index db05de71b71..d633de77e21 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java @@ -413,6 +413,7 @@ public class JnaQuicheConnection extends QuicheConnection } } + @Override public byte[] getPeerCertificate() { try (AutoLock ignore = lock.lock()) @@ -424,6 +425,8 @@ public class JnaQuicheConnection extends QuicheConnection size_t_pointer out_len = new size_t_pointer(); LibQuiche.INSTANCE.quiche_conn_peer_cert(quicheConn, out, out_len); int len = out_len.getPointee().intValue(); + if (len <= 0) + return null; return out.getValueAsBytes(len); } } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml index b78feb1279f..682078b859d 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml @@ -112,7 +112,7 @@ @{argLine} ${jetty.surefire.argLine} - --enable-native-access org.eclipse.jetty.quic.quiche.foreign + --enable-native-access=ALL-UNNAMED
diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java index 806ada98f31..a04f32398b8 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java @@ -102,6 +102,14 @@ public class AbstractTest return transports; } + public static Collection transportsSecure() + { + EnumSet transports = EnumSet.of(Transport.HTTPS, Transport.H2, Transport.H3); + if ("ci".equals(System.getProperty("env"))) + transports.remove(Transport.H3); + return transports; + } + @BeforeEach public void prepare() { diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/NeedClientAuthTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/NeedClientAuthTest.java new file mode 100644 index 00000000000..1191480d51a --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/NeedClientAuthTest.java @@ -0,0 +1,60 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.ee10.test.client.transport; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class NeedClientAuthTest extends AbstractTest +{ + @ParameterizedTest + @MethodSource("transportsSecure") + public void testNeedClientAuth(Transport transport) throws Exception + { + prepareServer(transport, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + // Verify that the request attribute is present. + assertNotNull(request.getAttribute(ServletContextRequest.PEER_CERTIFICATES)); + } + }); + sslContextFactoryServer.setNeedClientAuth(true); + server.start(); + + startClient(transport, httpClient -> + { + // Configure the SslContextFactory to send a certificate to the server. + SslContextFactory.Client clientSSL = httpClient.getSslContextFactory(); + clientSSL.setKeyStorePath("src/test/resources/keystore.p12"); + clientSSL.setKeyStorePassword("storepwd"); + clientSSL.setCertAlias("mykey"); + }); + + ContentResponse response = client.newRequest(newURI(transport)).send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + } +} From a616acac9e93c0633a77f3f83328e95c0a88ba1b Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 12 Jun 2024 13:21:56 -0500 Subject: [PATCH 08/18] Issue #11909 - duplicate --modules= can trigger ConcurrentModificationException + Reworked tracking of enabled modules to not trigger change in "sources" Set if the module is already enabled. --- .../org/eclipse/jetty/start/StartArgs.java | 8 ++++--- .../org/eclipse/jetty/start/MainTest.java | 24 +++++++++++++++++++ .../src/test/resources/dist-home/start.ini | 1 - .../resources/jetty-logging.properties | 0 .../overdeclared-modules/start.d/config.ini | 1 + 5 files changed, 30 insertions(+), 4 deletions(-) delete mode 100644 jetty-core/jetty-start/src/test/resources/dist-home/start.ini create mode 100644 jetty-core/jetty-start/src/test/resources/overdeclared-modules/resources/jetty-logging.properties create mode 100644 jetty-core/jetty-start/src/test/resources/overdeclared-modules/start.d/config.ini diff --git a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java index 083c87cb5f3..d9f54b04984 100644 --- a/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java +++ b/jetty-core/jetty-start/src/main/java/org/eclipse/jetty/start/StartArgs.java @@ -1426,9 +1426,11 @@ public class StartArgs { for (String moduleName : moduleNames) { - modules.add(moduleName); - Set set = sources.computeIfAbsent(moduleName, k -> new HashSet<>()); - set.add(source); + if (modules.add(moduleName)) + { + Set set = sources.computeIfAbsent(moduleName, k -> new HashSet<>()); + set.add(source); + } } } diff --git a/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java b/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java index 98051e391eb..2b8d2946877 100644 --- a/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java +++ b/jetty-core/jetty-start/src/test/java/org/eclipse/jetty/start/MainTest.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -183,4 +184,27 @@ public class MainTest ); assertThat(commandLine, containsString(expectedExpansion)); } + + @Test + public void testModulesDeclaredTwice() throws Exception + { + List cmdLineArgs = new ArrayList<>(); + + Path homePath = MavenPaths.findTestResourceDir("dist-home"); + Path basePath = MavenPaths.findTestResourceDir("overdeclared-modules"); + cmdLineArgs.add("jetty.home=" + homePath); + cmdLineArgs.add("user.dir=" + basePath); + + Main main = new Main(); + + cmdLineArgs.add("--module=main"); + + // The "main" module is enabled in both ... + // 1) overdeclared-modules/start.d/config.ini + // 2) command-line + // This shouldn't result in an error + StartArgs args = main.processCommandLine(cmdLineArgs.toArray(new String[0])); + + assertThat(args.getSelectedModules(), hasItem("main")); + } } diff --git a/jetty-core/jetty-start/src/test/resources/dist-home/start.ini b/jetty-core/jetty-start/src/test/resources/dist-home/start.ini deleted file mode 100644 index 4cae2dd2f88..00000000000 --- a/jetty-core/jetty-start/src/test/resources/dist-home/start.ini +++ /dev/null @@ -1 +0,0 @@ ---module=main diff --git a/jetty-core/jetty-start/src/test/resources/overdeclared-modules/resources/jetty-logging.properties b/jetty-core/jetty-start/src/test/resources/overdeclared-modules/resources/jetty-logging.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jetty-core/jetty-start/src/test/resources/overdeclared-modules/start.d/config.ini b/jetty-core/jetty-start/src/test/resources/overdeclared-modules/start.d/config.ini new file mode 100644 index 00000000000..32c20512608 --- /dev/null +++ b/jetty-core/jetty-start/src/test/resources/overdeclared-modules/start.d/config.ini @@ -0,0 +1 @@ +--modules=main \ No newline at end of file From 8efde86dee8620aaa011f8c65c418bbd967aa72e Mon Sep 17 00:00:00 2001 From: Jesse McConnell Date: Wed, 12 Jun 2024 15:38:20 -0500 Subject: [PATCH 09/18] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 053e5427cc5..6e70ff0061e 100644 --- a/README.md +++ b/README.md @@ -53,19 +53,19 @@ $ cd jetty.project $ mvn -Pfast clean install # fast build bypasses tests and other checks ``` -For more detailed information on building and contributing to the Jetty project, please see the [Contribution Guide](https://eclipse.dev/jetty/documentation/contribution-guide/index.html). +For more detailed information on building and contributing to the Jetty project, please see the [Contribution Guide](https://jetty.org/docs/contribution-guide/index.html). # Documentation -[Jetty's documentation](https://eclipse.dev/jetty/documentation) is available on the Eclipse Jetty website. +[Jetty's documentation](https://jetty.org/docs) is available on the Eclipse Jetty website. The documentation is divided into three guides, based on use case: -* The [Operations Guide](https://eclipse.dev/jetty/documentation/jetty-12/operations-guide/index.html) targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications. +* The [Operations Guide](https://jetty.org/docs/jetty/12/operations-guide/index.html) targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications. -* The [Programming Guide](https://eclipse.dev/jetty/documentation/jetty-12/programming-guide/index.html) targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications. +* The [Programming Guide](https://jetty.org/docs/jetty/12/programming-guide/index.html) targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications. -* The [Contribution Guide](https://eclipse.dev/jetty/documentation/contribution-guide/index.html) targets developers that wish to contribute to the Jetty Project with code patches or documentation improvements. +* The [Contribution Guide](https://jetty.org/docs/contribution-guide/index.html) targets developers that wish to contribute to the Jetty Project with code patches or documentation improvements. # Commercial Support From 1eb66934ffa2aeb22d8799db86e20a8beefce23e Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 13 Jun 2024 16:26:35 +1000 Subject: [PATCH 10/18] PR #11907 - re-enable OpenId distribution test for ee9 Signed-off-by: Lachlan Roberts --- .../eclipse/jetty/ee9/tests/distribution/OpenIdTests.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/OpenIdTests.java b/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/OpenIdTests.java index 410dd07f49c..78744609f36 100644 --- a/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/OpenIdTests.java +++ b/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/OpenIdTests.java @@ -22,8 +22,8 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest; import org.eclipse.jetty.tests.testers.JettyHomeTester; import org.eclipse.jetty.tests.testers.Tester; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -31,11 +31,10 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +@Isolated public class OpenIdTests extends AbstractJettyHomeTest { @Test - // FIXME - @Disabled public void testOpenID() throws Exception { Path jettyBase = newTestJettyBaseDirectory(); From 34e27067f6b1039268c2807fe2db4725d565b945 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 13 Jun 2024 16:33:44 +1000 Subject: [PATCH 11/18] PR #11907 - changes from review Signed-off-by: Lachlan Roberts --- jetty-core/jetty-openid/src/main/config/modules/openid.mod | 2 +- .../{etc => modules/openid}/jetty-openid-baseloginservice.xml | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename jetty-core/jetty-openid/src/main/config/{etc => modules/openid}/jetty-openid-baseloginservice.xml (100%) diff --git a/jetty-core/jetty-openid/src/main/config/modules/openid.mod b/jetty-core/jetty-openid/src/main/config/modules/openid.mod index 48d1b4e6ca4..ddd36c8ec90 100644 --- a/jetty-core/jetty-openid/src/main/config/modules/openid.mod +++ b/jetty-core/jetty-openid/src/main/config/modules/openid.mod @@ -12,7 +12,7 @@ lib/jetty-util-ajax-${jetty.version}.jar lib/jetty-openid-${jetty.version}.jar [files] -basehome:etc/jetty-openid-baseloginservice.xml|etc/jetty-openid-baseloginservice.xml +basehome:modules/openid/jetty-openid-baseloginservice.xml|etc/jetty-openid-baseloginservice.xml [xml] etc/jetty-openid-baseloginservice.xml diff --git a/jetty-core/jetty-openid/src/main/config/etc/jetty-openid-baseloginservice.xml b/jetty-core/jetty-openid/src/main/config/modules/openid/jetty-openid-baseloginservice.xml similarity index 100% rename from jetty-core/jetty-openid/src/main/config/etc/jetty-openid-baseloginservice.xml rename to jetty-core/jetty-openid/src/main/config/modules/openid/jetty-openid-baseloginservice.xml From d34556749f8c022005f9588e2ec3519617b779a6 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 14 Jun 2024 08:31:46 +1000 Subject: [PATCH 12/18] Simplified Reactive Stream update for ContentSourcePublisher (#11849) * Issue #11803 - Follow Reactive Streams specification * Simplification of #11804 for Reactive Stream specification support --------- Co-authored-by: Artem Golovko Co-authored-by: Olivier Lamy --- Jenkinsfile | 2 +- jetty-core/jetty-io/pom.xml | 21 ++ .../io/content/ContentSourcePublisher.java | 329 +++++++++++++----- .../content/ContentSourcePublisherTest.java | 251 +++++++++++++ pom.xml | 12 + 5 files changed, 526 insertions(+), 89 deletions(-) create mode 100644 jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/content/ContentSourcePublisherTest.java diff --git a/Jenkinsfile b/Jenkinsfile index c794594c44f..b15de87c1d5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -127,7 +127,7 @@ def mavenBuild(jdk, cmdline, mvnName) { } finally { - junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml', allowEmptyResults: true + junit testResults: '**/target/surefire-reports/**/*.xml,**/target/invoker-reports/TEST*.xml', allowEmptyResults: true } } } diff --git a/jetty-core/jetty-io/pom.xml b/jetty-core/jetty-io/pom.xml index b1169af256e..706ad10427a 100644 --- a/jetty-core/jetty-io/pom.xml +++ b/jetty-core/jetty-io/pom.xml @@ -41,6 +41,11 @@ jetty-test-helper test + + org.reactivestreams + reactive-streams-tck-flow + test + @@ -50,6 +55,22 @@ @{argLine} ${jetty.surefire.argLine} --add-reads org.eclipse.jetty.io=org.eclipse.jetty.logging + + + + org.apache.maven.surefire + surefire-junit-platform + ${maven.surefire.plugin.version} + + + org.apache.maven.surefire + surefire-testng + ${maven.surefire.plugin.version} + + diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java index e48419104f5..0e3642daadf 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/content/ContentSourcePublisher.java @@ -13,11 +13,17 @@ package org.eclipse.jetty.io.content; +import java.util.Objects; import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.MathUtils; -import org.eclipse.jetty.util.thread.AutoLock; +import org.eclipse.jetty.util.StaticException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

Wraps a {@link Content.Source} as a {@link Flow.Publisher}. @@ -25,135 +31,282 @@ import org.eclipse.jetty.util.thread.AutoLock; * read from the passed {@link Content.Source} and passed to {@link Flow.Subscriber#onNext(Object)}. * If no content is available, then the {@link Content.Source#demand(Runnable)} method is used to * ultimately call {@link Flow.Subscriber#onNext(Object)} once content is available.

+ *

{@link Content.Source} can be consumed only once and does not support multicast subscription. + * {@link Content.Source} will be consumed fully, otherwise will be failed in case of any errors + * to prevent resource leaks.

*/ public class ContentSourcePublisher implements Flow.Publisher { - private final Content.Source content; + private static final Logger LOG = LoggerFactory.getLogger(ContentSourcePublisher.class); + + private final AtomicReference content; public ContentSourcePublisher(Content.Source content) { - this.content = content; + Objects.requireNonNull(content, "Content.Source must not be null"); + this.content = new AtomicReference<>(content); } @Override public void subscribe(Flow.Subscriber subscriber) { - subscriber.onSubscribe(new SubscriptionImpl(content, subscriber)); + // As per rule 1.11, we have decided to support SINGLE subscriber + // in a UNICAST configuration for this implementation. It means + // that Content.Source can be consumed only once. + Content.Source content = this.content.getAndSet(null); + if (content != null) + onSubscribe(subscriber, content); + else + onMultiSubscribe(subscriber); } - private static class SubscriptionImpl implements Flow.Subscription + private void onSubscribe(Flow.Subscriber subscriber, Content.Source content) { - private final AutoLock lock = new AutoLock(); - private final Content.Source content; - private final Flow.Subscriber subscriber; - private long demand; - private boolean stalled; - private boolean cancelled; - private boolean terminated; - - public SubscriptionImpl(Content.Source content, Flow.Subscriber subscriber) + // As per rule 1.9, we need to throw a `java.lang.NullPointerException` + // if the `Subscriber` is `null` + if (subscriber == null) { - this.content = content; - this.subscriber = subscriber; - this.stalled = true; + NullPointerException error = new NullPointerException("Flow.Subscriber must not be null"); + content.fail(error); + throw error; } + ActiveSubscription subscription = new ActiveSubscription(content, subscriber); + // As per rule 1.9, this method must return normally (i.e. not throw). + try + { + subscriber.onSubscribe(subscription); + } + catch (Throwable err) + { + // As per rule 2.13, we MUST consider subscription cancelled and + // MUST raise this error condition in a fashion that is adequate for the runtime environment. + subscription.cancel(new SuppressedException(err)); + if (LOG.isTraceEnabled()) + LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err); + } + } + + private void onMultiSubscribe(Flow.Subscriber subscriber) + { + // As per rule 1.9, we need to throw a `java.lang.NullPointerException` + // if the `Subscriber` is `null` + if (subscriber == null) + throw new NullPointerException("Flow.Subscriber must not be null"); + + ExhaustedSubscription subscription = new ExhaustedSubscription(); + // As per 1.9, this method must return normally (i.e. not throw). + try + { + // As per rule 1.9, the only legal way to signal about Subscriber rejection + // is by calling onError (after calling onSubscribe). + subscriber.onSubscribe(subscription); + subscriber.onError(new IllegalStateException("Content.Source was exhausted.")); + } + catch (Throwable err) + { + // As per rule 2.13, we MUST consider subscription cancelled and + // MUST raise this error condition in a fashion that is adequate for the runtime environment. + if (LOG.isTraceEnabled()) + LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err); + } + } + + private static final class ExhaustedSubscription implements Flow.Subscription + { @Override public void request(long n) { - boolean process = false; - Throwable failure = null; - try (AutoLock ignored = lock.lock()) - { - if (cancelled || terminated) - return; - if (n <= 0) - { - terminated = true; - failure = new IllegalArgumentException("invalid demand " + n); - } - demand = MathUtils.cappedAdd(demand, n); - if (stalled) - { - stalled = false; - process = true; - } - } - if (failure != null) - subscriber.onError(failure); - else if (process) - process(); + // As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs. } @Override public void cancel() { - try (AutoLock ignored = lock.lock()) - { - cancelled = true; - } + // As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs. + } + } + + private static final class ActiveSubscription extends IteratingCallback implements Flow.Subscription + { + private static final long NO_MORE_DEMAND = -1; + private static final Throwable COMPLETED = new StaticException("Source.Content read fully"); + private final AtomicReference cancelled; + private final AtomicLong demand; + private Content.Source content; + private Flow.Subscriber subscriber; + + public ActiveSubscription(Content.Source content, Flow.Subscriber subscriber) + { + this.cancelled = new AtomicReference<>(null); + this.demand = new AtomicLong(0); + this.content = content; + this.subscriber = subscriber; } - private void process() + // As per rule 3.3, Subscription MUST place an upper bound on possible synchronous + // recursion between Publisher and Subscriber + // + // As per rule 1.3, onSubscribe, onNext, onError and onComplete signaled to a + // Subscriber MUST be signaled serially. + // + // IteratingCallback guarantee that process() method will be executed by one thread only. + // The process() method can be only initiated from request() or cancel() demands methods. + @Override + protected Action process() { - while (true) + Throwable cancelled = this.cancelled.get(); + if (cancelled != null) { - try (AutoLock ignored = lock.lock()) + // As per rule 3.13, Subscription.cancel() MUST request the Publisher to eventually + // drop any references to the corresponding subscriber. + this.demand.set(NO_MORE_DEMAND); + if (cancelled != COMPLETED) + this.content.fail(cancelled); + this.content = null; + try { - if (demand > 0) - { - --demand; - } - else - { - stalled = true; - return; - } + if (cancelled == COMPLETED) + this.subscriber.onComplete(); + else if (!(cancelled instanceof SuppressedException)) + this.subscriber.onError(cancelled); } - - Content.Chunk chunk = content.read(); - - if (chunk == null) + catch (Throwable err) { - try (AutoLock ignored = lock.lock()) - { - // Restore the demand decremented above. - ++demand; - stalled = true; - } - content.demand(this::process); - return; + if (LOG.isTraceEnabled()) + LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err); } + this.subscriber = null; + return Action.SUCCEEDED; + } - if (Content.Chunk.isFailure(chunk)) - { - terminate(); - if (!chunk.isLast()) - content.fail(chunk.getFailure()); - subscriber.onError(chunk.getFailure()); - return; - } + Content.Chunk chunk = content.read(); - subscriber.onNext(chunk); + if (chunk == null) + { + content.demand(this::succeeded); + return Action.SCHEDULED; + } + + if (Content.Chunk.isFailure(chunk)) + { + cancel(chunk.getFailure()); chunk.release(); - - if (chunk.isLast()) - { - terminate(); - // Reactive Stream specification rule 2.9 allows Publishers to call onComplete() - // even without demand, and Subscribers must be prepared to handle this case. - subscriber.onComplete(); - return; - } + return Action.IDLE; } + + try + { + this.subscriber.onNext(chunk); + } + catch (Throwable err) + { + cancel(new SuppressedException(err)); + if (LOG.isTraceEnabled()) + LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err); + } + chunk.release(); + + if (chunk.isLast()) + { + cancel(COMPLETED); + return Action.IDLE; + } + + if (demand.decrementAndGet() > 0) + this.iterate(); + + return Action.IDLE; } - private void terminate() + @Override + public void request(long n) { - try (AutoLock ignored = lock.lock()) + // As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs. + if (cancelled.get() != null) + return; + + // As per rule 3.9, MUST signal onError with a java.lang.IllegalArgumentException if the argument is <= 0. + if (n <= 0L) { - terminated = true; + String errorMsg = "Flow.Subscriber " + subscriber + " violated rule 3.9: non-positive requests are not allowed."; + cancel(new IllegalArgumentException(errorMsg)); + return; } + + // As per rule 3.17, when demand overflows `Long.MAX_VALUE` + // we treat the signalled demand as "effectively unbounded" + if (demand.updateAndGet(it -> it == NO_MORE_DEMAND ? it : MathUtils.cappedAdd(it, n)) != NO_MORE_DEMAND) + this.iterate(); + } + + @Override + public void cancel() + { + cancel(new CancelledException()); + } + + public void cancel(Throwable cause) + { + // As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs. + // + // As per rule 3.5, this handles cancellation requests, and is idempotent, thread-safe and not + // synchronously performing heavy computations + if (cancelled.compareAndSet(null, cause)) + this.iterate(); + } + + // Publisher notes + // + // 1.6 If a Publisher signals either onError or onComplete on a Subscriber, + // that Subscriber’s Subscription MUST be considered cancelled. + // 2.4 Subscriber.onComplete() and Subscriber.onError(Throwable t) MUST consider the + // Subscription cancelled after having received the signal. + // + // Publisher failed -> cancel(Throwable) + // 1.4 If a Publisher fails it MUST signal an onError. + // + // Publisher succeeded -> cancel(COMPLETED) + // 1.5 If a Publisher terminates successfully (finite stream) it MUST signal an onComplete. + + // Subscriber + // 2.13 In the case that this rule is violated, any associated Subscription to the Subscriber + // MUST be considered as cancelled, and the caller MUST raise this error condition in a + // fashion that is adequate for the runtime environment. + // + // Subscriber.onSubscribe/onNext/onError/onComplete failed -> cancel(new Suppressed(cause)) + + // Subscription notes + // + // Subscription.cancel -> cancel(new Cancelled()) + // It's not clearly specified in the specification, but according to: + // - the issue: https://github.com/reactive-streams/reactive-streams-jvm/issues/458 + // - TCK test 'untested_spec108_possiblyCanceledSubscriptionShouldNotReceiveOnErrorOrOnCompleteSignals' + // - 1.8 If a Subscription is cancelled its Subscriber MUST eventually stop being signaled. + // + // Subscription.request with negative argument -> cancel(err) + // 3.9 While the Subscription is not cancelled, Subscription.request(long n) MUST signal onError with a + // java.lang.IllegalArgumentException if the argument is <= 0. + } + + private static class SuppressedException extends Exception + { + SuppressedException(String message) + { + super(message); + } + + SuppressedException(Throwable cause) + { + super(cause.getMessage(), cause); + } + } + + private static class CancelledException extends SuppressedException + { + CancelledException() + { + super("Subscription was cancelled"); } } } diff --git a/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/content/ContentSourcePublisherTest.java b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/content/ContentSourcePublisherTest.java new file mode 100644 index 00000000000..415e6ed2098 --- /dev/null +++ b/jetty-core/jetty-io/src/test/java/org/eclipse/jetty/io/content/ContentSourcePublisherTest.java @@ -0,0 +1,251 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.io.content; + +import java.nio.ByteBuffer; +import java.util.Random; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.io.Content; +import org.reactivestreams.tck.TestEnvironment; +import org.reactivestreams.tck.flow.FlowPublisherVerification; +import org.testng.annotations.Test; + +@Test +public final class ContentSourcePublisherTest extends FlowPublisherVerification +{ + public ContentSourcePublisherTest() + { + super(new TestEnvironment()); + } + + @Override + public Flow.Publisher createFlowPublisher(long elements) + { + Content.Source source = new SyntheticContentSource(elements); + return new ContentSourcePublisher(source); + } + + @Override + public Flow.Publisher createFailedFlowPublisher() + { + Content.Source source = new SyntheticContentSource(0); + Flow.Publisher publisher = new ContentSourcePublisher(source); + // Simulate exhausted Content.Source + publisher.subscribe(new Flow.Subscriber<>() + { + @Override + public void onSubscribe(Flow.Subscription subscription) + { + subscription.cancel(); + } + + @Override + public void onNext(Content.Chunk item) + { + } + + @Override + public void onError(Throwable throwable) + { + } + + @Override + public void onComplete() + { + } + }); + return publisher; + } + + private static final class SyntheticContentSource implements Content.Source + { + private final AtomicReference state; + private final long contentSize; + + public SyntheticContentSource(long chunksToRead) + { + this.state = new AtomicReference<>(new State.Reading(chunksToRead)); + this.contentSize = State.Reading.chunkSize * Math.max(chunksToRead, 0); + } + + @Override + public long getLength() + { + return contentSize; + } + + @Override + public Content.Chunk read() + { + return state.getAndUpdate(before -> before.read()).chunk(); + } + + @Override + public void demand(Runnable demandCallback) + { + // recursive stack overflow not necessary for this test + demandCallback.run(); + } + + @Override + public void fail(Throwable failure) + { + fail(failure, true); + } + + @Override + public void fail(Throwable failure, boolean last) + { + state.getAndUpdate(before -> before.fail(failure, last)); + } + + @Override + public boolean rewind() + { + return false; + } + + private sealed interface State permits State.Reading, State.ReadFailed, State.ReadCompleted + { + Content.Chunk chunk(); + + State read(); + + State fail(Throwable failure, boolean last); + + final class Reading implements State + { + public static final int chunkSize = 16; + private static final Random random = new Random(); + + private final long chunksToRead; + private final Content.Chunk chunk; + + public Reading(long chunksToRead) + { + this.chunksToRead = chunksToRead; + this.chunk = generateValidChunk(chunksToRead); + } + + public Reading(long chunksToRead, Throwable transientFailure) + { + this.chunksToRead = chunksToRead; + this.chunk = generateFailureChunk(transientFailure); + } + + @Override + public Content.Chunk chunk() + { + return chunk; + } + + @Override + public State read() + { + long leftToRead = leftToRead(); + if (leftToRead <= 0) + return new ReadCompleted(); + return new Reading(leftToRead); + } + + @Override + public State fail(Throwable failure, boolean last) + { + if (last) + return new ReadFailed(failure); + return new Reading(chunksToRead, failure); + } + + private long leftToRead() + { + if (chunksToRead == Long.MAX_VALUE) // endless source + return chunksToRead; + return chunksToRead - 1; + } + + private static Content.Chunk generateFailureChunk(Throwable transientFailure) + { + return Content.Chunk.from(transientFailure, false); + } + + private static Content.Chunk generateValidChunk(long chunksToRead) + { + if (chunksToRead <= 0) + return Content.Chunk.EOF; + if (chunksToRead == 1) + return Content.Chunk.from(randomPayload(), true); + return Content.Chunk.from(randomPayload(), false); + } + + private static ByteBuffer randomPayload() + { + byte[] payload = new byte[chunkSize]; + random.nextBytes(payload); + return ByteBuffer.wrap(payload); + } + } + + final class ReadFailed implements State + { + private final Content.Chunk chunk; + + public ReadFailed(Throwable failure) + { + this.chunk = Content.Chunk.from(failure, true); + } + + @Override + public Content.Chunk chunk() + { + return chunk; + } + + @Override + public State read() + { + return this; + } + + @Override + public State fail(Throwable failure, boolean last) + { + return this; + } + } + + final class ReadCompleted implements State + { + @Override + public Content.Chunk chunk() + { + return Content.Chunk.EOF; + } + + @Override + public State read() + { + return this; + } + + @Override + public State fail(Throwable failure, boolean last) + { + return this; + } + } + } + } +} diff --git a/pom.xml b/pom.xml index b1cd6d3139a..1b4fa90a607 100644 --- a/pom.xml +++ b/pom.xml @@ -300,6 +300,7 @@ 4.0.3 2023-06-05T23:12:49Z UTF-8 + 1.0.4 src/it/settings.xml 2.0.12 1.3.7 @@ -308,6 +309,7 @@ 0 1.8.3 1.19.7 + 7.10.2 3.0.0 2.16.2 1.7.0.Final @@ -1155,6 +1157,11 @@ osgi.core ${org.osgi.core.version} + + org.reactivestreams + reactive-streams-tck-flow + ${reactive-streams.version} + org.slf4j jcl104-over-slf4j @@ -1191,6 +1198,11 @@ + + org.testng + testng + ${testng.version} + org.wildfly.common wildfly-common From 619c539af37613464828c7a7f20da26663570c2c Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 14 Jun 2024 11:18:31 +0200 Subject: [PATCH 13/18] Issue #11911 Fix documentation example for Request.getHttpUri (#11916) * Issue #11911 Fix documentation example for Request.getHttpUri --- .../docs/programming/migration/ServletToHandlerDocs.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java index 3fd14d54da3..fb64158148f 100644 --- a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java @@ -27,6 +27,7 @@ import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.Trailers; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.Context; @@ -60,10 +61,11 @@ public class ServletToHandlerDocs // - servletRequest.getProtocol(); String protocol = request.getConnectionMetaData().getProtocol(); - // Gets the full request URI. + // Gets the request URL. // Replaces: // - servletRequest.getRequestURL(); - String fullRequestURI = request.getHttpURI().asString(); + HttpURI httpURI = HttpURI.build(request.getHttpURI()).query(null); + StringBuffer requestURL = new StringBuffer(httpURI.asString()); // Gets the request context. // Replaces: From 9546b3ab4957d635e148086864bc5356056266d7 Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Tue, 18 Jun 2024 20:00:04 +0200 Subject: [PATCH 14/18] Fix missing notifyRemoteAsyncErrors http config wiring (#11897) Fixed missing notifyRemoteAsyncErrors http config wiring and add tests Signed-off-by: Ludovic Orban --- .../server/HTTP2ServerConnectionFactory.java | 3 +- .../server/internal/HttpStreamOverHTTP2.java | 5 +- .../jetty/http2/tests/AbstractTest.java | 7 +- .../jetty/http2/tests/AsyncIOTest.java | 72 ++++++++ .../jetty/http2/tests/AsyncServletTest.java | 52 ------ .../jetty/http3/HTTP3StreamConnection.java | 14 +- .../server/internal/HttpStreamOverHTTP3.java | 6 +- .../jetty/quic/common/QuicStreamEndPoint.java | 25 ++- .../foreign/ForeignQuicheConnection.java | 12 +- .../quic/quiche/jna/JnaQuicheConnection.java | 3 + .../org/eclipse/jetty/server/HttpChannel.java | 15 +- .../server/internal/HttpChannelState.java | 15 +- .../jetty/ee10/servlet/HttpOutput.java | 4 +- .../transport/Http2AsyncIOServletTest.java | 151 ++++++++++++++++ .../transport/Http3AsyncIOServletTest.java | 161 ++++++++++++++++++ .../eclipse/jetty/ee9/nested/HttpOutput.java | 4 +- .../jetty-ee9-test-client-transports/pom.xml | 2 +- .../transport/Http2AsyncIOServletTest.java | 150 ++++++++++++++++ .../transport/Http3AsyncIOServletTest.java | 161 ++++++++++++++++++ 19 files changed, 781 insertions(+), 81 deletions(-) create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http2AsyncIOServletTest.java create mode 100644 jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http3AsyncIOServletTest.java create mode 100644 jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http2AsyncIOServletTest.java create mode 100644 jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http3AsyncIOServletTest.java diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java index ba2b27b520b..498b54fd9db 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http2.server; +import java.io.EOFException; import java.util.Map; import java.util.concurrent.TimeoutException; @@ -155,7 +156,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF @Override public void onReset(Stream stream, ResetFrame frame, Callback callback) { - EofException failure = new EofException("Reset " + ErrorCode.toString(frame.getError(), null)); + EOFException failure = new EOFException("Reset " + ErrorCode.toString(frame.getError(), null)); onFailure(stream, failure, callback); } diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java index 9de03026603..f03e9479843 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http2.server.internal; +import java.io.EOFException; import java.nio.ByteBuffer; import java.util.concurrent.TimeoutException; import java.util.function.BiConsumer; @@ -38,6 +39,7 @@ import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.Request; @@ -587,7 +589,8 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server @Override public Runnable onFailure(Throwable failure, Callback callback) { - Runnable runnable = _httpChannel.onFailure(failure); + boolean remote = failure instanceof EOFException; + Runnable runnable = remote ? _httpChannel.onRemoteFailure(new EofException(failure)) : _httpChannel.onFailure(failure); return () -> { if (runnable != null) diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java index 90bc346b4ce..9a5f944f14d 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AbstractTest.java @@ -58,7 +58,12 @@ public class AbstractTest protected void start(Handler handler) throws Exception { - HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(new HttpConfiguration()); + start(handler, new HttpConfiguration()); + } + + protected void start(Handler handler, HttpConfiguration httpConfiguration) throws Exception + { + HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(httpConfiguration); connectionFactory.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); prepareServer(connectionFactory); diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java index 430d721bcf7..c32a6969fce 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncIOTest.java @@ -17,22 +17,33 @@ import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.FuturePromise; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -241,6 +252,67 @@ public class AsyncIOTest extends AbstractTest */ } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testClientResetRemoteErrorNotification(boolean notify) throws Exception + { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference responseRef = new AtomicReference<>(); + AtomicReference failureRef = new AtomicReference<>(); + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setNotifyRemoteAsyncErrors(notify); + start(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + request.addFailureListener(failureRef::set); + responseRef.set(response); + latch.countDown(); + return true; + } + }, httpConfiguration); + + Session session = newClientSession(new Session.Listener() {}); + MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, null, true); + FuturePromise promise = new FuturePromise<>(); + session.newStream(frame, promise, null); + Stream stream = promise.get(5, TimeUnit.SECONDS); + + // Wait for the server to be idle. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + sleep(500); + + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); + + if (notify) + // Wait for the reset to be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).until(failureRef::get, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(failureRef::get, nullValue()); + + // Assert that writing to the response fails. + var cb = new Callback() + { + private Throwable failure = null; + + @Override + public void failed(Throwable x) + { + failure = x; + } + + Throwable failure() + { + return failure; + } + }; + responseRef.get().write(true, BufferUtil.EMPTY_BUFFER, cb); + await().atMost(5, TimeUnit.SECONDS).until(cb::failure, instanceOf(EofException.class)); + } + private static void sleep(long ms) throws InterruptedIOException { try diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java index 328e2157196..5ca981b723b 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/AsyncServletTest.java @@ -154,58 +154,6 @@ public class AsyncServletTest extends AbstractTest // assertTrue(clientLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); // } // -// @Test -// public void testStartAsyncThenClientResetWithoutRemoteErrorNotification() throws Exception -// { -// HttpConfiguration httpConfiguration = new HttpConfiguration(); -// httpConfiguration.setNotifyRemoteAsyncErrors(false); -// prepareServer(new HTTP2ServerConnectionFactory(httpConfiguration)); -// ServletContextHandler context = new ServletContextHandler(server, "/"); -// AtomicReference asyncContextRef = new AtomicReference<>(); -// CountDownLatch latch = new CountDownLatch(1); -// context.addServlet(new ServletHolder(new HttpServlet() -// { -// @Override -// protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException -// { -// AsyncContext asyncContext = request.startAsync(); -// asyncContext.setTimeout(0); -// asyncContextRef.set(asyncContext); -// latch.countDown(); -// } -// }), servletPath + "/*"); -// server.start(); -// -// prepareClient(); -// client.start(); -// Session session = newClient(new Session.Listener() {}); -// MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY); -// HeadersFrame frame = new HeadersFrame(metaData, null, true); -// FuturePromise promise = new FuturePromise<>(); -// session.newStream(frame, promise, null); -// Stream stream = promise.get(5, TimeUnit.SECONDS); -// -// // Wait for the server to be in ASYNC_WAIT. -// assertTrue(latch.await(5, TimeUnit.SECONDS)); -// sleep(500); -// -// stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); -// -// // Wait for the reset to be processed by the server. -// sleep(500); -// -// AsyncContext asyncContext = asyncContextRef.get(); -// ServletResponse response = asyncContext.getResponse(); -// ServletOutputStream output = response.getOutputStream(); -// -// assertThrows(IOException.class, -// () -> -// { -// // Large writes or explicit flush() must -// // fail because the stream has been reset. -// output.flush(); -// }); -// } // // @Test // public void testStartAsyncThenServerSessionIdleTimeout() throws Exception diff --git a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java index 29542da3d1e..52b0bd715e9 100644 --- a/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java +++ b/jetty-core/jetty-http3/jetty-http3-common/src/main/java/org/eclipse/jetty/http3/HTTP3StreamConnection.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.http3; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; @@ -275,7 +274,7 @@ public abstract class HTTP3StreamConnection extends AbstractConnection } } - private MessageParser.Result parseAndFill(boolean setFillInterest) + private MessageParser.Result parseAndFill(boolean setFillInterest) throws IOException { try { @@ -336,16 +335,9 @@ public abstract class HTTP3StreamConnection extends AbstractConnection } } - private int fill(ByteBuffer byteBuffer) + private int fill(ByteBuffer byteBuffer) throws IOException { - try - { - return getEndPoint().fill(byteBuffer); - } - catch (IOException x) - { - throw new UncheckedIOException(x.getMessage(), x); - } + return getEndPoint().fill(byteBuffer); } private void processHeaders(HeadersFrame frame, boolean wasBlocked, Runnable delegate) diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java index 1552659ad97..33e9828772d 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http3.server.internal; +import java.io.EOFException; import java.nio.ByteBuffer; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; @@ -33,6 +34,7 @@ import org.eclipse.jetty.http3.api.Stream; import org.eclipse.jetty.http3.frames.DataFrame; import org.eclipse.jetty.http3.frames.HeadersFrame; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.Request; @@ -536,6 +538,8 @@ public class HttpStreamOverHTTP3 implements HttpStream chunk = Content.Chunk.from(failure, true); } connection.onFailure(failure); - return httpChannel.onFailure(failure); + + boolean remote = failure instanceof EOFException; + return remote ? httpChannel.onRemoteFailure(new EofException(failure)) : httpChannel.onFailure(failure); } } diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java index b8a2a731a63..227af953034 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.quic.common; +import java.io.EOFException; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; @@ -41,6 +42,7 @@ public class QuicStreamEndPoint extends AbstractEndPoint { private static final Logger LOG = LoggerFactory.getLogger(QuicStreamEndPoint.class); private static final ByteBuffer LAST_FLAG = ByteBuffer.allocate(0); + private static final ByteBuffer EMPTY_WRITABLE_BUFFER = ByteBuffer.allocate(0); private final QuicSession session; private final long streamId; @@ -265,12 +267,25 @@ public class QuicStreamEndPoint extends AbstractEndPoint } else { - QuicStreamEndPoint streamEndPoint = getQuicSession().getStreamEndPoint(streamId); - if (streamEndPoint.isStreamFinished()) + if (isStreamFinished()) { - EofException e = new EofException(); - streamEndPoint.getFillInterest().onFail(e); - streamEndPoint.getQuicSession().onFailure(e); + // Check if the stream was finished normally. + try + { + fill(EMPTY_WRITABLE_BUFFER); + } + catch (EOFException x) + { + // Got reset. + getFillInterest().onFail(x); + getQuicSession().onFailure(x); + } + catch (Throwable x) + { + EofException e = new EofException(x); + getFillInterest().onFail(e); + getQuicSession().onFailure(e); + } } } return interested; diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java index 2327aa66494..9aa018d0efe 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.quic.quiche.foreign; import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; @@ -918,14 +919,19 @@ public class ForeignQuicheConnection extends QuicheConnection MemorySegment fin = scope.allocate(NativeHelper.C_CHAR); read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment, buffer.remaining(), fin); - int prevPosition = buffer.position(); - buffer.put(bufferSegment.asByteBuffer().limit((int)read)); - buffer.position(prevPosition); + if (read > 0) + { + int prevPosition = buffer.position(); + buffer.put(bufferSegment.asByteBuffer().limit((int)read)); + buffer.position(prevPosition); + } } } if (read == quiche_error.QUICHE_ERR_DONE) return isStreamFinished(streamId) ? -1 : 0; + if (read == quiche_error.QUICHE_ERR_STREAM_RESET) + throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); if (read < 0L) throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); buffer.position((int)(buffer.position() + read)); diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java index d633de77e21..b81021957a2 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.quic.quiche.jna; import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -747,6 +748,8 @@ public class JnaQuicheConnection extends QuicheConnection int read = LibQuiche.INSTANCE.quiche_conn_stream_recv(quicheConn, new uint64_t(streamId), buffer, new size_t(buffer.remaining()), fin).intValue(); if (read == quiche_error.QUICHE_ERR_DONE) return isStreamFinished(streamId) ? -1 : 0; + if (read == quiche_error.QUICHE_ERR_STREAM_RESET) + throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); if (read < 0L) throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); buffer.position(buffer.position() + read); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index a565ccce5b5..61a157ebd9e 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -88,8 +88,7 @@ public interface HttpChannel extends Invocable /** *

Notifies this {@code HttpChannel} that an asynchronous failure happened.

- *

Typical failure examples could be HTTP/2 resets or - * protocol failures (for example, invalid request bytes).

+ *

Typical failure examples could be protocol failures (for example, invalid request bytes).

* * @param failure the failure cause. * @return a {@code Runnable} that performs the failure action, or {@code null} @@ -98,6 +97,18 @@ public interface HttpChannel extends Invocable */ Runnable onFailure(Throwable failure); + /** + *

Notifies this {@code HttpChannel} that an asynchronous notification was received indicating + * a remote failure happened.

+ *

Typical failure examples could be HTTP/2 resets.

+ * + * @param failure the failure cause. + * @return a {@code Runnable} that performs the failure action, or {@code null} + * if no failure action needs be performed by the calling thread + * @see Request#addFailureListener(Consumer) + */ + Runnable onRemoteFailure(Throwable failure); + /** *

Notifies this {@code HttpChannel} that an asynchronous close happened.

* diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java index dc2b3be55d6..e97331b573a 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpChannelState.java @@ -392,6 +392,17 @@ public class HttpChannelState implements HttpChannel, Components @Override public Runnable onFailure(Throwable x) + { + return onFailure(x, false); + } + + @Override + public Runnable onRemoteFailure(Throwable x) + { + return onFailure(x, true); + } + + private Runnable onFailure(Throwable x, boolean remote) { HttpStream stream; Runnable task; @@ -437,7 +448,9 @@ public class HttpChannelState implements HttpChannel, Components // Notify the failure listeners only once. Consumer onFailure = _onFailure; _onFailure = null; - Runnable invokeOnFailureListeners = onFailure == null ? null : () -> + + boolean skipListeners = remote && !getHttpConfiguration().isNotifyRemoteAsyncErrors(); + Runnable invokeOnFailureListeners = onFailure == null || skipListeners ? null : () -> { try { diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java index 419fc0d9ebf..b9e6270401d 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java @@ -676,7 +676,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable catch (Throwable t) { onWriteComplete(false, t); - throw t; + if (t instanceof IOException) + throw t; + throw new IOException(t); } } } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http2AsyncIOServletTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http2AsyncIOServletTest.java new file mode 100644 index 00000000000..c55aadae64c --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http2AsyncIOServletTest.java @@ -0,0 +1,151 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.ee10.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http2AsyncIOServletTest +{ + private Server server; + private ServerConnector connector; + private HTTP2Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory(httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler("/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.setHandler(servletContextHandler); + server.start(); + + client = new HTTP2Client(); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + FuturePromise sessionPromise = new FuturePromise<>(); + client.connect(address, new Session.Listener() {}, sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, null, false); + Stream stream = session.newStream(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code)); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +} diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http3AsyncIOServletTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http3AsyncIOServletTest.java new file mode 100644 index 00000000000..64d5eaf00bb --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/Http3AsyncIOServletTest.java @@ -0,0 +1,161 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.ee10.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.HTTP3ErrorCode; +import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.eclipse.jetty.http3.api.Session.Client; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(WorkDirExtension.class) +public class Http3AsyncIOServletTest +{ + public WorkDir workDir; + + private Server server; + private QuicServerConnector connector; + private HTTP3Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server(); + serverSslContextFactory.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + serverSslContextFactory.setKeyStorePassword("storepwd"); + ServerQuicConfiguration serverQuicConfiguration = new ServerQuicConfiguration(serverSslContextFactory, workDir.getEmptyPathDir()); + connector = new QuicServerConnector(server, serverQuicConfiguration, new HTTP3ServerConnectionFactory(serverQuicConfiguration, httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler("/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.setHandler(servletContextHandler); + server.start(); + + client = new HTTP3Client(new ClientQuicConfiguration(new SslContextFactory.Client(true), null)); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + Client session = client.connect(address, new Client.Listener() {}).get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_3, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, false); + Stream stream = session.newRequest(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new Exception()); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +} diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java index c69bb366410..b326486e504 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/HttpOutput.java @@ -740,7 +740,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable catch (Throwable t) { onWriteComplete(false, t); - throw t; + if (t instanceof IOException) + throw t; + throw new IOException(t); } } } diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml index e98c0af0c96..4a28e6e5dfd 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/pom.xml @@ -112,7 +112,7 @@ @{argLine} ${jetty.surefire.argLine} - --enable-native-access org.eclipse.jetty.quic.quiche.foreign + --enable-native-access=ALL-UNNAMED diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http2AsyncIOServletTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http2AsyncIOServletTest.java new file mode 100644 index 00000000000..12e6a7cfef6 --- /dev/null +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http2AsyncIOServletTest.java @@ -0,0 +1,150 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.ee9.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Http2AsyncIOServletTest +{ + private Server server; + private ServerConnector connector; + private HTTP2Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory(httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.start(); + + client = new HTTP2Client(); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + FuturePromise sessionPromise = new FuturePromise<>(); + client.connect(address, new Session.Listener() {}, sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, null, false); + Stream stream = session.newStream(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code)); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +} diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http3AsyncIOServletTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http3AsyncIOServletTest.java new file mode 100644 index 00000000000..737e29e4dd0 --- /dev/null +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/Http3AsyncIOServletTest.java @@ -0,0 +1,161 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.ee9.test.client.transport; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.HTTP3ErrorCode; +import org.eclipse.jetty.http3.api.Session; +import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.awaitility.Awaitility.await; +import static org.eclipse.jetty.http3.api.Session.Client; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(WorkDirExtension.class) +public class Http3AsyncIOServletTest +{ + public WorkDir workDir; + + private Server server; + private QuicServerConnector connector; + private HTTP3Client client; + + private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception + { + server = new Server(); + SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server(); + serverSslContextFactory.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + serverSslContextFactory.setKeyStorePassword("storepwd"); + ServerQuicConfiguration serverQuicConfiguration = new ServerQuicConfiguration(serverSslContextFactory, workDir.getEmptyPathDir()); + connector = new QuicServerConnector(server, serverQuicConfiguration, new HTTP3ServerConnectionFactory(serverQuicConfiguration, httpConfig)); + server.addConnector(connector); + ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/"); + servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*"); + server.start(); + + client = new HTTP3Client(new ClientQuicConfiguration(new SslContextFactory.Client(true), null)); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setNotifyRemoteAsyncErrors(notify); + + AtomicReference errorAsyncEventRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + start(httpConfig, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + AsyncContext asyncContext = request.startAsync(); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + errorAsyncEventRef.set(event); + asyncContext.complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + }); + asyncContext.setTimeout(0); + latch.countDown(); + } + }); + + InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort()); + Session.Client session = client.connect(address, new Client.Listener() {}).get(5, TimeUnit.SECONDS); + MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_3, HttpFields.EMPTY); + HeadersFrame frame = new HeadersFrame(metaData, false); + Stream stream = session.newRequest(frame, null).get(5, TimeUnit.SECONDS); + + // Wait for the server to be in ASYNC_WAIT. + assertTrue(latch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new Exception()); + + if (notify) + // Wait for the reset to be notified to the async context listener. + await().atMost(5, TimeUnit.SECONDS).until(() -> + { + AsyncEvent asyncEvent = errorAsyncEventRef.get(); + return asyncEvent == null ? null : asyncEvent.getThrowable(); + }, instanceOf(EofException.class)); + else + // Wait for the reset to NOT be notified to the failure listener. + await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue()); + } +} From 95059356c988db708ef167bc0b1ddf443d837be3 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 19 Jun 2024 09:32:49 +1000 Subject: [PATCH 15/18] Fix contains in HttpFields name set and prove random access to HttpFields via EnumMap not worth it. (#11846) Fix #11811 with javadoc and benchmark --- .../org/eclipse/jetty/http/HttpFields.java | 35 +++- .../jetty/http/ImmutableHttpFields.java | 14 +- .../eclipse/jetty/http/MutableHttpFields.java | 4 +- .../eclipse/jetty/http/HttpFieldsTest.java | 24 ++- .../server/jmh/HashMapVsEnumMapBenchmark.java | 151 ++++++++++++++++++ 5 files changed, 208 insertions(+), 20 deletions(-) create mode 100644 tests/jetty-jmh/src/main/java/org/eclipse/jetty/server/jmh/HashMapVsEnumMapBenchmark.java diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java index 58be45a97b4..05bed910083 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java @@ -38,9 +38,16 @@ import java.util.stream.StreamSupport; /** *

An ordered collection of {@link HttpField}s that represent the HTTP headers * or HTTP trailers of an HTTP request or an HTTP response.

+ * *

{@link HttpFields} is immutable and typically used in server-side HTTP requests * and client-side HTTP responses, while {@link HttpFields.Mutable} is mutable and * typically used in server-side HTTP responses and client-side HTTP requests.

+ * + *

Access is always more efficient using {@link HttpHeader} keys rather than {@link String} field names.

+ * + *

The primary implementations of {@code HttpFields} have been optimized assuming few + * lookup operations, thus typically if many {@link HttpField}s need to looked up, it may be + * better to use an {@link Iterator} to find multiple fields in a single iteration.

*/ public interface HttpFields extends Iterable, Supplier { @@ -350,10 +357,12 @@ public interface HttpFields extends Iterable, Supplier /** *

Returns whether this instance contains the given field name.

*

The comparison of field name is case-insensitive via - * {@link HttpField#is(String)}. + * {@link HttpField#is(String)}. If possible, it is more efficient to use + * {@link #contains(HttpHeader)}. * * @param name the case-insensitive field name to search for * @return whether this instance contains the given field name + * @see #contains(HttpHeader) */ default boolean contains(String name) { @@ -412,7 +421,7 @@ public interface HttpFields extends Iterable, Supplier *

Returns the encoded value of the first field with the given field name, * or {@code null} if no such field is present.

*

The comparison of field name is case-insensitive via - * {@link HttpField#is(String)}.

+ * {@link HttpField#is(String)}. If possible, it is more efficient to use {@link #get(HttpHeader)}.

*

In case of multi-valued fields, the returned value is the encoded * value, including commas and quotes, as returned by {@link HttpField#getValue()}.

* @@ -420,6 +429,7 @@ public interface HttpFields extends Iterable, Supplier * @return the raw value of the first field with the given field name, * or {@code null} if no such field is present * @see HttpField#getValue() + * @see #get(HttpHeader) */ default String get(String name) { @@ -594,12 +604,13 @@ public interface HttpFields extends Iterable, Supplier *

Returns a {@link Set} of the field names.

*

Case-sensitivity of the field names is preserved.

* - * @return a {@link Set} of the field names + * @return an immutable {@link Set} of the field names. Changes made to the + * {@code HttpFields} after this call are not reflected in the set. */ default Set getFieldNamesCollection() { Set seenByHeader = EnumSet.noneOf(HttpHeader.class); - Set seenByName = null; + Set buildByName = null; List list = new ArrayList<>(size()); for (HttpField f : this) @@ -607,9 +618,9 @@ public interface HttpFields extends Iterable, Supplier HttpHeader header = f.getHeader(); if (header == null) { - if (seenByName == null) - seenByName = new TreeSet<>(String::compareToIgnoreCase); - if (seenByName.add(f.getName())) + if (buildByName == null) + buildByName = new TreeSet<>(String::compareToIgnoreCase); + if (buildByName.add(f.getName())) list.add(f.getName()); } else if (seenByHeader.add(header)) @@ -618,6 +629,8 @@ public interface HttpFields extends Iterable, Supplier } } + Set seenByName = buildByName; + // use the list to retain a rough ordering return new AbstractSet<>() { @@ -632,6 +645,14 @@ public interface HttpFields extends Iterable, Supplier { return list.size(); } + + @Override + public boolean contains(Object o) + { + if (o instanceof String s) + return seenByName != null && seenByName.contains(s) || seenByHeader.contains(HttpHeader.CACHE.get(s)); + return false; + } }; } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ImmutableHttpFields.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ImmutableHttpFields.java index b700258b269..7075c145679 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ImmutableHttpFields.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ImmutableHttpFields.java @@ -20,12 +20,7 @@ import java.util.Objects; import java.util.stream.Stream; /** - * HTTP Fields. A collection of HTTP header and or Trailer fields. - * - *

This class is not synchronized as it is expected that modifications will only be performed by a - * single thread. - * - *

The cookie handling provided by this class is guided by the Servlet specification and RFC6265. + * An immutable implementation of {@link HttpFields}. */ class ImmutableHttpFields implements HttpFields { @@ -70,10 +65,9 @@ class ImmutableHttpFields implements HttpFields { if (this == o) return true; - if (!(o instanceof org.eclipse.jetty.http.ImmutableHttpFields)) - return false; - - return isEqualTo((HttpFields)o); + if (o instanceof HttpFields httpFields) + return isEqualTo(httpFields); + return false; } @Override diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MutableHttpFields.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MutableHttpFields.java index 390c403b426..b60b342dee4 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MutableHttpFields.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MutableHttpFields.java @@ -67,7 +67,7 @@ class MutableHttpFields implements HttpFields.Mutable */ protected MutableHttpFields(HttpFields fields) { - if (fields instanceof ImmutableHttpFields immutable) + if (fields instanceof org.eclipse.jetty.http.ImmutableHttpFields immutable) { _immutable = true; _fields = immutable._fields; @@ -180,7 +180,7 @@ class MutableHttpFields implements HttpFields.Mutable public HttpFields asImmutable() { _immutable = true; - return new ImmutableHttpFields(_fields, _size); + return new org.eclipse.jetty.http.ImmutableHttpFields(_fields, _size); } private void copyImmutable() diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java index f2d3c8cdd1a..e4eba2d9fd0 100644 --- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java +++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java @@ -25,6 +25,7 @@ import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -352,16 +353,27 @@ public class HttpFieldsTest assertThat(header.get("EXPECT"), is("100")); assertThat(header.get("eXpEcT"), is("100")); assertThat(header.get(HttpHeader.EXPECT), is("100")); + assertTrue(header.contains("expect")); + assertTrue(header.contains("Expect")); + assertTrue(header.contains("EXPECT")); + assertTrue(header.contains("eXpEcT")); assertThat(header.get("random"), is("value")); assertThat(header.get("Random"), is("value")); assertThat(header.get("RANDOM"), is("value")); assertThat(header.get("rAnDoM"), is("value")); assertThat(header.get("RaNdOm"), is("value")); + assertTrue(header.contains("random")); + assertTrue(header.contains("Random")); + assertTrue(header.contains("RANDOM")); + assertTrue(header.contains("rAnDoM")); + assertTrue(header.contains("RaNdOm")); assertThat(header.get("Accept-Charset"), is("UTF-8")); assertThat(header.get("accept-charset"), is("UTF-8")); assertThat(header.get(HttpHeader.ACCEPT_CHARSET), is("UTF-8")); + assertTrue(header.contains("Accept-Charset")); + assertTrue(header.contains("accept-charset")); assertThat(header.getValuesList("Accept-Charset"), contains("UTF-8", "UTF-16")); assertThat(header.getValuesList("accept-charset"), contains("UTF-8", "UTF-16")); @@ -371,9 +383,19 @@ public class HttpFieldsTest assertThat(header.get("Foo-Bar"), is("one")); assertThat(header.getValuesList("foo-bar"), contains("one", "two")); assertThat(header.getValuesList("Foo-Bar"), contains("one", "two")); + assertTrue(header.contains("foo-bar")); + assertTrue(header.contains("Foo-Bar")); // We know the order of the set is deterministic - assertThat(header.getFieldNamesCollection(), contains("expect", "RaNdOm", "Accept-Charset", "foo-bar")); + Set names = header.getFieldNamesCollection(); + assertThat(names, contains("expect", "RaNdOm", "Accept-Charset", "foo-bar")); + assertTrue(names.contains("expect")); + assertTrue(names.contains("Expect")); + assertTrue(names.contains("random")); + assertTrue(names.contains("accept-charset")); + assertTrue(names.contains("Accept-Charset")); + assertTrue(names.contains("foo-bar")); + assertTrue(names.contains("Foo-Bar")); } @ParameterizedTest diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/server/jmh/HashMapVsEnumMapBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/server/jmh/HashMapVsEnumMapBenchmark.java new file mode 100644 index 00000000000..bc3ad439f36 --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/server/jmh/HashMapVsEnumMapBenchmark.java @@ -0,0 +1,151 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.jmh; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Thread) +@BenchmarkMode(Mode.Throughput) +@Fork(1) +@Warmup(iterations = 6, time = 2000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 3, time = 2000, timeUnit = TimeUnit.MILLISECONDS) +public class HashMapVsEnumMapBenchmark +{ + private static final HttpHeader[] HEADERS = HttpHeader.values(); + private static final HttpHeader[] HEADER_NAMES = + { + // These will be hits + HttpHeader.HOST, + HttpHeader.CONTENT_TYPE, + HttpHeader.CONTENT_LENGTH, + HttpHeader.ACCEPT, + + // These will be misses + HttpHeader.TRANSFER_ENCODING, + HttpHeader.AUTHORIZATION + }; + + private List newHeaders() + { + List list = new ArrayList<>(); + list.add(new HttpField(HttpHeader.HOST, "Localhost")); + list.add(new HttpField(HttpHeader.CONTENT_TYPE, "application/json")); + list.add(new HttpField(HttpHeader.CONTENT_LENGTH, "123")); + list.add(new HttpField(HttpHeader.USER_AGENT, "JMH Benchmark")); + list.add(new HttpField(HttpHeader.ACCEPT, "application/json")); + return list; + } + + @Benchmark + @OperationsPerInvocation(5) + public long testListLookup() + { + // Build the HashMap + List list = newHeaders(); + + // Perform lookups + long result = 0; + for (HttpHeader header : HEADER_NAMES) + { + for (HttpField field : list) + { + if (field.getHeader() == header) + { + result ^= field.getValue().hashCode(); + break; + } + } + } + return result; + } + + @Benchmark + @OperationsPerInvocation(5) + public long testHashMapBuildAndLookup() + { + // Build the HashMap + List list = newHeaders(); + Map hashMap = new HashMap<>(); + for (HttpField field : list) + { + hashMap.put(field.getName(), field); + } + + // Perform lookups + long result = 0; + for (HttpHeader header : HEADER_NAMES) + { + HttpField field = hashMap.get(header.asString()); + if (field != null) + result ^= field.getValue().hashCode(); + } + return result; + } + + @Benchmark + @OperationsPerInvocation(5) + public long testEnumMapBuildAndLookup() + { + // Build the EnumMap + Map enumMap = new EnumMap<>(HttpHeader.class); + + List list = newHeaders(); + for (HttpField field : list) + { + enumMap.put(field.getHeader(), field); + } + + // Perform lookups + long result = 0; + for (HttpHeader header : HEADERS) + { + HttpField field = enumMap.get(header); + if (field != null) + result ^= field.getValue().hashCode(); + } + return result; + } + + public static void main(String[] args) throws RunnerException + { + Options opt = new OptionsBuilder() + .include(HashMapVsEnumMapBenchmark.class.getSimpleName()) + // .addProfiler(GCProfiler.class) + .forks(1) + .build(); + + new Runner(opt).run(); + } +} From 6ee17f002ccd5e163ae0055e8711ce97b054b91a Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Wed, 19 Jun 2024 09:36:06 +0200 Subject: [PATCH 16/18] Issue #11847 implement environment context xml (#11859) * Issue #11847 implement environment context xml --- .../deploy/providers/ContextProvider.java | 119 +++++++++++++----- .../jetty/deploy/BarContextHandler.java | 20 +++ .../providers/ContextProviderStartupTest.java | 63 +++++++--- .../src/test/resources/etc/core-context.xml | 22 ++++ .../webapps/bar-core-context.properties | 2 + .../org/eclipse/jetty/server/Deployable.java | 2 + 6 files changed, 181 insertions(+), 47 deletions(-) create mode 100644 jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/BarContextHandler.java create mode 100644 jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml create mode 100644 jetty-core/jetty-deploy/src/test/resources/webapps/bar-core-context.properties diff --git a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java index 82d905b2155..37e7f7f6ff1 100644 --- a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java +++ b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java @@ -21,6 +21,7 @@ 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.HashMap; @@ -77,8 +78,8 @@ import org.slf4j.LoggerFactory; *

For XML configured contexts, the ID map will contain a reference to the {@link Server} instance called "Server" and * properties for the webapp file such as "jetty.webapp" and directory as "jetty.webapps". * The properties will be initialized with:

    - *
  • The properties set on the application via {@link App#getProperties()}; otherwise:
  • - *
  • The properties set on this provider via {@link #getProperties()}
  • + *
  • The properties set on the application via {@link App#getProperties()}; otherwise:
  • + *
  • The properties set on this provider via {@link #getProperties()}
  • *
*/ @ManagedObject("Provider for start-up deployment of webapps based on presence in directory") @@ -243,6 +244,7 @@ public class ContextProvider extends ScanningAppProvider /** * This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property. + * * @param configurations The configuration class names as a comma separated list */ public void setConfigurationClasses(String configurations) @@ -252,6 +254,7 @@ public class ContextProvider extends ScanningAppProvider /** * This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property. + * * @param configurations The configuration class names. */ public void setConfigurationClasses(String[] configurations) @@ -262,8 +265,8 @@ public class ContextProvider extends ScanningAppProvider } /** - * * This is equivalent to getting the {@link Deployable#CONFIGURATION_CLASSES} property. + * * @return The configuration class names. */ @ManagedAttribute("configuration classes for webapps to be processed through") @@ -341,32 +344,48 @@ public class ContextProvider extends ScanningAppProvider // prepare properties Map properties = new HashMap<>(); + + //add in properties from start mechanism properties.putAll(getProperties()); + + Object context = null; + //check if there is a specific ContextHandler type to create set in the + //properties associated with the webapp. If there is, we create it _before_ + //applying the environment xml file. + String contextHandlerClassName = app.getProperties().get(Deployable.CONTEXT_HANDLER_CLASS); + if (contextHandlerClassName != null) + context = Class.forName(contextHandlerClassName).getDeclaredConstructor().newInstance(); + + //add in environment-specific properties + String env = app.getEnvironmentName() == null ? "" : app.getEnvironmentName(); + Path envProperties = app.getPath().getParent().resolve(env + ".properties"); + if (Files.exists(envProperties)) + { + try (InputStream stream = Files.newInputStream(envProperties)) + { + Properties p = new Properties(); + p.load(stream); + p.stringPropertyNames().forEach(k -> properties.put(k, p.getProperty(k))); + } + + String str = properties.get(Deployable.ENVIRONMENT_XML); + if (!StringUtil.isEmpty(str)) + { + Path envXmlPath = Paths.get(str); + if (!envXmlPath.isAbsolute()) + envXmlPath = getMonitoredDirResource().getPath().getParent().resolve(envXmlPath); + + context = applyXml(context, envXmlPath, env, properties); + } + } + + //add in properties specific to the deployable properties.putAll(app.getProperties()); // Handle a context XML file if (FileID.isXml(path)) { - XmlConfiguration xmlc = new XmlConfiguration(ResourceFactory.of(this).newResource(path), null, properties) - { - @Override - public void initializeDefaults(Object context) - { - super.initializeDefaults(context); - ContextProvider.this.initializeContextHandler(context, path, properties); - } - }; - - xmlc.getIdMap().put("Environment", environment); - xmlc.setJettyStandardIdsAndProperties(getDeploymentManager().getServer(), path); - - // If it is a core context environment, then look for a classloader - ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null; - if (coreContextClassLoader != null) - Thread.currentThread().setContextClassLoader(coreContextClassLoader); - - // Create the context by running the configuration - Object context = xmlc.configure(); + context = applyXml(context, path, env, properties); // Look for the contextHandler itself ContextHandler contextHandler = null; @@ -382,27 +401,33 @@ public class ContextProvider extends ScanningAppProvider throw new IllegalStateException("Unknown context type of " + context); // Set the classloader if we have a coreContextClassLoader + ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null; if (coreContextClassLoader != null) contextHandler.setClassLoader(coreContextClassLoader); return contextHandler; } + // Otherwise it must be a directory or an archive else if (!Files.isDirectory(path) && !FileID.isWebArchive(path)) { throw new IllegalStateException("unable to create ContextHandler for " + app); } - // Build the web application - String contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass"); - if (StringUtil.isBlank(contextHandlerClassName)) - throw new IllegalStateException("No ContextHandler classname for " + app); - Class contextHandlerClass = Loader.loadClass(contextHandlerClassName); - if (contextHandlerClass == null) - throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app); + // Build the web application if necessary + if (context == null) + { + contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass"); + if (StringUtil.isBlank(contextHandlerClassName)) + throw new IllegalStateException("No ContextHandler classname for " + app); + Class contextHandlerClass = Loader.loadClass(contextHandlerClassName); + if (contextHandlerClass == null) + throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app); + + context = contextHandlerClass.getDeclaredConstructor().newInstance(); + properties.put(Deployable.WAR, path.toString()); + } - Object context = contextHandlerClass.getDeclaredConstructor().newInstance(); - properties.put(Deployable.WAR, path.toString()); return initializeContextHandler(context, path, properties); } finally @@ -411,6 +436,36 @@ public class ContextProvider extends ScanningAppProvider } } + protected Object applyXml(Object context, Path xml, String environment, Map properties) throws Exception + { + if (!FileID.isXml(xml)) + return null; + + XmlConfiguration xmlc = new XmlConfiguration(ResourceFactory.of(this).newResource(xml), null, properties) + { + @Override + public void initializeDefaults(Object context) + { + super.initializeDefaults(context); + ContextProvider.this.initializeContextHandler(context, xml, properties); + } + }; + + xmlc.getIdMap().put("Environment", environment); + xmlc.setJettyStandardIdsAndProperties(getDeploymentManager().getServer(), xml); + + // If it is a core context environment, then look for a classloader + ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(xml) : null; + if (coreContextClassLoader != null) + Thread.currentThread().setContextClassLoader(coreContextClassLoader); + + // Create or configure the context + if (context == null) + return xmlc.configure(); + + return xmlc.configure(context); + } + protected ClassLoader findCoreContextClassLoader(Path path) throws IOException { Path webapps = path.getParent(); diff --git a/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/BarContextHandler.java b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/BarContextHandler.java new file mode 100644 index 00000000000..0ccd1dc10ff --- /dev/null +++ b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/BarContextHandler.java @@ -0,0 +1,20 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.deploy; + +import org.eclipse.jetty.server.handler.ContextHandler; + +public class BarContextHandler extends ContextHandler +{ +} diff --git a/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java index 573b4d558d1..c9072312e46 100644 --- a/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java +++ b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java @@ -15,27 +15,18 @@ package org.eclipse.jetty.deploy.providers; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.TimeUnit; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; -import org.eclipse.jetty.deploy.AppProvider; -import org.eclipse.jetty.deploy.DeploymentManager; +import org.eclipse.jetty.deploy.BarContextHandler; import org.eclipse.jetty.deploy.test.XmlConfiguredJetty; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.Deployable; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenPaths; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.util.Scanner; -import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.LifeCycle; -import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -77,7 +69,10 @@ public class ContextProviderStartupTest // Should not throw an Exception jetty.load(); + } + public void startJetty() throws Exception + { // Start it jetty.start(); } @@ -89,9 +84,47 @@ public class ContextProviderStartupTest } @Test - public void testStartupContext() + public void testStartupContext() throws Exception { + startJetty(); + // Check Server for Handlers jetty.assertContextHandlerExists("/bar"); + + } + + @Test + public void testStartupWithRelativeEnvironmentContext() throws Exception + { + Path jettyBase = jetty.getJettyBasePath(); + Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = etc/core-context.xml", StandardOpenOption.CREATE_NEW); + assertTrue(Files.exists(propsFile)); + Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING); + jetty.copyWebapp("bar-core-context.properties", "bar.properties"); + startJetty(); + + //check environment context xml was applied to the produced context + ContextHandler context = jetty.getContextHandler("/bar"); + assertNotNull(context); + assertThat(context.getAttribute("somename"), equalTo("somevalue")); + assertTrue(context instanceof BarContextHandler); + + } + + @Test + public void testStartupWithAbsoluteEnvironmentContext() throws Exception + { + Path jettyBase = jetty.getJettyBasePath(); + Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = " + + MavenPaths.findTestResourceFile("etc/core-context.xml"), StandardOpenOption.CREATE_NEW); + assertTrue(Files.exists(propsFile)); + Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING); + jetty.copyWebapp("bar-core-context.properties", "bar-core-context.properties"); + startJetty(); + + //check environment context xml was applied to the produced context + ContextHandler context = jetty.getContextHandler("/bar"); + assertNotNull(context); + assertThat(context.getAttribute("somename"), equalTo("somevalue")); } } diff --git a/jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml b/jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml new file mode 100644 index 00000000000..1eb453c7599 --- /dev/null +++ b/jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + /global + + somename + somevalue + + diff --git a/jetty-core/jetty-deploy/src/test/resources/webapps/bar-core-context.properties b/jetty-core/jetty-deploy/src/test/resources/webapps/bar-core-context.properties new file mode 100644 index 00000000000..2ad51333374 --- /dev/null +++ b/jetty-core/jetty-deploy/src/test/resources/webapps/bar-core-context.properties @@ -0,0 +1,2 @@ +environment: core +jetty.deploy.contextHandlerClass: org.eclipse.jetty.deploy.BarContextHandler \ No newline at end of file diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Deployable.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Deployable.java index 9beedc42b22..d08275f8ccc 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Deployable.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Deployable.java @@ -56,8 +56,10 @@ public interface Deployable String CONFIGURATION_CLASSES = "jetty.deploy.configurationClasses"; String CONTAINER_SCAN_JARS = "jetty.deploy.containerScanJarPattern"; String CONTEXT_PATH = "jetty.deploy.contextPath"; + String CONTEXT_HANDLER_CLASS = "jetty.deploy.contextHandlerClass"; String DEFAULTS_DESCRIPTOR = "jetty.deploy.defaultsDescriptor"; String ENVIRONMENT = "environment"; + String ENVIRONMENT_XML = "jetty.deploy.environmentXml"; String EXTRACT_WARS = "jetty.deploy.extractWars"; String PARENT_LOADER_PRIORITY = "jetty.deploy.parentLoaderPriority"; String SCI_EXCLUSION_PATTERN = "jetty.deploy.servletContainerInitializerExclusionPattern"; From 7011827a8aca9f5e42988339a98b69eb462df624 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 6 Jun 2024 14:40:39 -0500 Subject: [PATCH 17/18] Introduce Cross Context Dispatch testing --- tests/pom.xml | 1 + .../ccd-common/pom.xml | 31 +++ .../jetty/tests/ccd/common/DispatchPlan.java | 215 +++++++++++++++++ .../tests/ccd/common/DispatchPlanHandler.java | 77 +++++++ .../jetty/tests/ccd/common/DispatchType.java | 20 ++ .../jetty/tests/ccd/common/HttpRequest.java | 75 ++++++ .../tests/ccd/common/PlanSessionCache.java | 105 +++++++++ .../jetty/tests/ccd/common/Property.java | 69 ++++++ .../eclipse/jetty/tests/ccd/common/Step.java | 183 +++++++++++++++ .../ccd/common/DispatchPlanLoadTest.java | 76 ++++++ .../test/resources/forward-include-dump.txt | 30 +++ .../ccd-ee10-webapp/pom.xml | 39 ++++ .../jetty/tests/ccd/ee10/CCDServlet.java | 108 +++++++++ .../jetty/tests/ccd/ee10/DumpServlet.java | 135 +++++++++++ .../jetty/tests/ccd/ee10/ForwardServlet.java | 35 +++ .../ccd/ee10/InternalRequestURIFilter.java | 56 +++++ .../src/main/webapp/WEB-INF/web.xml | 29 +++ .../ccd-ee8-webapp/pom.xml | 39 ++++ .../jetty/tests/ccd/ee8/CCDServlet.java | 107 +++++++++ .../jetty/tests/ccd/ee8/DumpServlet.java | 135 +++++++++++ .../jetty/tests/ccd/ee8/ForwardServlet.java | 34 +++ .../ccd/ee8/InternalRequestURIFilter.java | 55 +++++ .../src/main/webapp/WEB-INF/web.xml | 38 +++ .../ccd-ee9-webapp/pom.xml | 39 ++++ .../jetty/tests/ccd/ee9/CCDServlet.java | 108 +++++++++ .../jetty/tests/ccd/ee9/DumpServlet.java | 135 +++++++++++ .../jetty/tests/ccd/ee9/ForwardServlet.java | 35 +++ .../ccd/ee9/InternalRequestURIFilter.java | 56 +++++ .../src/main/webapp/WEB-INF/web.xml | 29 +++ .../ccd-tests/pom.xml | 86 +++++++ .../redispatch/AbstractRedispatchTest.java | 216 ++++++++++++++++++ .../redispatch/RedispatchPlansTests.java | 179 +++++++++++++++ .../tests/redispatch/RedispatchTests.java | 108 +++++++++ .../tests/redispatch/ResponseDetails.java | 49 ++++ .../src/test/resources/error-handler.xml | 10 + .../test/resources/install-ccd-handler.xml | 11 + .../plans/context-ee10-forward-dump.txt | 12 + .../plans/context-ee8-forward-dump.txt | 12 + .../plans/context-ee8-include-dump.txt | 14 ++ .../ee10-forward-to-ee8-include-ee9-dump.txt | 30 +++ .../plans/ee10-request-forward-dump.txt | 12 + .../plans/ee10-request-include-dump.txt | 12 + .../plans/ee10-session-ee8-ee9-ee8.txt | 36 +++ ...orward-to-ee8-session-include-ee9-dump.txt | 50 ++++ .../plans/ee8-request-forward-dump.txt | 12 + .../plans/ee8-request-include-dump.txt | 12 + .../test/resources/webapp-xmls/ccd-ee10.xml | 19 ++ .../test/resources/webapp-xmls/ccd-ee8.xml | 21 ++ .../test/resources/webapp-xmls/ccd-ee9.xml | 21 ++ tests/test-cross-context-dispatch/pom.xml | 45 ++++ 50 files changed, 3061 insertions(+) create mode 100644 tests/test-cross-context-dispatch/ccd-common/pom.xml create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlan.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanHandler.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchType.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/HttpRequest.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/PlanSessionCache.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Property.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Step.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/test/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanLoadTest.java create mode 100644 tests/test-cross-context-dispatch/ccd-common/src/test/resources/forward-include-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-ee10-webapp/pom.xml create mode 100644 tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/CCDServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/DumpServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/ForwardServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/InternalRequestURIFilter.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/webapp/WEB-INF/web.xml create mode 100644 tests/test-cross-context-dispatch/ccd-ee8-webapp/pom.xml create mode 100644 tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/CCDServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/DumpServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/ForwardServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/InternalRequestURIFilter.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/webapp/WEB-INF/web.xml create mode 100644 tests/test-cross-context-dispatch/ccd-ee9-webapp/pom.xml create mode 100644 tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/CCDServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/DumpServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/ForwardServlet.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/InternalRequestURIFilter.java create mode 100644 tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/webapp/WEB-INF/web.xml create mode 100644 tests/test-cross-context-dispatch/ccd-tests/pom.xml create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/AbstractRedispatchTest.java create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchPlansTests.java create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchTests.java create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/ResponseDetails.java create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/error-handler.xml create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/install-ccd-handler.xml create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee10-forward-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-forward-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-include-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-forward-to-ee8-include-ee9-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-forward-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-include-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-ee8-ee9-ee8.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-forward-to-ee8-session-include-ee9-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-forward-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-include-dump.txt create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee10.xml create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee8.xml create mode 100644 tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee9.xml create mode 100644 tests/test-cross-context-dispatch/pom.xml diff --git a/tests/pom.xml b/tests/pom.xml index aa2779f15b6..70ae1ce5d8b 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -15,6 +15,7 @@ jetty-jmh jetty-test-multipart jetty-test-session-common + test-cross-context-dispatch test-distribution test-integration test-jpms diff --git a/tests/test-cross-context-dispatch/ccd-common/pom.xml b/tests/test-cross-context-dispatch/ccd-common/pom.xml new file mode 100644 index 00000000000..e63a4187157 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + org.eclipse.jetty.tests.ccd + test-cross-context-dispatch + 12.0.11-SNAPSHOT + + ccd-common + jar + Tests :: Cross Context Dispatch :: Common + + + + org.eclipse.jetty + jetty-server + ${project.version} + + + org.eclipse.jetty + jetty-session + ${project.version} + + + org.eclipse.jetty + jetty-slf4j-impl + ${project.version} + test + + + diff --git a/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlan.java b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlan.java new file mode 100644 index 00000000000..e1c8c86081c --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlan.java @@ -0,0 +1,215 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.LinkedBlockingDeque; + +public class DispatchPlan +{ + private final Deque steps = new LinkedBlockingDeque<>(); + private final List events = new ArrayList<>(); + private final List expectedEvents = new ArrayList<>(); + private final List expectedProperties = new ArrayList<>(); + private final List expectedOutput = new ArrayList<>(); + private HttpRequest requestStep; + private String id; + private String expectedContentType; + // if true, assert that all Session.id seen in request attributes are the same id. + private boolean expectedSessionIds; + + public DispatchPlan() + { + } + + public static DispatchPlan read(Path inputText) throws IOException + { + DispatchPlan plan = new DispatchPlan(); + + plan.id = inputText.getFileName().toString(); + + for (String line : Files.readAllLines(inputText, StandardCharsets.UTF_8)) + { + if (line.startsWith("#")) + continue; // skip + if (line.startsWith("REQUEST|")) + { + plan.setRequestStep(HttpRequest.parse(line)); + } + else if (line.startsWith("STEP|")) + { + plan.addStep(Step.parse(line)); + } + else if (line.startsWith("EXPECTED_CONTENT_TYPE|")) + { + plan.setExpectedContentType(dropType(line)); + } + else if (line.startsWith("EXPECTED_EVENT|")) + { + plan.addExpectedEvent(dropType(line)); + } + else if (line.startsWith("EXPECTED_PROP|")) + { + plan.addExpectedProperty(Property.parse(line)); + } + else if (line.startsWith("EXPECTED_OUTPUT|")) + { + plan.addExpectedOutput(dropType(line)); + } + else if (line.startsWith("EXPECTED_SESSION_IDS|")) + { + plan.setExpectedSessionIds(Boolean.parseBoolean(dropType(line))); + } + } + return plan; + } + + private static String dropType(String line) + { + int idx = line.indexOf("|"); + return line.substring(idx + 1); + } + + public void addEvent(String format, Object... args) + { + events.add(String.format(format, args)); + } + + public void addExpectedEvent(String event) + { + expectedEvents.add(event); + } + + public void addExpectedOutput(String output) + { + expectedOutput.add(output); + } + + public void addExpectedProperty(String name, String value) + { + expectedProperties.add(new Property(name, value)); + } + + public void addExpectedProperty(Property property) + { + expectedProperties.add(property); + } + + public void addStep(Step step) + { + steps.add(step); + } + + public List getEvents() + { + return events; + } + + public String getExpectedContentType() + { + return expectedContentType; + } + + public void setExpectedContentType(String expectedContentType) + { + this.expectedContentType = expectedContentType; + } + + public List getExpectedEvents() + { + return expectedEvents; + } + + public void setExpectedEvents(String[] events) + { + expectedEvents.clear(); + expectedEvents.addAll(List.of(events)); + } + + public List getExpectedOutput() + { + return expectedOutput; + } + + public void setExpectedOutput(String[] output) + { + expectedOutput.clear(); + expectedOutput.addAll(List.of(output)); + } + + public List getExpectedProperties() + { + return expectedProperties; + } + + public void setExpectedProperties(Property[] properties) + { + expectedProperties.clear(); + expectedProperties.addAll(List.of(properties)); + } + + public HttpRequest getRequestStep() + { + return requestStep; + } + + public void setRequestStep(HttpRequest requestStep) + { + this.requestStep = requestStep; + } + + public Deque getSteps() + { + return steps; + } + + public void setSteps(Step[] stepArr) + { + steps.clear(); + for (Step step: stepArr) + steps.add(step); + } + + public boolean isExpectedSessionIds() + { + return expectedSessionIds; + } + + public void setExpectedSessionIds(boolean expectedSessionIds) + { + this.expectedSessionIds = expectedSessionIds; + } + + public String id() + { + return id; + } + + public Step popStep() + { + return steps.pollFirst(); + } + + @Override + public String toString() + { + return "DispatchPlan[id=" + id + "]"; + } +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanHandler.java b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanHandler.java new file mode 100644 index 00000000000..7eba06f42c9 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanHandler.java @@ -0,0 +1,77 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DispatchPlanHandler extends Handler.Wrapper +{ + private static final Logger LOG = LoggerFactory.getLogger(DispatchPlanHandler.class); + private Path plansDir; + + public Path getPlansDir() + { + return plansDir; + } + + public void setPlansDir(Path plansDir) + { + this.plansDir = plansDir; + } + + public void setPlansDir(String plansDir) + { + this.setPlansDir(Path.of(plansDir)); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + DispatchPlan dispatchPlan = (DispatchPlan)request.getAttribute(DispatchPlan.class.getName()); + + if (dispatchPlan == null) + { + String planName = request.getHeaders().get("X-DispatchPlan"); + if (planName != null) + { + Path planPath = plansDir.resolve(planName); + if (!Files.isRegularFile(planPath)) + { + callback.failed(new IOException("Unable to find: " + planPath)); + } + dispatchPlan = DispatchPlan.read(planPath); + dispatchPlan.addEvent("Initial plan: %s", planName); + request.setAttribute(DispatchPlan.class.getName(), dispatchPlan); + } + else + { + LOG.info("Missing Request Header [X-DispatchPlan], skipping DispatchPlan behaviors for this request: {}", request.getHttpURI().toURI()); + } + } + + if (dispatchPlan != null) + dispatchPlan.addEvent("DispatchPlanHandler.handle() method=%s path-query=%s", request.getMethod(), request.getHttpURI().getPathQuery()); + + return super.handle(request, response, callback); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchType.java b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchType.java new file mode 100644 index 00000000000..806852eab2b --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/DispatchType.java @@ -0,0 +1,20 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +public enum DispatchType +{ + INCLUDE, + FORWARD; +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/HttpRequest.java b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/HttpRequest.java new file mode 100644 index 00000000000..da18adc0420 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/HttpRequest.java @@ -0,0 +1,75 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +import java.util.Map; + +public class HttpRequest implements Step +{ + private String method; + private String requestPath; + private String body; + private Map headers; + + public static HttpRequest parse(String line) + { + String[] parts = line.split("\\|"); + HttpRequest request = new HttpRequest(); + request.setMethod(parts[1]); + request.setRequestPath(parts[2]); + if (parts.length > 4) + request.setBody(parts[3]); + return request; + } + + public String getMethod() + { + return method; + } + + public void setMethod(String method) + { + this.method = method; + } + + public String getRequestPath() + { + return requestPath; + } + + public void setRequestPath(String requestPath) + { + this.requestPath = requestPath; + } + + public String getBody() + { + return body; + } + + public void setBody(String body) + { + this.body = body; + } + + public Map getHeaders() + { + return headers; + } + + public void setHeaders(Map headers) + { + this.headers = headers; + } +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/PlanSessionCache.java b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/PlanSessionCache.java new file mode 100644 index 00000000000..13a14c8109c --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/PlanSessionCache.java @@ -0,0 +1,105 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.eclipse.jetty.session.DefaultSessionCache; +import org.eclipse.jetty.session.ManagedSession; +import org.eclipse.jetty.session.NullSessionDataStore; +import org.eclipse.jetty.session.SessionData; +import org.eclipse.jetty.session.SessionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PlanSessionCache extends DefaultSessionCache +{ + private static final Logger LOG = LoggerFactory.getLogger(PlanSessionCache.class); + private final Path outputFile; + + public PlanSessionCache(SessionManager manager) + { + super(manager); + outputFile = Path.of(System.getProperty("jetty.base"), "work/session.log"); + setSessionDataStore(new NullSessionDataStore()); + } + + @Override + public ManagedSession newSession(SessionData data) + { + logEvent("newSession()", data); + return super.newSession(data); + } + + @Override + public void commit(ManagedSession session) throws Exception + { + logEvent("commit()", session); + super.commit(session); + } + + @Override + public void release(ManagedSession session) throws Exception + { + logEvent("release()", session); + super.release(session); + } + + private void logEvent(String eventType, SessionData data) + { + String name = "SessionCache.event." + eventType; + String value = ""; + if (data != null) + { + value = String.format("id=%s|contextPath=%s", data.getId(), data.getContextPath()); + } + logAttribute(name, value); + } + + private void logEvent(String eventType, ManagedSession session) + { + String name = "SessionCache.event." + eventType; + String value = ""; + if (session != null) + { + value = String.format("id=%s", session.getId()); + SessionData data = session.getSessionData(); + if (data != null) + { + value = String.format("id=%s|contextPath=%s", data.getId(), data.getContextPath()); + } + } + logAttribute(name, value); + } + + private void logAttribute(String name, String value) + { + String line = name + "=" + value; + if (LOG.isInfoEnabled()) + LOG.info(line); + + try + { + Files.writeString(outputFile, line + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND); + } + catch (IOException e) + { + LOG.warn("Unable to write to " + outputFile, e); + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Property.java b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Property.java new file mode 100644 index 00000000000..cb07bc36052 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Property.java @@ -0,0 +1,69 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +public class Property +{ + private String name; + private String value; + + public Property() + { + } + + public Property(String name, String value) + { + this.name = name; + this.value = value; + } + + public static Property parse(String line) + { + String[] parts = line.split("\\|"); + String name = parts[1]; + String value = null; + if (parts.length > 2) + value = parts[2]; + return new Property(name, value); + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getValue() + { + return value; + } + + public void setValue(String value) + { + this.value = value; + } + + @Override + public String toString() + { + return "Property{" + + "name='" + name + '\'' + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Step.java b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Step.java new file mode 100644 index 00000000000..199851f4688 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/main/java/org/eclipse/jetty/tests/ccd/common/Step.java @@ -0,0 +1,183 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +public interface Step +{ + static Step parse(String line) + { + String[] parts = line.split("\\|"); + switch (parts[1]) + { + case "CONTEXT_FORWARD" -> + { + ContextRedispatch step = new ContextRedispatch(); + step.setDispatchType(DispatchType.FORWARD); + step.setContextPath(parts[2]); + step.setDispatchPath(parts[3]); + return step; + } + case "CONTEXT_INCLUDE" -> + { + ContextRedispatch step = new ContextRedispatch(); + step.setDispatchType(DispatchType.INCLUDE); + step.setContextPath(parts[2]); + step.setDispatchPath(parts[3]); + return step; + } + case "REQUEST_FORWARD" -> + { + RequestDispatch step = new RequestDispatch(); + step.setDispatchType(DispatchType.FORWARD); + step.setDispatchPath(parts[2]); + return step; + } + case "REQUEST_INCLUDE" -> + { + RequestDispatch step = new RequestDispatch(); + step.setDispatchType(DispatchType.INCLUDE); + step.setDispatchPath(parts[2]); + return step; + } + case "GET_HTTP_SESSION_ATTRIBUTE" -> + { + GetHttpSession step = new GetHttpSession(); + step.setName(parts[2]); + return step; + } + case "SET_HTTP_SESSION_ATTRIBUTE" -> + { + String name = parts[2]; + String value = parts[3]; + Property prop = new Property(name, value); + HttpSessionSetAttribute step = new HttpSessionSetAttribute(prop); + return step; + } + } + throw new RuntimeException("Unknown STEP type [" + parts[1] + "]"); + } + + /** + * Will cause an Attribute to be set on the HttpSession via {@code HttpSession.setAttribute(String, Object)} + */ + class HttpSessionSetAttribute implements Step + { + private Property property; + + public HttpSessionSetAttribute(Property property) + { + this.property = property; + } + + public Property getProperty() + { + return property; + } + } + + /** + * Will cause the HttpSession to be fetched via {@code HttpServletRequest#getHttpSession(false)} + * and report the state of the HttpSession in the events (even if null). + */ + class GetHttpSession implements Step + { + private String name; + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + } + + /** + * Performs a Redispatch with FORWARD or INCLUDE types using the {@code ServletContext}. + * Uses the {@code ServletContext.getContext(contextPath)} to obtain the + * {@code ServletContext} to then use {@code ServletContext.getRequestDispatcher(dispatchPath)} + * against, which then results in a {@code RequestDispatcher.include} or {@code RequestDispatcher.forward} + * call. + */ + class ContextRedispatch implements Step + { + private DispatchType dispatchType; + private String contextPath; + private String dispatchPath; + + public DispatchType getDispatchType() + { + return dispatchType; + } + + public void setDispatchType(DispatchType dispatchType) + { + this.dispatchType = dispatchType; + } + + public String getContextPath() + { + return contextPath; + } + + public void setContextPath(String contextPath) + { + this.contextPath = contextPath; + } + + public String getDispatchPath() + { + return dispatchPath; + } + + public void setDispatchPath(String dispatchPath) + { + this.dispatchPath = dispatchPath; + } + } + + /** + * Performs a Redispatch with FORWARD or INCLUDE types using the {@code HttpServletRequest}. + * Uses the {@code HttpServletRequest.getRequestDispatcher(dispatchPath)} which then + * results in a {@code RequestDispatcher.include} or {@code RequestDispatcher.forward} + * call. + */ + class RequestDispatch implements Step + { + private DispatchType dispatchType; + private String dispatchPath; + + public DispatchType getDispatchType() + { + return dispatchType; + } + + public void setDispatchType(DispatchType dispatchType) + { + this.dispatchType = dispatchType; + } + + public String getDispatchPath() + { + return dispatchPath; + } + + public void setDispatchPath(String dispatchPath) + { + this.dispatchPath = dispatchPath; + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/test/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanLoadTest.java b/tests/test-cross-context-dispatch/ccd-common/src/test/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanLoadTest.java new file mode 100644 index 00000000000..722108dd364 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/test/java/org/eclipse/jetty/tests/ccd/common/DispatchPlanLoadTest.java @@ -0,0 +1,76 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.common; + +import java.io.IOException; +import java.nio.file.Path; + +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ExtendWith(WorkDirExtension.class) +public class DispatchPlanLoadTest +{ + @Test + public void testRead() throws IOException + { + Path planPath = MavenPaths.findTestResourceFile("forward-include-dump.txt"); + DispatchPlan plan = DispatchPlan.read(planPath); + assertNotNull(plan); + + assertThat("plan.id", plan.id(), is(planPath.getFileName().toString())); + + HttpRequest httpRequestStep = plan.getRequestStep(); + assertThat(httpRequestStep.getMethod(), is("GET")); + assertThat(httpRequestStep.getRequestPath(), is("/ccd-ee10/redispatch/ee10")); + assertThat(httpRequestStep.getBody(), is(nullValue())); + + assertEquals(3, plan.getSteps().size()); + + Step step = plan.popStep(); + assertThat(step, instanceOf(Step.ContextRedispatch.class)); + Step.ContextRedispatch contextRedispatchStep = (Step.ContextRedispatch)step; + assertThat(contextRedispatchStep.getDispatchType(), is(DispatchType.FORWARD)); + assertThat(contextRedispatchStep.getContextPath(), is("/ccd-ee8")); + assertThat(contextRedispatchStep.getDispatchPath(), is("/redispatch/ee8")); + + step = plan.popStep(); + assertThat(step, instanceOf(Step.ContextRedispatch.class)); + contextRedispatchStep = (Step.ContextRedispatch)step; + assertThat(contextRedispatchStep.getDispatchType(), is(DispatchType.FORWARD)); + assertThat(contextRedispatchStep.getContextPath(), is("/ccd-ee9")); + assertThat(contextRedispatchStep.getDispatchPath(), is("/redispatch/ee9")); + + step = plan.popStep(); + assertThat(step, instanceOf(Step.RequestDispatch.class)); + Step.RequestDispatch requestRedispatchStep = (Step.RequestDispatch)step; + assertThat(requestRedispatchStep.getDispatchType(), is(DispatchType.INCLUDE)); + assertThat(requestRedispatchStep.getDispatchPath(), is("/dump/ee9")); + + assertThat(plan.getExpectedContentType(), is("text/x-java-properties; charset=utf-8")); + + assertThat("Expected Events", plan.getExpectedEvents().size(), is(6)); + assertThat("Expected Output", plan.getExpectedOutput().size(), is(1)); + assertThat("Expected Properties", plan.getExpectedProperties().size(), is(18)); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-common/src/test/resources/forward-include-dump.txt b/tests/test-cross-context-dispatch/ccd-common/src/test/resources/forward-include-dump.txt new file mode 100644 index 00000000000..b13d4539b7d --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-common/src/test/resources/forward-include-dump.txt @@ -0,0 +1,30 @@ +REQUEST|GET|/ccd-ee10/redispatch/ee10 +STEP|CONTEXT_FORWARD|/ccd-ee8|/redispatch/ee8 +STEP|CONTEXT_FORWARD|/ccd-ee9|/redispatch/ee9 +STEP|REQUEST_INCLUDE|/dump/ee9 +EXPECTED_CONTENT_TYPE|text/x-java-properties; charset=utf-8 +EXPECTED_EVENT|Initial plan: ee10-forward-to-ee8-include-ee9-dump.json +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee9/redispatch/ee9 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee9/redispatch/ee9 +EXPECTED_PROP|request.dispatcherType|INCLUDE +EXPECTED_PROP|request.requestURI|/ccd-ee9/redispatch/ee9 +EXPECTED_PROP|attr[jakarta.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|attr[jakarta.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|attr[jakarta.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|attr[jakarta.servlet.forward.servlet_path]|/redispatch +EXPECTED_PROP|attr[jakarta.servlet.include.context_path]/ccd-ee9 +EXPECTED_PROP|attr[jakarta.servlet.include.path_info]|/ee9 +EXPECTED_PROP|attr[jakarta.servlet.include.request_uri]|/ccd-ee9/dump/ee9 +EXPECTED_PROP|attr[jakarta.servlet.include.servlet_path]/dump +EXPECTED_PROP|attr[javax.servlet.include.context_path]| +EXPECTED_PROP|attr[javax.servlet.include.path_info]| +EXPECTED_PROP|attr[javax.servlet.include.request_uri]| +EXPECTED_PROP|attr[javax.servlet.include.servlet_path]| +EXPECTED_PROP|attr[javax.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|attr[javax.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|attr[javax.servlet.forward.servlet_path]|/redispatch +EXPECTED_OUTPUT|foo-bar \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-ee10-webapp/pom.xml b/tests/test-cross-context-dispatch/ccd-ee10-webapp/pom.xml new file mode 100644 index 00000000000..944dc528d8e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee10-webapp/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.eclipse.jetty.tests.ccd + test-cross-context-dispatch + 12.0.11-SNAPSHOT + + ccd-ee10-webapp + war + Tests :: Cross Context Dispatch :: ee10 WebApp + + + + + org.eclipse.jetty.ee10 + jetty-ee10-bom + ${project.version} + pom + import + + + + + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided + + + org.eclipse.jetty.tests.ccd + ccd-common + ${project.version} + provided + + + diff --git a/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/CCDServlet.java b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/CCDServlet.java new file mode 100644 index 00000000000..6d33524295f --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/CCDServlet.java @@ -0,0 +1,108 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee10; + +import java.io.IOException; +import java.util.Objects; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.eclipse.jetty.tests.ccd.common.DispatchPlan; +import org.eclipse.jetty.tests.ccd.common.Property; +import org.eclipse.jetty.tests.ccd.common.Step; + +public class CCDServlet extends HttpServlet +{ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName()); + + if (dispatchPlan == null) + throw new ServletException("Unable to find DispatchPlan"); + + dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s", + this.getClass().getName(), + req.getDispatcherType(), req.getMethod(), req.getRequestURI()); + + Step step; + + while ((step = dispatchPlan.popStep()) != null) + { + if (step instanceof Step.ContextRedispatch contextRedispatchStep) + { + ServletContext otherContext = getServletContext().getContext(contextRedispatchStep.getContextPath()); + if (otherContext == null) + throw new NullPointerException("ServletContext.getContext(\"" + contextRedispatchStep.getContextPath() + "\") returned null"); + RequestDispatcher dispatcher = otherContext.getRequestDispatcher(contextRedispatchStep.getDispatchPath()); + if (dispatcher == null) + throw new NullPointerException("ServletContext.getRequestDispatcher(\"" + contextRedispatchStep.getDispatchPath() + "\") returned null"); + switch (contextRedispatchStep.getDispatchType()) + { + case FORWARD -> dispatcher.forward(req, resp); + case INCLUDE -> dispatcher.include(req, resp); + } + return; + } + else if (step instanceof Step.RequestDispatch requestDispatchStep) + { + RequestDispatcher dispatcher = req.getRequestDispatcher(requestDispatchStep.getDispatchPath()); + if (dispatcher == null) + throw new NullPointerException("HttpServletRequest.getRequestDispatcher(\"" + requestDispatchStep.getDispatchPath() + "\") returned null"); + switch (requestDispatchStep.getDispatchType()) + { + case FORWARD -> dispatcher.forward(req, resp); + case INCLUDE -> dispatcher.include(req, resp); + } + return; + } + else if (step instanceof Step.GetHttpSession getHttpSessionTask) + { + HttpSession session = req.getSession(false); + if (session == null) + { + dispatchPlan.addEvent("%s.service() HttpSession is null", + this.getClass().getName()); + } + else + { + String name = getHttpSessionTask.getName(); + Object value = session.getAttribute(name); + dispatchPlan.addEvent("%s.service() HttpSession exists: [%s]=[%s]", + this.getClass().getName(), + name, + Objects.toString(value) + ); + } + + } + else if (step instanceof Step.HttpSessionSetAttribute sessionSetAttribute) + { + HttpSession session = req.getSession(true); + req.setAttribute("session[" + req.getRequestURI() + "].id", session.getId()); + Property prop = sessionSetAttribute.getProperty(); + session.setAttribute(prop.getName(), prop.getValue()); + } + else + { + throw new RuntimeException("Unable to execute task " + step + " in " + this.getClass().getName()); + } + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/DumpServlet.java b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/DumpServlet.java new file mode 100644 index 00000000000..7958508d64e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/DumpServlet.java @@ -0,0 +1,135 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee10; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.function.Function; +import java.util.function.Supplier; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.eclipse.jetty.tests.ccd.common.DispatchPlan; + +public class DumpServlet extends HttpServlet +{ + private static final String NULL = ""; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException + { + DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName()); + + if (dispatchPlan != null) + { + dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s", + this.getClass().getName(), + req.getDispatcherType(), req.getMethod(), req.getRequestURI()); + } + + Properties props = new Properties(); + props.setProperty("requestType", req.getClass().getName()); + props.setProperty("responseType", resp.getClass().getName()); + + props.setProperty("request.authType", Objects.toString(req.getAuthType(), NULL)); + props.setProperty("request.characterEncoding", Objects.toString(req.getCharacterEncoding(), NULL)); + props.setProperty("request.contentLength", Long.toString(req.getContentLengthLong())); + props.setProperty("request.contentType", Objects.toString(req.getContentType(), NULL)); + props.setProperty("request.contextPath", Objects.toString(req.getContextPath(), NULL)); + props.setProperty("request.dispatcherType", Objects.toString(req.getDispatcherType(), NULL)); + props.setProperty("request.localAddr", Objects.toString(req.getLocalAddr(), NULL)); + props.setProperty("request.localName", Objects.toString(req.getLocalName(), NULL)); + props.setProperty("request.localPort", Integer.toString(req.getLocalPort())); + props.setProperty("request.locale", Objects.toString(req.getLocale(), NULL)); + props.setProperty("request.method", Objects.toString(req.getMethod(), NULL)); + props.setProperty("request.pathInfo", Objects.toString(req.getPathInfo(), NULL)); + props.setProperty("request.pathTranslated", Objects.toString(req.getPathTranslated(), NULL)); + props.setProperty("request.protocol", Objects.toString(req.getProtocol(), NULL)); + props.setProperty("request.queryString", Objects.toString(req.getQueryString(), NULL)); + props.setProperty("request.remoteAddr", Objects.toString(req.getRemoteAddr(), NULL)); + props.setProperty("request.remoteHost", Objects.toString(req.getRemoteHost(), NULL)); + props.setProperty("request.remotePort", Integer.toString(req.getRemotePort())); + props.setProperty("request.remoteUser", Objects.toString(req.getRemoteUser(), NULL)); + props.setProperty("request.requestedSessionId", Objects.toString(req.getRequestedSessionId(), NULL)); + props.setProperty("request.requestURI", Objects.toString(req.getRequestURI(), NULL)); + props.setProperty("request.requestURL", Objects.toString(req.getRequestURL(), NULL)); + props.setProperty("request.serverPort", Integer.toString(req.getServerPort())); + props.setProperty("request.servletPath", Objects.toString(req.getServletPath(), NULL)); + + props.setProperty("request.session.exists", "false"); + HttpSession httpSession = req.getSession(false); + if (httpSession != null) + { + props.setProperty("request.session.exists", "true"); + List attrNames = Collections.list(httpSession.getAttributeNames()); + attrNames + .forEach((name) -> + { + Object attrVal = httpSession.getAttribute(name); + props.setProperty("session[" + name + "]", Objects.toString(attrVal, NULL)); + }); + } + + addAttributes(props, "req", req::getAttributeNames, req::getAttribute); + addAttributes(props, "context", + () -> getServletContext().getAttributeNames(), + (name) -> getServletContext().getAttribute(name)); + + List headerNames = Collections.list(req.getHeaderNames()); + headerNames + .forEach((name) -> + { + String headerVal = req.getHeader(name); + props.setProperty("header[" + name + "]", Objects.toString(headerVal, NULL)); + }); + + if (dispatchPlan != null) + { + int eventCount = dispatchPlan.getEvents().size(); + props.setProperty("dispatchPlan.events.count", Integer.toString(dispatchPlan.getEvents().size())); + for (int i = 0; i < eventCount; i++) + { + props.setProperty("dispatchPlan.event[" + i + "]", dispatchPlan.getEvents().get(i)); + } + } + + resp.setStatus(HttpServletResponse.SC_OK); + resp.setCharacterEncoding("utf-8"); + resp.setContentType("text/x-java-properties"); + PrintWriter out = resp.getWriter(); + props.store(out, "From " + this.getClass().getName()); + } + + private void addAttributes(Properties props, + String prefix, + Supplier> getNamesSupplier, + Function getAttributeFunction) + { + List attrNames = Collections.list(getNamesSupplier.get()); + attrNames + .forEach((name) -> + { + Object attrVal = getAttributeFunction.apply(name); + props.setProperty(prefix + ".attr[" + name + "]", Objects.toString(attrVal, NULL)); + }); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/ForwardServlet.java b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/ForwardServlet.java new file mode 100644 index 00000000000..d33864381b7 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/ForwardServlet.java @@ -0,0 +1,35 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee10; + +import java.io.IOException; +import java.util.Objects; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class ForwardServlet extends HttpServlet +{ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + String forwardTo = req.getHeader("X-ForwardTo"); + Objects.requireNonNull(forwardTo); + RequestDispatcher requestDispatcher = req.getRequestDispatcher(forwardTo); + requestDispatcher.forward(req, resp); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/InternalRequestURIFilter.java b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/InternalRequestURIFilter.java new file mode 100644 index 00000000000..9281b7d618e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee10/InternalRequestURIFilter.java @@ -0,0 +1,56 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee10; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; + +/** + * A servlet filter that will harshly change the return value of + * {@link HttpServletRequest#getRequestURI()} to something that does + * not satisfy the Servlet spec URI invariant {@code request URI == context path + servlet path + path info} + */ +public class InternalRequestURIFilter implements Filter +{ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + HttpServletRequest httpServletRequest = (HttpServletRequest)request; + HttpServletResponse httpServletResponse = (HttpServletResponse)response; + InternalRequestURIWrapper requestURIWrapper = new InternalRequestURIWrapper(httpServletRequest); + chain.doFilter(requestURIWrapper, httpServletResponse); + } + + private static class InternalRequestURIWrapper extends HttpServletRequestWrapper + { + public InternalRequestURIWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String getRequestURI() + { + return "/internal/"; + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..5ca499ae4d3 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee10-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,29 @@ + + + + ccd-ee10 + + + ccd + org.eclipse.jetty.tests.ccd.ee10.CCDServlet + + + + dump + org.eclipse.jetty.tests.ccd.ee10.DumpServlet + + + + ccd + /redispatch/* + + + + dump + /dump/* + + + \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-ee8-webapp/pom.xml b/tests/test-cross-context-dispatch/ccd-ee8-webapp/pom.xml new file mode 100644 index 00000000000..71313613edc --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee8-webapp/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.eclipse.jetty.tests.ccd + test-cross-context-dispatch + 12.0.11-SNAPSHOT + + ccd-ee8-webapp + war + Tests :: Cross Context Dispatch :: ee8 WebApp + + + + + org.eclipse.jetty.ee8 + jetty-ee8-bom + ${project.version} + pom + import + + + + + + + jakarta.servlet + jakarta.servlet-api + 4.0.4 + provided + + + org.eclipse.jetty.tests.ccd + ccd-common + ${project.version} + provided + + + diff --git a/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/CCDServlet.java b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/CCDServlet.java new file mode 100644 index 00000000000..618a43e7c62 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/CCDServlet.java @@ -0,0 +1,107 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee8; + +import java.io.IOException; +import java.util.Objects; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.tests.ccd.common.DispatchPlan; +import org.eclipse.jetty.tests.ccd.common.Property; +import org.eclipse.jetty.tests.ccd.common.Step; + +public class CCDServlet extends HttpServlet +{ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName()); + + if (dispatchPlan == null) + throw new ServletException("Unable to find DispatchPlan"); + + dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s", + this.getClass().getName(), + req.getDispatcherType(), req.getMethod(), req.getRequestURI()); + + Step step; + + while ((step = dispatchPlan.popStep()) != null) + { + if (step instanceof Step.ContextRedispatch contextRedispatchStep) + { + ServletContext otherContext = getServletContext().getContext(contextRedispatchStep.getContextPath()); + if (otherContext == null) + throw new NullPointerException("ServletContext.getContext(\"" + contextRedispatchStep.getContextPath() + "\") returned null"); + RequestDispatcher dispatcher = otherContext.getRequestDispatcher(contextRedispatchStep.getDispatchPath()); + if (dispatcher == null) + throw new NullPointerException("ServletContext.getRequestDispatcher(\"" + contextRedispatchStep.getDispatchPath() + "\") returned null"); + switch (contextRedispatchStep.getDispatchType()) + { + case FORWARD -> dispatcher.forward(req, resp); + case INCLUDE -> dispatcher.include(req, resp); + } + return; + } + else if (step instanceof Step.RequestDispatch requestDispatchStep) + { + RequestDispatcher dispatcher = req.getRequestDispatcher(requestDispatchStep.getDispatchPath()); + if (dispatcher == null) + throw new NullPointerException("HttpServletRequest.getRequestDispatcher(\"" + requestDispatchStep.getDispatchPath() + "\") returned null"); + switch (requestDispatchStep.getDispatchType()) + { + case FORWARD -> dispatcher.forward(req, resp); + case INCLUDE -> dispatcher.include(req, resp); + } + return; + } + else if (step instanceof Step.GetHttpSession getHttpSessionTask) + { + HttpSession session = req.getSession(false); + if (session == null) + { + dispatchPlan.addEvent("%s.service() HttpSession is null", + this.getClass().getName()); + } + else + { + String name = getHttpSessionTask.getName(); + Object value = session.getAttribute(name); + dispatchPlan.addEvent("%s.service() HttpSession exists: [%s]=[%s]", + this.getClass().getName(), + name, + Objects.toString(value) + ); + } + } + else if (step instanceof Step.HttpSessionSetAttribute sessionSetAttribute) + { + HttpSession session = req.getSession(true); + req.setAttribute("session[" + req.getRequestURI() + "].id", session.getId()); + Property prop = sessionSetAttribute.getProperty(); + session.setAttribute(prop.getName(), prop.getValue()); + } + else + { + throw new RuntimeException("Unable to execute task " + step + " in " + this.getClass().getName()); + } + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/DumpServlet.java b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/DumpServlet.java new file mode 100644 index 00000000000..180e7efcd1c --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/DumpServlet.java @@ -0,0 +1,135 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee8; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.tests.ccd.common.DispatchPlan; + +public class DumpServlet extends HttpServlet +{ + private static final String NULL = ""; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException + { + DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName()); + + if (dispatchPlan != null) + { + dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s", + this.getClass().getName(), + req.getDispatcherType(), req.getMethod(), req.getRequestURI()); + } + + Properties props = new Properties(); + props.setProperty("requestType", req.getClass().getName()); + props.setProperty("responseType", resp.getClass().getName()); + + props.setProperty("request.authType", Objects.toString(req.getAuthType(), NULL)); + props.setProperty("request.characterEncoding", Objects.toString(req.getCharacterEncoding(), NULL)); + props.setProperty("request.contentLength", Long.toString(req.getContentLengthLong())); + props.setProperty("request.contentType", Objects.toString(req.getContentType(), NULL)); + props.setProperty("request.contextPath", Objects.toString(req.getContextPath(), NULL)); + props.setProperty("request.dispatcherType", Objects.toString(req.getDispatcherType(), NULL)); + props.setProperty("request.localAddr", Objects.toString(req.getLocalAddr(), NULL)); + props.setProperty("request.localName", Objects.toString(req.getLocalName(), NULL)); + props.setProperty("request.localPort", Integer.toString(req.getLocalPort())); + props.setProperty("request.locale", Objects.toString(req.getLocale(), NULL)); + props.setProperty("request.method", Objects.toString(req.getMethod(), NULL)); + props.setProperty("request.pathInfo", Objects.toString(req.getPathInfo(), NULL)); + props.setProperty("request.pathTranslated", Objects.toString(req.getPathTranslated(), NULL)); + props.setProperty("request.protocol", Objects.toString(req.getProtocol(), NULL)); + props.setProperty("request.queryString", Objects.toString(req.getQueryString(), NULL)); + props.setProperty("request.remoteAddr", Objects.toString(req.getRemoteAddr(), NULL)); + props.setProperty("request.remoteHost", Objects.toString(req.getRemoteHost(), NULL)); + props.setProperty("request.remotePort", Integer.toString(req.getRemotePort())); + props.setProperty("request.remoteUser", Objects.toString(req.getRemoteUser(), NULL)); + props.setProperty("request.requestedSessionId", Objects.toString(req.getRequestedSessionId(), NULL)); + props.setProperty("request.requestURI", Objects.toString(req.getRequestURI(), NULL)); + props.setProperty("request.requestURL", Objects.toString(req.getRequestURL(), NULL)); + props.setProperty("request.serverPort", Integer.toString(req.getServerPort())); + props.setProperty("request.servletPath", Objects.toString(req.getServletPath(), NULL)); + + props.setProperty("request.session.exists", "false"); + HttpSession httpSession = req.getSession(false); + if (httpSession != null) + { + props.setProperty("request.session.exists", "true"); + List attrNames = Collections.list(httpSession.getAttributeNames()); + attrNames + .forEach((name) -> + { + Object attrVal = httpSession.getAttribute(name); + props.setProperty("session[" + name + "]", Objects.toString(attrVal, NULL)); + }); + } + + addAttributes(props, "req", req::getAttributeNames, req::getAttribute); + addAttributes(props, "context", + () -> getServletContext().getAttributeNames(), + (name) -> getServletContext().getAttribute(name)); + + List headerNames = Collections.list(req.getHeaderNames()); + headerNames + .forEach((name) -> + { + String headerVal = req.getHeader(name); + props.setProperty("header[" + name + "]", Objects.toString(headerVal, NULL)); + }); + + if (dispatchPlan != null) + { + int eventCount = dispatchPlan.getEvents().size(); + props.setProperty("dispatchPlan.events.count", Integer.toString(dispatchPlan.getEvents().size())); + for (int i = 0; i < eventCount; i++) + { + props.setProperty("dispatchPlan.event[" + i + "]", dispatchPlan.getEvents().get(i)); + } + } + + resp.setStatus(HttpServletResponse.SC_OK); + resp.setCharacterEncoding("utf-8"); + resp.setContentType("text/x-java-properties"); + PrintWriter out = resp.getWriter(); + props.store(out, "From " + this.getClass().getName()); + } + + private void addAttributes(Properties props, + String prefix, + Supplier> getNamesSupplier, + Function getAttributeFunction) + { + List attrNames = Collections.list(getNamesSupplier.get()); + attrNames + .forEach((name) -> + { + Object attrVal = getAttributeFunction.apply(name); + props.setProperty(prefix + ".attr[" + name + "]", Objects.toString(attrVal, NULL)); + }); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/ForwardServlet.java b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/ForwardServlet.java new file mode 100644 index 00000000000..0068f6dd093 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/ForwardServlet.java @@ -0,0 +1,34 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee8; + +import java.io.IOException; +import java.util.Objects; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class ForwardServlet extends HttpServlet +{ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + String forwardTo = req.getHeader("X-ForwardTo"); + Objects.requireNonNull(forwardTo); + RequestDispatcher requestDispatcher = req.getRequestDispatcher(forwardTo); + requestDispatcher.forward(req, resp); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/InternalRequestURIFilter.java b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/InternalRequestURIFilter.java new file mode 100644 index 00000000000..a5a20445f42 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee8/InternalRequestURIFilter.java @@ -0,0 +1,55 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee8; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +/** + * A servlet filter that will harshly change the return value of + * {@link HttpServletRequest#getRequestURI()} to something that does + * not satisfy the Servlet spec URI invariant {@code request URI == context path + servlet path + path info} + */ +public class InternalRequestURIFilter implements Filter +{ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + HttpServletRequest httpServletRequest = (HttpServletRequest)request; + HttpServletResponse httpServletResponse = (HttpServletResponse)response; + InternalRequestURIWrapper requestURIWrapper = new InternalRequestURIWrapper(httpServletRequest); + chain.doFilter(requestURIWrapper, httpServletResponse); + } + + private static class InternalRequestURIWrapper extends HttpServletRequestWrapper + { + public InternalRequestURIWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String getRequestURI() + { + return "/internal/"; + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..8c986e30253 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee8-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,38 @@ + + + + ccd-ee8 + + + ccd + org.eclipse.jetty.tests.ccd.ee8.CCDServlet + + + + forwardto + org.eclipse.jetty.tests.ccd.ee8.ForwardServlet + + + + dump + org.eclipse.jetty.tests.ccd.ee8.DumpServlet + + + + ccd + /redispatch/* + + + + forwardto + /forwardto/* + + + + dump + /dump/* + + \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-ee9-webapp/pom.xml b/tests/test-cross-context-dispatch/ccd-ee9-webapp/pom.xml new file mode 100644 index 00000000000..7071fec7b7e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee9-webapp/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.eclipse.jetty.tests.ccd + test-cross-context-dispatch + 12.0.11-SNAPSHOT + + ccd-ee9-webapp + war + Tests :: Cross Context Dispatch :: ee9 WebApp + + + + + org.eclipse.jetty.ee9 + jetty-ee9-bom + ${project.version} + pom + import + + + + + + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + provided + + + org.eclipse.jetty.tests.ccd + ccd-common + ${project.version} + provided + + + diff --git a/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/CCDServlet.java b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/CCDServlet.java new file mode 100644 index 00000000000..cdf72500eda --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/CCDServlet.java @@ -0,0 +1,108 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee9; + +import java.io.IOException; +import java.util.Objects; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.eclipse.jetty.tests.ccd.common.DispatchPlan; +import org.eclipse.jetty.tests.ccd.common.Property; +import org.eclipse.jetty.tests.ccd.common.Step; + +public class CCDServlet extends HttpServlet +{ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName()); + + if (dispatchPlan == null) + throw new ServletException("Unable to find DispatchPlan"); + + dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s", + this.getClass().getName(), + req.getDispatcherType(), req.getMethod(), req.getRequestURI()); + + Step step; + + while ((step = dispatchPlan.popStep()) != null) + { + if (step instanceof Step.ContextRedispatch contextRedispatchStep) + { + ServletContext otherContext = getServletContext().getContext(contextRedispatchStep.getContextPath()); + if (otherContext == null) + throw new NullPointerException("ServletContext.getContext(\"" + contextRedispatchStep.getContextPath() + "\") returned null"); + RequestDispatcher dispatcher = otherContext.getRequestDispatcher(contextRedispatchStep.getDispatchPath()); + if (dispatcher == null) + throw new NullPointerException("ServletContext.getRequestDispatcher(\"" + contextRedispatchStep.getDispatchPath() + "\") returned null"); + switch (contextRedispatchStep.getDispatchType()) + { + case FORWARD -> dispatcher.forward(req, resp); + case INCLUDE -> dispatcher.include(req, resp); + } + return; + } + else if (step instanceof Step.RequestDispatch requestDispatchStep) + { + RequestDispatcher dispatcher = req.getRequestDispatcher(requestDispatchStep.getDispatchPath()); + if (dispatcher == null) + throw new NullPointerException("HttpServletRequest.getRequestDispatcher(\"" + requestDispatchStep.getDispatchPath() + "\") returned null"); + switch (requestDispatchStep.getDispatchType()) + { + case FORWARD -> dispatcher.forward(req, resp); + case INCLUDE -> dispatcher.include(req, resp); + } + return; + } + else if (step instanceof Step.GetHttpSession getHttpSessionTask) + { + HttpSession session = req.getSession(false); + if (session == null) + { + dispatchPlan.addEvent("%s.service() HttpSession is null", + this.getClass().getName()); + } + else + { + String name = getHttpSessionTask.getName(); + Object value = session.getAttribute(name); + dispatchPlan.addEvent("%s.service() HttpSession exists: [%s]=[%s]", + this.getClass().getName(), + name, + Objects.toString(value) + ); + } + + } + else if (step instanceof Step.HttpSessionSetAttribute sessionSetAttribute) + { + HttpSession session = req.getSession(true); + req.setAttribute("session[" + req.getRequestURI() + "].id", session.getId()); + Property prop = sessionSetAttribute.getProperty(); + session.setAttribute(prop.getName(), prop.getValue()); + } + else + { + throw new RuntimeException("Unable to execute task " + step + " in " + this.getClass().getName()); + } + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/DumpServlet.java b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/DumpServlet.java new file mode 100644 index 00000000000..024a696f40e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/DumpServlet.java @@ -0,0 +1,135 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee9; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.function.Function; +import java.util.function.Supplier; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.eclipse.jetty.tests.ccd.common.DispatchPlan; + +public class DumpServlet extends HttpServlet +{ + private static final String NULL = ""; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException + { + DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName()); + + if (dispatchPlan != null) + { + dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s", + this.getClass().getName(), + req.getDispatcherType(), req.getMethod(), req.getRequestURI()); + } + + Properties props = new Properties(); + props.setProperty("requestType", req.getClass().getName()); + props.setProperty("responseType", resp.getClass().getName()); + + props.setProperty("request.authType", Objects.toString(req.getAuthType(), NULL)); + props.setProperty("request.characterEncoding", Objects.toString(req.getCharacterEncoding(), NULL)); + props.setProperty("request.contentLength", Long.toString(req.getContentLengthLong())); + props.setProperty("request.contentType", Objects.toString(req.getContentType(), NULL)); + props.setProperty("request.contextPath", Objects.toString(req.getContextPath(), NULL)); + props.setProperty("request.dispatcherType", Objects.toString(req.getDispatcherType(), NULL)); + props.setProperty("request.localAddr", Objects.toString(req.getLocalAddr(), NULL)); + props.setProperty("request.localName", Objects.toString(req.getLocalName(), NULL)); + props.setProperty("request.localPort", Integer.toString(req.getLocalPort())); + props.setProperty("request.locale", Objects.toString(req.getLocale(), NULL)); + props.setProperty("request.method", Objects.toString(req.getMethod(), NULL)); + props.setProperty("request.pathInfo", Objects.toString(req.getPathInfo(), NULL)); + props.setProperty("request.pathTranslated", Objects.toString(req.getPathTranslated(), NULL)); + props.setProperty("request.protocol", Objects.toString(req.getProtocol(), NULL)); + props.setProperty("request.queryString", Objects.toString(req.getQueryString(), NULL)); + props.setProperty("request.remoteAddr", Objects.toString(req.getRemoteAddr(), NULL)); + props.setProperty("request.remoteHost", Objects.toString(req.getRemoteHost(), NULL)); + props.setProperty("request.remotePort", Integer.toString(req.getRemotePort())); + props.setProperty("request.remoteUser", Objects.toString(req.getRemoteUser(), NULL)); + props.setProperty("request.requestedSessionId", Objects.toString(req.getRequestedSessionId(), NULL)); + props.setProperty("request.requestURI", Objects.toString(req.getRequestURI(), NULL)); + props.setProperty("request.requestURL", Objects.toString(req.getRequestURL(), NULL)); + props.setProperty("request.serverPort", Integer.toString(req.getServerPort())); + props.setProperty("request.servletPath", Objects.toString(req.getServletPath(), NULL)); + + props.setProperty("request.session.exists", "false"); + HttpSession httpSession = req.getSession(false); + if (httpSession != null) + { + props.setProperty("request.session.exists", "true"); + List attrNames = Collections.list(httpSession.getAttributeNames()); + attrNames + .forEach((name) -> + { + Object attrVal = httpSession.getAttribute(name); + props.setProperty("session[" + name + "]", Objects.toString(attrVal, NULL)); + }); + } + + addAttributes(props, "req", req::getAttributeNames, req::getAttribute); + addAttributes(props, "context", + () -> getServletContext().getAttributeNames(), + (name) -> getServletContext().getAttribute(name)); + + List headerNames = Collections.list(req.getHeaderNames()); + headerNames + .forEach((name) -> + { + String headerVal = req.getHeader(name); + props.setProperty("header[" + name + "]", Objects.toString(headerVal, NULL)); + }); + + if (dispatchPlan != null) + { + int eventCount = dispatchPlan.getEvents().size(); + props.setProperty("dispatchPlan.events.count", Integer.toString(dispatchPlan.getEvents().size())); + for (int i = 0; i < eventCount; i++) + { + props.setProperty("dispatchPlan.event[" + i + "]", dispatchPlan.getEvents().get(i)); + } + } + + resp.setStatus(HttpServletResponse.SC_OK); + resp.setCharacterEncoding("utf-8"); + resp.setContentType("text/x-java-properties"); + PrintWriter out = resp.getWriter(); + props.store(out, "From " + this.getClass().getName()); + } + + private void addAttributes(Properties props, + String prefix, + Supplier> getNamesSupplier, + Function getAttributeFunction) + { + List attrNames = Collections.list(getNamesSupplier.get()); + attrNames + .forEach((name) -> + { + Object attrVal = getAttributeFunction.apply(name); + props.setProperty(prefix + ".attr[" + name + "]", Objects.toString(attrVal, NULL)); + }); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/ForwardServlet.java b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/ForwardServlet.java new file mode 100644 index 00000000000..8e11efc514e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/ForwardServlet.java @@ -0,0 +1,35 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee9; + +import java.io.IOException; +import java.util.Objects; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class ForwardServlet extends HttpServlet +{ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + String forwardTo = req.getHeader("X-ForwardTo"); + Objects.requireNonNull(forwardTo); + RequestDispatcher requestDispatcher = req.getRequestDispatcher(forwardTo); + requestDispatcher.forward(req, resp); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/InternalRequestURIFilter.java b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/InternalRequestURIFilter.java new file mode 100644 index 00000000000..030cbb9d1fc --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/java/org/eclipse/jetty/tests/ccd/ee9/InternalRequestURIFilter.java @@ -0,0 +1,56 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.ccd.ee9; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; + +/** + * A servlet filter that will harshly change the return value of + * {@link HttpServletRequest#getRequestURI()} to something that does + * not satisfy the Servlet spec URI invariant {@code request URI == context path + servlet path + path info} + */ +public class InternalRequestURIFilter implements Filter +{ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + HttpServletRequest httpServletRequest = (HttpServletRequest)request; + HttpServletResponse httpServletResponse = (HttpServletResponse)response; + InternalRequestURIWrapper requestURIWrapper = new InternalRequestURIWrapper(httpServletRequest); + chain.doFilter(requestURIWrapper, httpServletResponse); + } + + private static class InternalRequestURIWrapper extends HttpServletRequestWrapper + { + public InternalRequestURIWrapper(HttpServletRequest request) + { + super(request); + } + + @Override + public String getRequestURI() + { + return "/internal/"; + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..6101cb282e2 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-ee9-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,29 @@ + + + + ccd-ee9 + + + ccd + org.eclipse.jetty.tests.ccd.ee9.CCDServlet + + + + dump + org.eclipse.jetty.tests.ccd.ee9.DumpServlet + + + + ccd + /redispatch/* + + + + dump + /dump/* + + + \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-tests/pom.xml b/tests/test-cross-context-dispatch/ccd-tests/pom.xml new file mode 100644 index 00000000000..17a1e5f67c6 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + org.eclipse.jetty.tests.ccd + test-cross-context-dispatch + 12.0.11-SNAPSHOT + + ccd-tests + jar + Tests :: Cross Context Dispatch :: Tests + + + + + 1 + + + + + + org.eclipse.jetty + jetty-bom + ${project.version} + pom + import + + + + + + + org.eclipse.jetty.tests.ccd + ccd-common + ${project.version} + provided + + + org.eclipse.jetty + jetty-client + test + + + org.eclipse.jetty + jetty-deploy + ${project.version} + test + + + org.eclipse.jetty + jetty-slf4j-impl + ${project.version} + test + + + org.eclipse.jetty.tests + jetty-testers + ${project.version} + test + + + org.eclipse.jetty.toolchain + jetty-test-helper + test + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${session.repositorySession.localRepository.basedir.absolutePath} + ${project.version} + $(distribution.debug.port} + ${home.start.timeout} + + + + + + + diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/AbstractRedispatchTest.java b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/AbstractRedispatchTest.java new file mode 100644 index 00000000000..9684de6d7e7 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/AbstractRedispatchTest.java @@ -0,0 +1,216 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.redispatch; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.tests.testers.JettyHomeTester; +import org.eclipse.jetty.tests.testers.Tester; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.util.component.LifeCycle; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class AbstractRedispatchTest +{ + protected static final int START_TIMEOUT = Integer.getInteger("home.start.timeout", 30); + protected static final List ENVIRONMENTS = List.of("ee8", "ee9", "ee10"); + + static String toResponseDetails(ContentResponse response) + { + return new ResponseDetails(response).get(); + } + + static class InitializedJettyBase + { + public Path jettyBase; + public JettyHomeTester distribution; + public int httpPort; + + public InitializedJettyBase(TestInfo testInfo) throws Exception + { + Path testsDir = MavenPaths.targetTests(); + String cleanBaseName = toCleanDirectoryName(testInfo); + jettyBase = testsDir.resolve(cleanBaseName); + FS.ensureEmpty(jettyBase); + String jettyVersion = System.getProperty("jettyVersion"); + distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(jettyBase) + .build(); + + httpPort = Tester.freePort(); + + List configList = new ArrayList<>(); + configList.add("--add-modules=http,resources"); + for (String env : ENVIRONMENTS) + { + configList.add("--add-modules=" + env + "-deploy," + env + "-webapp"); + } + + try (JettyHomeTester.Run runConfig = distribution.start(configList)) + { + assertTrue(runConfig.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); + assertEquals(0, runConfig.getExitValue()); + + Path libDir = jettyBase.resolve("lib"); + FS.ensureDirExists(libDir); + Path etcDir = jettyBase.resolve("etc"); + FS.ensureDirExists(etcDir); + Path modulesDir = jettyBase.resolve("modules"); + FS.ensureDirExists(modulesDir); + Path startDir = jettyBase.resolve("start.d"); + FS.ensureDirExists(startDir); + + // Configure the DispatchPlanHandler + Path ccdJar = distribution.resolveArtifact("org.eclipse.jetty.tests.ccd:ccd-common:jar:" + jettyVersion); + Files.copy(ccdJar, libDir.resolve(ccdJar.getFileName())); + + Path installDispatchPlanXml = MavenPaths.findTestResourceFile("install-ccd-handler.xml"); + Files.copy(installDispatchPlanXml, etcDir.resolve(installDispatchPlanXml.getFileName())); + + String module = """ + [depend] + server + + [lib] + lib/jetty-util-ajax-$J.jar + lib/ccd-common-$J.jar + + [xml] + etc/install-ccd-handler.xml + + [ini] + jetty.webapp.addProtectedClasses+=,org.eclipse.jetty.tests.ccd.common. + jetty.webapp.addHiddenClasses+=,-org.eclipse.jetty.tests.ccd.common. + """.replace("$J", jettyVersion); + Files.writeString(modulesDir.resolve("ccd.mod"), module, StandardCharsets.UTF_8); + + // -- Error Handler + Path errorHandlerXml = MavenPaths.findTestResourceFile("error-handler.xml"); + Files.copy(errorHandlerXml, etcDir.resolve("error-handler.xml")); + String errorHandlerIni = """ + etc/error-handler.xml + """; + Files.writeString(startDir.resolve("error-handler.ini"), errorHandlerIni); + + // -- Plans Dir + Path plansDir = MavenPaths.findTestResourceDir("plans"); + + Path ccdIni = startDir.resolve("ccd.ini"); + String ini = """ + --module=ccd + ccd-plans-dir=$D + """.replace("$D", plansDir.toString()); + Files.writeString(ccdIni, ini, StandardCharsets.UTF_8); + + // -- Add the test wars + for (String env : ENVIRONMENTS) + { + Path war = distribution.resolveArtifact("org.eclipse.jetty.tests.ccd:ccd-" + env + "-webapp:war:" + jettyVersion); + distribution.installWar(war, "ccd-" + env); + Path warProperties = jettyBase.resolve("webapps/ccd-" + env + ".properties"); + Files.writeString(warProperties, "environment: " + env, StandardCharsets.UTF_8); + + Path webappXmlSrc = MavenPaths.findTestResourceFile("webapp-xmls/ccd-" + env + ".xml"); + Path webappXmlDest = jettyBase.resolve("webapps/ccd-" + env + ".xml"); + Files.copy(webappXmlSrc, webappXmlDest); + } + } + } + + /** + * Create a name that can be used as a Jetty Base home directory in a safe way. + * + * Note: unlike the WorkDir object, this strips out {@code [} and {@code ]} characters + * and also makes any non-alpha-numeric character just {@code _}, which results in + * a happy {@code ${jetty.base}} and {@code start.jar}. + * + * Failure to use this method can result in start.jar behaving in unintended ways + * when it goes through the Java -> Runtime.exec -> OS behaviors. + * + * This change also makes the created directory named {@code target/tests/.} + * live and suitable for execution via a console without accidental shell interpretation of special + * characters in the directory name (that can result from characters like "[]" used in a directory name) + * + * @param testInfo the TestInfo to use to generate directory name from. + * @return the safe to use directory name. + */ + public static String toCleanDirectoryName(TestInfo testInfo) + { + StringBuilder name = new StringBuilder(); + if (testInfo.getTestMethod().isPresent()) + { + name.append(testInfo.getTestMethod().get().getName()); + name.append("."); + } + for (char c: testInfo.getDisplayName().toCharArray()) + { + if (Character.isLetterOrDigit(c) || c == '.' || c == '-') + name.append(c); + else if (c != '[' && c != ']') + name.append("_"); + } + return name.toString(); + } + } + + protected HttpClient client; + + @BeforeEach + public void startClient() throws Exception + { + client = new HttpClient(); + client.start(); + } + + @AfterEach + public void stopClient() + { + LifeCycle.stop(client); + } + + public static void dumpProperties(Properties props) + { + props.stringPropertyNames().stream() + .sorted() + .forEach((name) -> + System.out.printf(" %s=%s%n", name, props.getProperty(name))); + } + + public static void assertProperty(Properties props, String name, Matcher valueMatcher) + { + assertThat("Property [" + name + "]", props.getProperty(name), valueMatcher); + } + + public static void assertProperty(String id, Properties props, String name, Matcher valueMatcher) + { + assertThat("id[" + id + "] property[" + name + "]", props.getProperty(name), valueMatcher); + } +} diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchPlansTests.java b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchPlansTests.java new file mode 100644 index 00000000000..a9f41c5de21 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchPlansTests.java @@ -0,0 +1,179 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.redispatch; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.tests.ccd.common.DispatchPlan; +import org.eclipse.jetty.tests.ccd.common.HttpRequest; +import org.eclipse.jetty.tests.ccd.common.Property; +import org.eclipse.jetty.tests.testers.JettyHomeTester; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RedispatchPlansTests extends AbstractRedispatchTest +{ + private InitializedJettyBase jettyBase; + private JettyHomeTester.Run runStart; + + @BeforeEach + public void startJettyBase(TestInfo testInfo) throws Exception + { + jettyBase = new InitializedJettyBase(testInfo); + + String[] argsStart = { + "jetty.http.port=" + jettyBase.httpPort + }; + + runStart = jettyBase.distribution.start(argsStart); + + assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + } + + @AfterEach + public void stopJettyBase() + { + if (runStart.getProcess().isAlive()) + runStart.close(); + } + + public static Stream dispatchPlans() throws IOException + { + List plans = new ArrayList<>(); + + List disabledTests = new ArrayList<>(); + disabledTests.add("ee10-session-ee8-ee9-ee8.txt"); // causes an ISE + + Path testPlansDir = MavenPaths.findTestResourceDir("plans"); + try (Stream plansStream = Files.list(testPlansDir)) + { + List testPlans = plansStream + .filter(Files::isRegularFile) + .filter((file) -> file.getFileName().toString().endsWith(".txt")) + .filter((file) -> !disabledTests.contains(file.getFileName().toString())) + .toList(); + + for (Path plansText : testPlans) + { + plans.add(Arguments.of(DispatchPlan.read(plansText))); + } + } + + return plans.stream(); + } + + @ParameterizedTest + @MethodSource("dispatchPlans") + public void testRedispatch(DispatchPlan dispatchPlan) throws Exception + { + HttpRequest requestStep = dispatchPlan.getRequestStep(); + assertNotNull(requestStep); + ContentResponse response = client.newRequest("localhost", jettyBase.httpPort) + .method(requestStep.getMethod()) + .headers((headers) -> + headers.put("X-DispatchPlan", dispatchPlan.id())) + .path(requestStep.getRequestPath()) + .send(); + String responseDetails = toResponseDetails(response); + assertThat(responseDetails, response.getStatus(), is(HttpStatus.OK_200)); + + Properties responseProps = new Properties(); + try (StringReader stringReader = new StringReader(response.getContentAsString())) + { + responseProps.load(stringReader); + } + + dumpProperties(responseProps); + + int expectedEventCount = dispatchPlan.getExpectedEvents().size(); + assertThat(responseProps.getProperty("dispatchPlan.events.count"), is(Integer.toString(expectedEventCount))); + for (int i = 0; i < expectedEventCount; i++) + { + assertThat("id[" + dispatchPlan.id() + "] event[" + i + "]", responseProps.getProperty("dispatchPlan.event[" + i + "]"), is(dispatchPlan.getExpectedEvents().get(i))); + } + + if (dispatchPlan.getExpectedContentType() != null) + { + assertThat("Expected ContentType", response.getHeaders().get(HttpHeader.CONTENT_TYPE), is(dispatchPlan.getExpectedContentType())); + } + + for (Property expectedProperty : dispatchPlan.getExpectedProperties()) + { + assertProperty(dispatchPlan.id(), responseProps, expectedProperty.getName(), is(expectedProperty.getValue())); + } + + // Ensure that all seen session ids are the same. + if (dispatchPlan.isExpectedSessionIds()) + { + // Verify that Request Attributes for Session.id are in agreement + List attrNames = responseProps.keySet().stream() + .map(Object::toString) + .filter((name) -> name.startsWith("req.attr[session[")) + .toList(); + + if (attrNames.size() > 1) + { + String expectedId = responseProps.getProperty(attrNames.get(0)); + for (String name : attrNames) + { + assertEquals(expectedId, responseProps.getProperty(name)); + } + } + + // stop the forked running server. + // we need to verify the session behaviors, and can only do that on a stopped server. + runStart.close(); + + // Verify that Context Attributes for Session.id are in agreement + // And that all ids have had their .commit() and .release() methods called. + Path sessionLog = jettyBase.jettyBase.resolve("work/session.log"); + assertTrue(Files.isRegularFile(sessionLog), "Missing " + sessionLog); + + List logEntries = Files.readAllLines(sessionLog); + List newSessions = logEntries.stream() + .filter(line -> line.contains("SessionCache.event.newSession()")) + .map(line -> line.substring(line.indexOf("=") + 1)) + .toList(); + // we should have the commit() and release() for each new Session. + for (String sessionId : newSessions) + { + assertThat(logEntries, hasItem("SessionCache.event.commit()=" + sessionId)); + assertThat(logEntries, hasItem("SessionCache.event.release()=" + sessionId)); + } + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchTests.java b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchTests.java new file mode 100644 index 00000000000..bf1ff53fdf0 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/RedispatchTests.java @@ -0,0 +1,108 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.redispatch; + +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.tests.testers.JettyHomeTester; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RedispatchTests extends AbstractRedispatchTest +{ + private JettyHomeTester.Run runStart; + + @AfterEach + public void stopRun() + { + runStart.close(); + } + + /** + * Test ee8 behavior if an HttpServletRequestWrapper messes with the + * {@code getRequestURI()} method. + * see {@code org.eclipse.jetty.tests.ccd.ee8.InternalRequestURIFilter} + */ + @Test + public void testEe8FilterWithAwkwardRequestURI(TestInfo testInfo) throws Exception + { + InitializedJettyBase jettyBase = new InitializedJettyBase(testInfo); + + // Now add the filter to the webapp xml init + String xml = """ + + + + /ccd-ee8 + /ccd-ee8 + true + + org.eclipse.jetty.tests.ccd.ee8.InternalRequestURIFilter + /* + + + + + + + + + + """; + // Note: the InternalRequestURIFilter messes with the requestURI + Files.writeString(jettyBase.jettyBase.resolve("webapps/ccd-ee8.xml"), xml, StandardCharsets.UTF_8); + + // Start Jetty instance + String[] argsStart = { + "jetty.http.port=" + jettyBase.httpPort + }; + + runStart = jettyBase.distribution.start(argsStart); + assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + + ContentResponse response = client.newRequest("localhost", jettyBase.httpPort) + .method(HttpMethod.GET) + .headers((headers) -> + headers.put("X-ForwardTo", "/dump/ee8") + ) + .path("/ccd-ee8/forwardto/ee8") + .send(); + + String responseDetails = toResponseDetails(response); + assertThat(responseDetails, response.getStatus(), is(HttpStatus.OK_200)); + + Properties responseProps = new Properties(); + try (StringReader stringReader = new StringReader(response.getContentAsString())) + { + responseProps.load(stringReader); + } + + dumpProperties(responseProps); + + assertProperty(responseProps, "request.dispatcherType", is("FORWARD")); + assertProperty(responseProps, "request.requestURI", is("/internal/")); // the key change to look for + } +} diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/ResponseDetails.java b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/ResponseDetails.java new file mode 100644 index 00000000000..8135a9e643e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/java/org/eclipse/jetty/tests/redispatch/ResponseDetails.java @@ -0,0 +1,49 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.tests.redispatch; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.function.Supplier; + +import org.eclipse.jetty.client.ContentResponse; + +class ResponseDetails implements Supplier +{ + private final ContentResponse response; + + public ResponseDetails(ContentResponse response) + { + this.response = response; + } + + @Override + public String get() + { + try (StringWriter str = new StringWriter(); + PrintWriter out = new PrintWriter(str)) + { + out.println(response.toString()); + out.println(response.getHeaders().toString()); + out.println(response.getContentAsString()); + out.flush(); + return str.toString(); + } + catch (IOException e) + { + throw new RuntimeException("Unable to produce Response details", e); + } + } +} diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/error-handler.xml b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/error-handler.xml new file mode 100644 index 00000000000..e1556dea29f --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/error-handler.xml @@ -0,0 +1,10 @@ + + + + + + + true + + + diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/install-ccd-handler.xml b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/install-ccd-handler.xml new file mode 100644 index 00000000000..7eefabf9bfc --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/install-ccd-handler.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee10-forward-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee10-forward-dump.txt new file mode 100644 index 00000000000..d32f41cd86c --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee10-forward-dump.txt @@ -0,0 +1,12 @@ +REQUEST|GET|/ccd-ee10/redispatch/ee10 +STEP|CONTEXT_FORWARD|/ccd-ee10|/dump/ee10 +EXPECTED_EVENT|Initial plan: context-ee10-forward-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.DumpServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee10/dump/ee10 +EXPECTED_PROP|request.dispatcherType|FORWARD +EXPECTED_PROP|request.requestURI|/ccd-ee10/dump/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.context_path]|/ccd-ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.path_info]|/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.request_uri]|/ccd-ee10/redispatch/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.servlet_path]|/redispatch diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-forward-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-forward-dump.txt new file mode 100644 index 00000000000..418581be356 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-forward-dump.txt @@ -0,0 +1,12 @@ +REQUEST|GET|/ccd-ee8/redispatch/ee8 +STEP|CONTEXT_FORWARD|/ccd-ee8|/dump/ee8 +EXPECTED_EVENT|Initial plan: context-ee8-forward-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.DumpServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/dump/ee8 +EXPECTED_PROP|request.dispatcherType|FORWARD +EXPECTED_PROP|request.requestURI|/ccd-ee8/dump/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.servlet_path]|/redispatch \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-include-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-include-dump.txt new file mode 100644 index 00000000000..4da6dd41b13 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/context-ee8-include-dump.txt @@ -0,0 +1,14 @@ +REQUEST|GET|/ccd-ee8/redispatch/ee8 +STEP|CONTEXT_INCLUDE|/ccd-ee8|/dump/ee8 +EXPECTED_EVENT|Initial plan: context-ee8-include-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|request.dispatcherType|INCLUDE +EXPECTED_PROP|request.contextPath|/ccd-ee8 +EXPECTED_PROP|request.pathInfo|/ee8 +EXPECTED_PROP|request.requestURI|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.path_info]|/ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.request_uri]|/ccd-ee8/dump/ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.servlet_path]|/dump diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-forward-to-ee8-include-ee9-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-forward-to-ee8-include-ee9-dump.txt new file mode 100644 index 00000000000..38caa6fb4f9 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-forward-to-ee8-include-ee9-dump.txt @@ -0,0 +1,30 @@ +REQUEST|GET|/ccd-ee10/redispatch/ee10 +STEP|SET_HTTP_SESSION_ATTRIBUTE|test-name-10|test-value-ee10 +STEP|CONTEXT_FORWARD|/ccd-ee8|/redispatch/ee8 +STEP|CONTEXT_FORWARD|/ccd-ee9|/redispatch/ee9 +STEP|REQUEST_INCLUDE|/dump/ee9 +EXPECTED_EVENT|Initial plan: ee10-forward-to-ee8-include-ee9-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee9/redispatch/ee9 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee9/redispatch/ee9 +EXPECTED_PROP|request.dispatcherType|INCLUDE +EXPECTED_PROP|request.requestURI|/ccd-ee9/redispatch/ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.servlet_path]|/redispatch +EXPECTED_PROP|req.attr[jakarta.servlet.include.context_path]/ccd-ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.include.path_info]|/ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.include.request_uri]|/ccd-ee9/dump/ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.include.servlet_path]/dump +EXPECTED_PROP|req.attr[javax.servlet.include.context_path]| +EXPECTED_PROP|req.attr[javax.servlet.include.path_info]| +EXPECTED_PROP|req.attr[javax.servlet.include.request_uri]| +EXPECTED_PROP|req.attr[javax.servlet.include.servlet_path]| +EXPECTED_PROP|req.attr[javax.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.servlet_path]|/redispatch +EXPECTED_SESSION_IDS|true \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-forward-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-forward-dump.txt new file mode 100644 index 00000000000..4c520902d08 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-forward-dump.txt @@ -0,0 +1,12 @@ +REQUEST|GET|/ccd-ee10/redispatch/ee10 +STEP|REQUEST_FORWARD|/dump/ee10 +EXPECTED_EVENT|Initial plan: ee10-request-forward-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.DumpServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee10/dump/ee10 +EXPECTED_PROP|request.dispatcherType|FORWARD +EXPECTED_PROP|request.requestURI|/ccd-ee10/dump/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.context_path]|/ccd-ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.path_info]|/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.request_uri]|/ccd-ee10/redispatch/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.servlet_path]|/redispatch diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-include-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-include-dump.txt new file mode 100644 index 00000000000..3ad53760355 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-request-include-dump.txt @@ -0,0 +1,12 @@ +REQUEST|GET|/ccd-ee10/redispatch/ee10 +STEP|REQUEST_INCLUDE|/dump/ee10 +EXPECTED_EVENT|Initial plan: ee10-request-include-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee10/redispatch/ee10 +EXPECTED_PROP|request.dispatcherType|INCLUDE +EXPECTED_PROP|request.requestURI|/ccd-ee10/redispatch/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.include.context_path]|/ccd-ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.include.path_info]|/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.include.request_uri]|/ccd-ee10/dump/ee10 +EXPECTED_PROP|req.attr[jakarta.servlet.include.servlet_path]|/dump diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-ee8-ee9-ee8.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-ee8-ee9-ee8.txt new file mode 100644 index 00000000000..dc32f92a425 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-ee8-ee9-ee8.txt @@ -0,0 +1,36 @@ +REQUEST|GET|/ccd-ee10/redispatch/ee10 +# we reach ee10 +STEP|SET_HTTP_SESSION_ATTRIBUTE|test-name-10|test-value-ee10 +STEP|CONTEXT_FORWARD|/ccd-ee8|/redispatch/ee8 +# we reach ee8 +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-10 +STEP|SET_HTTP_SESSION_ATTRIBUTE|test-name-8|test-value-ee8 +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-10 +STEP|CONTEXT_FORWARD|/ccd-ee9|/redispatch/ee9 +# we reach ee9 +STEP|CONTEXT_FORWARD|/ccd-ee8|/redispatch/ee8 +# we reach ee8 again (does the HttpSession still exist, and has values?) +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-8 +STEP|REQUEST_FORWARD|/dump/ee8 +EXPECTED_EVENT|Initial plan: ee10-session-ee8-ee9-ee8.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10 +# we reach ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10 +# we reach ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() HttpSession is null +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() HttpSession exists: [test-name-10]=[null] +# we reach ee9 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee9/redispatch/ee9 +# we reached ee8 again +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() HttpSession exists: [test-name-8]=[test-value-ee8] +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.DumpServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/dump/ee8 +EXPECTED_PROP|request.dispatcherType|FORWARD +EXPECTED_PROP|request.requestURI|/ccd-ee8/dump/ee8 +EXPECTED_PROP|request.session.exists|true +EXPECTED_PROP|session[test-name-8]|test-value-ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.request_uri]|/ccd-ee8/dump/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.servlet_path]|/dump +EXPECTED_SESSION_IDS|true \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-forward-to-ee8-session-include-ee9-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-forward-to-ee8-session-include-ee9-dump.txt new file mode 100644 index 00000000000..5dcb7537cc6 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee10-session-forward-to-ee8-session-include-ee9-dump.txt @@ -0,0 +1,50 @@ +REQUEST|GET|/ccd-ee10/redispatch/ee10 +# we reach ee10 +STEP|SET_HTTP_SESSION_ATTRIBUTE|test-name-10|test-value-ee10 +STEP|CONTEXT_FORWARD|/ccd-ee8|/redispatch/ee8 +# we reach ee8 +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-10 +STEP|SET_HTTP_SESSION_ATTRIBUTE|test-name-8|test-value-ee8 +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-10 +STEP|CONTEXT_FORWARD|/ccd-ee9|/redispatch/ee9 +# we reach ee9 +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-10 +STEP|SET_HTTP_SESSION_ATTRIBUTE|test-name|test-value-ee9 +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-10 +STEP|GET_HTTP_SESSION_ATTRIBUTE|test-name-8 +STEP|REQUEST_INCLUDE|/dump/ee9 +EXPECTED_EVENT|Initial plan: ee10-session-forward-to-ee8-session-include-ee9-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10 +# we reach ee10 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10 +# we reach ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() HttpSession is null +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() HttpSession exists: [test-name-10]=[null] +# we reach ee9 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee9/redispatch/ee9 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() HttpSession is null +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() HttpSession exists: [test-name-10]=[null] +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() HttpSession exists: [test-name-8]=[null] +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee9/redispatch/ee9 +EXPECTED_PROP|request.dispatcherType|INCLUDE +EXPECTED_PROP|request.requestURI|/ccd-ee9/redispatch/ee9 +EXPECTED_PROP|request.session.exists|true +EXPECTED_PROP|session[test-name]|test-value-ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[jakarta.servlet.forward.servlet_path]|/redispatch +EXPECTED_PROP|req.attr[jakarta.servlet.include.context_path]/ccd-ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.include.path_info]|/ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.include.request_uri]|/ccd-ee9/dump/ee9 +EXPECTED_PROP|req.attr[jakarta.servlet.include.servlet_path]/dump +EXPECTED_PROP|req.attr[javax.servlet.include.context_path]| +EXPECTED_PROP|req.attr[javax.servlet.include.path_info]| +EXPECTED_PROP|req.attr[javax.servlet.include.request_uri]| +EXPECTED_PROP|req.attr[javax.servlet.include.servlet_path]| +EXPECTED_PROP|req.attr[javax.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.servlet_path]|/redispatch +EXPECTED_SESSION_IDS|true \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-forward-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-forward-dump.txt new file mode 100644 index 00000000000..dc75244d58e --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-forward-dump.txt @@ -0,0 +1,12 @@ +REQUEST|GET|/ccd-ee8/redispatch/ee8 +STEP|REQUEST_FORWARD|/dump/ee8 +EXPECTED_EVENT|Initial plan: ee8-request-forward-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.DumpServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/dump/ee8 +EXPECTED_PROP|request.dispatcherType|FORWARD +EXPECTED_PROP|request.requestURI|/ccd-ee8/dump/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.path_info]|/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[javax.servlet.forward.servlet_path]|/redispatch \ No newline at end of file diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-include-dump.txt b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-include-dump.txt new file mode 100644 index 00000000000..423ba6f1db7 --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/plans/ee8-request-include-dump.txt @@ -0,0 +1,12 @@ +REQUEST|GET|/ccd-ee8/redispatch/ee8 +STEP|REQUEST_INCLUDE|/dump/ee8 +EXPECTED_EVENT|Initial plan: ee8-request-include-dump.txt +EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|request.dispatcherType|INCLUDE +EXPECTED_PROP|request.requestURI|/ccd-ee8/redispatch/ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.context_path]|/ccd-ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.path_info]|/ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.request_uri]|/ccd-ee8/dump/ee8 +EXPECTED_PROP|req.attr[javax.servlet.include.servlet_path]|/dump diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee10.xml b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee10.xml new file mode 100644 index 00000000000..4a6dd71471a --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee10.xml @@ -0,0 +1,19 @@ + + + + + /ccd-ee10 + /ccd-ee10 + true + + + + + + + + + + + + diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee8.xml b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee8.xml new file mode 100644 index 00000000000..7d9e5b7db3d --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee8.xml @@ -0,0 +1,21 @@ + + + + + /ccd-ee8 + /ccd-ee8 + true + + + + + + + + + + + + + + diff --git a/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee9.xml b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee9.xml new file mode 100644 index 00000000000..07fd3c8344a --- /dev/null +++ b/tests/test-cross-context-dispatch/ccd-tests/src/test/resources/webapp-xmls/ccd-ee9.xml @@ -0,0 +1,21 @@ + + + + + /ccd-ee9 + /ccd-ee9 + true + + + + + + + + + + + + + + diff --git a/tests/test-cross-context-dispatch/pom.xml b/tests/test-cross-context-dispatch/pom.xml new file mode 100644 index 00000000000..297ceed4da1 --- /dev/null +++ b/tests/test-cross-context-dispatch/pom.xml @@ -0,0 +1,45 @@ + + + + 4.0.0 + + org.eclipse.jetty.tests + tests + 12.0.11-SNAPSHOT + + org.eclipse.jetty.tests.ccd + test-cross-context-dispatch + pom + Tests :: Cross Context Dispatch :: Parent + + + ccd-common + ccd-ee10-webapp + ccd-ee9-webapp + ccd-ee8-webapp + ccd-tests + + + + -1 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${session.repositoryCrossContext.localRepository.basedir.absolutePath} + ${project.version} + $(distribution.debug.port} + ${home.start.timeout} + + + + + + + From 4f72b84c59ae5f12413b3e0892bad05d84ce78a3 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 17 Jun 2024 11:58:35 -0500 Subject: [PATCH 18/18] Cleanup ee9 DefaultServletTest + Fix various warnings + Disable ODD_JAR / extraClassPath (temporarily) + Reenable disabled test Test + Reenable disabled test testListingContextBreakout --- .../jetty/ee9/servlet/DefaultServletTest.java | 1265 +++++++++++------ 1 file changed, 838 insertions(+), 427 deletions(-) diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java index 3824d5f5f6f..49846109bf9 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/DefaultServletTest.java @@ -15,7 +15,6 @@ package org.eclipse.jetty.ee9.servlet; import java.io.File; import java.io.IOException; -import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; import java.nio.ByteBuffer; @@ -33,7 +32,6 @@ import java.util.stream.Stream; import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; @@ -57,6 +55,7 @@ import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker; import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenPaths; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; @@ -67,7 +66,6 @@ import org.eclipse.jetty.util.resource.ResourceFactory; 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.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtendWith; @@ -91,43 +89,41 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @ExtendWith(WorkDirExtension.class) public class DefaultServletTest { - public Path docRoot; + public WorkDir workDir; // The name of the odd-jar used for testing "jar:file://" based resource access. private static final String ODD_JAR = "jar-resource-odd.jar"; private Server server; private LocalConnector connector; - private ServletContextHandler context; @BeforeEach - public void init(WorkDir workDir) throws Exception + public void ensureFileSystemPoolIsSane() { assertThat(FileSystemPool.INSTANCE.mounts(), empty()); - docRoot = workDir.getEmptyPathDir().resolve("docroot"); - FS.ensureDirExists(docRoot); + } + public void startServer(Consumer contextInit) throws Exception + { server = new Server(); connector = new LocalConnector(server); connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false); - File extraJarResources = MavenTestingUtils.getTargetFile(ODD_JAR); - URL[] urls = new URL[]{extraJarResources.toURI().toURL()}; - - ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader(); - URLClassLoader extraClassLoader = new URLClassLoader(urls, parentClassLoader); - - context = new ServletContextHandler(); - context.setBaseResource(ResourceFactory.of(context).newResource(docRoot)); + ServletContextHandler context = new ServletContextHandler(); + // DO NOT SET Base Resource here (some tests rely on alternate/none base resources) + // context.setBaseResource(); context.setContextPath("/context"); context.setWelcomeFiles(new String[]{"index.html", "index.jsp", "index.htm"}); - context.setClassLoader(extraClassLoader); + + if (contextInit != null) + contextInit.accept(context); server.setHandler(context); server.addConnector(connector); @@ -138,18 +134,31 @@ public class DefaultServletTest @AfterEach public void destroy() throws Exception { - server.stop(); - server.join(); + if (server != null) + { + server.stop(); + server.join(); + } assertThat(FileSystemPool.INSTANCE.mounts(), empty()); } @Test public void testListingWithSession() throws Exception { - ServletHolder defholder = context.addServlet(DefaultServlet.class, "/*"); - defholder.setInitParameter("dirAllowed", "true"); - defholder.setInitParameter("redirectWelcome", "false"); - defholder.setInitParameter("gzip", "false"); + Path docRoot = workDir.getEmptyPathDir().resolve("docroot"); + FS.ensureDirExists(docRoot); + + startServer((context) -> + { + context.setBaseResource(ResourceFactory.of(context).newResource(docRoot)); + + // this url-pattern is intentionally at '/*' to test behaviors when there + // are two (or more) DefaultServlets on overlapping url-patterns. + ServletHolder defholder = context.addServlet(DefaultServlet.class, "/*"); + defholder.setInitParameter("dirAllowed", "true"); + defholder.setInitParameter("redirectWelcome", "false"); + defholder.setInitParameter("gzip", "false"); + }); /* create some content in the docroot */ FS.ensureDirExists(docRoot.resolve("one")); @@ -171,10 +180,20 @@ public class DefaultServletTest @Test public void testListingXSS() throws Exception { - ServletHolder defholder = context.addServlet(DefaultServlet.class, "/*"); - defholder.setInitParameter("dirAllowed", "true"); - defholder.setInitParameter("redirectWelcome", "false"); - defholder.setInitParameter("gzip", "false"); + Path docRoot = workDir.getEmptyPathDir().resolve("docroot"); + FS.ensureDirExists(docRoot); + + startServer((context) -> + { + context.setBaseResource(ResourceFactory.of(context).newResource(docRoot)); + + // this url-pattern is intentionally at '/*' to test behaviors when there + // are two (or more) DefaultServlets on overlapping url-patterns. + ServletHolder defholder = context.addServlet(DefaultServlet.class, "/*"); + defholder.setInitParameter("dirAllowed", "true"); + defholder.setInitParameter("redirectWelcome", "false"); + defholder.setInitParameter("gzip", "false"); + }); /* create some content in the docroot */ Path one = docRoot.resolve("one"); @@ -189,16 +208,20 @@ public class DefaultServletTest * Intentionally bad request URI. Sending a non-encoded URI with typically * encoded characters '<', '>', and '"'. */ - String req1 = "GET /context/; HTTP/1.0\r\n" + - "\r\n"; + String req1 = """ + GET /context/; HTTP/1.0\r + \r + """; String rawResponse = connector.getResponse(req1); HttpTester.Response response = HttpTester.parseResponse(rawResponse); String body = response.getContent(); assertThat(body, not(containsString("