Merged branch 'jetty-9.4.x' into 'jetty-9.4.x-2191-jpms_automatic_module_name'.
This commit is contained in:
commit
bd3eeeaa3f
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# Format is <user>:<password>,<roles>
|
||||
basic:basic
|
||||
digest:digest
|
||||
spnego_client:,admin
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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()}))
|
||||
|
|
|
@ -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);
|
||||
|
|
7
pom.xml
7
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue