diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneServletContext.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneServletContext.java index a09dab91db1..832aa5af384 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneServletContext.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneServletContext.java @@ -20,8 +20,23 @@ package org.eclipse.jetty.embedded; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ListenerHolder; import org.eclipse.jetty.servlet.ServletContextHandler; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.util.EnumSet; + public class OneServletContext { public static void main( String[] args ) throws Exception @@ -35,11 +50,72 @@ public class OneServletContext server.setHandler(context); // Add dump servlet - context.addServlet(DumpServlet.class, "/dump/*"); - // Add default servlet + context.addServlet( + context.addServlet(DumpServlet.class, "/dump/*"), + "*.dump"); + context.addServlet(HelloServlet.class, "/hello/*"); context.addServlet(DefaultServlet.class, "/"); + context.addFilter(TestFilter.class,"/*", EnumSet.of(DispatcherType.REQUEST)); + context.addFilter(TestFilter.class,"/test", EnumSet.of(DispatcherType.REQUEST,DispatcherType.ASYNC)); + context.addFilter(TestFilter.class,"*.test", EnumSet.of(DispatcherType.REQUEST,DispatcherType.INCLUDE,DispatcherType.FORWARD)); + + context.getServletHandler().addListener(new ListenerHolder(InitListener.class)); + context.getServletHandler().addListener(new ListenerHolder(RequestListener.class)); + server.start(); + server.dumpStdErr(); server.join(); } + + + public static class TestFilter implements Filter + { + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + chain.doFilter(request, response); + } + + @Override + public void destroy() + { + + } + } + + public static class InitListener implements ServletContextListener + { + @Override + public void contextInitialized(ServletContextEvent sce) + { + } + + @Override + public void contextDestroyed(ServletContextEvent sce) + { + } + } + + + public static class RequestListener implements ServletRequestListener + { + @Override + public void requestDestroyed(ServletRequestEvent sre) + { + + } + + @Override + public void requestInitialized(ServletRequestEvent sre) + { + + } + } } diff --git a/jetty-client/pom.xml b/jetty-client/pom.xml index 6062d1e3c03..b30b5b21d4a 100644 --- a/jetty-client/pom.xml +++ b/jetty-client/pom.xml @@ -105,6 +105,7 @@ ${project.version} true + org.eclipse.jetty jetty-server @@ -117,6 +118,17 @@ ${project.version} test + + org.apache.kerby + kerb-simplekdc + 1.1.1 + test + + + org.slf4j + slf4j-simple + test + org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index b0441b334cb..11328247ad1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -49,7 +49,7 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler private final int maxContentLength; private final ResponseNotifier notifier; - private static final Pattern CHALLENGE_PATTERN = Pattern.compile("(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)|(?:(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s+)?(?:(?[a-zA-Z0-9\\-._~+\\/]+=*)|(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s*=\\s*(?:(?.*)))"); + private static final Pattern CHALLENGE_PATTERN = Pattern.compile("(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)|(?:(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s+)?(?:(?[a-zA-Z0-9\\-._~+/]+=*)|(?[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s*=\\s*(?:(?.*)))"); protected AuthenticationProtocolHandler(HttpClient client, int maxContentLength) { @@ -122,7 +122,6 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler return headerInfos; } - private class AuthenticationListener extends BufferingResponseListener { private AuthenticationListener() @@ -225,13 +224,12 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler copyIfAbsent(request, newRequest, HttpHeader.AUTHORIZATION); copyIfAbsent(request, newRequest, HttpHeader.PROXY_AUTHORIZATION); - newRequest.onResponseSuccess(r -> client.getAuthenticationStore().addAuthenticationResult(authnResult)); - + AfterAuthenticationListener listener = new AfterAuthenticationListener(authnResult); Connection connection = (Connection)request.getAttributes().get(Connection.class.getName()); if (connection != null) - connection.send(newRequest, null); + connection.send(newRequest, listener); else - newRequest.send(null); + newRequest.send(listener); } catch (Throwable x) { @@ -298,4 +296,20 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler return result; } } + + private class AfterAuthenticationListener extends Response.Listener.Adapter + { + private final Authentication.Result authenticationResult; + + private AfterAuthenticationListener(Authentication.Result authenticationResult) + { + this.authenticationResult = authenticationResult; + } + + @Override + public void onSuccess(Response response) + { + client.getAuthenticationStore().addAuthenticationResult(authenticationResult); + } + } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java new file mode 100644 index 00000000000..601f4abfd24 --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java @@ -0,0 +1,378 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.util; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +/** + *

Implementation of the SPNEGO (or "Negotiate") authentication defined in RFC 4559.

+ *

A {@link #getUserName() user} is logged in via JAAS (either via userName/password or + * via userName/keyTab) once only.

+ *

For every request that needs authentication, a {@link GSSContext} is initiated and + * later established after reading the response from the server.

+ *

Applications should create objects of this class and add them to the + * {@link AuthenticationStore} retrieved from the {@link HttpClient} + * via {@link HttpClient#getAuthenticationStore()}.

+ */ +public class SPNEGOAuthentication extends AbstractAuthentication +{ + private static final Logger LOG = Log.getLogger(SPNEGOAuthentication.class); + private static final String NEGOTIATE = HttpHeader.NEGOTIATE.asString(); + + private final GSSManager gssManager = GSSManager.getInstance(); + private String userName; + private String userPassword; + private Path userKeyTabPath; + private String serviceName; + private boolean useTicketCache; + private Path ticketCachePath; + private boolean renewTGT; + + public SPNEGOAuthentication(URI uri) + { + super(uri, ANY_REALM); + } + + @Override + public String getType() + { + return NEGOTIATE; + } + + /** + * @return the user name of the user to login + */ + public String getUserName() + { + return userName; + } + + /** + * @param userName user name of the user to login + */ + public void setUserName(String userName) + { + this.userName = userName; + } + + /** + * @return the password of the user to login + */ + public String getUserPassword() + { + return userPassword; + } + + /** + * @param userPassword the password of the user to login + * @see #setUserKeyTabPath(Path) + */ + public void setUserPassword(String userPassword) + { + this.userPassword = userPassword; + } + + /** + * @return the path of the keyTab file with the user credentials + */ + public Path getUserKeyTabPath() + { + return userKeyTabPath; + } + + /** + * @param userKeyTabPath the path of the keyTab file with the user credentials + * @see #setUserPassword(String) + */ + public void setUserKeyTabPath(Path userKeyTabPath) + { + this.userKeyTabPath = userKeyTabPath; + } + + /** + * @return the name of the service to use + */ + public String getServiceName() + { + return serviceName; + } + + /** + * @param serviceName the name of the service to use + */ + public void setServiceName(String serviceName) + { + this.serviceName = serviceName; + } + + /** + * @return whether to use the ticket cache during login + */ + public boolean isUseTicketCache() + { + return useTicketCache; + } + + /** + * @param useTicketCache whether to use the ticket cache during login + * @see #setTicketCachePath(Path) + */ + public void setUseTicketCache(boolean useTicketCache) + { + this.useTicketCache = useTicketCache; + } + + /** + * @return the path of the ticket cache file + */ + public Path getTicketCachePath() + { + return ticketCachePath; + } + + /** + * @param ticketCachePath the path of the ticket cache file + * @see #setUseTicketCache(boolean) + */ + public void setTicketCachePath(Path ticketCachePath) + { + this.ticketCachePath = ticketCachePath; + } + + /** + * @return whether to renew the ticket granting ticket + */ + public boolean isRenewTGT() + { + return renewTGT; + } + + /** + * @param renewTGT whether to renew the ticket granting ticket + */ + public void setRenewTGT(boolean renewTGT) + { + this.renewTGT = renewTGT; + } + + @Override + public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) + { + SPNEGOContext spnegoContext = (SPNEGOContext)context.getAttribute(SPNEGOContext.ATTRIBUTE); + if (LOG.isDebugEnabled()) + LOG.debug("Authenticate with context {}", spnegoContext); + if (spnegoContext == null) + { + spnegoContext = login(); + context.setAttribute(SPNEGOContext.ATTRIBUTE, spnegoContext); + } + + String b64Input = headerInfo.getBase64(); + byte[] input = b64Input == null ? new byte[0] : Base64.getDecoder().decode(b64Input); + byte[] output = Subject.doAs(spnegoContext.subject, initGSSContext(spnegoContext, request.getHost(), input)); + String b64Output = output == null ? null : new String(Base64.getEncoder().encode(output)); + + // The result cannot be used for subsequent requests, + // so it always has a null URI to avoid being cached. + return new SPNEGOResult(null, b64Output); + } + + private SPNEGOContext login() + { + try + { + // First login via JAAS using the Kerberos AS_REQ call, with a client user. + // This will populate the Subject with the client user principal and the TGT. + String user = getUserName(); + if (LOG.isDebugEnabled()) + LOG.debug("Logging in user {}", user); + CallbackHandler callbackHandler = new PasswordCallbackHandler(); + LoginContext loginContext = new LoginContext("", null, callbackHandler, new SPNEGOConfiguration()); + loginContext.login(); + Subject subject = loginContext.getSubject(); + + SPNEGOContext spnegoContext = new SPNEGOContext(); + spnegoContext.subject = subject; + if (LOG.isDebugEnabled()) + LOG.debug("Initialized {}", spnegoContext); + return spnegoContext; + } + catch (LoginException x) + { + throw new RuntimeException(x); + } + } + + private PrivilegedAction initGSSContext(SPNEGOContext spnegoContext, String host, byte[] bytes) + { + return () -> + { + try + { + // The call to initSecContext with the service name will + // trigger the Kerberos TGS_REQ call, asking for the SGT, + // which will be added to the Subject credentials because + // initSecContext() is called from within Subject.doAs(). + GSSContext gssContext = spnegoContext.gssContext; + if (gssContext == null) + { + String principal = getServiceName() + "@" + host; + GSSName serviceName = gssManager.createName(principal, GSSName.NT_HOSTBASED_SERVICE); + Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); + gssContext = gssManager.createContext(serviceName, spnegoOid, null, GSSContext.INDEFINITE_LIFETIME); + spnegoContext.gssContext = gssContext; + gssContext.requestMutualAuth(true); + } + byte[] result = gssContext.initSecContext(bytes, 0, bytes.length); + if (LOG.isDebugEnabled()) + LOG.debug("{} {}", gssContext.isEstablished() ? "Initialized" : "Initializing", gssContext); + return result; + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + public static class SPNEGOResult implements Result + { + private final URI uri; + private final HttpHeader header; + private final String value; + + public SPNEGOResult(URI uri, String token) + { + this(uri, HttpHeader.AUTHORIZATION, token); + } + + public SPNEGOResult(URI uri, HttpHeader header, String token) + { + this.uri = uri; + this.header = header; + this.value = NEGOTIATE + (token == null ? "" : " " + token); + } + + @Override + public URI getURI() + { + return uri; + } + + @Override + public void apply(Request request) + { + request.header(header, value); + } + } + + private static class SPNEGOContext + { + private static final String ATTRIBUTE = SPNEGOContext.class.getName(); + + private Subject subject; + private GSSContext gssContext; + + @Override + public String toString() + { + return String.format("%s@%x[context=%s]", getClass().getSimpleName(), hashCode(), gssContext); + } + } + + private class PasswordCallbackHandler implements CallbackHandler + { + @Override + public void handle(Callback[] callbacks) throws IOException + { + PasswordCallback callback = Arrays.stream(callbacks) + .filter(PasswordCallback.class::isInstance) + .map(PasswordCallback.class::cast) + .findAny() + .filter(c -> c.getPrompt().contains(getUserName())) + .orElseThrow(IOException::new); + callback.setPassword(getUserPassword().toCharArray()); + } + } + + private class SPNEGOConfiguration extends Configuration + { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) + { + Map options = new HashMap<>(); + if (LOG.isDebugEnabled()) + options.put("debug", "true"); + options.put("refreshKrb5Config", "true"); + options.put("principal", getUserName()); + options.put("isInitiator", "true"); + Path keyTabPath = getUserKeyTabPath(); + if (keyTabPath != null) + { + options.put("doNotPrompt", "true"); + options.put("useKeyTab", "true"); + options.put("keyTab", keyTabPath.toAbsolutePath().toString()); + options.put("storeKey", "true"); + } + boolean useTicketCache = isUseTicketCache(); + if (useTicketCache) + { + options.put("useTicketCache", "true"); + Path ticketCachePath = getTicketCachePath(); + if (ticketCachePath != null) + options.put("ticketCache", ticketCachePath.toAbsolutePath().toString()); + options.put("renewTGT", String.valueOf(isRenewTGT())); + } + + String moduleClass = "com.sun.security.auth.module.Krb5LoginModule"; + AppConfigurationEntry config = new AppConfigurationEntry(moduleClass, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); + return new AppConfigurationEntry[]{config}; + } + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java b/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java index be39d16ee17..dca7a45c53b 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/EmptyServerHandler.java @@ -26,7 +26,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.util.log.Log; public class EmptyServerHandler extends AbstractHandler.ErrorDispatchHandler { @@ -39,6 +38,5 @@ public class EmptyServerHandler extends AbstractHandler.ErrorDispatchHandler protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - Log.getRootLogger().info("EMPTY service {}",target); } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java index a738af91c95..b30c0a39765 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.client.ssl; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.BufferedReader; import java.io.File; import java.io.InputStream; @@ -54,9 +49,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.JRE; -/* This whole test is very specific to how TLS < 1.3 works. - * Starting in Java 11, TLS/1.3 is now enabled by default. - */ +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +// This whole test is very specific to how TLS < 1.3 works. +// Starting in Java 11, TLS/1.3 is now enabled by default. @EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10}) public class SslBytesClientTest extends SslBytesTest { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java index 92c3ff6ef8c..1fc70235043 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.client.ssl; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.junit.jupiter.api.condition.OS.LINUX; -import static org.junit.jupiter.api.condition.OS.WINDOWS; - import java.io.BufferedReader; import java.io.EOFException; import java.io.File; @@ -72,20 +66,31 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.JavaVersion; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.JRE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.condition.OS.LINUX; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + +// This whole test is very specific to how TLS < 1.3 works. +@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10}) public class SslBytesServerTest extends SslBytesTest { private final AtomicInteger sslFills = new AtomicInteger(); @@ -101,8 +106,6 @@ public class SslBytesServerTest extends SslBytesTest private SimpleProxy proxy; private Runnable idleHook; - // This whole test is very specific to how TLS < 1.3 works. - @DisabledOnJre( JRE.JAVA_11 ) @BeforeEach public void init() throws Exception { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java new file mode 100644 index 00000000000..8fa771ad4cd --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java @@ -0,0 +1,301 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.eclipse.jetty.client.AbstractHttpClientServerTest; +import org.eclipse.jetty.client.EmptyServerHandler; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.AuthenticationStore; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.security.ConfigurableSpnegoLoginService; +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.security.HashLoginService; +import org.eclipse.jetty.security.authentication.AuthorizationService; +import org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.session.DefaultSessionIdManager; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.Constraint; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +// Apparently only JDK 11 is able to run these tests. +// See for example: https://bugs.openjdk.java.net/browse/JDK-8202439 +// where apparently the compiler gets the AES CPU instructions wrong. +@DisabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10}) +public class SPNEGOAuthenticationTest extends AbstractHttpClientServerTest +{ + private static final Logger LOG = Log.getLogger(SPNEGOAuthenticationTest.class); + + static + { + if (LOG.isDebugEnabled()) + { + System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug"); + System.setProperty("sun.security.jgss.debug", "true"); + System.setProperty("sun.security.krb5.debug", "true"); + System.setProperty("sun.security.spnego.debug", "true"); + } + } + + private Path testDirPath = MavenTestingUtils.getTargetTestingPath(SPNEGOAuthenticationTest.class.getSimpleName()); + private String clientName = "spnego_client"; + private String clientPassword = "spnego_client_pwd"; + private String serviceName = "srvc"; + private String serviceHost = "localhost"; + private String realm = "jetty.org"; + private Path realmPropsPath = MavenTestingUtils.getTestResourcePath("realm.properties"); + private Path serviceKeyTabPath = testDirPath.resolve("service.keytab"); + private Path clientKeyTabPath = testDirPath.resolve("client.keytab"); + private SimpleKdcServer kdc; + private ConfigurableSpnegoAuthenticator authenticator; + + @BeforeEach + public void prepare() throws Exception + { + IO.delete(testDirPath.toFile()); + Files.createDirectories(testDirPath); + System.setProperty("java.security.krb5.conf", testDirPath.toAbsolutePath().toString()); + + kdc = new SimpleKdcServer(); + kdc.setAllowUdp(false); + kdc.setAllowTcp(true); + kdc.setKdcRealm(realm); + kdc.setWorkDir(testDirPath.toFile()); + kdc.init(); + + kdc.createAndExportPrincipals(serviceKeyTabPath.toFile(), serviceName + "/" + serviceHost); + kdc.createPrincipal(clientName + "@" + realm, clientPassword); + kdc.exportPrincipal(clientName, clientKeyTabPath.toFile()); + kdc.start(); + + if (LOG.isDebugEnabled()) + { + LOG.debug("KDC started on port {}", kdc.getKdcTcpPort()); + String krb5 = Files.readAllLines(testDirPath.resolve("krb5.conf")).stream() + .filter(line -> !line.startsWith("#")) + .collect(Collectors.joining(System.lineSeparator())); + LOG.debug("krb5.conf{}{}", System.lineSeparator(), krb5); + } + } + + @AfterEach + public void dispose() throws Exception + { + if (kdc != null) + kdc.stop(); + } + + private void startSPNEGO(Scenario scenario, Handler handler) throws Exception + { + server = new Server(); + server.setSessionIdManager(new DefaultSessionIdManager(server)); + HashLoginService authorizationService = new HashLoginService(realm, realmPropsPath.toString()); + ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService(realm, AuthorizationService.from(authorizationService, "")); + loginService.addBean(authorizationService); + loginService.setKeyTabPath(serviceKeyTabPath); + loginService.setServiceName(serviceName); + loginService.setHostName(serviceHost); + server.addBean(loginService); + + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + Constraint constraint = new Constraint(); + constraint.setAuthenticate(true); + constraint.setRoles(new String[]{"**"}); //allow any authenticated user + ConstraintMapping mapping = new ConstraintMapping(); + mapping.setPathSpec("/secure"); + mapping.setConstraint(constraint); + securityHandler.addConstraintMapping(mapping); + authenticator = new ConfigurableSpnegoAuthenticator(); + securityHandler.setAuthenticator(authenticator); + securityHandler.setLoginService(loginService); + securityHandler.setHandler(handler); + + SessionHandler sessionHandler = new SessionHandler(); + sessionHandler.setHandler(securityHandler); + start(scenario, sessionHandler); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testPasswordSPNEGOAuthentication(Scenario scenario) throws Exception + { + testSPNEGOAuthentication(scenario, false); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testKeyTabSPNEGOAuthentication(Scenario scenario) throws Exception + { + testSPNEGOAuthentication(scenario, true); + } + + private void testSPNEGOAuthentication(Scenario scenario, boolean useKeyTab) throws Exception + { + startSPNEGO(scenario, new EmptyServerHandler()); + authenticator.setAuthenticationDuration(Duration.ZERO); + + URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort()); + + // Request without Authentication causes a 401 + Request request = client.newRequest(uri).path("/secure"); + ContentResponse response = request.timeout(15, TimeUnit.SECONDS).send(); + assertNotNull(response); + assertEquals(401, response.getStatus()); + + // Add authentication. + SPNEGOAuthentication authentication = new SPNEGOAuthentication(uri); + authentication.setUserName(clientName + "@" + realm); + if (useKeyTab) + authentication.setUserKeyTabPath(clientKeyTabPath); + else + authentication.setUserPassword(clientPassword); + authentication.setServiceName(serviceName); + AuthenticationStore authenticationStore = client.getAuthenticationStore(); + authenticationStore.addAuthentication(authentication); + + // Request with authentication causes a 401 (no previous successful authentication) + 200 + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertNotNull(response); + assertEquals(200, response.getStatus()); + // Authentication results for SPNEGO cannot be cached. + Authentication.Result authnResult = authenticationStore.findAuthenticationResult(uri); + assertNull(authnResult); + + AtomicInteger requests = new AtomicInteger(); + client.getRequestListeners().add(new Request.Listener.Adapter() + { + @Override + public void onSuccess(Request request) + { + requests.incrementAndGet(); + } + }); + + // The server has infinite authentication duration, so + // subsequent requests will be preemptively authorized. + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertNotNull(response); + assertEquals(200, response.getStatus()); + assertEquals(1, requests.get()); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testAuthenticationExpiration(Scenario scenario) throws Exception + { + startSPNEGO(scenario, new EmptyServerHandler() + { + @Override + protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException + { + IO.readBytes(request.getInputStream()); + } + }); + long timeout = 1000; + authenticator.setAuthenticationDuration(Duration.ofMillis(timeout)); + + URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort()); + + // Add authentication. + SPNEGOAuthentication authentication = new SPNEGOAuthentication(uri); + authentication.setUserName(clientName + "@" + realm); + authentication.setUserPassword(clientPassword); + authentication.setServiceName(serviceName); + AuthenticationStore authenticationStore = client.getAuthenticationStore(); + authenticationStore.addAuthentication(authentication); + + AtomicInteger requests = new AtomicInteger(); + client.getRequestListeners().add(new Request.Listener.Adapter() + { + @Override + public void onSuccess(Request request) + { + requests.incrementAndGet(); + } + }); + + Request request = client.newRequest(uri).path("/secure"); + Response response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Expect 401 + 200. + assertEquals(2, requests.get()); + + requests.set(0); + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Authentication not expired on server, expect 200 only. + assertEquals(1, requests.get()); + + // Let authentication expire. + Thread.sleep(2 * timeout); + + requests.set(0); + request = client.newRequest(uri).path("/secure"); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Authentication expired, expect 401 + 200. + assertEquals(2, requests.get()); + + // Let authentication expire again. + Thread.sleep(2 * timeout); + + requests.set(0); + ByteArrayInputStream input = new ByteArrayInputStream("hello_world".getBytes(StandardCharsets.UTF_8)); + request = client.newRequest(uri).method("POST").path("/secure").content(new InputStreamContentProvider(input)); + response = request.timeout(15, TimeUnit.SECONDS).send(); + assertEquals(200, response.getStatus()); + // Authentication expired, but POSTs are allowed. + assertEquals(1, requests.get()); + } +} diff --git a/jetty-client/src/test/resources/jetty-logging.properties b/jetty-client/src/test/resources/jetty-logging.properties index 50f3165273b..f74a4da98d1 100644 --- a/jetty-client/src/test/resources/jetty-logging.properties +++ b/jetty-client/src/test/resources/jetty-logging.properties @@ -1,5 +1,5 @@ -class=org.eclipse.jetty.util.log.StdErrLog -#org.eclipse.jetty.LEVEL=INFO +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +#org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG #org.eclipse.jetty.io.ChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.io.ssl.LEVEL=DEBUG diff --git a/jetty-client/src/test/resources/realm.properties b/jetty-client/src/test/resources/realm.properties index 54ace472cb6..27e300ad53c 100644 --- a/jetty-client/src/test/resources/realm.properties +++ b/jetty-client/src/test/resources/realm.properties @@ -1,3 +1,4 @@ # Format is :, basic:basic digest:digest +spnego_client:,admin diff --git a/jetty-documentation/src/main/asciidoc/development/websockets/intro/chapter.adoc b/jetty-documentation/src/main/asciidoc/development/websockets/intro/chapter.adoc index fab182278a7..9a43439e238 100644 --- a/jetty-documentation/src/main/asciidoc/development/websockets/intro/chapter.adoc +++ b/jetty-documentation/src/main/asciidoc/development/websockets/intro/chapter.adoc @@ -19,14 +19,11 @@ [[websocket-intro]] == WebSocket Introduction -WebSocket is a new protocol for bidirectional communications over HTTP. - -It is based on a low level framing protocol that delivers messages in either UTF-8 TEXT or BINARY format. - -A single message in WebSocket can be of any size (the underlying framing however does have a single frame limit of http://en.wikipedia.org/wiki/9223372036854775807[63-bits]) +WebSocket is a new protocol for bidirectional communications initiated via HTTP/1.1 upgrade and providing basic message framing, layered over TCP. +It is based on a low-level framing protocol that delivers messages in either UTF-8 TEXT or BINARY format. +A single message in WebSocket can be of any size (the underlying framing however does have a single frame limit of http://en.wikipedia.org/wiki/9223372036854775807[63-bits]). There can be an unlimited number of messages sent. - Messages are sent sequentially, the base protocol does not support interleaved messages. A WebSocket connection goes through some basic state changes: @@ -78,11 +75,9 @@ https://datatracker.ietf.org/doc/draft-ietf-hybi-websocket-perframe-compression/ Per Frame Compression Extension. + An early extension draft from the Google/Chromium team that would provide WebSocket frame compression. -+ perframe-compression using deflate algorithm is present on many versions of Chrome/Chromium. + Jetty's support for perframe-compression is based on the draft-04 spec. -+ This standard is being replaced with permessage-compression. https://datatracker.ietf.org/doc/draft-tyoshino-hybi-permessage-compression/[permessage-compression]:: @@ -108,12 +103,11 @@ Java WebSocket Server API:: === Enabling WebSocket -To enable websocket, you need to link:#enabling-modules[enable] the `websocket` link:#enabling-modules[module]. +To enable Websocket, you need to enable the `websocket` link:#enabling-modules[module]. -Once this module is enabled for your jetty base, it will apply to all webapps deployed to that base. -If you want to be more selective about which webapps use websocket, then you can: +Once this module is enabled for your Jetty base, it will apply to all webapps deployed to that base. If you want to be more selective about which webapps use Websocket, then you can: -Disable jsr-356 for a particular webapp::: +Disable JSR-356 for a particular webapp::: You can disable jsr-356 for a particular webapp by setting the link:#context_attributes[context attribute] `org.eclipse.jetty.websocket.jsr356` to `false`. This will mean that websockets are not available to your webapp, however deployment time scanning for websocket-related classes such as endpoints will still occur. This can be a significant impost if your webapp contains a lot of classes and/or jar files. diff --git a/jetty-osgi/test-jetty-osgi/pom.xml b/jetty-osgi/test-jetty-osgi/pom.xml index 921948f8ea6..40340aa2fbb 100644 --- a/jetty-osgi/test-jetty-osgi/pom.xml +++ b/jetty-osgi/test-jetty-osgi/pom.xml @@ -14,7 +14,7 @@ ${project.groupId}.boot.test.osgi http://download.eclipse.org/jetty/orbit/ target/distribution - 4.11.0 + 4.12.0 2.5.2 1.0 true @@ -45,15 +45,6 @@ pax-exam-junit4 ${exam.version} test - -
org.ops4j.pax.exam @@ -405,13 +396,11 @@ org.ow2.asm asm - ${asm.version} test org.ow2.asm asm-commons - ${asm.version} test @@ -438,6 +427,14 @@ ${settings.localRepository} + + + + org.apache.maven.surefire + surefire-junit47 + ${maven.surefire.version} + + diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java index 076945ed9db..715ca1e4967 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java @@ -18,16 +18,6 @@ package org.eclipse.jetty.proxy; -import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -73,7 +63,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.DuplexConnectionPool; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpContentResponse; import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; @@ -96,7 +85,6 @@ import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.hamcrest.Matchers; @@ -105,7 +93,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; + +import static org.eclipse.jetty.http.HttpFieldsMatchers.containsHeader; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ProxyServletTest { @@ -1400,8 +1396,8 @@ public class ProxyServletTest // Wait more than the idle timeout to break the connection. Thread.sleep(2 * idleTimeout); - assertTrue(serverLatch.await(555, TimeUnit.SECONDS)); - assertTrue(clientLatch.await(555, TimeUnit.SECONDS)); + assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); + assertTrue(clientLatch.await(5, TimeUnit.SECONDS)); } @ParameterizedTest diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java new file mode 100644 index 00000000000..fe256d8e749 --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java @@ -0,0 +1,331 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security; + +import java.io.Serializable; +import java.net.InetAddress; +import java.nio.file.Path; +import java.security.PrivilegedAction; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.security.authentication.AuthorizationService; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +/** + *

A configurable (as opposed to using system properties) SPNEGO LoginService.

+ *

At startup, this LoginService will login via JAAS the service principal, composed + * of the {@link #getServiceName() service name} and the {@link #getHostName() host name}, + * for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal + * credentials.

+ *

Upon receiving a HTTP request, the server tries to authenticate the client + * calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to + * verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is + * established.

+ */ +public class ConfigurableSpnegoLoginService extends ContainerLifeCycle implements LoginService +{ + private static final Logger LOG = Log.getLogger(ConfigurableSpnegoLoginService.class); + + private final GSSManager _gssManager = GSSManager.getInstance(); + private final String _realm; + private final AuthorizationService _authorizationService; + private IdentityService _identityService = new DefaultIdentityService(); + private String _serviceName; + private Path _keyTabPath; + private String _hostName; + private SpnegoContext _context; + + public ConfigurableSpnegoLoginService(String realm, AuthorizationService authorizationService) + { + _realm = realm; + _authorizationService = authorizationService; + } + + /** + * @return the realm name + */ + @Override + public String getName() + { + return _realm; + } + + /** + * @return the path of the keyTab file containing service credentials + */ + public Path getKeyTabPath() + { + return _keyTabPath; + } + + /** + * @param keyTabFile the path of the keyTab file containing service credentials + */ + public void setKeyTabPath(Path keyTabFile) + { + _keyTabPath = keyTabFile; + } + + /** + * @return the service name, typically "HTTP" + * @see #getHostName() + */ + public String getServiceName() + { + return _serviceName; + } + + /** + * @param serviceName the service name + * @see #setHostName(String) + */ + public void setServiceName(String serviceName) + { + _serviceName = serviceName; + } + + /** + * @return the host name of the service + * @see #setServiceName(String) + */ + public String getHostName() + { + return _hostName; + } + + /** + * @param hostName the host name of the service + */ + public void setHostName(String hostName) + { + _hostName = hostName; + } + + @Override + protected void doStart() throws Exception + { + if (_hostName == null) + _hostName = InetAddress.getLocalHost().getCanonicalHostName(); + if (LOG.isDebugEnabled()) + LOG.debug("Retrieving credentials for service {}/{}", getServiceName(), getHostName()); + LoginContext loginContext = new LoginContext("", null, null, new SpnegoConfiguration()); + loginContext.login(); + Subject subject = loginContext.getSubject(); + _context = Subject.doAs(subject, newSpnegoContext(subject)); + super.doStart(); + } + + private PrivilegedAction newSpnegoContext(Subject subject) + { + return () -> + { + try + { + GSSName serviceName = _gssManager.createName(getServiceName() + "@" + getHostName(), GSSName.NT_HOSTBASED_SERVICE); + Oid kerberosOid = new Oid("1.2.840.113554.1.2.2"); + Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); + Oid[] mechanisms = new Oid[]{kerberosOid, spnegoOid}; + GSSCredential serviceCredential = _gssManager.createCredential(serviceName, GSSCredential.DEFAULT_LIFETIME, mechanisms, GSSCredential.ACCEPT_ONLY); + SpnegoContext context = new SpnegoContext(); + context._subject = subject; + context._serviceCredential = serviceCredential; + return context; + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + @Override + public UserIdentity login(String username, Object credentials, ServletRequest req) + { + Subject subject = _context._subject; + HttpServletRequest request = (HttpServletRequest)req; + HttpSession httpSession = request.getSession(false); + GSSContext gssContext = null; + if (httpSession != null) + { + GSSContextHolder holder = (GSSContextHolder)httpSession.getAttribute(GSSContextHolder.ATTRIBUTE); + gssContext = holder == null ? null : holder.gssContext; + } + if (gssContext == null) + gssContext = Subject.doAs(subject, newGSSContext()); + + byte[] input = Base64.getDecoder().decode((String)credentials); + byte[] output = Subject.doAs(_context._subject, acceptGSSContext(gssContext, input)); + String token = Base64.getEncoder().encodeToString(output); + + String userName = toUserName(gssContext); + // Save the token in the principal so it can be sent in the response. + SpnegoUserPrincipal principal = new SpnegoUserPrincipal(userName, token); + if (gssContext.isEstablished()) + { + if (httpSession != null) + httpSession.removeAttribute(GSSContextHolder.ATTRIBUTE); + + UserIdentity roles = _authorizationService.getUserIdentity(request, userName); + return new SpnegoUserIdentity(subject, principal, roles); + } + else + { + // The GSS context is not established yet, save it into the HTTP session. + if (httpSession == null) + httpSession = request.getSession(true); + GSSContextHolder holder = new GSSContextHolder(gssContext); + httpSession.setAttribute(GSSContextHolder.ATTRIBUTE, holder); + + // Return an unestablished UserIdentity. + return new SpnegoUserIdentity(subject, principal, null); + } + } + + private PrivilegedAction newGSSContext() + { + return () -> + { + try + { + return _gssManager.createContext(_context._serviceCredential); + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + private PrivilegedAction acceptGSSContext(GSSContext gssContext, byte[] token) + { + return () -> + { + try + { + return gssContext.acceptSecContext(token, 0, token.length); + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + }; + } + + private String toUserName(GSSContext gssContext) + { + try + { + String name = gssContext.getSrcName().toString(); + int at = name.indexOf('@'); + if (at < 0) + return name; + return name.substring(0, at); + } + catch (GSSException x) + { + throw new RuntimeException(x); + } + } + + @Override + public boolean validate(UserIdentity user) + { + return false; + } + + @Override + public IdentityService getIdentityService() + { + return _identityService; + } + + @Override + public void setIdentityService(IdentityService identityService) + { + _identityService = identityService; + } + + @Override + public void logout(UserIdentity user) + { + } + + private class SpnegoConfiguration extends Configuration + { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) + { + String principal = getServiceName() + "/" + getHostName(); + Map options = new HashMap<>(); + if (LOG.isDebugEnabled()) + options.put("debug", "true"); + options.put("doNotPrompt", "true"); + options.put("refreshKrb5Config", "true"); + options.put("principal", principal); + options.put("useKeyTab", "true"); + Path keyTabPath = getKeyTabPath(); + if (keyTabPath != null) + options.put("keyTab", keyTabPath.toAbsolutePath().toString()); + // This option is required to store the service credentials in + // the Subject, so that it can be later used by acceptSecContext(). + options.put("storeKey", "true"); + options.put("isInitiator", "false"); + String moduleClass = "com.sun.security.auth.module.Krb5LoginModule"; + AppConfigurationEntry config = new AppConfigurationEntry(moduleClass, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options); + return new AppConfigurationEntry[]{config}; + } + } + + private static class SpnegoContext + { + private Subject _subject; + private GSSCredential _serviceCredential; + } + + private static class GSSContextHolder implements Serializable + { + public static final String ATTRIBUTE = GSSContextHolder.class.getName(); + + private transient final GSSContext gssContext; + + private GSSContextHolder(GSSContext gssContext) + { + this.gssContext = gssContext; + } + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java index 5ea42ddb145..41f30f0a736 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoLoginService.java @@ -36,6 +36,10 @@ import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; +/** + * @deprecated use {@link ConfigurableSpnegoLoginService} instead + */ +@Deprecated public class SpnegoLoginService extends AbstractLifeCycle implements LoginService { private static final Logger LOG = Log.getLogger(SpnegoLoginService.class); diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java index d787d98c5bf..307f4ba5e41 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserIdentity.java @@ -19,7 +19,6 @@ package org.eclipse.jetty.security; import java.security.Principal; -import java.util.List; import javax.security.auth.Subject; @@ -27,18 +26,17 @@ import org.eclipse.jetty.server.UserIdentity; public class SpnegoUserIdentity implements UserIdentity { - private Subject _subject; - private Principal _principal; - private List _roles; + private final Subject _subject; + private final Principal _principal; + private final UserIdentity _roleDelegate; - public SpnegoUserIdentity( Subject subject, Principal principal, List roles ) + public SpnegoUserIdentity(Subject subject, Principal principal, UserIdentity roleDelegate) { _subject = subject; _principal = principal; - _roles = roles; + _roleDelegate = roleDelegate; } - @Override public Subject getSubject() { @@ -54,7 +52,11 @@ public class SpnegoUserIdentity implements UserIdentity @Override public boolean isUserInRole(String role, Scope scope) { - return _roles.contains(role); + return _roleDelegate != null && _roleDelegate.isUserInRole(role, scope); } + public boolean isEstablished() + { + return _roleDelegate != null; + } } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java index b503cf6d2be..4ad45083953 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SpnegoUserPrincipal.java @@ -19,8 +19,7 @@ package org.eclipse.jetty.security; import java.security.Principal; - -import org.eclipse.jetty.util.B64Code; +import java.util.Base64; public class SpnegoUserPrincipal implements Principal { @@ -28,13 +27,13 @@ public class SpnegoUserPrincipal implements Principal private byte[] _token; private String _encodedToken; - public SpnegoUserPrincipal( String name, String encodedToken ) + public SpnegoUserPrincipal(String name, String encodedToken) { _name = name; _encodedToken = encodedToken; } - public SpnegoUserPrincipal( String name, byte[] token ) + public SpnegoUserPrincipal(String name, byte[] token) { _name = name; _token = token; @@ -48,19 +47,15 @@ public class SpnegoUserPrincipal implements Principal public byte[] getToken() { - if ( _token == null ) - { - _token = B64Code.decode(_encodedToken); - } + if (_token == null) + _token = Base64.getDecoder().decode(_encodedToken); return _token; } public String getEncodedToken() { - if ( _encodedToken == null ) - { - _encodedToken = new String(B64Code.encode(_token,true)); - } + if (_encodedToken == null) + _encodedToken = new String(Base64.getEncoder().encode(_token)); return _encodedToken; } } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java new file mode 100644 index 00000000000..87adc2db830 --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security.authentication; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.server.UserIdentity; + +/** + *

A service to query for user roles.

+ */ +@FunctionalInterface +public interface AuthorizationService +{ + /** + * @param request the current HTTP request + * @param name the user name + * @return a {@link UserIdentity} to query for roles of the given user + */ + UserIdentity getUserIdentity(HttpServletRequest request, String name); + + /** + *

Wraps a {@link LoginService} as an AuthorizationService

+ * + * @param loginService the {@link LoginService} to wrap + * @param credentials + * @return an AuthorizationService that delegates the query for roles to the given {@link LoginService} + */ + public static AuthorizationService from(LoginService loginService, Object credentials) + { + return (request, name) -> loginService.login(name, credentials, request); + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java new file mode 100644 index 00000000000..6cae1a4745b --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java @@ -0,0 +1,232 @@ +// +// ======================================================================== +// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security.authentication; + +import java.io.IOException; +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.security.SpnegoUserIdentity; +import org.eclipse.jetty.security.SpnegoUserPrincipal; +import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Authentication.User; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.Constraint; + +/** + *

A LoginAuthenticator that uses SPNEGO and the GSS API to authenticate requests.

+ *

A successful authentication from a client is cached for a configurable + * {@link #getAuthenticationDuration() duration} using the HTTP session; this avoids + * that the client is asked to authenticate for every request.

+ * + * @see org.eclipse.jetty.security.ConfigurableSpnegoLoginService + */ +public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator +{ + private static final Logger LOG = Log.getLogger(ConfigurableSpnegoAuthenticator.class); + + private final String _authMethod; + private Duration _authenticationDuration = Duration.ofNanos(-1); + + public ConfigurableSpnegoAuthenticator() + { + this(Constraint.__SPNEGO_AUTH); + } + + /** + * Allow for a custom authMethod value to be set for instances where SPNEGO may not be appropriate + * + * @param authMethod the auth method + */ + public ConfigurableSpnegoAuthenticator(String authMethod) + { + _authMethod = authMethod; + } + + @Override + public String getAuthMethod() + { + return _authMethod; + } + + /** + * @return the authentication duration + */ + public Duration getAuthenticationDuration() + { + return _authenticationDuration; + } + + /** + *

Sets the duration of the authentication.

+ *

A negative duration means that the authentication is only valid for the current request.

+ *

A zero duration means that the authentication is valid forever.

+ *

A positive value means that the authentication is valid for the specified duration.

+ * + * @param authenticationDuration the authentication duration + */ + public void setAuthenticationDuration(Duration authenticationDuration) + { + _authenticationDuration = authenticationDuration; + } + + @Override + public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException + { + if (!mandatory) + return new DeferredAuthentication(this); + + HttpServletRequest request = (HttpServletRequest)req; + HttpServletResponse response = (HttpServletResponse)res; + + String header = request.getHeader(HttpHeader.AUTHORIZATION.asString()); + String spnegoToken = getSpnegoToken(header); + HttpSession httpSession = request.getSession(false); + + // We have a token from the client, so run the login. + if (header != null && spnegoToken != null) + { + SpnegoUserIdentity identity = (SpnegoUserIdentity)login(null, spnegoToken, request); + if (identity.isEstablished()) + { + if (!DeferredAuthentication.isDeferred(response)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Sending final token"); + // Send to the client the final token so that the + // client can establish the GSS context with the server. + SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal(); + setSpnegoToken(response, principal.getEncodedToken()); + } + + Duration authnDuration = getAuthenticationDuration(); + if (!authnDuration.isNegative()) + { + if (httpSession == null) + httpSession = request.getSession(true); + httpSession.setAttribute(UserIdentityHolder.ATTRIBUTE, new UserIdentityHolder(identity)); + } + return new UserAuthentication(getAuthMethod(), identity); + } + else + { + if (DeferredAuthentication.isDeferred(response)) + return Authentication.UNAUTHENTICATED; + if (LOG.isDebugEnabled()) + LOG.debug("Sending intermediate challenge"); + SpnegoUserPrincipal principal = (SpnegoUserPrincipal)identity.getUserPrincipal(); + sendChallenge(response, principal.getEncodedToken()); + return Authentication.SEND_CONTINUE; + } + } + // No token from the client; check if the client has logged in + // successfully before and the authentication has not expired. + else if (httpSession != null) + { + UserIdentityHolder holder = (UserIdentityHolder)httpSession.getAttribute(UserIdentityHolder.ATTRIBUTE); + if (holder != null) + { + UserIdentity identity = holder._userIdentity; + if (identity != null) + { + Duration authnDuration = getAuthenticationDuration(); + if (!authnDuration.isNegative()) + { + boolean expired = !authnDuration.isZero() && Instant.now().isAfter(holder._validFrom.plus(authnDuration)); + // Allow non-GET requests even if they're expired, so that + // the client does not need to send the request content again. + if (!expired || !HttpMethod.GET.is(request.getMethod())) + return new UserAuthentication(getAuthMethod(), identity); + } + } + } + } + + if (DeferredAuthentication.isDeferred(response)) + return Authentication.UNAUTHENTICATED; + + if (LOG.isDebugEnabled()) + LOG.debug("Sending initial challenge"); + sendChallenge(response, null); + return Authentication.SEND_CONTINUE; + } + + private void sendChallenge(HttpServletResponse response, String token) throws ServerAuthException + { + try + { + setSpnegoToken(response, token); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + catch (IOException x) + { + throw new ServerAuthException(x); + } + } + + private void setSpnegoToken(HttpServletResponse response, String token) + { + String value = HttpHeader.NEGOTIATE.asString(); + if (token != null) + value += " " + token; + response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), value); + } + + private String getSpnegoToken(String header) + { + if (header == null) + return null; + String scheme = HttpHeader.NEGOTIATE.asString() + " "; + if (header.regionMatches(true, 0, scheme, 0, scheme.length())) + return header.substring(scheme.length()).trim(); + return null; + } + + @Override + public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, User validatedUser) + { + return true; + } + + private static class UserIdentityHolder implements Serializable + { + private static final String ATTRIBUTE = UserIdentityHolder.class.getName(); + + private transient final Instant _validFrom = Instant.now(); + private transient final UserIdentity _userIdentity; + + private UserIdentityHolder(UserIdentity userIdentity) + { + _userIdentity = userIdentity; + } + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java index 0dfe1b6add5..8142eb7c4d8 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticator.java @@ -35,6 +35,10 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; +/** + * @deprecated use {@link ConfigurableSpnegoAuthenticator} instead. + */ +@Deprecated public class SpnegoAuthenticator extends LoginAuthenticator { private static final Logger LOG = Log.getLogger(SpnegoAuthenticator.class); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index bba38c90b6e..c1baef1553d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -31,13 +31,18 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; public class Dispatcher implements RequestDispatcher { + private static final Logger LOG = Log.getLogger(Dispatcher.class); + public final static String __ERROR_DISPATCH="org.eclipse.jetty.server.Dispatcher.ERROR"; /** Dispatch include attribute names */ @@ -195,8 +200,27 @@ public class Dispatcher implements RequestDispatcher baseRequest.setContextPath(_contextHandler.getContextPath()); baseRequest.setServletPath(null); baseRequest.setPathInfo(_pathInContext); - if (_uri.getQuery()!=null || old_uri.getQuery()!=null) - baseRequest.mergeQueryParameters(old_uri.getQuery(),_uri.getQuery(), true); + + if (_uri.getQuery() != null || old_uri.getQuery() != null) + { + try + { + baseRequest.mergeQueryParameters(old_uri.getQuery(), _uri.getQuery(), true); + } + catch (BadMessageException e) + { + // Only throw BME if not in Error Dispatch Mode + // This allows application ErrorPageErrorHandler to handle BME messages + if (dispatch != DispatcherType.ERROR) + { + throw e; + } + else + { + LOG.warn("Ignoring Original Bad Request Query String: " + old_uri, e); + } + } + } baseRequest.setAttributes(attr); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index e3fa2e32d81..e9560d603b2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -201,7 +201,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu private final List _servletRequestAttributeListeners = new CopyOnWriteArrayList<>(); private final List _contextListeners = new CopyOnWriteArrayList<>(); private final List _durableListeners = new CopyOnWriteArrayList<>(); - private Map _managedAttributes; private String[] _protectedTargets; private final CopyOnWriteArrayList _aliasChecks = new CopyOnWriteArrayList(); @@ -258,9 +257,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu public void dump(Appendable out, String indent) throws IOException { dumpBeans(out,indent,Collections.singletonList(new ClassLoaderDump(getClassLoader())), - Collections.singletonList(new DumpableCollection("Handler attributes " + this,((AttributesMap)getAttributes()).getAttributeEntrySet())), - Collections.singletonList(new DumpableCollection("Context attributes " + this,((Context)getServletContext()).getAttributeEntrySet())), - Collections.singletonList(new DumpableCollection("Initparams " + this,getInitParams().entrySet()))); + Collections.singletonList(new DumpableCollection("eventListeners "+this,_eventListeners)), + Collections.singletonList(new DumpableCollection("handler attributes " + this,((AttributesMap)getAttributes()).getAttributeEntrySet())), + Collections.singletonList(new DumpableCollection("context attributes " + this,((Context)getServletContext()).getAttributeEntrySet())), + Collections.singletonList(new DumpableCollection("initparams " + this,getInitParams().entrySet()))); } /* ------------------------------------------------------------ */ @@ -1553,10 +1553,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu } /* ------------------------------------------------------------ */ + @Deprecated public void setManagedAttribute(String name, Object value) { - Object old = _managedAttributes.put(name,value); - updateBean(old,value); } /* ------------------------------------------------------------ */ diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java index e37eef01166..50a1f6bf94e 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java @@ -19,7 +19,9 @@ package org.eclipse.jetty.servlet; import java.io.IOException; +import java.util.Arrays; import java.util.EnumSet; +import java.util.stream.Collectors; import javax.servlet.DispatcherType; @@ -86,11 +88,31 @@ public class FilterMapping implements Dumpable throw new IllegalArgumentException(type.toString()); } + /* ------------------------------------------------------------ */ + /** Dispatch type from name + * @param type the dispatcher type + * @return the type constant ({@link #REQUEST}, {@link #ASYNC}, {@link #FORWARD}, {@link #INCLUDE}, or {@link #ERROR}) + */ + public static DispatcherType dispatch(int type) + { + switch(type) + { + case REQUEST: + return DispatcherType.REQUEST; + case ASYNC: + return DispatcherType.ASYNC; + case FORWARD: + return DispatcherType.FORWARD; + case INCLUDE: + return DispatcherType.INCLUDE; + case ERROR: + return DispatcherType.ERROR; + } + throw new IllegalArgumentException(Integer.toString(type)); + } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ - - private int _dispatches=DEFAULT; private String _filterName; private transient FilterHolder _holder; @@ -122,7 +144,7 @@ public class FilterMapping implements Dumpable /* ------------------------------------------------------------ */ /** Check if this filter applies to a particular dispatch type. * @param type The type of request: - * {@link Handler#REQUEST}, {@link Handler#FORWARD}, {@link Handler#INCLUDE} or {@link Handler#ERROR}. + * {@link #REQUEST}, {@link #FORWARD}, {@link #INCLUDE} or {@link #ERROR}. * @return true if this filter applies */ boolean appliesTo(int type) @@ -295,9 +317,9 @@ public class FilterMapping implements Dumpable public String toString() { return - TypeUtil.asList(_pathSpecs)+"/"+ - TypeUtil.asList(_servletNames)+"=="+ - _dispatches+"=>"+ + TypeUtil.asList(_pathSpecs)+"/"+ + TypeUtil.asList(_servletNames)+"/"+ + Arrays.stream(DispatcherType.values()).filter(this::appliesTo).collect(Collectors.toSet())+"=>"+ _filterName; } diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java index 9e94f463f62..b3aa0e4c845 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ListenerHolder.java @@ -46,7 +46,12 @@ public class ListenerHolder extends BaseHolder { super(source); } - + + public ListenerHolder(Class listenerClass) + { + super(Source.EMBEDDED); + setHeldClass(listenerClass); + } public EventListener getListener() { diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java index 8729025410b..21f51b86ad7 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java @@ -153,7 +153,7 @@ public class ServletContextHandler extends ContextHandler /* ------------------------------------------------------------ */ public ServletContextHandler(HandlerContainer parent, String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler,int options) { - super((ContextHandler.Context)null); + super(parent, contextPath); _options=options; _scontext = new Context(); _sessionHandler = sessionHandler; @@ -163,15 +163,6 @@ public class ServletContextHandler extends ContextHandler _objFactory = new DecoratedObjectFactory(); _objFactory.addDecorator(new DeprecationWarning()); - if (contextPath!=null) - setContextPath(contextPath); - - if (parent instanceof HandlerWrapper) - ((HandlerWrapper)parent).setHandler(this); - else if (parent instanceof HandlerCollection) - ((HandlerCollection)parent).addHandler(this); - - // Link the handlers relinkHandlers(); diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java index 4ddaf61793f..34adff4bf68 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java @@ -32,6 +32,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Stream; import javax.servlet.DispatcherType; import javax.servlet.Filter; @@ -67,6 +68,7 @@ import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.DumpableCollection; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -124,8 +126,6 @@ public class ServletHandler extends ScopedHandler @SuppressWarnings("unchecked") protected final Queue[] _chainLRU = new Queue[FilterMapping.ALL]; - - /* ------------------------------------------------------------ */ /** Constructor. */ @@ -133,6 +133,18 @@ public class ServletHandler extends ScopedHandler { } + /* ------------------------------------------------------------ */ + @Override + public void dump(Appendable out, String indent) throws IOException + { + dumpBeans(out,indent, + Collections.singletonList(new DumpableCollection("listeners "+this,_listeners)), + Collections.singletonList(new DumpableCollection("filters "+this,_filters)), + Collections.singletonList(new DumpableCollection("filterMappings "+this,_filterMappings)), + Collections.singletonList(new DumpableCollection("servlets "+this,_servlets)), + Collections.singletonList(new DumpableCollection("servletMappings "+this,_servletMappings))); + } + /* ----------------------------------------------------------------- */ @Override protected synchronized void doStart() @@ -178,7 +190,7 @@ public class ServletHandler extends ScopedHandler if (_contextHandler==null) initialize(); - + super.doStart(); } @@ -257,10 +269,8 @@ public class ServletHandler extends ScopedHandler //Retain only filters and mappings that were added using jetty api (ie Source.EMBEDDED) FilterHolder[] fhs = (FilterHolder[]) LazyList.toArray(filterHolders, FilterHolder.class); - updateBeans(_filters, fhs); _filters = fhs; FilterMapping[] fms = (FilterMapping[]) LazyList.toArray(filterMappings, FilterMapping.class); - updateBeans(_filterMappings, fms); _filterMappings = fms; _matchAfterIndex = (_filterMappings == null || _filterMappings.length == 0 ? -1 : _filterMappings.length-1); @@ -302,10 +312,8 @@ public class ServletHandler extends ScopedHandler //Retain only Servlets and mappings added via jetty apis (ie Source.EMBEDDED) ServletHolder[] shs = (ServletHolder[]) LazyList.toArray(servletHolders, ServletHolder.class); - updateBeans(_servlets, shs); _servlets = shs; ServletMapping[] sms = (ServletMapping[])LazyList.toArray(servletMappings, ServletMapping.class); - updateBeans(_servletMappings, sms); _servletMappings = sms; //Retain only Listeners added via jetty apis (is Source.EMBEDDED) @@ -327,7 +335,6 @@ public class ServletHandler extends ScopedHandler } } ListenerHolder[] listeners = (ListenerHolder[])LazyList.toArray(listenerHolders, ListenerHolder.class); - updateBeans(_listeners, listeners); _listeners = listeners; //will be regenerated on next start @@ -730,60 +737,26 @@ public class ServletHandler extends ScopedHandler { MultiException mx = new MultiException(); - //start filter holders now - if (_filters != null) - { - for (FilterHolder f: _filters) - { + Stream.concat(Stream.concat( + Arrays.stream(_filters), + Arrays.stream(_servlets).sorted()), + Arrays.stream(_listeners)) + .forEach(h->{ try { - f.start(); - f.initialize(); - } - catch (Exception e) - { - mx.add(e); - } - } - } - - // Sort and Initialize servlets - if (_servlets!=null) - { - ServletHolder[] servlets = _servlets.clone(); - Arrays.sort(servlets); - for (ServletHolder servlet : servlets) - { - try - { - servlet.start(); - servlet.initialize(); + if (!h.isStarted()) + { + h.start(); + h.initialize(); + } } catch (Throwable e) { LOG.debug(Log.EXCEPTION, e); mx.add(e); } - } - } + }); - //any other beans - for (Holder h: getBeans(Holder.class)) - { - try - { - if (!h.isStarted()) - { - h.start(); - h.initialize(); - } - } - catch (Exception e) - { - mx.add(e); - } - } - mx.ifExceptionThrow(); } @@ -820,7 +793,6 @@ public class ServletHandler extends ScopedHandler for (ListenerHolder holder:listeners) holder.setServletHandler(this); - updateBeans(_listeners,listeners); _listeners = listeners; } @@ -1537,7 +1509,6 @@ public class ServletHandler extends ScopedHandler */ public void setFilterMappings(FilterMapping[] filterMappings) { - updateBeans(_filterMappings,filterMappings); _filterMappings = filterMappings; if (isStarted()) updateMappings(); invalidateChainsCache(); @@ -1550,7 +1521,6 @@ public class ServletHandler extends ScopedHandler for (FilterHolder holder:holders) holder.setServletHandler(this); - updateBeans(_filters,holders); _filters=holders; updateNameMappings(); invalidateChainsCache(); @@ -1562,7 +1532,6 @@ public class ServletHandler extends ScopedHandler */ public void setServletMappings(ServletMapping[] servletMappings) { - updateBeans(_servletMappings,servletMappings); _servletMappings = servletMappings; if (isStarted()) updateMappings(); invalidateChainsCache(); @@ -1578,7 +1547,6 @@ public class ServletHandler extends ScopedHandler for (ServletHolder holder:holders) holder.setServletHandler(this); - updateBeans(_servlets,holders); _servlets=holders; updateNameMappings(); invalidateChainsCache(); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java index dd2f1f7bfec..a0c230d5fec 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ErrorPageTest.java @@ -31,6 +31,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.LocalConnector; @@ -63,11 +64,16 @@ public class ErrorPageTest context.addServlet(FailServlet.class, "/fail/*"); context.addServlet(FailClosedServlet.class, "/fail-closed/*"); context.addServlet(ErrorServlet.class, "/error/*"); + context.addServlet(AppServlet.class, "/app/*"); + context.addServlet(LongerAppServlet.class, "/longer.app/*"); ErrorPageErrorHandler error = new ErrorPageErrorHandler(); context.setErrorHandler(error); error.addErrorPage(599,"/error/599"); + error.addErrorPage(400,"/error/400"); + // error.addErrorPage(500,"/error/500"); error.addErrorPage(IllegalStateException.class.getCanonicalName(),"/error/TestException"); + error.addErrorPage(BadMessageException.class,"/error/BadMessageException"); error.addErrorPage(ErrorPageErrorHandler.GLOBAL_ERROR_PAGE,"/error/GlobalErrorPage"); _server.start(); @@ -86,7 +92,6 @@ public class ErrorPageTest public void testSendErrorClosedResponse() throws Exception { String response = _connector.getResponse("GET /fail-closed/ HTTP/1.0\r\n\r\n"); - System.out.println(response); assertThat(response,Matchers.containsString("HTTP/1.1 599 599")); assertThat(response,Matchers.containsString("DISPATCH: ERROR")); assertThat(response,Matchers.containsString("ERROR_PAGE: /599")); @@ -157,6 +162,44 @@ public class ErrorPageTest } } + @Test + public void testBadMessage() throws Exception + { + try (StacklessLogging ignore = new StacklessLogging(Dispatcher.class)) + { + String response = _connector.getResponse("GET /app?baa=%88%A4 HTTP/1.0\r\n\r\n"); + assertThat(response, Matchers.containsString("HTTP/1.1 400 Bad query encoding")); + assertThat(response, Matchers.containsString("ERROR_PAGE: /BadMessageException")); + assertThat(response, Matchers.containsString("ERROR_MESSAGE: Bad query encoding")); + assertThat(response, Matchers.containsString("ERROR_CODE: 400")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION: org.eclipse.jetty.http.BadMessageException: 400: Bad query encoding")); + assertThat(response, Matchers.containsString("ERROR_EXCEPTION_TYPE: class org.eclipse.jetty.http.BadMessageException")); + assertThat(response, Matchers.containsString("ERROR_SERVLET: org.eclipse.jetty.servlet.ErrorPageTest$AppServlet-")); + assertThat(response, Matchers.containsString("ERROR_REQUEST_URI: /app")); + assertThat(response, Matchers.containsString("getParameterMap()= {}")); + } + } + + + public static class AppServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + request.getRequestDispatcher("/longer.app/").forward(request, response); + } + } + + public static class LongerAppServlet extends HttpServlet implements Servlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + PrintWriter writer = response.getWriter(); + writer.println(request.getRequestURI()); + } + } + public static class FailServlet extends HttpServlet implements Servlet { @Override @@ -202,6 +245,7 @@ public class ErrorPageTest writer.println("ERROR_EXCEPTION_TYPE: " + request.getAttribute(Dispatcher.ERROR_EXCEPTION_TYPE)); writer.println("ERROR_SERVLET: " + request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); writer.println("ERROR_REQUEST_URI: " + request.getAttribute(Dispatcher.ERROR_REQUEST_URI)); + writer.println("getParameterMap()= " + request.getParameterMap()); } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/component/DumpableCollection.java b/jetty-util/src/main/java/org/eclipse/jetty/util/component/DumpableCollection.java index 369e440758e..0fb4c13f139 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/component/DumpableCollection.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/component/DumpableCollection.java @@ -19,7 +19,9 @@ package org.eclipse.jetty.util.component; import java.io.IOException; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; public class DumpableCollection implements Dumpable { @@ -31,7 +33,12 @@ public class DumpableCollection implements Dumpable _name=name; _collection=collection; } - + + public DumpableCollection(String name,Object... items) + { + this(name, items==null?Collections.emptyList():Arrays.asList(items)); + } + @Override public String dump() { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java index e22e98065b2..eaffe9edd5b 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiReleaseJarFileTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.util; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.File; import java.net.URL; import java.net.URLClassLoader; @@ -31,9 +27,13 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.MultiReleaseJarFile.VersionedJarEntry; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.JRE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class MultiReleaseJarFileTest { private File example = MavenTestingUtils.getTestResourceFile("example.jar"); @@ -124,7 +124,7 @@ public class MultiReleaseJarFileTest @Test - @EnabledOnJre({JRE.JAVA_9, JRE.JAVA_10, JRE.JAVA_11}) + @DisabledOnJre(JRE.JAVA_8) public void testClassLoaderJava9() throws Exception { try(URLClassLoader loader = new URLClassLoader(new URL[]{example.toURI().toURL()})) diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java index fdfaede611b..21454cb806d 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java @@ -1900,14 +1900,10 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor { //Servlet Spec 3.0 p 74 //Duplicate listener declarations don't result in duplicate listener instances - EventListener[] listeners=context.getEventListeners(); - if (listeners!=null) + for (ListenerHolder holder : context.getServletHandler().getListeners()) { - for (EventListener l : listeners) - { - if (l.getClass().getName().equals(className)) - return; - } + if (holder.getClassName().equals(className)) + return; } ((WebDescriptor)descriptor).addClassName(className); diff --git a/pom.xml b/pom.xml index 011626e9bd2..18f65cb915c 100644 --- a/pom.xml +++ b/pom.xml @@ -469,13 +469,6 @@ org.apache.maven.plugins maven-failsafe-plugin ${maven.surefire.version} - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - -
org.apache.maven.plugins diff --git a/tests/test-sessions/test-mongodb-sessions/pom.xml b/tests/test-sessions/test-mongodb-sessions/pom.xml index c1f719beadc..bc6224e3b78 100644 --- a/tests/test-sessions/test-mongodb-sessions/pom.xml +++ b/tests/test-sessions/test-mongodb-sessions/pom.xml @@ -121,7 +121,7 @@ com.github.joelittlejohn.embedmongo embedmongo-maven-plugin - 0.3.5 + 0.4.1 @@ -137,6 +137,8 @@ false + https://jenkins.webtide.net/userContent/ + 2.2.1