From 2f22767dc10fbabd448054289b97f37c523d29fc Mon Sep 17 00:00:00 2001 From: Tony Copping Date: Mon, 19 Aug 2024 18:41:35 -0600 Subject: [PATCH 1/9] Issue #12175 Update SslContextFactory to use Credential instead of Password Signed-off-by: Tony Copping --- .../jetty/util/ssl/SslContextFactory.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index 83cf59cd1d8..1351a653400 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -86,7 +86,7 @@ import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.Resources; import org.eclipse.jetty.util.security.CertificateUtils; import org.eclipse.jetty.util.security.CertificateValidator; -import org.eclipse.jetty.util.security.Password; +import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -157,9 +157,9 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du private Resource _trustStoreResource; private String _trustStoreProvider; private String _trustStoreType; - private Password _keyStorePassword; - private Password _keyManagerPassword; - private Password _trustStorePassword; + private Credential _keyStorePassword; + private Credential _keyManagerPassword; + private Credential _trustStorePassword; private String _sslProvider; private String _sslProtocol = "TLS"; private String _secureRandomAlgorithm; @@ -1148,7 +1148,7 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du { String type = Objects.toString(getTrustStoreType(), getKeyStoreType()); String provider = Objects.toString(getTrustStoreProvider(), getKeyStoreProvider()); - Password passwd = _trustStorePassword; + Credential passwd = _trustStorePassword; if (resource == null || resource.equals(_keyStoreResource)) { resource = _keyStoreResource; @@ -1614,23 +1614,23 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du * Returns the password object for the given realm. * * @param realm the realm - * @return the Password object + * @return the Credential object */ - protected Password getPassword(String realm) + protected Credential getPassword(String realm) { String password = System.getProperty(realm); return password == null ? null : newPassword(password); } /** - * Creates a new Password object. + * Creates a new Credential object. * * @param password the password string - * @return the new Password object + * @return the new Credential object */ - public Password newPassword(String password) + public Credential newPassword(String password) { - return new Password(password); + return Credential.getCredential(password); } public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException From 66e4bd862f038e5ac50b93f217319748210d23d3 Mon Sep 17 00:00:00 2001 From: Tony Copping Date: Sat, 24 Aug 2024 09:59:28 -0600 Subject: [PATCH 2/9] Issue #12175 Updates based on feedback Signed-off-by: Tony Copping --- .../jetty/util/ssl/SslContextFactory.java | 72 ++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index 1351a653400..bf08c19a08f 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -87,6 +87,7 @@ import org.eclipse.jetty.util.resource.Resources; import org.eclipse.jetty.util.security.CertificateUtils; import org.eclipse.jetty.util.security.CertificateValidator; import org.eclipse.jetty.util.security.Credential; +import org.eclipse.jetty.util.security.Password; import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -157,9 +158,9 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du private Resource _trustStoreResource; private String _trustStoreProvider; private String _trustStoreType; - private Credential _keyStorePassword; - private Credential _keyManagerPassword; - private Credential _trustStorePassword; + private Credential _keyStoreCredential; + private Credential _keyManagerCredential; + private Credential _trustStoreCredential; private String _sslProvider; private String _sslProtocol = "TLS"; private String _secureRandomAlgorithm; @@ -811,46 +812,42 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du public String getKeyStorePassword() { - return _keyStorePassword == null ? null : _keyStorePassword.toString(); + return _keyStoreCredential == null ? null : _keyStoreCredential.toString(); } /** - * @param password The password for the key store. If null is passed and - * a keystore is set, then - * the {@link #getPassword(String)} is used to - * obtain a password either from the {@value #PASSWORD_PROPERTY} - * system property. + * @param password The password for the key store. If null is passed then + * {@link #getCredential(String)} is used to obtain a password from + * the {@value #PASSWORD_PROPERTY} system property. */ public void setKeyStorePassword(String password) { - _keyStorePassword = password == null ? getPassword(PASSWORD_PROPERTY) : newPassword(password); + _keyStoreCredential = password == null ? getCredential(PASSWORD_PROPERTY) : newCredential(password); } public String getKeyManagerPassword() { - return _keyManagerPassword == null ? null : _keyManagerPassword.toString(); + return _keyManagerCredential == null ? null : _keyManagerCredential.toString(); } /** * @param password The password (if any) for the specific key within the key store. - * If null is passed and the {@value #KEYPASSWORD_PROPERTY} system property is set, - * then the {@link #getPassword(String)} is used to + * If null is passed then {@link #getCredential(String)} is used to * obtain a password from the {@value #KEYPASSWORD_PROPERTY} system property. */ public void setKeyManagerPassword(String password) { - _keyManagerPassword = password == null ? getPassword(KEYPASSWORD_PROPERTY) : newPassword(password); + _keyManagerCredential = password == null ? getCredential(KEYPASSWORD_PROPERTY) : newCredential(password); } /** * @param password The password for the truststore. If null is passed then - * the {@link #getPassword(String)} is used to - * obtain a password from the {@value #PASSWORD_PROPERTY} + * {@link #getCredential(String)} is used to obtain a password from the {@value #PASSWORD_PROPERTY} * system property. */ public void setTrustStorePassword(String password) { - _trustStorePassword = password == null ? getPassword(PASSWORD_PROPERTY) : newPassword(password); + _trustStoreCredential = password == null ? getCredential(PASSWORD_PROPERTY) : newCredential(password); } /** @@ -1133,7 +1130,7 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du */ protected KeyStore loadKeyStore(Resource resource) throws Exception { - String storePassword = Objects.toString(_keyStorePassword, null); + String storePassword = Objects.toString(_keyStoreCredential, null); return CertificateUtils.getKeyStore(resource, getKeyStoreType(), getKeyStoreProvider(), storePassword); } @@ -1148,12 +1145,12 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du { String type = Objects.toString(getTrustStoreType(), getKeyStoreType()); String provider = Objects.toString(getTrustStoreProvider(), getKeyStoreProvider()); - Credential passwd = _trustStorePassword; + Credential passwd = _trustStoreCredential; if (resource == null || resource.equals(_keyStoreResource)) { resource = _keyStoreResource; if (passwd == null) - passwd = _keyStorePassword; + passwd = _keyStoreCredential; } return CertificateUtils.getKeyStore(resource, type, provider, Objects.toString(passwd, null)); } @@ -1180,7 +1177,7 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du if (keyStore != null) { KeyManagerFactory keyManagerFactory = getKeyManagerFactoryInstance(); - keyManagerFactory.init(keyStore, _keyManagerPassword == null ? (_keyStorePassword == null ? null : _keyStorePassword.toString().toCharArray()) : _keyManagerPassword.toString().toCharArray()); + keyManagerFactory.init(keyStore, _keyManagerCredential == null ? (_keyStoreCredential == null ? null : _keyStoreCredential.toString().toCharArray()) : _keyManagerCredential.toString().toCharArray()); managers = keyManagerFactory.getKeyManagers(); if (managers != null) @@ -1614,21 +1611,48 @@ public abstract class SslContextFactory extends ContainerLifeCycle implements Du * Returns the password object for the given realm. * * @param realm the realm - * @return the Credential object + * @return the Password object + * @deprecated use {#link getCredential} instead. */ - protected Credential getPassword(String realm) + @Deprecated(since = "12.0.13", forRemoval = true) + protected Password getPassword(String realm) { String password = System.getProperty(realm); return password == null ? null : newPassword(password); } + /** + * Creates a new Password object. + * + * @param password the password string + * @return the new Password object + * @deprecated use {#link newCredential} instead. + */ + @Deprecated(since = "12.0.13", forRemoval = true) + public Password newPassword(String password) + { + return new Password(password); + } + + /** + * Returns the credential object for the given realm. + * + * @param realm the realm + * @return the Credential object + */ + protected Credential getCredential(String realm) + { + String password = System.getProperty(realm); + return password == null ? null : newCredential(password); + } + /** * Creates a new Credential object. * * @param password the password string * @return the new Credential object */ - public Credential newPassword(String password) + public Credential newCredential(String password) { return Credential.getCredential(password); } From 9756387020d7bb62f4f213ad71355183c824e8ba Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Mon, 26 Aug 2024 11:13:24 +1000 Subject: [PATCH 3/9] move jetty-openid from jetty-core to jetty-integrations Signed-off-by: Lachlan Roberts --- jetty-core/pom.xml | 1 - {jetty-core => jetty-integrations}/jetty-openid/pom.xml | 2 +- .../jetty-openid/src/main/config/etc/jetty-openid.xml | 0 .../jetty-openid/src/main/config/modules/openid.mod | 0 .../config/modules/openid/jetty-openid-baseloginservice.xml | 0 .../jetty-openid/src/main/java/module-info.java | 0 .../main/java/org/eclipse/jetty/security/openid/JwtDecoder.java | 0 .../org/eclipse/jetty/security/openid/OpenIdAuthenticator.java | 0 .../jetty/security/openid/OpenIdAuthenticatorConfiguration.java | 0 .../jetty/security/openid/OpenIdAuthenticatorFactory.java | 0 .../org/eclipse/jetty/security/openid/OpenIdConfiguration.java | 0 .../org/eclipse/jetty/security/openid/OpenIdCredentials.java | 0 .../org/eclipse/jetty/security/openid/OpenIdLoginService.java | 0 .../org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java | 0 .../services/org.eclipse.jetty.security.Authenticator$Factory | 0 .../java/org/eclipse/jetty/security/openid/JwtDecoderTest.java | 0 .../test/java/org/eclipse/jetty/security/openid/JwtEncoder.java | 0 .../eclipse/jetty/security/openid/OpenIdAuthenticationTest.java | 0 .../eclipse/jetty/security/openid/OpenIdCredentialsTest.java | 0 .../java/org/eclipse/jetty/security/openid/OpenIdProvider.java | 0 .../org/eclipse/jetty/security/openid/OpenIdRealmNameTest.java | 0 .../jetty-openid/src/test/resources/jetty-logging.properties | 0 jetty-integrations/pom.xml | 1 + 23 files changed, 2 insertions(+), 2 deletions(-) rename {jetty-core => jetty-integrations}/jetty-openid/pom.xml (98%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/config/etc/jetty-openid.xml (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/config/modules/openid.mod (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/config/modules/openid/jetty-openid-baseloginservice.xml (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/module-info.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/JwtDecoder.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorConfiguration.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdCredentialsTest.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdRealmNameTest.java (100%) rename {jetty-core => jetty-integrations}/jetty-openid/src/test/resources/jetty-logging.properties (100%) diff --git a/jetty-core/pom.xml b/jetty-core/pom.xml index c3670faf6a5..e966acb0a00 100644 --- a/jetty-core/pom.xml +++ b/jetty-core/pom.xml @@ -28,7 +28,6 @@ jetty-jndi jetty-keystore jetty-maven - jetty-openid jetty-osgi jetty-plus jetty-proxy diff --git a/jetty-core/jetty-openid/pom.xml b/jetty-integrations/jetty-openid/pom.xml similarity index 98% rename from jetty-core/jetty-openid/pom.xml rename to jetty-integrations/jetty-openid/pom.xml index d550bb80bdf..5fcdea7b47f 100644 --- a/jetty-core/jetty-openid/pom.xml +++ b/jetty-integrations/jetty-openid/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.eclipse.jetty - jetty-core + jetty-integrations 12.1.0-SNAPSHOT jetty-openid diff --git a/jetty-core/jetty-openid/src/main/config/etc/jetty-openid.xml b/jetty-integrations/jetty-openid/src/main/config/etc/jetty-openid.xml similarity index 100% rename from jetty-core/jetty-openid/src/main/config/etc/jetty-openid.xml rename to jetty-integrations/jetty-openid/src/main/config/etc/jetty-openid.xml diff --git a/jetty-core/jetty-openid/src/main/config/modules/openid.mod b/jetty-integrations/jetty-openid/src/main/config/modules/openid.mod similarity index 100% rename from jetty-core/jetty-openid/src/main/config/modules/openid.mod rename to jetty-integrations/jetty-openid/src/main/config/modules/openid.mod diff --git a/jetty-core/jetty-openid/src/main/config/modules/openid/jetty-openid-baseloginservice.xml b/jetty-integrations/jetty-openid/src/main/config/modules/openid/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-integrations/jetty-openid/src/main/config/modules/openid/jetty-openid-baseloginservice.xml diff --git a/jetty-core/jetty-openid/src/main/java/module-info.java b/jetty-integrations/jetty-openid/src/main/java/module-info.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/module-info.java rename to jetty-integrations/jetty-openid/src/main/java/module-info.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/JwtDecoder.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/JwtDecoder.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/JwtDecoder.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/JwtDecoder.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorConfiguration.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorConfiguration.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorConfiguration.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorConfiguration.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java diff --git a/jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java b/jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java similarity index 100% rename from jetty-core/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java rename to jetty-integrations/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java diff --git a/jetty-core/jetty-openid/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory b/jetty-integrations/jetty-openid/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory similarity index 100% rename from jetty-core/jetty-openid/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory rename to jetty-integrations/jetty-openid/src/main/resources/META-INF/services/org.eclipse.jetty.security.Authenticator$Factory diff --git a/jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java similarity index 100% rename from jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java rename to jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java diff --git a/jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java similarity index 100% rename from jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java rename to jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java diff --git a/jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java similarity index 100% rename from jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java rename to jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java diff --git a/jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdCredentialsTest.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdCredentialsTest.java similarity index 100% rename from jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdCredentialsTest.java rename to jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdCredentialsTest.java diff --git a/jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java similarity index 100% rename from jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java rename to jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java diff --git a/jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdRealmNameTest.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdRealmNameTest.java similarity index 100% rename from jetty-core/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdRealmNameTest.java rename to jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdRealmNameTest.java diff --git a/jetty-core/jetty-openid/src/test/resources/jetty-logging.properties b/jetty-integrations/jetty-openid/src/test/resources/jetty-logging.properties similarity index 100% rename from jetty-core/jetty-openid/src/test/resources/jetty-logging.properties rename to jetty-integrations/jetty-openid/src/test/resources/jetty-logging.properties diff --git a/jetty-integrations/pom.xml b/jetty-integrations/pom.xml index c1785dba714..c5f9aafc19a 100644 --- a/jetty-integrations/pom.xml +++ b/jetty-integrations/pom.xml @@ -13,6 +13,7 @@ jetty-ethereum + jetty-openid jetty-gcloud jetty-hazelcast jetty-infinispan From 063e839962fd382b48b41982415dc7721237ec42 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Mon, 26 Aug 2024 11:48:10 +1000 Subject: [PATCH 4/9] reduce duplication for OpenIdProvider testing class Signed-off-by: Lachlan Roberts --- jetty-ee8/jetty-ee8-openid/pom.xml | 5 + jetty-ee9/jetty-ee9-openid/pom.xml | 5 + .../jetty/ee9/security/openid/JwtEncoder.java | 53 --- .../openid/OpenIdAuthenticationTest.java | 1 + .../ee9/security/openid/OpenIdProvider.java | 402 ---------------- jetty-integrations/jetty-openid/pom.xml | 5 + .../jetty/security/openid/JwtDecoderTest.java | 1 + .../openid/OpenIdAuthenticationTest.java | 1 + .../jetty/security/openid/OpenIdProvider.java | 445 ------------------ pom.xml | 5 + tests/jetty-test-util/pom.xml | 29 ++ .../org/eclipse/jetty/tests}/JwtEncoder.java | 2 +- .../eclipse/jetty/tests/OpenIdProvider.java | 421 +++++++++++++++++ tests/pom.xml | 1 + .../test-ee10-distribution/pom.xml | 5 + .../ee10/tests/distribution/OpenIdTests.java | 2 +- .../tests/distribution/openid/JwtEncoder.java | 53 --- .../distribution/openid/OpenIdProvider.java | 402 ---------------- .../test-ee11-distribution/pom.xml | 5 + .../ee11/tests/distribution/OpenIdTests.java | 2 +- .../tests/distribution/openid/JwtEncoder.java | 53 --- .../distribution/openid/OpenIdProvider.java | 402 ---------------- .../test-ee9-distribution/pom.xml | 5 + .../ee9/tests/distribution/OpenIdTests.java | 2 +- .../tests/distribution/openid/JwtEncoder.java | 53 --- .../distribution/openid/OpenIdProvider.java | 402 ---------------- 26 files changed, 493 insertions(+), 2269 deletions(-) delete mode 100644 jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/JwtEncoder.java delete mode 100644 jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdProvider.java delete mode 100644 jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java create mode 100644 tests/jetty-test-util/pom.xml rename {jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid => tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests}/JwtEncoder.java (97%) create mode 100644 tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java delete mode 100644 tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/JwtEncoder.java delete mode 100644 tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/OpenIdProvider.java delete mode 100644 tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/JwtEncoder.java delete mode 100644 tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/OpenIdProvider.java delete mode 100644 tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/JwtEncoder.java delete mode 100644 tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/OpenIdProvider.java diff --git a/jetty-ee8/jetty-ee8-openid/pom.xml b/jetty-ee8/jetty-ee8-openid/pom.xml index a2b2bf3bb7f..9474870eb21 100644 --- a/jetty-ee8/jetty-ee8-openid/pom.xml +++ b/jetty-ee8/jetty-ee8-openid/pom.xml @@ -52,6 +52,11 @@ jetty-ee8-servlet test + + org.eclipse.jetty.tests + jetty-test-util + test + org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-ee9/jetty-ee9-openid/pom.xml b/jetty-ee9/jetty-ee9-openid/pom.xml index 5ad8a155b62..8c92210726b 100644 --- a/jetty-ee9/jetty-ee9-openid/pom.xml +++ b/jetty-ee9/jetty-ee9-openid/pom.xml @@ -55,6 +55,11 @@ jetty-ee9-servlet test + + org.eclipse.jetty.tests + jetty-test-util + test + org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/JwtEncoder.java b/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/JwtEncoder.java deleted file mode 100644 index 6de75b9e4ac..00000000000 --- a/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/JwtEncoder.java +++ /dev/null @@ -1,53 +0,0 @@ -// -// ======================================================================== -// 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.security.openid; - -import java.util.Base64; - -/** - * A basic JWT encoder for testing purposes. - */ -public class JwtEncoder -{ - private static final Base64.Encoder ENCODER = Base64.getUrlEncoder(); - private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}"; - private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow"; - - public static String encode(String idToken) - { - return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes())); - } - - private static String stripPadding(String paddedBase64) - { - return paddedBase64.split("=")[0]; - } - - /** - * Create a basic JWT for testing using argument supplied attributes. - */ - public static String createIdToken(String provider, String clientId, String subject, String name, long expiry) - { - return "{" + - "\"iss\": \"" + provider + "\"," + - "\"sub\": \"" + subject + "\"," + - "\"aud\": \"" + clientId + "\"," + - "\"exp\": " + expiry + "," + - "\"name\": \"" + name + "\"," + - "\"email\": \"" + name + "@example.com" + "\"" + - "}"; - } -} diff --git a/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdAuthenticationTest.java b/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdAuthenticationTest.java index dfa5d06537b..b9893781184 100644 --- a/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdAuthenticationTest.java +++ b/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdAuthenticationTest.java @@ -43,6 +43,7 @@ import org.eclipse.jetty.security.openid.OpenIdConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.session.FileSessionDataStoreFactory; +import org.eclipse.jetty.tests.OpenIdProvider; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.security.Password; diff --git a/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdProvider.java b/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdProvider.java deleted file mode 100644 index 21f1ff6b7cb..00000000000 --- a/jetty-ee9/jetty-ee9-openid/src/test/java/org/eclipse/jetty/ee9/security/openid/OpenIdProvider.java +++ /dev/null @@ -1,402 +0,0 @@ -// -// ======================================================================== -// 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.security.openid; - -import java.io.IOException; -import java.io.PrintWriter; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import jakarta.servlet.ServletException; -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.security.openid.OpenIdConfiguration; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.util.component.ContainerLifeCycle; -import org.eclipse.jetty.util.statistic.CounterStatistic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OpenIdProvider extends ContainerLifeCycle -{ - private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class); - - private static final String CONFIG_PATH = "/.well-known/openid-configuration"; - private static final String AUTH_PATH = "/auth"; - private static final String TOKEN_PATH = "/token"; - private static final String END_SESSION_PATH = "/end_session"; - private final Map issuedAuthCodes = new HashMap<>(); - - protected final String clientId; - protected final String clientSecret; - protected final List redirectUris = new ArrayList<>(); - private final ServerConnector connector; - private final Server server; - private int port = 0; - private String provider; - private User preAuthedUser; - private final CounterStatistic loggedInUsers = new CounterStatistic(); - private long _idTokenDuration = Duration.ofSeconds(10).toMillis(); - - public static void main(String[] args) throws Exception - { - String clientId = "CLIENT_ID123"; - String clientSecret = "PASSWORD123"; - int port = 5771; - String redirectUri = "http://localhost:8080/j_security_check"; - - OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret); - openIdProvider.addRedirectUri(redirectUri); - openIdProvider.setPort(port); - openIdProvider.start(); - try - { - openIdProvider.join(); - } - finally - { - openIdProvider.stop(); - } - } - - public OpenIdProvider(String clientId, String clientSecret) - { - this.clientId = clientId; - this.clientSecret = clientSecret; - - server = new Server(); - connector = new ServerConnector(server); - server.addConnector(connector); - - ServletContextHandler contextHandler = new ServletContextHandler(); - contextHandler.setContextPath("/"); - contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH); - contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH); - contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH); - contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH); - server.setHandler(contextHandler); - - addBean(server); - } - - public void setIdTokenDuration(long duration) - { - _idTokenDuration = duration; - } - - public long getIdTokenDuration() - { - return _idTokenDuration; - } - - public void join() throws InterruptedException - { - server.join(); - } - - public OpenIdConfiguration getOpenIdConfiguration() - { - String provider = getProvider(); - String authEndpoint = provider + AUTH_PATH; - String tokenEndpoint = provider + TOKEN_PATH; - return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null); - } - - public CounterStatistic getLoggedInUsers() - { - return loggedInUsers; - } - - @Override - protected void doStart() throws Exception - { - connector.setPort(port); - super.doStart(); - provider = "http://localhost:" + connector.getLocalPort(); - } - - public void setPort(int port) - { - if (isStarted()) - throw new IllegalStateException(); - this.port = port; - } - - public void setUser(User user) - { - this.preAuthedUser = user; - } - - public String getProvider() - { - if (!isStarted() && port == 0) - throw new IllegalStateException("Port of OpenIdProvider not configured"); - return provider; - } - - public void addRedirectUri(String uri) - { - redirectUris.add(uri); - } - - public class AuthEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - if (!clientId.equals(req.getParameter("client_id"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id"); - return; - } - - String redirectUri = req.getParameter("redirect_uri"); - if (!redirectUris.contains(redirectUri)) - { - LOG.warn("invalid redirectUri {}", redirectUri); - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String scopeString = req.getParameter("scope"); - List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" ")); - if (!scopes.contains("openid")) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope"); - return; - } - - if (!"code".equals(req.getParameter("response_type"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - if (preAuthedUser == null) - { - PrintWriter writer = resp.getWriter(); - resp.setContentType("text/html"); - writer.println("

Login to OpenID Connect Provider

"); - writer.println("
"); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println("
"); - } - else - { - redirectUser(resp, preAuthedUser, redirectUri, state); - } - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String redirectUri = req.getParameter("redirectUri"); - if (!redirectUris.contains(redirectUri)) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - String username = req.getParameter("username"); - if (username == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username"); - return; - } - - User user = new User(username); - redirectUser(resp, user, redirectUri, state); - } - - public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException - { - String authCode = UUID.randomUUID().toString().replace("-", ""); - issuedAuthCodes.put(authCode, user); - - try - { - redirectUri += "?code=" + authCode + "&state=" + state; - response.sendRedirect(response.encodeRedirectURL(redirectUri)); - } - catch (Throwable t) - { - issuedAuthCodes.remove(authCode); - throw t; - } - } - } - - private class TokenEndpoint extends HttpServlet - { - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - String code = req.getParameter("code"); - - if (!clientId.equals(req.getParameter("client_id")) || - !clientSecret.equals(req.getParameter("client_secret")) || - !redirectUris.contains(req.getParameter("redirect_uri")) || - !"authorization_code".equals(req.getParameter("grant_type")) || - code == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request"); - return; - } - - User user = issuedAuthCodes.remove(code); - if (user == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code"); - return; - } - - String accessToken = "ABCDEFG"; - long accessTokenDuration = Duration.ofMinutes(10).toSeconds(); - String response = "{" + - "\"access_token\": \"" + accessToken + "\"," + - "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," + - "\"expires_in\": " + accessTokenDuration + "," + - "\"token_type\": \"Bearer\"" + - "}"; - - loggedInUsers.increment(); - resp.setContentType("text/plain"); - resp.getWriter().print(response); - } - } - - private class EndSessionEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - doPost(req, resp); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String idToken = req.getParameter("id_token_hint"); - if (idToken == null) - { - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint"); - return; - } - - String logoutRedirect = req.getParameter("post_logout_redirect_uri"); - if (logoutRedirect == null) - { - resp.setStatus(HttpServletResponse.SC_OK); - resp.getWriter().println("logout success on end_session_endpoint"); - return; - } - - loggedInUsers.decrement(); - resp.setContentType("text/plain"); - resp.sendRedirect(logoutRedirect); - } - } - - private class ConfigServlet extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String discoveryDocument = "{" + - "\"issuer\": \"" + provider + "\"," + - "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," + - "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," + - "\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," + - "}"; - - resp.getWriter().write(discoveryDocument); - } - } - - public static class User - { - private final String subject; - private final String name; - - public User(String name) - { - this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name); - } - - public User(String subject, String name) - { - this.subject = subject; - this.name = name; - } - - public String getName() - { - return name; - } - - public String getSubject() - { - return subject; - } - - public String getIdToken(String provider, String clientId, long duration) - { - long expiryTime = Instant.now().plusMillis(duration).getEpochSecond(); - return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime); - } - - @Override - public boolean equals(Object obj) - { - if (!(obj instanceof User)) - return false; - return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name); - } - - @Override - public int hashCode() - { - return Objects.hash(subject, name); - } - } -} diff --git a/jetty-integrations/jetty-openid/pom.xml b/jetty-integrations/jetty-openid/pom.xml index 5fcdea7b47f..f67543aa805 100644 --- a/jetty-integrations/jetty-openid/pom.xml +++ b/jetty-integrations/jetty-openid/pom.xml @@ -47,6 +47,11 @@ jetty-slf4j-impl test
+ + org.eclipse.jetty.tests + jetty-test-util + test + org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java index b343b222e52..32a377d313c 100644 --- a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java +++ b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtDecoderTest.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.security.openid; import java.util.Map; import java.util.stream.Stream; +import org.eclipse.jetty.tests.JwtEncoder; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java index 79baf390aea..e122d9b8e5d 100644 --- a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java +++ b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java @@ -43,6 +43,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.session.FileSessionDataStoreFactory; import org.eclipse.jetty.session.SessionHandler; +import org.eclipse.jetty.tests.OpenIdProvider; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; diff --git a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java b/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java deleted file mode 100644 index 512237b8c6e..00000000000 --- a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java +++ /dev/null @@ -1,445 +0,0 @@ -// -// ======================================================================== -// 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.security.openid; - -import java.io.IOException; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import org.eclipse.jetty.http.BadMessageException; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.ContextHandlerCollection; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.Fields; -import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.component.ContainerLifeCycle; -import org.eclipse.jetty.util.statistic.CounterStatistic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OpenIdProvider extends ContainerLifeCycle -{ - private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class); - - private static final String CONFIG_PATH = "/.well-known/openid-configuration"; - private static final String AUTH_PATH = "/auth"; - private static final String TOKEN_PATH = "/token"; - private static final String END_SESSION_PATH = "/end_session"; - private final Map issuedAuthCodes = new HashMap<>(); - - protected final String clientId; - protected final String clientSecret; - protected final List redirectUris = new ArrayList<>(); - private final ServerConnector connector; - private final Server server; - private int port = 0; - private String provider; - private User preAuthedUser; - private final CounterStatistic loggedInUsers = new CounterStatistic(); - private long _idTokenDuration = Duration.ofSeconds(10).toMillis(); - - public static void main(String[] args) throws Exception - { - String clientId = "CLIENT_ID123"; - String clientSecret = "PASSWORD123"; - int port = 5771; - String redirectUri = "http://localhost:8080/j_security_check"; - - OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret); - openIdProvider.addRedirectUri(redirectUri); - openIdProvider.setPort(port); - openIdProvider.start(); - try - { - openIdProvider.join(); - } - finally - { - openIdProvider.stop(); - } - } - - public OpenIdProvider(String clientId, String clientSecret) - { - this.clientId = clientId; - this.clientSecret = clientSecret; - - server = new Server(); - connector = new ServerConnector(server); - server.addConnector(connector); - - ContextHandlerCollection contexts = new ContextHandlerCollection(); - contexts.addHandler(new ConfigServlet(CONFIG_PATH)); - contexts.addHandler(new AuthEndpoint(AUTH_PATH)); - contexts.addHandler(new TokenEndpoint(TOKEN_PATH)); - contexts.addHandler(new EndSessionEndpoint(END_SESSION_PATH)); - server.setHandler(contexts); - - addBean(server); - } - - public void setIdTokenDuration(long duration) - { - _idTokenDuration = duration; - } - - public long getIdTokenDuration() - { - return _idTokenDuration; - } - - public void join() throws InterruptedException - { - server.join(); - } - - public OpenIdConfiguration getOpenIdConfiguration() - { - String provider = getProvider(); - String authEndpoint = provider + AUTH_PATH; - String tokenEndpoint = provider + TOKEN_PATH; - return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null); - } - - public CounterStatistic getLoggedInUsers() - { - return loggedInUsers; - } - - @Override - protected void doStart() throws Exception - { - connector.setPort(port); - super.doStart(); - provider = "http://localhost:" + connector.getLocalPort(); - } - - public void setPort(int port) - { - if (isStarted()) - throw new IllegalStateException(); - this.port = port; - } - - public void setUser(User user) - { - this.preAuthedUser = user; - } - - public String getProvider() - { - if (!isStarted() && port == 0) - throw new IllegalStateException("Port of OpenIdProvider not configured"); - return provider; - } - - public void addRedirectUri(String uri) - { - redirectUris.add(uri); - } - - public class AuthEndpoint extends ContextHandler - { - public AuthEndpoint(String contextPath) - { - super(contextPath); - } - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - switch (request.getMethod()) - { - case "GET": - doGet(request, response, callback); - break; - case "POST": - doPost(request, response, callback); - break; - default: - throw new BadMessageException("Unsupported HTTP Method"); - } - - return true; - } - - protected void doGet(Request request, Response response, Callback callback) throws Exception - { - Fields parameters = Request.getParameters(request); - if (!clientId.equals(parameters.getValue("client_id"))) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid client_id"); - return; - } - - String redirectUri = parameters.getValue("redirect_uri"); - if (!redirectUris.contains(redirectUri)) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri"); - return; - } - - String scopeString = parameters.getValue("scope"); - List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString)); - if (!scopes.contains("openid")) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no openid scope"); - return; - } - - if (!"code".equals(parameters.getValue("response_type"))) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "response_type must be code"); - return; - } - - String state = parameters.getValue("state"); - if (state == null) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param"); - return; - } - - if (preAuthedUser == null) - { - response.getHeaders().add(HttpHeader.CONTENT_TYPE, "text/html"); - - String content = - "

Login to OpenID Connect Provider

" + - "
" + - "" + - "" + - "" + - "" + - "
"; - response.write(true, BufferUtil.toBuffer(content), callback); - } - else - { - redirectUser(request, response, callback, preAuthedUser, redirectUri, state); - } - } - - protected void doPost(Request request, Response response, Callback callback) throws Exception - { - Fields parameters = Request.getParameters(request); - - String redirectUri = parameters.getValue("redirectUri"); - if (!redirectUris.contains(redirectUri)) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri"); - return; - } - - String state = parameters.getValue("state"); - if (state == null) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param"); - return; - } - - String username = parameters.getValue("username"); - if (username == null) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no username"); - return; - } - - User user = new User(username); - redirectUser(request, response, callback, user, redirectUri, state); - } - - public void redirectUser(Request request, Response response, Callback callback, User user, String redirectUri, String state) throws IOException - { - String authCode = UUID.randomUUID().toString().replace("-", ""); - issuedAuthCodes.put(authCode, user); - - try - { - redirectUri += "?code=" + authCode + "&state=" + state; - Response.sendRedirect(request, response, callback, redirectUri); - } - catch (Throwable t) - { - issuedAuthCodes.remove(authCode); - throw t; - } - } - } - - private class TokenEndpoint extends ContextHandler - { - public TokenEndpoint(String contextPath) - { - super(contextPath); - } - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - Fields parameters = Request.getParameters(request); - - String code = parameters.getValue("code"); - - if (!clientId.equals(parameters.getValue("client_id")) || - !clientSecret.equals(parameters.getValue("client_secret")) || - !redirectUris.contains(parameters.getValue("redirect_uri")) || - !"authorization_code".equals(parameters.getValue("grant_type")) || - code == null) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "bad auth request"); - return true; - } - - User user = issuedAuthCodes.remove(code); - if (user == null) - { - Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid auth code"); - return true; - } - - String accessToken = "ABCDEFG"; - long accessTokenDuration = Duration.ofMinutes(10).toSeconds(); - String content = "{" + - "\"access_token\": \"" + accessToken + "\"," + - "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," + - "\"expires_in\": " + accessTokenDuration + "," + - "\"token_type\": \"Bearer\"" + - "}"; - - loggedInUsers.increment(); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); - response.write(true, BufferUtil.toBuffer(content), callback); - return true; - } - } - - private class EndSessionEndpoint extends ContextHandler - { - public EndSessionEndpoint(String contextPath) - { - super(contextPath); - } - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - Fields parameters = Request.getParameters(request); - - String idToken = parameters.getValue("id_token_hint"); - if (idToken == null) - { - Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "no id_token_hint"); - return true; - } - - String logoutRedirect = parameters.getValue("post_logout_redirect_uri"); - if (logoutRedirect == null) - { - response.setStatus(HttpStatus.OK_200); - response.write(true, BufferUtil.toBuffer("logout success on end_session_endpoint"), callback); - return true; - } - - loggedInUsers.decrement(); - response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); - Response.sendRedirect(request, response, callback, logoutRedirect); - return true; - } - } - - private class ConfigServlet extends ContextHandler - { - public ConfigServlet(String contextPath) - { - super(contextPath); - } - - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception - { - String discoveryDocument = "{" + - "\"issuer\": \"" + provider + "\"," + - "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," + - "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," + - "\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," + - "}"; - - response.write(true, BufferUtil.toBuffer(discoveryDocument), callback); - return true; - } - } - - public static class User - { - private final String subject; - private final String name; - - public User(String name) - { - this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name); - } - - public User(String subject, String name) - { - this.subject = subject; - this.name = name; - } - - public String getName() - { - return name; - } - - public String getSubject() - { - return subject; - } - - public String getIdToken(String provider, String clientId, long duration) - { - long expiryTime = Instant.now().plusMillis(duration).getEpochSecond(); - return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime); - } - - @Override - public boolean equals(Object obj) - { - if (!(obj instanceof User)) - return false; - return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name); - } - - @Override - public int hashCode() - { - return Objects.hash(subject, name); - } - } -} diff --git a/pom.xml b/pom.xml index 69bc91da4f7..68311850742 100644 --- a/pom.xml +++ b/pom.xml @@ -1060,6 +1060,11 @@ jetty-test-session-common ${project.version}
+ + org.eclipse.jetty.tests + jetty-test-util + ${project.version} + org.eclipse.jetty.tests jetty-testers diff --git a/tests/jetty-test-util/pom.xml b/tests/jetty-test-util/pom.xml new file mode 100644 index 00000000000..cab19941710 --- /dev/null +++ b/tests/jetty-test-util/pom.xml @@ -0,0 +1,29 @@ + + + + 4.0.0 + + org.eclipse.jetty.tests + tests + 12.1.0-SNAPSHOT + + jetty-test-util + jar + Tests :: Test Utilities + + + ${project.groupId}.testers + + + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-util + + + + diff --git a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java b/tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java similarity index 97% rename from jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java rename to tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java index 928a51fd822..d4611f07a77 100644 --- a/jetty-integrations/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/JwtEncoder.java +++ b/tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java @@ -11,7 +11,7 @@ // ======================================================================== // -package org.eclipse.jetty.security.openid; +package org.eclipse.jetty.tests; import java.util.Base64; diff --git a/tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java b/tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java new file mode 100644 index 00000000000..aba79c2a0c3 --- /dev/null +++ b/tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java @@ -0,0 +1,421 @@ +// +// ======================================================================== +// 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; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.statistic.CounterStatistic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OpenIdProvider extends ContainerLifeCycle +{ + private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class); + + private static final String CONFIG_PATH = "/.well-known/openid-configuration"; + private static final String AUTH_PATH = "/auth"; + private static final String TOKEN_PATH = "/token"; + private static final String END_SESSION_PATH = "/end_session"; + private final Map issuedAuthCodes = new HashMap<>(); + + protected final String clientId; + protected final String clientSecret; + protected final List redirectUris = new ArrayList<>(); + private final ServerConnector connector; + private final Server server; + private int port = 0; + private String provider; + private User preAuthedUser; + private final CounterStatistic loggedInUsers = new CounterStatistic(); + private long _idTokenDuration = Duration.ofSeconds(10).toMillis(); + + public static void main(String[] args) throws Exception + { + String clientId = "CLIENT_ID123"; + String clientSecret = "PASSWORD123"; + int port = 5771; + String redirectUri = "http://localhost:8080/j_security_check"; + + OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret); + openIdProvider.addRedirectUri(redirectUri); + openIdProvider.setPort(port); + openIdProvider.start(); + try + { + openIdProvider.join(); + } + finally + { + openIdProvider.stop(); + } + } + + public OpenIdProvider() + { + this("clientId" + StringUtil.randomAlphaNumeric(4), StringUtil.randomAlphaNumeric(10)); + } + + public OpenIdProvider(String clientId, String clientSecret) + { + this.clientId = clientId; + this.clientSecret = clientSecret; + + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + server.setHandler(new OpenIdProviderHandler()); + addBean(server); + } + + public String getClientId() + { + return clientId; + } + + public String getClientSecret() + { + return clientSecret; + } + + public void setIdTokenDuration(long duration) + { + _idTokenDuration = duration; + } + + public long getIdTokenDuration() + { + return _idTokenDuration; + } + + public void join() throws InterruptedException + { + server.join(); + } + + public CounterStatistic getLoggedInUsers() + { + return loggedInUsers; + } + + @Override + protected void doStart() throws Exception + { + connector.setPort(port); + super.doStart(); + provider = "http://localhost:" + connector.getLocalPort(); + } + + public void setPort(int port) + { + if (isStarted()) + throw new IllegalStateException(); + this.port = port; + } + + public void setUser(User user) + { + this.preAuthedUser = user; + } + + public String getProvider() + { + if (!isStarted() && port == 0) + throw new IllegalStateException("Port of OpenIdProvider not configured"); + return provider; + } + + public void addRedirectUri(String uri) + { + redirectUris.add(uri); + } + + public class OpenIdProviderHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + String pathInContext = Request.getPathInContext(request); + switch (pathInContext) + { + case CONFIG_PATH -> doGetConfigServlet(request, response, callback); + case AUTH_PATH -> doAuthEndpoint(request, response, callback); + case TOKEN_PATH -> doTokenEndpoint(request, response, callback); + case END_SESSION_PATH -> doEndSessionEndpoint(request, response, callback); + default -> Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404); + } + + return true; + } + } + + protected void doAuthEndpoint(Request request, Response response, Callback callback) throws Exception + { + String method = request.getMethod(); + switch (method) + { + case "GET" -> doGetAuthEndpoint(request, response, callback); + case "POST" -> doPostAuthEndpoint(request, response, callback); + default -> throw new BadMessageException("Unsupported HTTP method: " + method); + } + } + + protected void doGetAuthEndpoint(Request request, Response response, Callback callback) throws Exception + { + Fields parameters = Request.getParameters(request); + + if (!clientId.equals(parameters.getValue("client_id"))) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid client_id"); + return; + } + + String redirectUri = parameters.getValue("redirect_uri"); + if (!redirectUris.contains(redirectUri)) + { + LOG.warn("invalid redirectUri {}", redirectUri); + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri"); + return; + } + + String scopeString = parameters.getValue("scope"); + List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" ")); + if (!scopes.contains("openid")) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no openid scope"); + return; + } + + if (!"code".equals(parameters.getValue("response_type"))) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "response_type must be code"); + return; + } + + String state = parameters.getValue("state"); + if (state == null) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param"); + return; + } + + if (preAuthedUser == null) + { + String responseContent = String.format(""" +

Login to OpenID Connect Provider

+
+ + + + +
+ """, AUTH_PATH, redirectUri, state); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html"); + response.write(true, BufferUtil.toBuffer(responseContent), callback); + } + else + { + redirectUser(request, response, callback, preAuthedUser, redirectUri, state); + } + } + + protected void doPostAuthEndpoint(Request request, Response response, Callback callback) throws Exception + { + Fields parameters = Request.getParameters(request); + String redirectUri = parameters.getValue("redirectUri"); + if (!redirectUris.contains(redirectUri)) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid redirect_uri"); + return; + } + + String state = parameters.getValue("state"); + if (state == null) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no state param"); + return; + } + + String username = parameters.getValue("username"); + if (username == null) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "no username"); + return; + } + + User user = new User(username); + redirectUser(request, response, callback, user, redirectUri, state); + } + + public void redirectUser(Request request, Response response, Callback callback, User user, String redirectUri, String state) throws IOException + { + String authCode = UUID.randomUUID().toString().replace("-", ""); + issuedAuthCodes.put(authCode, user); + + try + { + redirectUri += "?code=" + authCode + "&state=" + state; + Response.sendRedirect(request, response, callback, redirectUri); + } + catch (Throwable t) + { + issuedAuthCodes.remove(authCode); + throw t; + } + } + + protected void doTokenEndpoint(Request request, Response response, Callback callback) throws Exception + { + if (!HttpMethod.POST.is(request.getMethod())) + throw new BadMessageException("Unsupported HTTP method for token Endpoint: " + request.getMethod()); + + Fields parameters = Request.getParameters(request); + String code = parameters.getValue("code"); + + if (!clientId.equals(parameters.getValue("client_id")) || + !clientSecret.equals(parameters.getValue("client_secret")) || + !redirectUris.contains(parameters.getValue("redirect_uri")) || + !"authorization_code".equals(parameters.getValue("grant_type")) || + code == null) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "bad auth request"); + return; + } + + User user = issuedAuthCodes.remove(code); + if (user == null) + { + Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403, "invalid auth code"); + return; + } + + String accessToken = "ABCDEFG"; + long accessTokenDuration = Duration.ofMinutes(10).toSeconds(); + String responseContent = "{" + + "\"access_token\": \"" + accessToken + "\"," + + "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," + + "\"expires_in\": " + accessTokenDuration + "," + + "\"token_type\": \"Bearer\"" + + "}"; + + loggedInUsers.increment(); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); + response.write(true, BufferUtil.toBuffer(responseContent), callback); + } + + protected void doEndSessionEndpoint(Request request, Response response, Callback callback) throws Exception + { + Fields parameters = Request.getParameters(request); + String idToken = parameters.getValue("id_token_hint"); + if (idToken == null) + { + Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "no id_token_hint"); + return; + } + + String logoutRedirect = parameters.getValue("post_logout_redirect_uri"); + if (logoutRedirect == null) + { + response.setStatus(HttpStatus.OK_200); + response.write(true, BufferUtil.toBuffer("logout success on end_session_endpoint"), callback); + return; + } + + loggedInUsers.decrement(); + Response.sendRedirect(request, response, callback, logoutRedirect); + } + + protected void doGetConfigServlet(Request request, Response response, Callback callback) throws IOException + { + String discoveryDocument = "{" + + "\"issuer\": \"" + provider + "\"," + + "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," + + "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," + + "\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," + + "}"; + + response.write(true, BufferUtil.toBuffer(discoveryDocument), callback); + } + + public static class User + { + private final String subject; + private final String name; + + public User(String name) + { + this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name); + } + + public User(String subject, String name) + { + this.subject = subject; + this.name = name; + } + + public String getName() + { + return name; + } + + public String getSubject() + { + return subject; + } + + public String getIdToken(String provider, String clientId, long duration) + { + long expiryTime = Instant.now().plusMillis(duration).getEpochSecond(); + return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime); + } + + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof User)) + return false; + return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name); + } + + @Override + public int hashCode() + { + return Objects.hash(subject, name); + } + } +} diff --git a/tests/pom.xml b/tests/pom.xml index 0ec5773e2db..b916216f008 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -15,6 +15,7 @@ jetty-jmh jetty-test-multipart jetty-test-session-common + jetty-test-util test-cross-context-dispatch test-distribution test-integration diff --git a/tests/test-distribution/test-ee10-distribution/pom.xml b/tests/test-distribution/test-ee10-distribution/pom.xml index dd0d2528018..37669d365f2 100644 --- a/tests/test-distribution/test-ee10-distribution/pom.xml +++ b/tests/test-distribution/test-ee10-distribution/pom.xml @@ -60,6 +60,11 @@ war test
+ + org.eclipse.jetty.tests + jetty-test-util + test + org.eclipse.jetty.tests jetty-testers diff --git a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/OpenIdTests.java b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/OpenIdTests.java index 0e06385efd3..d8b3cea3e5d 100644 --- a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/OpenIdTests.java +++ b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/OpenIdTests.java @@ -17,8 +17,8 @@ import java.nio.file.Path; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.ContentResponse; -import org.eclipse.jetty.ee10.tests.distribution.openid.OpenIdProvider; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.tests.OpenIdProvider; import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest; import org.eclipse.jetty.tests.testers.JettyHomeTester; import org.eclipse.jetty.tests.testers.Tester; diff --git a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/JwtEncoder.java b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/JwtEncoder.java deleted file mode 100644 index 91c5f9709e0..00000000000 --- a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/JwtEncoder.java +++ /dev/null @@ -1,53 +0,0 @@ -// -// ======================================================================== -// 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.tests.distribution.openid; - -import java.util.Base64; - -/** - * A basic JWT encoder for testing purposes. - */ -public class JwtEncoder -{ - private static final Base64.Encoder ENCODER = Base64.getUrlEncoder(); - private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}"; - private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow"; - - public static String encode(String idToken) - { - return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes())); - } - - private static String stripPadding(String paddedBase64) - { - return paddedBase64.split("=")[0]; - } - - /** - * Create a basic JWT for testing using argument supplied attributes. - */ - public static String createIdToken(String provider, String clientId, String subject, String name, long expiry) - { - return "{" + - "\"iss\": \"" + provider + "\"," + - "\"sub\": \"" + subject + "\"," + - "\"aud\": \"" + clientId + "\"," + - "\"exp\": " + expiry + "," + - "\"name\": \"" + name + "\"," + - "\"email\": \"" + name + "@example.com" + "\"" + - "}"; - } -} diff --git a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/OpenIdProvider.java b/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/OpenIdProvider.java deleted file mode 100644 index f1e2f27324b..00000000000 --- a/tests/test-distribution/test-ee10-distribution/src/test/java/org/eclipse/jetty/ee10/tests/distribution/openid/OpenIdProvider.java +++ /dev/null @@ -1,402 +0,0 @@ -// -// ======================================================================== -// 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.tests.distribution.openid; - -import java.io.IOException; -import java.io.PrintWriter; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import jakarta.servlet.ServletException; -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.security.openid.OpenIdConfiguration; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.util.component.ContainerLifeCycle; -import org.eclipse.jetty.util.statistic.CounterStatistic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OpenIdProvider extends ContainerLifeCycle -{ - private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class); - - private static final String CONFIG_PATH = "/.well-known/openid-configuration"; - private static final String AUTH_PATH = "/auth"; - private static final String TOKEN_PATH = "/token"; - private static final String END_SESSION_PATH = "/end_session"; - private final Map issuedAuthCodes = new HashMap<>(); - - protected final String clientId; - protected final String clientSecret; - protected final List redirectUris = new ArrayList<>(); - private final ServerConnector connector; - private final Server server; - private int port = 0; - private String provider; - private User preAuthedUser; - private final CounterStatistic loggedInUsers = new CounterStatistic(); - private long _idTokenDuration = Duration.ofSeconds(10).toMillis(); - - public static void main(String[] args) throws Exception - { - String clientId = "CLIENT_ID123"; - String clientSecret = "PASSWORD123"; - int port = 5771; - String redirectUri = "http://localhost:8080/j_security_check"; - - OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret); - openIdProvider.addRedirectUri(redirectUri); - openIdProvider.setPort(port); - openIdProvider.start(); - try - { - openIdProvider.join(); - } - finally - { - openIdProvider.stop(); - } - } - - public OpenIdProvider(String clientId, String clientSecret) - { - this.clientId = clientId; - this.clientSecret = clientSecret; - - server = new Server(); - connector = new ServerConnector(server); - server.addConnector(connector); - - ServletContextHandler contextHandler = new ServletContextHandler(); - contextHandler.setContextPath("/"); - contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH); - contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH); - contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH); - contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH); - server.setHandler(contextHandler); - - addBean(server); - } - - public void setIdTokenDuration(long duration) - { - _idTokenDuration = duration; - } - - public long getIdTokenDuration() - { - return _idTokenDuration; - } - - public void join() throws InterruptedException - { - server.join(); - } - - public OpenIdConfiguration getOpenIdConfiguration() - { - String provider = getProvider(); - String authEndpoint = provider + AUTH_PATH; - String tokenEndpoint = provider + TOKEN_PATH; - return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null); - } - - public CounterStatistic getLoggedInUsers() - { - return loggedInUsers; - } - - @Override - protected void doStart() throws Exception - { - connector.setPort(port); - super.doStart(); - provider = "http://localhost:" + connector.getLocalPort(); - } - - public void setPort(int port) - { - if (isStarted()) - throw new IllegalStateException(); - this.port = port; - } - - public void setUser(User user) - { - this.preAuthedUser = user; - } - - public String getProvider() - { - if (!isStarted() && port == 0) - throw new IllegalStateException("Port of OpenIdProvider not configured"); - return provider; - } - - public void addRedirectUri(String uri) - { - redirectUris.add(uri); - } - - public class AuthEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - if (!clientId.equals(req.getParameter("client_id"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id"); - return; - } - - String redirectUri = req.getParameter("redirect_uri"); - if (!redirectUris.contains(redirectUri)) - { - LOG.warn("invalid redirectUri {}", redirectUri); - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String scopeString = req.getParameter("scope"); - List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" ")); - if (!scopes.contains("openid")) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope"); - return; - } - - if (!"code".equals(req.getParameter("response_type"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - if (preAuthedUser == null) - { - PrintWriter writer = resp.getWriter(); - resp.setContentType("text/html"); - writer.println("

Login to OpenID Connect Provider

"); - writer.println("
"); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println("
"); - } - else - { - redirectUser(resp, preAuthedUser, redirectUri, state); - } - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String redirectUri = req.getParameter("redirectUri"); - if (!redirectUris.contains(redirectUri)) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - String username = req.getParameter("username"); - if (username == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username"); - return; - } - - User user = new User(username); - redirectUser(resp, user, redirectUri, state); - } - - public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException - { - String authCode = UUID.randomUUID().toString().replace("-", ""); - issuedAuthCodes.put(authCode, user); - - try - { - redirectUri += "?code=" + authCode + "&state=" + state; - response.sendRedirect(response.encodeRedirectURL(redirectUri)); - } - catch (Throwable t) - { - issuedAuthCodes.remove(authCode); - throw t; - } - } - } - - private class TokenEndpoint extends HttpServlet - { - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - String code = req.getParameter("code"); - - if (!clientId.equals(req.getParameter("client_id")) || - !clientSecret.equals(req.getParameter("client_secret")) || - !redirectUris.contains(req.getParameter("redirect_uri")) || - !"authorization_code".equals(req.getParameter("grant_type")) || - code == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request"); - return; - } - - User user = issuedAuthCodes.remove(code); - if (user == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code"); - return; - } - - String accessToken = "ABCDEFG"; - long accessTokenDuration = Duration.ofMinutes(10).toSeconds(); - String response = "{" + - "\"access_token\": \"" + accessToken + "\"," + - "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," + - "\"expires_in\": " + accessTokenDuration + "," + - "\"token_type\": \"Bearer\"" + - "}"; - - loggedInUsers.increment(); - resp.setContentType("text/plain"); - resp.getWriter().print(response); - } - } - - private class EndSessionEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - doPost(req, resp); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String idToken = req.getParameter("id_token_hint"); - if (idToken == null) - { - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint"); - return; - } - - String logoutRedirect = req.getParameter("post_logout_redirect_uri"); - if (logoutRedirect == null) - { - resp.setStatus(HttpServletResponse.SC_OK); - resp.getWriter().println("logout success on end_session_endpoint"); - return; - } - - loggedInUsers.decrement(); - resp.setContentType("text/plain"); - resp.sendRedirect(logoutRedirect); - } - } - - private class ConfigServlet extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String discoveryDocument = "{" + - "\"issuer\": \"" + provider + "\"," + - "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," + - "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," + - "\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," + - "}"; - - resp.getWriter().write(discoveryDocument); - } - } - - public static class User - { - private final String subject; - private final String name; - - public User(String name) - { - this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name); - } - - public User(String subject, String name) - { - this.subject = subject; - this.name = name; - } - - public String getName() - { - return name; - } - - public String getSubject() - { - return subject; - } - - public String getIdToken(String provider, String clientId, long duration) - { - long expiryTime = Instant.now().plusMillis(duration).getEpochSecond(); - return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime); - } - - @Override - public boolean equals(Object obj) - { - if (!(obj instanceof User)) - return false; - return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name); - } - - @Override - public int hashCode() - { - return Objects.hash(subject, name); - } - } -} diff --git a/tests/test-distribution/test-ee11-distribution/pom.xml b/tests/test-distribution/test-ee11-distribution/pom.xml index c315f8e994e..a00a3c20bf3 100644 --- a/tests/test-distribution/test-ee11-distribution/pom.xml +++ b/tests/test-distribution/test-ee11-distribution/pom.xml @@ -55,6 +55,11 @@ war test
+ + org.eclipse.jetty.tests + jetty-test-util + test + org.eclipse.jetty.tests jetty-testers diff --git a/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/OpenIdTests.java b/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/OpenIdTests.java index 67de446dd33..216003d492d 100644 --- a/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/OpenIdTests.java +++ b/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/OpenIdTests.java @@ -17,8 +17,8 @@ import java.nio.file.Path; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.ContentResponse; -import org.eclipse.jetty.ee11.tests.distribution.openid.OpenIdProvider; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.tests.OpenIdProvider; import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest; import org.eclipse.jetty.tests.testers.JettyHomeTester; import org.eclipse.jetty.tests.testers.Tester; diff --git a/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/JwtEncoder.java b/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/JwtEncoder.java deleted file mode 100644 index 6b47bfaed0f..00000000000 --- a/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/JwtEncoder.java +++ /dev/null @@ -1,53 +0,0 @@ -// -// ======================================================================== -// 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.ee11.tests.distribution.openid; - -import java.util.Base64; - -/** - * A basic JWT encoder for testing purposes. - */ -public class JwtEncoder -{ - private static final Base64.Encoder ENCODER = Base64.getUrlEncoder(); - private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}"; - private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow"; - - public static String encode(String idToken) - { - return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes())); - } - - private static String stripPadding(String paddedBase64) - { - return paddedBase64.split("=")[0]; - } - - /** - * Create a basic JWT for testing using argument supplied attributes. - */ - public static String createIdToken(String provider, String clientId, String subject, String name, long expiry) - { - return "{" + - "\"iss\": \"" + provider + "\"," + - "\"sub\": \"" + subject + "\"," + - "\"aud\": \"" + clientId + "\"," + - "\"exp\": " + expiry + "," + - "\"name\": \"" + name + "\"," + - "\"email\": \"" + name + "@example.com" + "\"" + - "}"; - } -} diff --git a/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/OpenIdProvider.java b/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/OpenIdProvider.java deleted file mode 100644 index 7df27ce1405..00000000000 --- a/tests/test-distribution/test-ee11-distribution/src/test/java/org/eclipse/jetty/ee11/tests/distribution/openid/OpenIdProvider.java +++ /dev/null @@ -1,402 +0,0 @@ -// -// ======================================================================== -// 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.ee11.tests.distribution.openid; - -import java.io.IOException; -import java.io.PrintWriter; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.ee11.servlet.ServletContextHandler; -import org.eclipse.jetty.ee11.servlet.ServletHolder; -import org.eclipse.jetty.security.openid.OpenIdConfiguration; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.util.component.ContainerLifeCycle; -import org.eclipse.jetty.util.statistic.CounterStatistic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OpenIdProvider extends ContainerLifeCycle -{ - private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class); - - private static final String CONFIG_PATH = "/.well-known/openid-configuration"; - private static final String AUTH_PATH = "/auth"; - private static final String TOKEN_PATH = "/token"; - private static final String END_SESSION_PATH = "/end_session"; - private final Map issuedAuthCodes = new HashMap<>(); - - protected final String clientId; - protected final String clientSecret; - protected final List redirectUris = new ArrayList<>(); - private final ServerConnector connector; - private final Server server; - private int port = 0; - private String provider; - private User preAuthedUser; - private final CounterStatistic loggedInUsers = new CounterStatistic(); - private long _idTokenDuration = Duration.ofSeconds(10).toMillis(); - - public static void main(String[] args) throws Exception - { - String clientId = "CLIENT_ID123"; - String clientSecret = "PASSWORD123"; - int port = 5771; - String redirectUri = "http://localhost:8080/j_security_check"; - - OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret); - openIdProvider.addRedirectUri(redirectUri); - openIdProvider.setPort(port); - openIdProvider.start(); - try - { - openIdProvider.join(); - } - finally - { - openIdProvider.stop(); - } - } - - public OpenIdProvider(String clientId, String clientSecret) - { - this.clientId = clientId; - this.clientSecret = clientSecret; - - server = new Server(); - connector = new ServerConnector(server); - server.addConnector(connector); - - ServletContextHandler contextHandler = new ServletContextHandler(); - contextHandler.setContextPath("/"); - contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH); - contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH); - contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH); - contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH); - server.setHandler(contextHandler); - - addBean(server); - } - - public void setIdTokenDuration(long duration) - { - _idTokenDuration = duration; - } - - public long getIdTokenDuration() - { - return _idTokenDuration; - } - - public void join() throws InterruptedException - { - server.join(); - } - - public OpenIdConfiguration getOpenIdConfiguration() - { - String provider = getProvider(); - String authEndpoint = provider + AUTH_PATH; - String tokenEndpoint = provider + TOKEN_PATH; - return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null); - } - - public CounterStatistic getLoggedInUsers() - { - return loggedInUsers; - } - - @Override - protected void doStart() throws Exception - { - connector.setPort(port); - super.doStart(); - provider = "http://localhost:" + connector.getLocalPort(); - } - - public void setPort(int port) - { - if (isStarted()) - throw new IllegalStateException(); - this.port = port; - } - - public void setUser(User user) - { - this.preAuthedUser = user; - } - - public String getProvider() - { - if (!isStarted() && port == 0) - throw new IllegalStateException("Port of OpenIdProvider not configured"); - return provider; - } - - public void addRedirectUri(String uri) - { - redirectUris.add(uri); - } - - public class AuthEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - if (!clientId.equals(req.getParameter("client_id"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id"); - return; - } - - String redirectUri = req.getParameter("redirect_uri"); - if (!redirectUris.contains(redirectUri)) - { - LOG.warn("invalid redirectUri {}", redirectUri); - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String scopeString = req.getParameter("scope"); - List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" ")); - if (!scopes.contains("openid")) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope"); - return; - } - - if (!"code".equals(req.getParameter("response_type"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - if (preAuthedUser == null) - { - PrintWriter writer = resp.getWriter(); - resp.setContentType("text/html"); - writer.println("

Login to OpenID Connect Provider

"); - writer.println("
"); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println("
"); - } - else - { - redirectUser(resp, preAuthedUser, redirectUri, state); - } - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String redirectUri = req.getParameter("redirectUri"); - if (!redirectUris.contains(redirectUri)) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - String username = req.getParameter("username"); - if (username == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username"); - return; - } - - User user = new User(username); - redirectUser(resp, user, redirectUri, state); - } - - public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException - { - String authCode = UUID.randomUUID().toString().replace("-", ""); - issuedAuthCodes.put(authCode, user); - - try - { - redirectUri += "?code=" + authCode + "&state=" + state; - response.sendRedirect(response.encodeRedirectURL(redirectUri)); - } - catch (Throwable t) - { - issuedAuthCodes.remove(authCode); - throw t; - } - } - } - - private class TokenEndpoint extends HttpServlet - { - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - String code = req.getParameter("code"); - - if (!clientId.equals(req.getParameter("client_id")) || - !clientSecret.equals(req.getParameter("client_secret")) || - !redirectUris.contains(req.getParameter("redirect_uri")) || - !"authorization_code".equals(req.getParameter("grant_type")) || - code == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request"); - return; - } - - User user = issuedAuthCodes.remove(code); - if (user == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code"); - return; - } - - String accessToken = "ABCDEFG"; - long accessTokenDuration = Duration.ofMinutes(10).toSeconds(); - String response = "{" + - "\"access_token\": \"" + accessToken + "\"," + - "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," + - "\"expires_in\": " + accessTokenDuration + "," + - "\"token_type\": \"Bearer\"" + - "}"; - - loggedInUsers.increment(); - resp.setContentType("text/plain"); - resp.getWriter().print(response); - } - } - - private class EndSessionEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - doPost(req, resp); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String idToken = req.getParameter("id_token_hint"); - if (idToken == null) - { - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint"); - return; - } - - String logoutRedirect = req.getParameter("post_logout_redirect_uri"); - if (logoutRedirect == null) - { - resp.setStatus(HttpServletResponse.SC_OK); - resp.getWriter().println("logout success on end_session_endpoint"); - return; - } - - loggedInUsers.decrement(); - resp.setContentType("text/plain"); - resp.sendRedirect(logoutRedirect); - } - } - - private class ConfigServlet extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String discoveryDocument = "{" + - "\"issuer\": \"" + provider + "\"," + - "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," + - "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," + - "\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," + - "}"; - - resp.getWriter().write(discoveryDocument); - } - } - - public static class User - { - private final String subject; - private final String name; - - public User(String name) - { - this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name); - } - - public User(String subject, String name) - { - this.subject = subject; - this.name = name; - } - - public String getName() - { - return name; - } - - public String getSubject() - { - return subject; - } - - public String getIdToken(String provider, String clientId, long duration) - { - long expiryTime = Instant.now().plusMillis(duration).getEpochSecond(); - return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime); - } - - @Override - public boolean equals(Object obj) - { - if (!(obj instanceof User)) - return false; - return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name); - } - - @Override - public int hashCode() - { - return Objects.hash(subject, name); - } - } -} diff --git a/tests/test-distribution/test-ee9-distribution/pom.xml b/tests/test-distribution/test-ee9-distribution/pom.xml index ce6164c980e..94a6acb2b33 100644 --- a/tests/test-distribution/test-ee9-distribution/pom.xml +++ b/tests/test-distribution/test-ee9-distribution/pom.xml @@ -56,6 +56,11 @@ war test
+ + org.eclipse.jetty.tests + jetty-test-util + test + org.eclipse.jetty.tests jetty-testers 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 78744609f36..cb7803bf787 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 @@ -17,8 +17,8 @@ import java.nio.file.Path; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.ContentResponse; -import org.eclipse.jetty.ee9.tests.distribution.openid.OpenIdProvider; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.tests.OpenIdProvider; import org.eclipse.jetty.tests.distribution.AbstractJettyHomeTest; import org.eclipse.jetty.tests.testers.JettyHomeTester; import org.eclipse.jetty.tests.testers.Tester; diff --git a/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/JwtEncoder.java b/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/JwtEncoder.java deleted file mode 100644 index 62b6f1dacfc..00000000000 --- a/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/JwtEncoder.java +++ /dev/null @@ -1,53 +0,0 @@ -// -// ======================================================================== -// 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.tests.distribution.openid; - -import java.util.Base64; - -/** - * A basic JWT encoder for testing purposes. - */ -public class JwtEncoder -{ - private static final Base64.Encoder ENCODER = Base64.getUrlEncoder(); - private static final String DEFAULT_HEADER = "{\"INFO\": \"this is not used or checked in our implementation\"}"; - private static final String DEFAULT_SIGNATURE = "we do not validate signature as we use the authorization code flow"; - - public static String encode(String idToken) - { - return stripPadding(ENCODER.encodeToString(DEFAULT_HEADER.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(idToken.getBytes())) + "." + - stripPadding(ENCODER.encodeToString(DEFAULT_SIGNATURE.getBytes())); - } - - private static String stripPadding(String paddedBase64) - { - return paddedBase64.split("=")[0]; - } - - /** - * Create a basic JWT for testing using argument supplied attributes. - */ - public static String createIdToken(String provider, String clientId, String subject, String name, long expiry) - { - return "{" + - "\"iss\": \"" + provider + "\"," + - "\"sub\": \"" + subject + "\"," + - "\"aud\": \"" + clientId + "\"," + - "\"exp\": " + expiry + "," + - "\"name\": \"" + name + "\"," + - "\"email\": \"" + name + "@example.com" + "\"" + - "}"; - } -} diff --git a/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/OpenIdProvider.java b/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/OpenIdProvider.java deleted file mode 100644 index 33ee1dc7dc9..00000000000 --- a/tests/test-distribution/test-ee9-distribution/src/test/java/org/eclipse/jetty/ee9/tests/distribution/openid/OpenIdProvider.java +++ /dev/null @@ -1,402 +0,0 @@ -// -// ======================================================================== -// 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.tests.distribution.openid; - -import java.io.IOException; -import java.io.PrintWriter; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; - -import jakarta.servlet.ServletException; -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.security.openid.OpenIdConfiguration; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.util.component.ContainerLifeCycle; -import org.eclipse.jetty.util.statistic.CounterStatistic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OpenIdProvider extends ContainerLifeCycle -{ - private static final Logger LOG = LoggerFactory.getLogger(OpenIdProvider.class); - - private static final String CONFIG_PATH = "/.well-known/openid-configuration"; - private static final String AUTH_PATH = "/auth"; - private static final String TOKEN_PATH = "/token"; - private static final String END_SESSION_PATH = "/end_session"; - private final Map issuedAuthCodes = new HashMap<>(); - - protected final String clientId; - protected final String clientSecret; - protected final List redirectUris = new ArrayList<>(); - private final ServerConnector connector; - private final Server server; - private int port = 0; - private String provider; - private User preAuthedUser; - private final CounterStatistic loggedInUsers = new CounterStatistic(); - private long _idTokenDuration = Duration.ofSeconds(10).toMillis(); - - public static void main(String[] args) throws Exception - { - String clientId = "CLIENT_ID123"; - String clientSecret = "PASSWORD123"; - int port = 5771; - String redirectUri = "http://localhost:8080/j_security_check"; - - OpenIdProvider openIdProvider = new OpenIdProvider(clientId, clientSecret); - openIdProvider.addRedirectUri(redirectUri); - openIdProvider.setPort(port); - openIdProvider.start(); - try - { - openIdProvider.join(); - } - finally - { - openIdProvider.stop(); - } - } - - public OpenIdProvider(String clientId, String clientSecret) - { - this.clientId = clientId; - this.clientSecret = clientSecret; - - server = new Server(); - connector = new ServerConnector(server); - server.addConnector(connector); - - ServletContextHandler contextHandler = new ServletContextHandler(); - contextHandler.setContextPath("/"); - contextHandler.addServlet(new ServletHolder(new ConfigServlet()), CONFIG_PATH); - contextHandler.addServlet(new ServletHolder(new AuthEndpoint()), AUTH_PATH); - contextHandler.addServlet(new ServletHolder(new TokenEndpoint()), TOKEN_PATH); - contextHandler.addServlet(new ServletHolder(new EndSessionEndpoint()), END_SESSION_PATH); - server.setHandler(contextHandler); - - addBean(server); - } - - public void setIdTokenDuration(long duration) - { - _idTokenDuration = duration; - } - - public long getIdTokenDuration() - { - return _idTokenDuration; - } - - public void join() throws InterruptedException - { - server.join(); - } - - public OpenIdConfiguration getOpenIdConfiguration() - { - String provider = getProvider(); - String authEndpoint = provider + AUTH_PATH; - String tokenEndpoint = provider + TOKEN_PATH; - return new OpenIdConfiguration(provider, authEndpoint, tokenEndpoint, clientId, clientSecret, null); - } - - public CounterStatistic getLoggedInUsers() - { - return loggedInUsers; - } - - @Override - protected void doStart() throws Exception - { - connector.setPort(port); - super.doStart(); - provider = "http://localhost:" + connector.getLocalPort(); - } - - public void setPort(int port) - { - if (isStarted()) - throw new IllegalStateException(); - this.port = port; - } - - public void setUser(User user) - { - this.preAuthedUser = user; - } - - public String getProvider() - { - if (!isStarted() && port == 0) - throw new IllegalStateException("Port of OpenIdProvider not configured"); - return provider; - } - - public void addRedirectUri(String uri) - { - redirectUris.add(uri); - } - - public class AuthEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - if (!clientId.equals(req.getParameter("client_id"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id"); - return; - } - - String redirectUri = req.getParameter("redirect_uri"); - if (!redirectUris.contains(redirectUri)) - { - LOG.warn("invalid redirectUri {}", redirectUri); - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String scopeString = req.getParameter("scope"); - List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(scopeString.split(" ")); - if (!scopes.contains("openid")) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope"); - return; - } - - if (!"code".equals(req.getParameter("response_type"))) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - if (preAuthedUser == null) - { - PrintWriter writer = resp.getWriter(); - resp.setContentType("text/html"); - writer.println("

Login to OpenID Connect Provider

"); - writer.println("
"); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println(""); - writer.println("
"); - } - else - { - redirectUser(resp, preAuthedUser, redirectUri, state); - } - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String redirectUri = req.getParameter("redirectUri"); - if (!redirectUris.contains(redirectUri)) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); - return; - } - - String state = req.getParameter("state"); - if (state == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); - return; - } - - String username = req.getParameter("username"); - if (username == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no username"); - return; - } - - User user = new User(username); - redirectUser(resp, user, redirectUri, state); - } - - public void redirectUser(HttpServletResponse response, User user, String redirectUri, String state) throws IOException - { - String authCode = UUID.randomUUID().toString().replace("-", ""); - issuedAuthCodes.put(authCode, user); - - try - { - redirectUri += "?code=" + authCode + "&state=" + state; - response.sendRedirect(response.encodeRedirectURL(redirectUri)); - } - catch (Throwable t) - { - issuedAuthCodes.remove(authCode); - throw t; - } - } - } - - private class TokenEndpoint extends HttpServlet - { - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - String code = req.getParameter("code"); - - if (!clientId.equals(req.getParameter("client_id")) || - !clientSecret.equals(req.getParameter("client_secret")) || - !redirectUris.contains(req.getParameter("redirect_uri")) || - !"authorization_code".equals(req.getParameter("grant_type")) || - code == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request"); - return; - } - - User user = issuedAuthCodes.remove(code); - if (user == null) - { - resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code"); - return; - } - - String accessToken = "ABCDEFG"; - long accessTokenDuration = Duration.ofMinutes(10).toSeconds(); - String response = "{" + - "\"access_token\": \"" + accessToken + "\"," + - "\"id_token\": \"" + JwtEncoder.encode(user.getIdToken(provider, clientId, _idTokenDuration)) + "\"," + - "\"expires_in\": " + accessTokenDuration + "," + - "\"token_type\": \"Bearer\"" + - "}"; - - loggedInUsers.increment(); - resp.setContentType("text/plain"); - resp.getWriter().print(response); - } - } - - private class EndSessionEndpoint extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - doPost(req, resp); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String idToken = req.getParameter("id_token_hint"); - if (idToken == null) - { - resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "no id_token_hint"); - return; - } - - String logoutRedirect = req.getParameter("post_logout_redirect_uri"); - if (logoutRedirect == null) - { - resp.setStatus(HttpServletResponse.SC_OK); - resp.getWriter().println("logout success on end_session_endpoint"); - return; - } - - loggedInUsers.decrement(); - resp.setContentType("text/plain"); - resp.sendRedirect(logoutRedirect); - } - } - - private class ConfigServlet extends HttpServlet - { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String discoveryDocument = "{" + - "\"issuer\": \"" + provider + "\"," + - "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," + - "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," + - "\"end_session_endpoint\": \"" + provider + END_SESSION_PATH + "\"," + - "}"; - - resp.getWriter().write(discoveryDocument); - } - } - - public static class User - { - private final String subject; - private final String name; - - public User(String name) - { - this(UUID.nameUUIDFromBytes(name.getBytes()).toString(), name); - } - - public User(String subject, String name) - { - this.subject = subject; - this.name = name; - } - - public String getName() - { - return name; - } - - public String getSubject() - { - return subject; - } - - public String getIdToken(String provider, String clientId, long duration) - { - long expiryTime = Instant.now().plusMillis(duration).getEpochSecond(); - return JwtEncoder.createIdToken(provider, clientId, subject, name, expiryTime); - } - - @Override - public boolean equals(Object obj) - { - if (!(obj instanceof User)) - return false; - return Objects.equals(subject, ((User)obj).subject) && Objects.equals(name, ((User)obj).name); - } - - @Override - public int hashCode() - { - return Objects.hash(subject, name); - } - } -} From e31360dd7f1e50e74240488fb505ee80f3e25235 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 27 Aug 2024 14:39:47 +1000 Subject: [PATCH 5/9] rename jetty-test-util to jetty-test-common Signed-off-by: Lachlan Roberts --- jetty-ee8/jetty-ee8-openid/pom.xml | 2 +- jetty-ee9/jetty-ee9-openid/pom.xml | 2 +- jetty-integrations/jetty-openid/pom.xml | 2 +- pom.xml | 2 +- tests/{jetty-test-util => jetty-test-common}/pom.xml | 2 +- .../src/main/java/org/eclipse/jetty/tests/JwtEncoder.java | 0 .../src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java | 0 tests/pom.xml | 2 +- tests/test-distribution/test-ee10-distribution/pom.xml | 2 +- tests/test-distribution/test-ee11-distribution/pom.xml | 2 +- tests/test-distribution/test-ee9-distribution/pom.xml | 2 +- 11 files changed, 9 insertions(+), 9 deletions(-) rename tests/{jetty-test-util => jetty-test-common}/pom.xml (95%) rename tests/{jetty-test-util => jetty-test-common}/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java (100%) rename tests/{jetty-test-util => jetty-test-common}/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java (100%) diff --git a/jetty-ee8/jetty-ee8-openid/pom.xml b/jetty-ee8/jetty-ee8-openid/pom.xml index 9474870eb21..c6af6f29179 100644 --- a/jetty-ee8/jetty-ee8-openid/pom.xml +++ b/jetty-ee8/jetty-ee8-openid/pom.xml @@ -54,7 +54,7 @@
org.eclipse.jetty.tests - jetty-test-util + jetty-test-common test diff --git a/jetty-ee9/jetty-ee9-openid/pom.xml b/jetty-ee9/jetty-ee9-openid/pom.xml index 8c92210726b..3dd45710155 100644 --- a/jetty-ee9/jetty-ee9-openid/pom.xml +++ b/jetty-ee9/jetty-ee9-openid/pom.xml @@ -57,7 +57,7 @@ org.eclipse.jetty.tests - jetty-test-util + jetty-test-common test diff --git a/jetty-integrations/jetty-openid/pom.xml b/jetty-integrations/jetty-openid/pom.xml index f67543aa805..ffcf4750e3a 100644 --- a/jetty-integrations/jetty-openid/pom.xml +++ b/jetty-integrations/jetty-openid/pom.xml @@ -49,7 +49,7 @@ org.eclipse.jetty.tests - jetty-test-util + jetty-test-common test diff --git a/pom.xml b/pom.xml index 68311850742..5a386d0f036 100644 --- a/pom.xml +++ b/pom.xml @@ -1062,7 +1062,7 @@ org.eclipse.jetty.tests - jetty-test-util + jetty-test-common ${project.version} diff --git a/tests/jetty-test-util/pom.xml b/tests/jetty-test-common/pom.xml similarity index 95% rename from tests/jetty-test-util/pom.xml rename to tests/jetty-test-common/pom.xml index cab19941710..d820ada4c30 100644 --- a/tests/jetty-test-util/pom.xml +++ b/tests/jetty-test-common/pom.xml @@ -7,7 +7,7 @@ tests 12.1.0-SNAPSHOT - jetty-test-util + jetty-test-common jar Tests :: Test Utilities diff --git a/tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java b/tests/jetty-test-common/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java similarity index 100% rename from tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java rename to tests/jetty-test-common/src/main/java/org/eclipse/jetty/tests/JwtEncoder.java diff --git a/tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java b/tests/jetty-test-common/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java similarity index 100% rename from tests/jetty-test-util/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java rename to tests/jetty-test-common/src/main/java/org/eclipse/jetty/tests/OpenIdProvider.java diff --git a/tests/pom.xml b/tests/pom.xml index b916216f008..022baf529be 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -15,7 +15,7 @@ jetty-jmh jetty-test-multipart jetty-test-session-common - jetty-test-util + jetty-test-common test-cross-context-dispatch test-distribution test-integration diff --git a/tests/test-distribution/test-ee10-distribution/pom.xml b/tests/test-distribution/test-ee10-distribution/pom.xml index 37669d365f2..11f3cedaf70 100644 --- a/tests/test-distribution/test-ee10-distribution/pom.xml +++ b/tests/test-distribution/test-ee10-distribution/pom.xml @@ -62,7 +62,7 @@ org.eclipse.jetty.tests - jetty-test-util + jetty-test-common test diff --git a/tests/test-distribution/test-ee11-distribution/pom.xml b/tests/test-distribution/test-ee11-distribution/pom.xml index a00a3c20bf3..1b91c48eab8 100644 --- a/tests/test-distribution/test-ee11-distribution/pom.xml +++ b/tests/test-distribution/test-ee11-distribution/pom.xml @@ -57,7 +57,7 @@ org.eclipse.jetty.tests - jetty-test-util + jetty-test-common test diff --git a/tests/test-distribution/test-ee9-distribution/pom.xml b/tests/test-distribution/test-ee9-distribution/pom.xml index 94a6acb2b33..15ac6a3ce38 100644 --- a/tests/test-distribution/test-ee9-distribution/pom.xml +++ b/tests/test-distribution/test-ee9-distribution/pom.xml @@ -58,7 +58,7 @@ org.eclipse.jetty.tests - jetty-test-util + jetty-test-common test From 0420e926a1fbfd04bdff99ec80acba95a09d8977 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 27 Aug 2024 11:03:48 +0300 Subject: [PATCH 6/9] Fixes #12171 - QoSHandler does not resume on a virtual thread. (#12174) Now QoSHandler resumes requests using Request.getComponents().getExecutor(). This Executor is configured to be the virtual thread executor, if present, otherwise the Server Executor. Removed warn() from VirtualThreads.isVirtualThread(), as it was too verbose. Signed-off-by: Simone Bordet --- .../org/eclipse/jetty/server/Components.java | 33 ++++++-- .../jetty/server/handler/QoSHandler.java | 9 ++- .../server/internal/HttpChannelState.java | 54 ++++++++++++- .../org/eclipse/jetty/server/ServerTest.java | 2 +- .../jetty/server/handler/QoSHandlerTest.java | 77 ++++++++++++++++++- .../eclipse/jetty/util/VirtualThreads.java | 1 - 6 files changed, 164 insertions(+), 12 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Components.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Components.java index b6e32ea937f..7151c38fdbc 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Components.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Components.java @@ -13,34 +13,55 @@ package org.eclipse.jetty.server; +import java.util.concurrent.Executor; + import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.util.thread.ThreadPool; /** - * Common components made available via a {@link Request} + * Common components made available via a {@link Request}. */ public interface Components { + /** + * @return the {@link ByteBufferPool} associated with the {@link Request} + */ ByteBufferPool getByteBufferPool(); + /** + * @return the {@link Scheduler} associated with the {@link Request} + */ Scheduler getScheduler(); + /** + * @return the {@link ThreadPool} associated with the {@link Request} + * @deprecated use {@link #getExecutor()} instead + */ + @Deprecated(since = "12.0.13", forRemoval = true) ThreadPool getThreadPool(); /** - * A Map which can be used as a cache for object (e.g. Cookie cache). - * The cache will have a life cycle limited by the connection, i.e. no cache map will live + * @return the {@link Executor} associated with the {@link Request} + */ + default Executor getExecutor() + { + return getThreadPool(); + } + + /** + *

A map-like object that can be used as a cache (for example, as a cookie cache).

+ *

The cache will have a life cycle limited by the connection, i.e. no cache map will live * longer that the connection associated with it. However, a cache may have a shorter life * than a connection (e.g. it may be discarded for implementation reasons). A cache map is * guaranteed to be given to only a single request concurrently (scoped by * {@link org.eclipse.jetty.server.internal.HttpChannelState}), so objects saved there do not * need to be made safe from access by simultaneous request. - * If the connection is known to be none-persistent then the cache may be a noop - * cache and discard all items set on it. + * If the connection is known to be non-persistent then the cache may be a noop + * cache and discard all items set on it.

* - * @return A Map, which may be an empty map that discards all items. + * @return A map-like object, which may be an empty implementation that discards all items. */ Attributes getCache(); } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/QoSHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/QoSHandler.java index 69722c13d40..9fef7b060d7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/QoSHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/QoSHandler.java @@ -397,13 +397,18 @@ public class QoSHandler extends ConditionalHandler.Abstract if (LOG.isDebugEnabled()) LOG.debug("{} resuming {}", this, entry.request); // Always dispatch to avoid StackOverflowError. - getServer().getThreadPool().execute(entry); + execute(entry.request, entry); return true; } } return false; } + private void execute(Request request, Runnable task) + { + request.getComponents().getExecutor().execute(task); + } + private class Entry implements CyclicTimeouts.Expirable, Runnable { private final Request request; @@ -458,7 +463,7 @@ public class QoSHandler extends ConditionalHandler.Abstract } if (removed) - failSuspended(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, new TimeoutException()); + execute(request, () -> failSuspended(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, new TimeoutException())); } @Override 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 af8ca07824e..f3455d38095 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 @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.LongAdder; import java.util.function.Consumer; @@ -63,6 +64,7 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.ExceptionUtil; import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.VirtualThreads; import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.Invocable; import org.eclipse.jetty.util.thread.Scheduler; @@ -231,7 +233,18 @@ public class HttpChannelState implements HttpChannel, Components @Override public ThreadPool getThreadPool() { - return getServer().getThreadPool(); + Executor executor = getExecutor(); + if (executor instanceof ThreadPool threadPool) + return threadPool; + return new ThreadPoolWrapper(executor); + } + + @Override + public Executor getExecutor() + { + Executor executor = getServer().getThreadPool(); + Executor virtualExecutor = VirtualThreads.getVirtualThreadsExecutor(executor); + return virtualExecutor != null ? virtualExecutor : executor; } @Override @@ -1948,4 +1961,43 @@ public class HttpChannelState implements HttpChannel, Components throw t; } } + + private static class ThreadPoolWrapper implements ThreadPool + { + private final Executor _executor; + + private ThreadPoolWrapper(Executor executor) + { + _executor = executor; + } + + @Override + public void execute(Runnable command) + { + _executor.execute(command); + } + + @Override + public void join() + { + } + + @Override + public int getThreads() + { + return 0; + } + + @Override + public int getIdleThreads() + { + return 0; + } + + @Override + public boolean isLowOnThreads() + { + return false; + } + } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ServerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ServerTest.java index dcca3eb0375..f3b7724420d 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ServerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ServerTest.java @@ -91,7 +91,7 @@ public class ServerTest { Runnable after = _afterHandle.getAndSet(null); if (after != null) - getThreadPool().execute(after); + getExecutor().execute(after); } }; } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java index 88544658be6..1083dbe6d91 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java @@ -13,6 +13,9 @@ package org.eclipse.jetty.server.handler; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -28,10 +31,15 @@ import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.VirtualThreads; import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -50,7 +58,8 @@ public class QoSHandlerTest private void start(QoSHandler qosHandler) throws Exception { - server = new Server(); + if (server == null) + server = new Server(); connector = new LocalConnector(server); server.addConnector(connector); server.setHandler(qosHandler); @@ -483,4 +492,70 @@ public class QoSHandlerTest } }); } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + public void testRequestInVirtualThreadIsResumedInVirtualThread() throws Exception + { + QoSHandler qosHandler = new QoSHandler(); + qosHandler.setMaxRequestCount(1); + List callbacks = new ArrayList<>(); + qosHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + response.setStatus(VirtualThreads.isVirtualThread() ? HttpStatus.OK_200 : HttpStatus.NOT_ACCEPTABLE_406); + // Save the callback but do not succeed it yet. + callbacks.add(callback); + return true; + } + }); + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("st"); + serverThreads.setVirtualThreadsExecutor(VirtualThreads.getNamedVirtualThreadsExecutor("vst")); + server = new Server(serverThreads); + ServerConnector networkConnector = new ServerConnector(server, 1, 1); + server.addConnector(networkConnector); + start(qosHandler); + + // Send the first request that will not be completed yet. + try (SocketChannel client1 = SocketChannel.open(new InetSocketAddress("localhost", networkConnector.getLocalPort()))) + { + client1.write(StandardCharsets.UTF_8.encode(""" + GET /first HTTP/1.1 + Host: localhost + + """)); + // Wait that the request arrives at the server. + await().atMost(5, TimeUnit.SECONDS).until(callbacks::size, is(1)); + + // Send the second request, it should be suspended by QoSHandler. + try (SocketChannel client2 = SocketChannel.open(new InetSocketAddress("localhost", networkConnector.getLocalPort()))) + { + client2.write(StandardCharsets.UTF_8.encode(""" + GET /second HTTP/1.1 + Host: localhost + + """)); + // Wait for the second request to be suspended. + await().atMost(5, TimeUnit.SECONDS).until(qosHandler::getSuspendedRequestCount, is(1)); + + // Finish the first request, so that the second can be resumed. + callbacks.remove(0).succeeded(); + client1.socket().setSoTimeout(5000); + HttpTester.Response response1 = HttpTester.parseResponse(client1); + assertEquals(HttpStatus.OK_200, response1.getStatus()); + + // Wait for the second request to arrive to the server. + await().atMost(5, TimeUnit.SECONDS).until(callbacks::size, is(1)); + + // Finish the second request. + callbacks.remove(0).succeeded(); + client2.socket().setSoTimeout(5000); + HttpTester.Response response2 = HttpTester.parseResponse(client2); + assertEquals(HttpStatus.OK_200, response2.getStatus()); + } + } + } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/VirtualThreads.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/VirtualThreads.java index 08ffd1f000a..a3bddae9b41 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/VirtualThreads.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/VirtualThreads.java @@ -101,7 +101,6 @@ public class VirtualThreads } catch (Throwable x) { - warn(); return false; } } From d2696dcdf3cd5917ba0b4fba02612f848ae14c2c Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 28 Aug 2024 08:34:01 +1000 Subject: [PATCH 7/9] remove disabled tags --- .../java/org/eclipse/jetty/server/handler/QoSHandlerTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java index a3a39d05105..1083dbe6d91 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/QoSHandlerTest.java @@ -37,7 +37,6 @@ import org.eclipse.jetty.util.VirtualThreads; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledForJreRange; import org.junit.jupiter.api.condition.JRE; @@ -85,7 +84,6 @@ public class QoSHandlerTest } @Test - @Disabled // TODO fix in #12171 public void testRequestIsSuspendedAndResumed() throws Exception { int maxRequests = 2; @@ -153,7 +151,6 @@ public class QoSHandlerTest } @Test - @Disabled // TODO fix in #12171 public void testSuspendedRequestTimesOut() throws Exception { int maxRequests = 1; From edbd21bdf08143ab57f66184cedf3e4c93015d70 Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Wed, 28 Aug 2024 18:09:15 +1000 Subject: [PATCH 8/9] Issue #11322 Upgrade to mongodb-driver-sync 5.x (#11681) Upgrade to mongodb driver async 5.x --------- Signed-off-by: Olivier Lamy --- jetty-integrations/jetty-nosql/pom.xml | 13 +- .../config/modules/session-store-mongo.mod | 8 +- .../src/main/java/module-info.java | 4 +- .../jetty/nosql/NoSqlSessionDataStore.java | 2 +- .../nosql/mongodb/MongoSessionDataStore.java | 202 ++++++++---------- .../mongodb/MongoSessionDataStoreFactory.java | 14 +- .../jetty/nosql/mongodb/MongoUtils.java | 26 ++- pom.xml | 9 +- tests/jetty-test-session-common/pom.xml | 2 +- .../session/test/tools/MongoTestHelper.java | 88 ++++---- .../MongodbSessionDistributionTests.java | 2 +- 11 files changed, 190 insertions(+), 180 deletions(-) diff --git a/jetty-integrations/jetty-nosql/pom.xml b/jetty-integrations/jetty-nosql/pom.xml index cef8dfdbd2d..a09cc643cea 100644 --- a/jetty-integrations/jetty-nosql/pom.xml +++ b/jetty-integrations/jetty-nosql/pom.xml @@ -23,10 +23,21 @@
org.mongodb - mongo-java-driver + bson ${mongodb.version} compile + + org.mongodb + mongodb-driver-core + ${mongodb.version} + compile + + + org.mongodb + mongodb-driver-sync + compile + org.slf4j slf4j-api diff --git a/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod b/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod index ea6e8f7b1d9..7d8123f936e 100644 --- a/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod +++ b/jetty-integrations/jetty-nosql/src/main/config/modules/session-store-mongo.mod @@ -14,11 +14,15 @@ sessions sessions/mongo/${connection-type} [files] -maven://org.mongodb/mongo-java-driver/${mongodb.version}|lib/nosql/mongo-java-driver-${mongodb.version}.jar +maven://org.mongodb/mongodb-driver-sync/${mongodb.version}|lib/nosql/mongodb-driver-sync-${mongodb.version}.jar +maven://org.mongodb/mongodb-driver-core/${mongodb.version}|lib/nosql/mongodb-driver-core-${mongodb.version}.jar +maven://org.mongodb/bson/${mongodb.version}|lib/nosql/bson-${mongodb.version}.jar [lib] lib/jetty-nosql-${jetty.version}.jar -lib/nosql/mongo-java-driver-${mongodb.version}.jar +lib/nosql/mongodb-driver-sync-${mongodb.version}.jar +lib/nosql/mongodb-driver-core-${mongodb.version}.jar +lib/nosql/bson-${mongodb.version}.jar [license] The java driver for the MongoDB document-based database system is hosted on GitHub and released under the Apache 2.0 license. diff --git a/jetty-integrations/jetty-nosql/src/main/java/module-info.java b/jetty-integrations/jetty-nosql/src/main/java/module-info.java index 2ef01b87a2b..0729e1c4a5d 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/module-info.java +++ b/jetty-integrations/jetty-nosql/src/main/java/module-info.java @@ -13,7 +13,9 @@ module org.eclipse.jetty.nosql { - requires transitive mongo.java.driver; + requires transitive org.mongodb.driver.core; + requires transitive org.mongodb.driver.sync.client; + requires transitive org.mongodb.bson; requires transitive org.eclipse.jetty.session; exports org.eclipse.jetty.nosql; diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java index 211812c75ba..8e92f1b5945 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionDataStore.java @@ -62,7 +62,7 @@ public abstract class NoSqlSessionDataStore extends ObjectStreamSessionDataStore public Set getAllAttributeNames() { - return new HashSet(_attributes.keySet()); + return new HashSet<>(_attributes.keySet()); } } diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java index 3395f9bb1f7..9d00ef0cdc2 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.java @@ -15,22 +15,29 @@ package org.eclipse.jetty.nosql.mongodb; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.util.HashMap; -import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; -import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObjectBuilder; -import com.mongodb.DBCollection; -import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.MongoException; -import com.mongodb.WriteConcern; -import com.mongodb.WriteResult; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexModel; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.Projections; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.UpdateResult; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.Binary; import org.eclipse.jetty.nosql.NoSqlSessionDataStore; import org.eclipse.jetty.session.SessionContext; import org.eclipse.jetty.session.SessionData; @@ -155,15 +162,15 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore /** * Access to MongoDB */ - private DBCollection _dbSessions; + private MongoCollection _dbSessions; - public void setDBCollection(DBCollection collection) + public void setDBCollection(MongoCollection collection) { _dbSessions = collection; } @ManagedAttribute(value = "DBCollection", readonly = true) - public DBCollection getDBCollection() + public MongoCollection getDBCollection() { return _dbSessions; } @@ -171,7 +178,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore @Override public SessionData doLoad(String id) throws Exception { - DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id)); + Document sessionDocument = _dbSessions.find(Filters.eq(__ID, id)).first(); try { @@ -191,7 +198,8 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore Object version = MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__VERSION)); Long lastSaved = (Long)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__LASTSAVED)); String lastNode = (String)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__LASTNODE)); - byte[] attributes = (byte[])MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__ATTRIBUTES)); + Binary binary = ((Binary)MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__ATTRIBUTES))); + byte[] attributes = binary == null ? null : binary.getData(); Long created = (Long)sessionDocument.get(__CREATED); Long accessed = (Long)sessionDocument.get(__ACCESSED); @@ -202,7 +210,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore NoSqlSessionData data = null; // get the session for the context - DBObject sessionSubDocumentForContext = (DBObject)MongoUtils.getNestedValue(sessionDocument, getContextField()); + Document sessionSubDocumentForContext = (Document)MongoUtils.getNestedValue(sessionDocument, getContextField()); if (LOG.isDebugEnabled()) LOG.debug("attrs {}", sessionSubDocumentForContext); @@ -239,9 +247,9 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore else { //attributes have special serialized format - try (ByteArrayInputStream bais = new ByteArrayInputStream(attributes);) + try (ByteArrayInputStream bais = new ByteArrayInputStream(attributes)) { - deserializeAttributes(data, bais); + deserializeAttributes(data, bais); } } } @@ -269,17 +277,17 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore * Check if the session exists and if it does remove the context * associated with this session */ - BasicDBObject mongoKey = new BasicDBObject(__ID, id); + Bson filterId = Filters.eq(__ID, id); - DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id)); + Document sessionDocument = _dbSessions.find(filterId).first(); if (sessionDocument != null) { - DBObject c = (DBObject)MongoUtils.getNestedValue(sessionDocument, __CONTEXT); + Document c = (Document)MongoUtils.getNestedValue(sessionDocument, __CONTEXT); if (c == null) { //delete whole doc - _dbSessions.remove(mongoKey, WriteConcern.SAFE); + _dbSessions.deleteOne(filterId); return false; } @@ -287,14 +295,14 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore if (contexts.isEmpty()) { //delete whole doc - _dbSessions.remove(mongoKey, WriteConcern.SAFE); + _dbSessions.deleteOne(filterId); return false; } if (contexts.size() == 1 && contexts.iterator().next().equals(getCanonicalContextId())) { //delete whole doc - _dbSessions.remove(new BasicDBObject(__ID, id), WriteConcern.SAFE); + _dbSessions.deleteOne(filterId); return true; } @@ -303,7 +311,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore BasicDBObject unsets = new BasicDBObject(); unsets.put(getContextField(), 1); remove.put("$unset", unsets); - _dbSessions.update(mongoKey, remove, false, false, WriteConcern.SAFE); + _dbSessions.updateOne(filterId, remove); return true; } else @@ -315,12 +323,9 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore @Override public boolean doExists(String id) throws Exception { - DBObject fields = new BasicDBObject(); - fields.put(__EXPIRY, 1); - fields.put(__VALID, 1); - fields.put(getContextSubfield(__VERSION), 1); - - DBObject sessionDocument = _dbSessions.findOne(new BasicDBObject(__ID, id), fields); + Bson projection = Projections.fields(Projections.include(__ID, __VALID, __EXPIRY, __VERSION, getContextField()), Projections.excludeId()); + Bson filterId = Filters.eq(__ID, id); + Document sessionDocument = _dbSessions.find(filterId).projection(projection).first(); if (sessionDocument == null) return false; //doesn't exist @@ -332,48 +337,33 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore Long expiry = (Long)sessionDocument.get(__EXPIRY); //expired? - if (expiry.longValue() > 0 && expiry.longValue() < System.currentTimeMillis()) + if (expiry != null && expiry > 0 && expiry < System.currentTimeMillis()) return false; //it's expired //does it exist for this context? Object version = MongoUtils.getNestedValue(sessionDocument, getContextSubfield(__VERSION)); - if (version == null) - return false; - - return true; + return version != null; } @Override public Set doCheckExpired(Set candidates, long time) { - Set expiredSessions = new HashSet<>(); //firstly ask mongo to verify if these candidate ids have expired - all of //these candidates will be for our node - BasicDBObject query = new BasicDBObject(); - query.append(__ID, new BasicDBObject("$in", candidates)); - query.append(__EXPIRY, new BasicDBObject("$gt", 0).append("$lte", time)); - - DBCursor verifiedExpiredSessions = null; - try - { - verifiedExpiredSessions = _dbSessions.find(query, new BasicDBObject(__ID, 1)); - for (DBObject session : verifiedExpiredSessions) - { - String id = (String)session.get(__ID); - if (LOG.isDebugEnabled()) - LOG.debug("{} Mongo confirmed expired session {}", _context, id); - expiredSessions.add(id); - } - } - finally - { - if (verifiedExpiredSessions != null) - verifiedExpiredSessions.close(); - } - + Bson query = Filters.and( + Filters.in(__ID, candidates), + Filters.gt(__EXPIRY, 0), + Filters.lte(__EXPIRY, time)); - //check through sessions that were candidates, but not found as expired. + + FindIterable verifiedExpiredSessions = _dbSessions.find(query); // , new BasicDBObject(__ID, 1) + Set expiredSessions = + StreamSupport.stream(verifiedExpiredSessions.spliterator(), false) + .map(document -> document.getString(__ID)) + .collect(Collectors.toSet()); + + //check through sessions that were candidates, but not found as expired. //they may no longer be persisted, in which case they are treated as expired. for (String c:candidates) { @@ -398,37 +388,17 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore { // now ask mongo to find sessions for this context, last managed by any // node, that expired before timeLimit - Set expiredSessions = new HashSet<>(); + Bson query = Filters.and( + Filters.gt(__EXPIRY, 0), + Filters.lte(__EXPIRY, timeLimit) + ); - BasicDBObject query = new BasicDBObject(); - BasicDBObject gt = new BasicDBObject(__EXPIRY, new BasicDBObject("$gt", 0)); - BasicDBObject lt = new BasicDBObject(__EXPIRY, new BasicDBObject("$lte", timeLimit)); - BasicDBList list = new BasicDBList(); - list.add(gt); - list.add(lt); - query.append("$and", list); - - DBCursor oldExpiredSessions = null; - try - { - BasicDBObject bo = new BasicDBObject(__ID, 1); - bo.append(__EXPIRY, 1); - - oldExpiredSessions = _dbSessions.find(query, bo); - for (DBObject session : oldExpiredSessions) - { - String id = (String)session.get(__ID); - - //TODO we should verify if there is a session for my context, not any context - expiredSessions.add(id); - } - } - finally - { - if (oldExpiredSessions != null) - oldExpiredSessions.close(); - } + //TODO we should verify if there is a session for my context, not any context + FindIterable documents = _dbSessions.find(query); + Set expiredSessions = StreamSupport.stream(documents.spliterator(), false) + .map(document -> document.getString(__ID)) + .collect(Collectors.toSet()); return expiredSessions; } @@ -438,9 +408,11 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore //Delete all session documents where the expiry time (which is always the most //up-to-date expiry of all contexts sharing that session id) has already past as //at the timeLimit. - BasicDBObject query = new BasicDBObject(); - query.append(__EXPIRY, new BasicDBObject("$gt", 0).append("$lte", timeLimit)); - _dbSessions.remove(query, WriteConcern.SAFE); + Bson query = Filters.and( + Filters.gt(__EXPIRY, 0), + Filters.lte(__EXPIRY, timeLimit) + ); + _dbSessions.deleteMany(query); } /** @@ -458,8 +430,7 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore public void doStore(String id, SessionData data, long lastSaveTime) throws Exception { // Form query for upsert - final BasicDBObject key = new BasicDBObject(__ID, id); - + Bson key = Filters.eq(__ID, id);; // Form updates BasicDBObject update = new BasicDBObject(); boolean upsert = false; @@ -487,12 +458,13 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore sets.put(getContextSubfield(__LASTNODE), data.getLastNode()); version = ((Number)version).longValue() + 1L; ((NoSqlSessionData)data).setVersion(version); - update.put("$inc", _version1); + // what is this?? this field is used no where... + //sets.put("$inc", _version1); //if max idle time and/or expiry is smaller for this context, then choose that for the whole session doc BasicDBObject fields = new BasicDBObject(); fields.append(__MAX_IDLE, true); fields.append(__EXPIRY, true); - DBObject o = _dbSessions.findOne(new BasicDBObject("id", id), fields); + Document o = _dbSessions.find(key).first(); if (o != null) { Long tmpLong = (Long)o.get(__MAX_IDLE); @@ -516,37 +488,39 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore try (ByteArrayOutputStream baos = new ByteArrayOutputStream();) { serializeAttributes(data, baos); - sets.put(getContextSubfield(__ATTRIBUTES), baos.toByteArray()); + Binary binary = new Binary(baos.toByteArray()); + sets.put(getContextSubfield(__ATTRIBUTES), binary); } // Do the upsert if (!sets.isEmpty()) update.put("$set", sets); - WriteResult res = _dbSessions.update(key, update, upsert, false, WriteConcern.SAFE); + UpdateResult res = _dbSessions.updateOne(key, update, new UpdateOptions().upsert(upsert)); if (LOG.isDebugEnabled()) LOG.debug("Save:db.sessions.update( {}, {},{} )", key, update, res); } protected void ensureIndexes() throws MongoException { - _version1 = new BasicDBObject(getContextSubfield(__VERSION), 1); - DBObject idKey = BasicDBObjectBuilder.start().add("id", 1).get(); - _dbSessions.createIndex(idKey, - BasicDBObjectBuilder.start() - .add("name", "id_1") - .add("ns", _dbSessions.getFullName()) - .add("sparse", false) - .add("unique", true) - .get()); - - DBObject versionKey = BasicDBObjectBuilder.start().add("id", 1).add("version", 1).get(); - _dbSessions.createIndex(versionKey, BasicDBObjectBuilder.start() - .add("name", "id_1_version_1") - .add("ns", _dbSessions.getFullName()) - .add("sparse", false) - .add("unique", true) - .get()); + var indexes = + StreamSupport.stream(_dbSessions.listIndexes().spliterator(), false) + .toList(); + var indexesNames = indexes.stream().map(document -> document.getString("name")).toList(); + if (!indexesNames.contains("id_1")) + { + String createResult = _dbSessions.createIndex(Indexes.text("id"), + new IndexOptions().unique(true).name("id_1").sparse(false)); + LOG.info("create index {}, result: {}", "id_1", createResult); + } + if (!indexesNames.contains("id_1_version_1")) + { + // Command failed with error 67 (CannotCreateIndex): 'only one text index per collection allowed, found existing text index "id_1"' + String createResult = _dbSessions.createIndex( + Indexes.compoundIndex(Indexes.descending("id"), Indexes.descending("version")), + new IndexOptions().unique(false).name("id_1_version_1").sparse(false)); + LOG.info("create index {}, result: {}", "id_1_version_1", createResult); + } if (LOG.isDebugEnabled()) LOG.debug("Done ensure Mongodb indexes existing"); //TODO perhaps index on expiry time? @@ -587,4 +561,4 @@ public class MongoSessionDataStore extends NoSqlSessionDataStore { return String.format("%s[collection=%s]", super.toString(), getDBCollection()); } -} +} \ No newline at end of file diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java index 14ee7fbe9c8..f1913d9aff2 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.nosql.mongodb; import java.net.UnknownHostException; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientURI; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import org.eclipse.jetty.session.AbstractSessionDataStoreFactory; import org.eclipse.jetty.session.SessionDataStore; import org.eclipse.jetty.session.SessionManager; @@ -136,14 +136,14 @@ public class MongoSessionDataStoreFactory extends AbstractSessionDataStoreFactor MongoClient mongo; if (!StringUtil.isBlank(getConnectionString())) - mongo = new MongoClient(new MongoClientURI(getConnectionString())); + mongo = MongoClients.create(getConnectionString()); else if (!StringUtil.isBlank(getHost()) && getPort() != -1) - mongo = new MongoClient(getHost(), getPort()); + mongo = MongoClients.create("mongodb://" + getHost() + ":" + getPort()); else if (!StringUtil.isBlank(getHost())) - mongo = new MongoClient(getHost()); + mongo = MongoClients.create("mongodb://" + getHost()); else - mongo = new MongoClient(); - store.setDBCollection(mongo.getDB(getDbName()).getCollection(getCollectionName())); + mongo = MongoClients.create(); + store.setDBCollection(mongo.getDatabase(getDbName()).getCollection(getCollectionName())); return store; } } diff --git a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java index 9d6ecd99b7b..285d0444ddd 100644 --- a/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java +++ b/jetty-integrations/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoUtils.java @@ -22,7 +22,8 @@ import java.util.HashMap; import java.util.Map; import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; +import org.bson.Document; +import org.bson.types.Binary; import org.eclipse.jetty.util.ClassLoadingObjectInputStream; import org.eclipse.jetty.util.URIUtil; @@ -40,6 +41,13 @@ public class MongoUtils { return valueToDecode; } + else if (valueToDecode instanceof Binary) + { + final byte[] decodeObject = ((Binary)valueToDecode).getData(); + final ByteArrayInputStream bais = new ByteArrayInputStream(decodeObject); + final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais); + return objectInputStream.readUnshared(); + } else if (valueToDecode instanceof byte[]) { final byte[] decodeObject = (byte[])valueToDecode; @@ -47,13 +55,13 @@ public class MongoUtils final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais); return objectInputStream.readUnshared(); } - else if (valueToDecode instanceof DBObject) + else if (valueToDecode instanceof Document) { - Map map = new HashMap(); - for (String name : ((DBObject)valueToDecode).keySet()) + Map map = new HashMap<>(); + for (String name : ((Document)valueToDecode).keySet()) { String attr = decodeName(name); - map.put(attr, decodeValue(((DBObject)valueToDecode).get(name))); + map.put(attr, decodeValue(((Document)valueToDecode).get(name))); } return map; } @@ -107,19 +115,19 @@ public class MongoUtils /** * Dig through a given dbObject for the nested value * - * @param dbObject the mongo object to search + * @param sessionDocument the mongo document to search * @param nestedKey the field key to find * @return the value of the field key */ - public static Object getNestedValue(DBObject dbObject, String nestedKey) + public static Object getNestedValue(Document sessionDocument, String nestedKey) { String[] keyChain = nestedKey.split("\\."); - DBObject temp = dbObject; + Document temp = sessionDocument; for (int i = 0; i < keyChain.length - 1; ++i) { - temp = (DBObject)temp.get(keyChain[i]); + temp = (Document)temp.get(keyChain[i]); if (temp == null) { diff --git a/pom.xml b/pom.xml index 0cce8d9092a..3124b3913b9 100644 --- a/pom.xml +++ b/pom.xml @@ -277,8 +277,8 @@ 3.9.0 3.4.0 2.2.3 - 3.2.20 - 3.12.14 + 5.0.26 + 5.1.3 4.1.109.Final 0.9.1 8.1.0 @@ -1074,6 +1074,11 @@ mariadb-java-client ${mariadb.version} + + org.mongodb + mongodb-driver-sync + ${mongodb.version} + org.mortbay.jetty.quiche jetty-quiche-native diff --git a/tests/jetty-test-session-common/pom.xml b/tests/jetty-test-session-common/pom.xml index 6d9cf6b3d36..11c0d8675ad 100644 --- a/tests/jetty-test-session-common/pom.xml +++ b/tests/jetty-test-session-common/pom.xml @@ -101,7 +101,7 @@ org.mongodb - mongo-java-driver + mongodb-driver-sync ${mongodb.version} compile diff --git a/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java b/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java index 525859be766..5c3d16d4a65 100644 --- a/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java +++ b/tests/jetty-test-session-common/src/main/java/org/eclipse/jetty/session/test/tools/MongoTestHelper.java @@ -18,13 +18,20 @@ import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.net.UnknownHostException; import java.util.Map; +import java.util.stream.StreamSupport; import com.mongodb.BasicDBObject; -import com.mongodb.DBCollection; import com.mongodb.DBObject; -import com.mongodb.MongoClient; import com.mongodb.MongoException; import com.mongodb.WriteConcern; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.UpdateOptions; +import org.bson.Document; +import org.bson.types.Binary; import org.eclipse.jetty.nosql.mongodb.MongoSessionDataStore; import org.eclipse.jetty.nosql.mongodb.MongoSessionDataStoreFactory; import org.eclipse.jetty.nosql.mongodb.MongoUtils; @@ -57,7 +64,7 @@ public class MongoTestHelper static { - mongo = new MongoDBContainer(DockerImageName.parse("mongo:" + System.getProperty("mongo.docker.version", "3.2.20"))) + mongo = new MongoDBContainer(DockerImageName.parse("mongo:" + System.getProperty("mongo.docker.version", "5.0.26"))) .withLogConsumer(new Slf4jLogConsumer(MONGO_LOG)); long start = System.currentTimeMillis(); mongo.start(); @@ -65,21 +72,21 @@ public class MongoTestHelper mongoPort = mongo.getMappedPort(MONGO_PORT); LOG.info("Mongo container started for {}:{} - {}ms", mongoHost, mongoPort, System.currentTimeMillis() - start); - mongoClient = new MongoClient(mongoHost, mongoPort); + mongoClient = MongoClients.create(mongo.getConnectionString()); } public static MongoClient getMongoClient() throws UnknownHostException { if (mongoClient == null) { - mongoClient = new MongoClient(mongoHost, mongoPort); + mongoClient = MongoClients.create(mongo.getConnectionString()); } return mongoClient; } public static void dropCollection(String dbName, String collectionName) throws Exception { - getMongoClient().getDB(dbName).getCollection(collectionName).drop(); + getMongoClient().getDatabase(dbName).getCollection(collectionName).withWriteConcern(WriteConcern.JOURNALED).drop(); } public static void shutdown() throws Exception @@ -89,12 +96,14 @@ public class MongoTestHelper public static void createCollection(String dbName, String collectionName) throws UnknownHostException, MongoException { - getMongoClient().getDB(dbName).createCollection(collectionName, null); + if (StreamSupport.stream(getMongoClient().getDatabase(dbName).listCollectionNames().spliterator(), false) + .filter(collectionName::equals).findAny().isEmpty()) + getMongoClient().getDatabase(dbName).withWriteConcern(WriteConcern.JOURNALED).createCollection(collectionName, new CreateCollectionOptions()); } - public static DBCollection getCollection(String dbName, String collectionName) throws UnknownHostException, MongoException + public static MongoCollection getCollection(String dbName, String collectionName) throws UnknownHostException, MongoException { - return getMongoClient().getDB(dbName).getCollection(collectionName); + return getMongoClient().getDatabase(dbName).getCollection(collectionName); } public static MongoSessionDataStoreFactory newSessionDataStoreFactory(String dbName, String collectionName) @@ -108,15 +117,15 @@ public class MongoTestHelper } public static boolean checkSessionExists(String id, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); DBObject fields = new BasicDBObject(); fields.put(MongoSessionDataStore.__EXPIRY, 1); fields.put(MongoSessionDataStore.__VALID, 1); - DBObject sessionDocument = collection.findOne(new BasicDBObject(MongoSessionDataStore.__ID, id), fields); + Document sessionDocument = collection.find(Filters.eq(MongoSessionDataStore.__ID, id)).first(); if (sessionDocument == null) return false; //doesn't exist @@ -125,21 +134,21 @@ public class MongoTestHelper } public static boolean checkSessionPersisted(SessionData data, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); DBObject fields = new BasicDBObject(); - DBObject sessionDocument = collection.findOne(new BasicDBObject(MongoSessionDataStore.__ID, data.getId()), fields); + Document sessionDocument = collection.find(Filters.eq(MongoSessionDataStore.__ID, data.getId())).first(); if (sessionDocument == null) return false; //doesn't exist LOG.debug("{}", sessionDocument); - Boolean valid = (Boolean)sessionDocument.get(MongoSessionDataStore.__VALID); + boolean valid = (Boolean)sessionDocument.get(MongoSessionDataStore.__VALID); - if (valid == null || !valid) + if (!valid) return false; Long created = (Long)sessionDocument.get(MongoSessionDataStore.__CREATED); @@ -149,13 +158,13 @@ public class MongoTestHelper Long expiry = (Long)sessionDocument.get(MongoSessionDataStore.__EXPIRY); Object version = MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__VERSION); + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__VERSION); Long lastSaved = (Long)MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTSAVED); + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTSAVED); String lastNode = (String)MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTNODE); - byte[] attributes = (byte[])MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__ATTRIBUTES); + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__LASTNODE); + byte[] attributes = ((Binary)MongoUtils.getNestedValue(sessionDocument, + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath() + "." + MongoSessionDataStore.__ATTRIBUTES)).getData(); assertEquals(data.getCreated(), created.longValue()); assertEquals(data.getAccessed(), accessed.longValue()); @@ -167,9 +176,9 @@ public class MongoTestHelper assertNotNull(lastSaved); // get the session for the context - DBObject sessionSubDocumentForContext = - (DBObject)MongoUtils.getNestedValue(sessionDocument, - MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath()); + Document sessionSubDocumentForContext = + (Document)MongoUtils.getNestedValue(sessionDocument, + MongoSessionDataStore.__CONTEXT + "." + data.getVhost().replace('.', '_') + ":" + data.getContextPath()); assertNotNull(sessionSubDocumentForContext); @@ -200,12 +209,9 @@ public class MongoTestHelper long lastAccessed, long maxIdle, long expiry, Map attributes, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); - - // Form query for upsert - BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); // Form updates BasicDBObject update = new BasicDBObject(); @@ -236,12 +242,12 @@ public class MongoTestHelper ObjectOutputStream oos = new ObjectOutputStream(baos)) { SessionData.serializeAttributes(tmp, oos); - sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, baos.toByteArray()); + sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, new Binary(baos.toByteArray())); } } update.put("$set", sets); - collection.update(key, update, upsert, false, WriteConcern.SAFE); + collection.updateOne(Filters.eq(MongoSessionDataStore.__ID, id), update, new UpdateOptions().upsert(true)); } public static void createSession(String id, String contextPath, String vhost, @@ -249,10 +255,10 @@ public class MongoTestHelper long lastAccessed, long maxIdle, long expiry, Map attributes, String dbName, String collectionName) - throws Exception + throws Exception { - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); // Form query for upsert BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); @@ -283,12 +289,12 @@ public class MongoTestHelper ObjectOutputStream oos = new ObjectOutputStream(baos)) { SessionData.serializeAttributes(tmp, oos); - sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, baos.toByteArray()); + sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoSessionDataStore.__ATTRIBUTES, new Binary(baos.toByteArray())); } } update.put("$set", sets); - collection.update(key, update, upsert, false, WriteConcern.SAFE); + collection.updateOne(key, update, new UpdateOptions().upsert(true)); } public static void createLegacySession(String id, String contextPath, String vhost, @@ -296,10 +302,10 @@ public class MongoTestHelper long lastAccessed, long maxIdle, long expiry, Map attributes, String dbName, String collectionName) - throws Exception + throws Exception { //make old-style session to test if we can retrieve it - DBCollection collection = getMongoClient().getDB(dbName).getCollection(collectionName); + MongoCollection collection = getMongoClient().getDatabase(dbName).getCollection(collectionName); // Form query for upsert BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); @@ -329,10 +335,10 @@ public class MongoTestHelper { Object value = attributes.get(name); sets.put(MongoSessionDataStore.__CONTEXT + "." + vhost.replace('.', '_') + ":" + contextPath + "." + MongoUtils.encodeName(name), - MongoUtils.encodeName(value)); + MongoUtils.encodeName(value)); } } update.put("$set", sets); - collection.update(key, update, upsert, false, WriteConcern.SAFE); + collection.updateOne(key, update, new UpdateOptions().upsert(true)); } -} +} \ No newline at end of file diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java index a4c0a17dbf7..7c54f9dd726 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/session/MongodbSessionDistributionTests.java @@ -35,7 +35,7 @@ public class MongodbSessionDistributionTests extends AbstractSessionDistribution private static final int MONGO_PORT = 27017; - final String imageName = "mongo:" + System.getProperty("mongo.docker.version", "3.2.20"); + final String imageName = "mongo:" + System.getProperty("mongo.docker.version", "5.0.26"); final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse(imageName)) From aa07995f373a82441373d2c978f85209461e8eeb Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Wed, 28 Aug 2024 20:24:10 +1000 Subject: [PATCH 9/9] sendError(-1) is an abort (#12206) restore behaviour from jetty <= 11 where a sendError(-1) is a true abort, without an attempt to send an error response. --- .../org/eclipse/jetty/server/Request.java | 30 +++ .../server/internal/HttpChannelState.java | 18 +- .../ee10/servlet/ServletApiResponse.java | 2 +- .../jetty/ee10/servlet/ErrorPageTest.java | 220 ++++++++++++++++++ .../eclipse/jetty/ee9/nested/Response.java | 2 +- .../jetty/ee9/servlet/ErrorPageTest.java | 46 +++- 6 files changed, 305 insertions(+), 13 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index d1ad6b93ca3..84fbc3cd238 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.http.MultiPartCompliance; import org.eclipse.jetty.http.MultiPartConfig; import org.eclipse.jetty.http.Trailers; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.internal.CompletionStreamWrapper; import org.eclipse.jetty.server.internal.HttpChannelState; import org.eclipse.jetty.util.Attributes; @@ -716,6 +717,7 @@ public interface Request extends Attributes, Content.Source * is returned, then this method must not generate a response, nor complete the callback. * @throws Exception if there is a failure during the handling. Catchers cannot assume that the callback will be * called and thus should attempt to complete the request as if a false had been returned. + * @see AbortException */ boolean handle(Request request, Response response, Callback callback) throws Exception; @@ -725,6 +727,34 @@ public interface Request extends Attributes, Content.Source { return InvocationType.BLOCKING; } + + /** + * A marker {@link Exception} that can be passed the {@link Callback#failed(Throwable)} of the {@link Callback} + * passed in {@link #handle(Request, Response, Callback)}, to cause request handling to be aborted. For HTTP/1 + * an abort is handled with a {@link EndPoint#close()}, for later versions of HTTP, a reset message will be sent. + */ + class AbortException extends Exception + { + public AbortException() + { + super(); + } + + public AbortException(String message) + { + super(message); + } + + public AbortException(String message, Throwable cause) + { + super(message, cause); + } + + public AbortException(Throwable cause) + { + super(cause); + } + } } /** 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 f3455d38095..d2068f6381e 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 @@ -1590,18 +1590,18 @@ public class HttpChannelState implements HttpChannel, Components httpChannelState._callbackFailure = failure; - // Consume any input. - Throwable unconsumed = stream.consumeAvailable(); - ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed); + if (!stream.isCommitted() && !(failure instanceof Request.Handler.AbortException)) + { + // Consume any input. + Throwable unconsumed = stream.consumeAvailable(); + ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed); - ChannelResponse response = httpChannelState._response; - if (LOG.isDebugEnabled()) - LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", stream.isCommitted(), response.isCommitted(), this); + ChannelResponse response = httpChannelState._response; + if (LOG.isDebugEnabled()) + LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", stream.isCommitted(), response.isCommitted(), this); - // There may have been an attempt to write an error response that failed. - // Do not try to write again an error response if already committed. - if (!stream.isCommitted()) errorResponse = new ErrorResponse(request); + } } if (errorResponse != null) diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java index 70a145bc116..45bf9b1252c 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java @@ -138,7 +138,7 @@ public class ServletApiResponse implements HttpServletResponse { switch (sc) { - case -1 -> getServletChannel().abort(new IOException(msg)); + case -1 -> getServletChannel().abort(new Request.Handler.AbortException(msg)); case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINTS_103 -> { if (!isCommitted()) diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java index c1b3d8234d8..3667b60f8dd 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ErrorPageTest.java @@ -13,8 +13,12 @@ package org.eclipse.jetty.ee10.servlet; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; +import java.net.Socket; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -50,6 +54,7 @@ import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -70,6 +75,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -780,6 +786,220 @@ public class ErrorPageTest assertThat(responseBody, Matchers.containsString("ERROR_REQUEST_URI: /fail/599")); } + @Test + public void testAbortWithSendError() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorChunked() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.getOutputStream().write("test".getBytes(StandardCharsets.UTF_8)); + response.flushBuffer(); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertTrue(chunked); + + line = in.readLine(); + assertThat(line, is("4")); + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorContent() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(10); + response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8)); + response.flushBuffer(); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertFalse(chunked); + + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + + @Test + public void testAbortWithSendErrorComplete() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + HttpServlet failServlet = new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.setContentLength(6); + response.getOutputStream().write("test\r\n".getBytes(StandardCharsets.UTF_8)); + response.sendError(-1); + } + }; + + contextHandler.addServlet(failServlet, "/abort"); + startServer(contextHandler); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertThat(line, is("HTTP/1.1 200 OK")); + + boolean chunked = false; + while (!line.isEmpty()) + { + line = in.readLine(); + assertNotNull(line); + chunked |= line.equals("Transfer-Encoding: chunked"); + } + assertFalse(chunked); + + line = in.readLine(); + assertThat(line, is("test")); + + line = in.readLine(); + assertNull(line); + } + } + @Test public void testErrorCodeNoDefaultServletNonExistentErrorLocation() throws Exception { diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java index 10e33baa484..37f94674459 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java @@ -488,7 +488,7 @@ public class Response implements HttpServletResponse switch (code) { - case -1 -> _channel.abort(new IOException(message)); + case -1 -> _channel.abort(new org.eclipse.jetty.server.Request.Handler.AbortException(message)); case HttpStatus.PROCESSING_102 -> sendProcessing(); case HttpStatus.EARLY_HINTS_103 -> sendEarlyHint(); default -> _channel.getState().sendError(code, message); diff --git a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java index 02b5682aa55..bbed1f62278 100644 --- a/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java +++ b/jetty-ee9/jetty-ee9-servlet/src/test/java/org/eclipse/jetty/ee9/servlet/ErrorPageTest.java @@ -13,8 +13,12 @@ package org.eclipse.jetty.ee9.servlet; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; +import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.EnumSet; import java.util.concurrent.ConcurrentHashMap; @@ -29,7 +33,6 @@ import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; -import jakarta.servlet.RequestDispatcher; import jakarta.servlet.Servlet; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; @@ -49,10 +52,10 @@ import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +65,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; //@Disabled // TODO @@ -105,6 +109,7 @@ public class ErrorPageTest _context.addServlet(ErrorAndStatusServlet.class, "/error-and-status/*"); _context.addServlet(ErrorContentTypeCharsetWriterInitializedServlet.class, "/error-mime-charset-writer/*"); _context.addServlet(ExceptionServlet.class, "/exception-servlet"); + _context.addServlet(AbortServlet.class, "/abort"); HandlerWrapper noopHandler = new HandlerWrapper() { @@ -300,6 +305,34 @@ public class ErrorPageTest assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /fail/code")); } + @Test + public void testAbortWithSendError() throws Exception + { + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + contextHandler.setContextPath("/"); + + ServerConnector connector = new ServerConnector(_server); + connector.setPort(0); + _server.addConnector(connector); + connector.start(); + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + OutputStream output = socket.getOutputStream(); + + String request = """ + GET /abort HTTP/1.1\r + Host: test\r + \r + """; + output.write(request.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + String line = in.readLine(); + assertNull(line); + } + } + @Test public void testErrorException() throws Exception { @@ -871,4 +904,13 @@ public class ErrorPageTest super(rootCause); } } + + public static class AbortServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException + { + response.sendError(-1); + } + } }