Merged branch 'jetty-9.4.x' into 'jetty-9.4.x-2191-jpms_automatic_module_name'.

This commit is contained in:
Simone Bordet 2018-10-04 12:27:31 +02:00
commit bd3eeeaa3f
32 changed files with 1634 additions and 196 deletions

View File

@ -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)
{
}
}
}

View File

@ -105,6 +105,7 @@
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
@ -117,6 +118,17 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-simplekdc</artifactId>
<version>1.1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>

View File

@ -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("(?<schemeOnly>[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)|(?:(?<scheme>[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s+)?(?:(?<token68>[a-zA-Z0-9\\-._~+\\/]+=*)|(?<paramName>[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s*=\\s*(?:(?<paramValue>.*)))");
private static final Pattern CHALLENGE_PATTERN = Pattern.compile("(?<schemeOnly>[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)|(?:(?<scheme>[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s+)?(?:(?<token68>[a-zA-Z0-9\\-._~+/]+=*)|(?<paramName>[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)\\s*=\\s*(?:(?<paramValue>.*)))");
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);
}
}
}

View File

@ -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;
/**
* <p>Implementation of the SPNEGO (or "Negotiate") authentication defined in RFC 4559.</p>
* <p>A {@link #getUserName() user} is logged in via JAAS (either via userName/password or
* via userName/keyTab) once only.</p>
* <p>For every request that needs authentication, a {@link GSSContext} is initiated and
* later established after reading the response from the server.</p>
* <p>Applications should create objects of this class and add them to the
* {@link AuthenticationStore} retrieved from the {@link HttpClient}
* via {@link HttpClient#getAuthenticationStore()}.</p>
*/
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<byte[]> 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<String, Object> 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};
}
}
}

View File

@ -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);
}
}

View File

@ -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
{

View File

@ -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
{

View File

@ -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());
}
}

View File

@ -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

View File

@ -1,3 +1,4 @@
# Format is <user>:<password>,<roles>
basic:basic
digest:digest
spnego_client:,admin

View File

@ -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.

View File

@ -14,7 +14,7 @@
<bundle-symbolic-name>${project.groupId}.boot.test.osgi</bundle-symbolic-name>
<jetty-orbit-url>http://download.eclipse.org/jetty/orbit/</jetty-orbit-url>
<assembly-directory>target/distribution</assembly-directory>
<exam.version>4.11.0</exam.version>
<exam.version>4.12.0</exam.version>
<url.version>2.5.2</url.version>
<injection.bundle.version>1.0</injection.bundle.version>
<skipTests>true</skipTests>
@ -45,15 +45,6 @@
<artifactId>pax-exam-junit4</artifactId>
<version>${exam.version}</version>
<scope>test</scope>
<!-- not anymore as others tests use junit 5 -->
<!--
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
-->
</dependency>
<dependency>
<groupId>org.ops4j.pax.exam</groupId>
@ -405,13 +396,11 @@
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@ -438,6 +427,14 @@
<mavenRepoPath>${settings.localRepository}</mavenRepoPath>
</systemPropertyVariables>
</configuration>
<!-- paxexam still using junit 4 so we have to force the provider here -->
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit47</artifactId>
<version>${maven.surefire.version}</version>
</dependency>
</dependencies>
</plugin>
<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
<plugin>

View File

@ -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

View File

@ -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;
/**
* <p>A configurable (as opposed to using system properties) SPNEGO LoginService.</p>
* <p>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.</p>
* <p>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.</p>
*/
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<SpnegoContext> 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<GSSContext> newGSSContext()
{
return () ->
{
try
{
return _gssManager.createContext(_context._serviceCredential);
}
catch (GSSException x)
{
throw new RuntimeException(x);
}
};
}
private PrivilegedAction<byte[]> 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<String, Object> 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;
}
}
}

View File

@ -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);

View File

@ -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<String> _roles;
private final Subject _subject;
private final Principal _principal;
private final UserIdentity _roleDelegate;
public SpnegoUserIdentity( Subject subject, Principal principal, List<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
/**
* <p>A service to query for user roles.</p>
*/
@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);
/**
* <p>Wraps a {@link LoginService} as an AuthorizationService</p>
*
* @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);
}
}

View File

@ -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;
/**
* <p>A LoginAuthenticator that uses SPNEGO and the GSS API to authenticate requests.</p>
* <p>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.</p>
*
* @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;
}
/**
* <p>Sets the duration of the authentication.</p>
* <p>A negative duration means that the authentication is only valid for the current request.</p>
* <p>A zero duration means that the authentication is valid forever.</p>
* <p>A positive value means that the authentication is valid for the specified duration.</p>
*
* @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;
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -201,7 +201,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners = new CopyOnWriteArrayList<>();
private final List<ContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
private final List<EventListener> _durableListeners = new CopyOnWriteArrayList<>();
private Map<String, Object> _managedAttributes;
private String[] _protectedTargets;
private final CopyOnWriteArrayList<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<ContextHandler.AliasCheck>();
@ -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);
}
/* ------------------------------------------------------------ */

View File

@ -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 <code>true</code> 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;
}

View File

@ -46,7 +46,12 @@ public class ListenerHolder extends BaseHolder<EventListener>
{
super(source);
}
public ListenerHolder(Class<? extends EventListener> listenerClass)
{
super(Source.EMBEDDED);
setHeldClass(listenerClass);
}
public EventListener getListener()
{

View File

@ -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();

View File

@ -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<String>[] _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();

View File

@ -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());
}
}

View File

@ -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()
{

View File

@ -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()}))

View File

@ -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);

View File

@ -469,13 +469,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven.surefire.version}</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@ -121,7 +121,7 @@
<plugin>
<groupId>com.github.joelittlejohn.embedmongo</groupId>
<artifactId>embedmongo-maven-plugin</artifactId>
<version>0.3.5</version>
<version>0.4.1</version>
<configuration>
<!--port>37017</port-->
<!-- allocates a random port and overrides embedmongo.port -->
@ -137,6 +137,8 @@
<!--storageEngine>wiredTiger</storageEngine-->
<!-- optional, skips this plugin entirely, use on the command line like -Dembedmongo.skip -->
<skip>false</skip>
<downloadPath>https://jenkins.webtide.net/userContent/</downloadPath>
<version>2.2.1</version>
</configuration>
<executions>
<execution>