mirror of
https://github.com/jetty/jetty.project.git
synced 2025-03-06 05:49:50 +00:00
Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-6566-WebSocketMessageSinks
This commit is contained in:
commit
7569e4b07a
@ -174,6 +174,12 @@ public class FastFileServer
|
||||
x.printStackTrace();
|
||||
async.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
};
|
||||
|
||||
// send "medium" files from an input stream
|
||||
|
@ -511,3 +511,19 @@ Below is an example which, like the one above, sets up a server with a `HashLogi
|
||||
----
|
||||
include::{SRCDIR}/examples/embedded/src/main/java/org/eclipse/jetty/embedded/SecuredHelloHandler.java[]
|
||||
----
|
||||
|
||||
==== JSR 196: Java Authentication Service Provider Interface for Containers (JASPI)
|
||||
|
||||
Jetty can utilize portable authentication modules that implements the Jakarta Authentication specification. This requires the jetty-jaspi module.
|
||||
|
||||
Only modules conforming to the ServerAuthModule interface in the https://www.jcp.org/en/jsr/detail?id=196[JASPI Spec] are supported. These modules must be configured before start-up.
|
||||
|
||||
The following illustrates a jetty module setting up HTTP Basic Authentication using an Authentication module that comes packaged with the jetty-jaspi module: `org.eclipse.jetty.security.jaspi.modules.BasicAuthenticationAuthModule`
|
||||
|
||||
[source, xml, subs="{sub-order}"]
|
||||
----
|
||||
include::{SRCDIR}/jetty-jaspi/src/main/config/etc/jaspi/jaspi-demo.xml[tags=documentation]
|
||||
----
|
||||
|
||||
Given the portability goal of Jakarta Authentication, custom or 3rd party `ServerAuthModule` implementations may be configured instead here.
|
||||
|
||||
|
@ -34,6 +34,7 @@ include::annotations/chapter.adoc[]
|
||||
include::jsp/chapter.adoc[]
|
||||
include::jndi/chapter.adoc[]
|
||||
include::jaas/chapter.adoc[]
|
||||
include::jaspi/chapter.adoc[]
|
||||
include::jmx/chapter.adoc[]
|
||||
include::logging/chapter.adoc[]
|
||||
include::troubleshooting/chapter.adoc[]
|
||||
|
@ -0,0 +1,68 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
[[og-jaspi]]
|
||||
=== JASPI
|
||||
|
||||
Enabling this module allows Jetty to utilize authentication modules that implement the JSR 196 (JASPI) specification. JASPI provides an SPI (Service Provider Interface) for pluggable, portable, and standardized authentication modules. Compatible modules are portable between servers that support the JASPI specification. This module provides a bridge from Java Authentication to the Jetty Security framework.
|
||||
|
||||
Only modules conforming to the "Servlet Container Profile" with the ServerAuthModule interface within the https://www.jcp.org/en/jsr/detail?id=196[JASPI Spec] are supported. These modules must be configured before start-up. Operations for runtime registering or de-registering authentication modules are not supported.
|
||||
|
||||
[[og-jaspi-configuration]]
|
||||
==== Configuration
|
||||
|
||||
[[og-jaspi-module]]
|
||||
===== The `jaspi` module
|
||||
|
||||
Enable the `jaspi` module:
|
||||
|
||||
----
|
||||
include::{JETTY_HOME}/modules/jaspi.mod[]
|
||||
----
|
||||
|
||||
[[og-jaspi-xml]]
|
||||
===== Configure JASPI
|
||||
|
||||
To enable the `jaspi` module you can use the following command (issued from within the `$JETTY_BASE` directory):
|
||||
|
||||
----
|
||||
$ java -jar $JETTY_HOME/start.jar --add-modules=jaspi
|
||||
----
|
||||
|
||||
You can then register a `AuthConfigProvider` onto the static `AuthConfigFactory` obtained with `AuthConfigFactory.getFactory()`. This registration can be done in the XML configuration file which will be copied to `$JETTY_BASE/etc/jaspi/jaspi-authmoduleconfig.xml` when the module is enabled.
|
||||
|
||||
====== JASPI Demo
|
||||
The `jaspi-demo` module illustrates setting up HTTP Basic Authentication using a Java Authentication module that comes packaged with jetty: `org.eclipse.jetty.security.jaspi.modules.BasicAuthenticationAuthModule`, and applies it for a context named `/test`.
|
||||
|
||||
[source, xml]
|
||||
----
|
||||
include::{JETTY_HOME}/etc/jaspi/jaspi-demo.xml[]
|
||||
----
|
||||
|
||||
This example uses the `AuthConfigProvider` implementation provided by Jetty to register a `ServerAuthModule` directly. Other custom or 3rd party modules that are compatible with the `ServerAuthModule` interface in JASPI can be registered in the same way.
|
||||
|
||||
===== Integration with Jetty Authentication Mechanisms
|
||||
|
||||
To integrate with Jetty authentication mechanisms you must add a `LoginService` to your context. The `LoginService` provides a way for you to obtain a `UserIdentity` from a username and credentials. JASPI can interact with this Jetty `LoginService` by using the `PasswordValidationCallback`.
|
||||
|
||||
The `CallerPrincipalCallback` and `GroupPrincipalCallback` do not require use of a Jetty `LoginService`. The principal from the `CallerPrincipalCallback` will be used directly with the `IdentityService` to produce a `UserIdentity`.
|
||||
|
||||
===== Replacing the Jetty DefaultAuthConfigFactory
|
||||
|
||||
Jetty provides an implementation of the `AuthConfigFactory` interface which is used to register `AuthConfigProviders`. This can be replaced by a custom implementation by adding a custom module which provides `auth-config-factory`.
|
||||
This custom module must reference an XML file which sets a new instance of the `AuthConfigFactory` with the static method `AuthConfigFactory.setFactory()`.
|
||||
For an example of this see the `jaspi-default-auth-config-factory` module, which provides the default implementation used by Jetty.
|
||||
|
||||
----
|
||||
include::{JETTY_HOME}/modules/jaspi-default-auth-config-factory.mod[]
|
||||
----
|
@ -60,14 +60,16 @@ import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.toolchain.test.Net;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.ExecutorThreadPool;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledOnJre;
|
||||
import org.junit.jupiter.api.condition.EnabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
@ -365,7 +367,7 @@ public class HttpClientTLSTest
|
||||
|
||||
// Excluded in JDK 11+ because resumed sessions cannot be compared
|
||||
// using their session IDs even though they are resumed correctly.
|
||||
@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10})
|
||||
@EnabledForJreRange(max = JRE.JAVA_10)
|
||||
@Test
|
||||
public void testHandshakeSucceededWithSessionResumption() throws Exception
|
||||
{
|
||||
@ -445,7 +447,7 @@ public class HttpClientTLSTest
|
||||
|
||||
// Excluded in JDK 11+ because resumed sessions cannot be compared
|
||||
// using their session IDs even though they are resumed correctly.
|
||||
@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10})
|
||||
@EnabledForJreRange(max = JRE.JAVA_10)
|
||||
@Test
|
||||
public void testClientRawCloseDoesNotInvalidateSession() throws Exception
|
||||
{
|
||||
@ -1013,7 +1015,6 @@ public class HttpClientTLSTest
|
||||
// Force TLS-level hostName verification, as we want to receive the correspondent certificate.
|
||||
clientTLS.setEndpointIdentificationAlgorithm("HTTPS");
|
||||
startClient(clientTLS);
|
||||
|
||||
clientTLS.setSNIProvider(SslContextFactory.Client.SniProvider.NON_DOMAIN_SNI_PROVIDER);
|
||||
|
||||
// Send a request with SNI "localhost", we should get the certificate at alias=localhost.
|
||||
@ -1027,18 +1028,40 @@ public class HttpClientTLSTest
|
||||
.scheme(HttpScheme.HTTPS.asString())
|
||||
.send();
|
||||
assertEquals(HttpStatus.OK_200, response2.getStatus());
|
||||
}
|
||||
|
||||
/* TODO Fix. See #6624
|
||||
if (Net.isIpv6InterfaceAvailable())
|
||||
@Test
|
||||
@EnabledForJreRange(max = JRE.JAVA_16, disabledReason = "Since Java 17, SNI host names can only have letter|digit|hyphen characters.")
|
||||
public void testForcedNonDomainSNIWithIPv6() throws Exception
|
||||
{
|
||||
Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable());
|
||||
|
||||
SslContextFactory.Server serverTLS = new SslContextFactory.Server();
|
||||
serverTLS.setKeyStorePath("src/test/resources/keystore_sni_non_domain.p12");
|
||||
serverTLS.setKeyStorePassword("storepwd");
|
||||
serverTLS.setSNISelector((keyType, issuers, session, sniHost, certificates) ->
|
||||
{
|
||||
// Send a request with SNI "[::1]", we should get the certificate at alias=ip.
|
||||
ContentResponse response3 = client.newRequest("[::1]", connector.getLocalPort())
|
||||
.scheme(HttpScheme.HTTPS.asString())
|
||||
.send();
|
||||
// We have forced the client to send the non-domain SNI.
|
||||
assertNotNull(sniHost);
|
||||
return serverTLS.sniSelect(keyType, issuers, session, sniHost, certificates);
|
||||
});
|
||||
startServer(serverTLS, new EmptyServerHandler());
|
||||
|
||||
assertEquals(HttpStatus.OK_200, response3.getStatus());
|
||||
}
|
||||
*/
|
||||
SslContextFactory.Client clientTLS = new SslContextFactory.Client();
|
||||
// Trust any certificate received by the server.
|
||||
clientTLS.setTrustStorePath("src/test/resources/keystore_sni_non_domain.p12");
|
||||
clientTLS.setTrustStorePassword("storepwd");
|
||||
// Force TLS-level hostName verification, as we want to receive the correspondent certificate.
|
||||
clientTLS.setEndpointIdentificationAlgorithm("HTTPS");
|
||||
startClient(clientTLS);
|
||||
clientTLS.setSNIProvider(SslContextFactory.Client.SniProvider.NON_DOMAIN_SNI_PROVIDER);
|
||||
|
||||
// Send a request with SNI "[::1]", we should get the certificate at alias=ip.
|
||||
ContentResponse response3 = client.newRequest("[::1]", connector.getLocalPort())
|
||||
.scheme(HttpScheme.HTTPS.asString())
|
||||
.send();
|
||||
|
||||
assertEquals(HttpStatus.OK_200, response3.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -68,7 +68,6 @@ 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.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnJre;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
@ -82,7 +81,6 @@ 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;
|
||||
|
||||
// Other JREs have slight differences in how TLS work
|
||||
// and this test expects a very specific TLS behavior.
|
||||
@ -1027,7 +1025,6 @@ public class SslBytesServerTest extends SslBytesTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(WINDOWS) // Don't run on Windows (buggy JVM)
|
||||
public void testRequestWithBigContentWriteBlockedThenReset() throws Exception
|
||||
{
|
||||
final SSLSocket client = newClient();
|
||||
@ -1082,7 +1079,6 @@ public class SslBytesServerTest extends SslBytesTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(WINDOWS) // Don't run on Windows (buggy JVM)
|
||||
public void testRequestWithBigContentReadBlockedThenReset() throws Exception
|
||||
{
|
||||
final SSLSocket client = newClient();
|
||||
|
@ -33,41 +33,49 @@ import org.eclipse.jetty.server.handler.DefaultHandler;
|
||||
import org.eclipse.jetty.server.handler.HandlerCollection;
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.Scanner;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
import org.eclipse.jetty.webapp.WebAppContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class DeploymentTempDirTest
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(DeploymentTempDirTest.class);
|
||||
public WorkDir workDir;
|
||||
|
||||
private final WebAppProvider webAppProvider = new WebAppProvider();
|
||||
private final ContextHandlerCollection contexts = new ContextHandlerCollection();
|
||||
private final Path testDir = MavenTestingUtils.getTargetTestingPath(DeploymentTempDirTest.class.getSimpleName());
|
||||
private final Path tmpDir = testDir.resolve("tmpDir");
|
||||
private final Path webapps = testDir.resolve("webapps");
|
||||
private final Server server = new Server();
|
||||
private Path tmpDir;
|
||||
private Path webapps;
|
||||
private Server server;
|
||||
private WebAppProvider webAppProvider;
|
||||
private ContextHandlerCollection contexts;
|
||||
private final TestListener listener = new TestListener();
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception
|
||||
{
|
||||
Path testDir = workDir.getEmptyPathDir();
|
||||
tmpDir = testDir.resolve("tmpDir");
|
||||
webapps = testDir.resolve("webapps");
|
||||
|
||||
FS.ensureDirExists(tmpDir);
|
||||
FS.ensureDirExists(webapps);
|
||||
|
||||
server = new Server();
|
||||
|
||||
ServerConnector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
FS.ensureEmpty(testDir);
|
||||
FS.ensureEmpty(tmpDir);
|
||||
FS.ensureEmpty(webapps);
|
||||
webAppProvider = new WebAppProvider();
|
||||
|
||||
webAppProvider.setMonitoredDirName(webapps.toString());
|
||||
webAppProvider.setScanInterval(0);
|
||||
@ -75,6 +83,7 @@ public class DeploymentTempDirTest
|
||||
deploymentManager.addAppProvider(webAppProvider);
|
||||
server.addBean(deploymentManager);
|
||||
|
||||
contexts = new ContextHandlerCollection();
|
||||
HandlerCollection handlerCollection = new HandlerCollection();
|
||||
handlerCollection.addHandler(contexts);
|
||||
handlerCollection.addHandler(new DefaultHandler());
|
||||
|
@ -26,13 +26,10 @@ import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
/**
|
||||
* Similar in scope to {@link ScanningAppProviderStartupTest}, except is concerned with the modification of existing
|
||||
* deployed webapps due to incoming changes identified by the {@link ScanningAppProvider}.
|
||||
@ -155,8 +152,6 @@ public class ScanningAppProviderRuntimeUpdatesTest
|
||||
* @throws Exception on test failure
|
||||
*/
|
||||
@Test
|
||||
@DisabledOnOs(WINDOWS)
|
||||
// This test will not work on Windows as second war file would, not be written over the first one because of a file lock
|
||||
public void testAfterStartupThenUpdateContext() throws Exception
|
||||
{
|
||||
jetty.copyWebapp("foo-webapp-1.war", "foo.war");
|
||||
|
@ -99,6 +99,8 @@ public class WebAppProviderTest
|
||||
@Test
|
||||
public void testStartupContext()
|
||||
{
|
||||
assumeTrue(symlinkSupported);
|
||||
|
||||
// Check Server for Handlers
|
||||
jetty.assertWebAppContextsExists("/bar", "/foo", "/bob");
|
||||
|
||||
|
@ -26,7 +26,7 @@ lib/logging/slf4j-jdk14-${slf4j.version}.jar
|
||||
-Djava.util.logging.config.file=${jetty.base}/resources/java-util-logging.properties
|
||||
|
||||
[ini]
|
||||
slf4j.version?=2.0.0-alpha1
|
||||
slf4j.version?=2.0.0-alpha4
|
||||
|
||||
[license]
|
||||
SLF4J is distributed under the MIT License.
|
||||
|
@ -24,7 +24,7 @@ lib/logging/logback-classic-${logback.version}.jar
|
||||
lib/logging/logback-core-${logback.version}.jar
|
||||
|
||||
[ini]
|
||||
logback.version?=1.3.0-alpha5
|
||||
logback.version?=1.3.0-alpha9
|
||||
jetty.webapp.addServerClasses+=,ch.qos.logback.
|
||||
|
||||
[license]
|
||||
|
@ -15,5 +15,5 @@ slf4j
|
||||
lib/logging/slf4j-api-${slf4j.version}.jar
|
||||
|
||||
[ini]
|
||||
slf4j.version?=2.0.0-alpha1
|
||||
slf4j.version?=2.0.0-alpha4
|
||||
jetty.webapp.addServerClasses+=,org.slf4j.
|
||||
|
@ -135,6 +135,12 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
|
||||
close();
|
||||
promise.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConnectionListener implements Connection.Listener
|
||||
|
@ -29,9 +29,12 @@ import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Unable to create all of the streams")
|
||||
public class ConcurrentStreamCreationTest extends AbstractTest
|
||||
{
|
||||
@Test
|
||||
|
@ -98,9 +98,8 @@ public class SmallThreadPoolLoadTest extends AbstractTest
|
||||
Thread testThread = Thread.currentThread();
|
||||
Scheduler.Task task = client.getScheduler().schedule(() ->
|
||||
{
|
||||
logger.warn("Interrupting test, it is taking too long{}Server:{}{}{}Client:{}{}",
|
||||
System.lineSeparator(), System.lineSeparator(), server.dump(),
|
||||
System.lineSeparator(), System.lineSeparator(), client.dump());
|
||||
logger.warn("Interrupting test, it is taking too long - \nServer: \n" +
|
||||
server.dump() + "\nClient: \n" + client.dump());
|
||||
testThread.interrupt();
|
||||
}, iterations * factor, TimeUnit.MILLISECONDS);
|
||||
|
||||
@ -184,9 +183,8 @@ public class SmallThreadPoolLoadTest extends AbstractTest
|
||||
if (success)
|
||||
latch.countDown();
|
||||
else
|
||||
logger.warn("Request {} took too long{}Server:{}{}{}Client:{}{}", requestId,
|
||||
System.lineSeparator(), System.lineSeparator(), server.dump(),
|
||||
System.lineSeparator(), System.lineSeparator(), client.dump());
|
||||
logger.warn("Request {} took too long - \nServer: \n" +
|
||||
server.dump() + "\nClient: \n" + client.dump(), requestId);
|
||||
return !reset.get();
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IteratingCallback;
|
||||
import org.eclipse.jetty.util.component.Dumpable;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.util.thread.Invocable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -50,6 +51,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||
private final Collection<Entry> processedEntries = new ArrayList<>();
|
||||
private final HTTP2Session session;
|
||||
private final ByteBufferPool.Lease lease;
|
||||
private InvocationType invocationType = InvocationType.NON_BLOCKING;
|
||||
private Throwable terminated;
|
||||
private Entry stalledEntry;
|
||||
|
||||
@ -59,6 +61,12 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||
this.lease = new ByteBufferPool.Lease(session.getGenerator().getByteBufferPool());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return invocationType;
|
||||
}
|
||||
|
||||
public void window(IStream stream, WindowUpdateFrame frame)
|
||||
{
|
||||
Throwable closed;
|
||||
@ -214,7 +222,10 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||
// We use ArrayList contains() + add() instead of HashSet add()
|
||||
// because that is faster for collections of size up to 250 entries.
|
||||
if (!processedEntries.contains(entry))
|
||||
{
|
||||
processedEntries.add(entry);
|
||||
invocationType = Invocable.combine(invocationType, Invocable.getInvocationType(entry.getCallback()));
|
||||
}
|
||||
|
||||
if (entry.getDataBytesRemaining() == 0)
|
||||
pending.remove();
|
||||
@ -311,6 +322,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||
|
||||
processedEntries.forEach(Entry::succeeded);
|
||||
processedEntries.clear();
|
||||
invocationType = InvocationType.NON_BLOCKING;
|
||||
|
||||
if (stalledEntry != null)
|
||||
{
|
||||
|
@ -72,6 +72,7 @@ import org.eclipse.jetty.util.annotation.ManagedObject;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
import org.eclipse.jetty.util.component.DumpableCollection;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.util.thread.Invocable;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -1991,7 +1992,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
||||
|
||||
private void sendGoAwayAndTerminate(GoAwayFrame frame, GoAwayFrame eventFrame)
|
||||
{
|
||||
sendGoAway(frame, Callback.from(() -> terminate(eventFrame)));
|
||||
sendGoAway(frame, Callback.from(Callback.NOOP, () -> terminate(eventFrame)));
|
||||
}
|
||||
|
||||
private void sendGoAway(GoAwayFrame frame, Callback callback)
|
||||
@ -2197,7 +2198,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
||||
stream.setListener(listener);
|
||||
stream.process(new PrefaceFrame(), Callback.NOOP);
|
||||
|
||||
Callback streamCallback = Callback.from(() -> promise.succeeded(stream), x ->
|
||||
Callback streamCallback = Callback.from(Invocable.InvocationType.NON_BLOCKING, () -> promise.succeeded(stream), x ->
|
||||
{
|
||||
HTTP2Session.this.onStreamDestroyed(streamId);
|
||||
promise.failed(x);
|
||||
|
@ -742,6 +742,15 @@ public class HTTP2Stream implements IStream, Callback, Dumpable, CyclicTimeouts.
|
||||
callback.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
return sendCallback != null ? sendCallback.getInvocationType() : Callback.super.getInvocationType();
|
||||
}
|
||||
}
|
||||
|
||||
private Callback endWrite()
|
||||
{
|
||||
try (AutoLock l = lock.lock())
|
||||
|
@ -63,6 +63,11 @@
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -0,0 +1,281 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http2.client.http;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.IntStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.HTTP2Session;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
|
||||
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
|
||||
import org.eclipse.jetty.io.AbstractEndPoint;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.FuturePromise;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class BlockedWritesWithSmallThreadPoolTest
|
||||
{
|
||||
private Server server;
|
||||
private ServerConnector connector;
|
||||
private QueuedThreadPool serverThreads;
|
||||
private HTTP2Client client;
|
||||
|
||||
private void start(Handler handler) throws Exception
|
||||
{
|
||||
// Threads: 1 acceptor, 1 selector, 1 reserved, 1 application.
|
||||
serverThreads = newSmallThreadPool("server", 4);
|
||||
server = new Server(serverThreads);
|
||||
HTTP2CServerConnectionFactory http2 = new HTTP2CServerConnectionFactory(new HttpConfiguration());
|
||||
connector = new ServerConnector(server, 1, 1, http2);
|
||||
server.addConnector(connector);
|
||||
server.setHandler(handler);
|
||||
server.start();
|
||||
}
|
||||
|
||||
private void start(RawHTTP2ServerConnectionFactory factory) throws Exception
|
||||
{
|
||||
// Threads: 1 acceptor, 1 selector, 1 reserved, 1 application.
|
||||
serverThreads = newSmallThreadPool("server", 4);
|
||||
server = new Server(serverThreads);
|
||||
connector = new ServerConnector(server, 1, 1, factory);
|
||||
server.addConnector(connector);
|
||||
server.start();
|
||||
}
|
||||
|
||||
private QueuedThreadPool newSmallThreadPool(String name, int maxThreads)
|
||||
{
|
||||
QueuedThreadPool pool = new QueuedThreadPool(maxThreads, maxThreads);
|
||||
pool.setName(name);
|
||||
pool.setReservedThreads(1);
|
||||
pool.setDetailedDump(true);
|
||||
return pool;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void dispose()
|
||||
{
|
||||
LifeCycle.stop(client);
|
||||
LifeCycle.stop(server);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServerThreadsBlockedInWrites() throws Exception
|
||||
{
|
||||
int contentLength = 16 * 1024 * 1024;
|
||||
AtomicReference<AbstractEndPoint> serverEndPointRef = new AtomicReference<>();
|
||||
start(new EmptyServerHandler()
|
||||
{
|
||||
@Override
|
||||
protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
|
||||
{
|
||||
serverEndPointRef.compareAndSet(null, (AbstractEndPoint)jettyRequest.getHttpChannel().getEndPoint());
|
||||
// Write a large content to cause TCP congestion.
|
||||
response.getOutputStream().write(new byte[contentLength]);
|
||||
}
|
||||
});
|
||||
|
||||
client = new HTTP2Client();
|
||||
// Set large flow control windows so the server hits TCP congestion.
|
||||
int window = 2 * contentLength;
|
||||
client.setInitialSessionRecvWindow(window);
|
||||
client.setInitialStreamRecvWindow(window);
|
||||
client.start();
|
||||
|
||||
FuturePromise<Session> promise = new FuturePromise<>();
|
||||
client.connect(new InetSocketAddress("localhost", connector.getLocalPort()), new Session.Listener.Adapter(), promise);
|
||||
Session session = promise.get(5, SECONDS);
|
||||
|
||||
CountDownLatch clientBlockLatch = new CountDownLatch(1);
|
||||
CountDownLatch clientDataLatch = new CountDownLatch(1);
|
||||
// Send a request to TCP congest the server.
|
||||
HttpURI uri = HttpURI.build("http://localhost:" + connector.getLocalPort() + "/congest");
|
||||
MetaData.Request request = new MetaData.Request("GET", uri, HttpVersion.HTTP_2, HttpFields.EMPTY);
|
||||
session.newStream(new HeadersFrame(request, null, true), new Promise.Adapter<>(), new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onData(Stream stream, DataFrame frame, Callback callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Block here to stop reading from the network
|
||||
// to cause the server to TCP congest.
|
||||
clientBlockLatch.await(5, SECONDS);
|
||||
callback.succeeded();
|
||||
if (frame.isEndStream())
|
||||
clientDataLatch.countDown();
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
callback.failed(x);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await().atMost(5, SECONDS).until(() ->
|
||||
{
|
||||
AbstractEndPoint serverEndPoint = serverEndPointRef.get();
|
||||
return serverEndPoint != null && serverEndPoint.getWriteFlusher().isPending();
|
||||
});
|
||||
// Wait for NIO on the server to be OP_WRITE interested.
|
||||
Thread.sleep(1000);
|
||||
|
||||
// Make sure there is a reserved thread.
|
||||
if (serverThreads.getAvailableReservedThreads() != 1)
|
||||
{
|
||||
assertFalse(serverThreads.tryExecute(() -> {}));
|
||||
await().atMost(5, SECONDS).until(() -> serverThreads.getAvailableReservedThreads() == 1);
|
||||
}
|
||||
// Use the reserved thread for a blocking operation, simulating another blocking write.
|
||||
CountDownLatch serverBlockLatch = new CountDownLatch(1);
|
||||
assertTrue(serverThreads.tryExecute(() -> await().atMost(20, SECONDS).until(() -> serverBlockLatch.await(15, SECONDS), b -> true)));
|
||||
|
||||
assertEquals(0, serverThreads.getReadyThreads());
|
||||
|
||||
// Unblock the client to read from the network, which should unblock the server write().
|
||||
clientBlockLatch.countDown();
|
||||
|
||||
assertTrue(clientDataLatch.await(10, SECONDS), server.dump());
|
||||
serverBlockLatch.countDown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientThreadsBlockedInWrite() throws Exception
|
||||
{
|
||||
int contentLength = 16 * 1024 * 1024;
|
||||
CountDownLatch serverBlockLatch = new CountDownLatch(1);
|
||||
RawHTTP2ServerConnectionFactory http2 = new RawHTTP2ServerConnectionFactory(new HttpConfiguration(), new ServerSessionListener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
return new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onData(Stream stream, DataFrame frame, Callback callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Block here to stop reading from the network
|
||||
// to cause the client to TCP congest.
|
||||
serverBlockLatch.await(5, SECONDS);
|
||||
callback.succeeded();
|
||||
if (frame.isEndStream())
|
||||
{
|
||||
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, HttpFields.EMPTY);
|
||||
stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP);
|
||||
}
|
||||
}
|
||||
catch (InterruptedException x)
|
||||
{
|
||||
callback.failed(x);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
int window = 2 * contentLength;
|
||||
http2.setInitialSessionRecvWindow(window);
|
||||
http2.setInitialStreamRecvWindow(window);
|
||||
start(http2);
|
||||
|
||||
client = new HTTP2Client();
|
||||
// Threads: 1 selector, 1 reserved, 1 application.
|
||||
QueuedThreadPool clientThreads = newSmallThreadPool("client", 3);
|
||||
client.setExecutor(clientThreads);
|
||||
client.setSelectors(1);
|
||||
client.start();
|
||||
|
||||
FuturePromise<Session> promise = new FuturePromise<>();
|
||||
client.connect(new InetSocketAddress("localhost", connector.getLocalPort()), new Session.Listener.Adapter(), promise);
|
||||
Session session = promise.get(5, SECONDS);
|
||||
|
||||
// Send a request to TCP congest the client.
|
||||
HttpURI uri = HttpURI.build("http://localhost:" + connector.getLocalPort() + "/congest");
|
||||
MetaData.Request request = new MetaData.Request("GET", uri, HttpVersion.HTTP_2, HttpFields.EMPTY);
|
||||
FuturePromise<Stream> streamPromise = new FuturePromise<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
session.newStream(new HeadersFrame(request, null, false), streamPromise, new Stream.Listener.Adapter()
|
||||
{
|
||||
@Override
|
||||
public void onHeaders(Stream stream, HeadersFrame frame)
|
||||
{
|
||||
MetaData.Response response = (MetaData.Response)frame.getMetaData();
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
Stream stream = streamPromise.get(5, SECONDS);
|
||||
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(contentLength), true), Callback.NOOP);
|
||||
|
||||
await().atMost(5, SECONDS).until(() ->
|
||||
{
|
||||
AbstractEndPoint clientEndPoint = (AbstractEndPoint)((HTTP2Session)session).getEndPoint();
|
||||
return clientEndPoint.getWriteFlusher().isPending();
|
||||
});
|
||||
// Wait for NIO on the client to be OP_WRITE interested.
|
||||
Thread.sleep(1000);
|
||||
|
||||
CountDownLatch clientBlockLatch = new CountDownLatch(1);
|
||||
// Make sure the application thread is blocked.
|
||||
clientThreads.execute(() -> await().until(() -> clientBlockLatch.await(15, SECONDS), b -> true));
|
||||
// Make sure the reserved thread is blocked.
|
||||
if (clientThreads.getAvailableReservedThreads() != 1)
|
||||
{
|
||||
assertFalse(clientThreads.tryExecute(() -> {}));
|
||||
await().atMost(5, SECONDS).until(() -> clientThreads.getAvailableReservedThreads() == 1);
|
||||
}
|
||||
// Use the reserved thread for a blocking operation, simulating another blocking write.
|
||||
assertTrue(clientThreads.tryExecute(() -> await().until(() -> clientBlockLatch.await(15, SECONDS), b -> true)));
|
||||
|
||||
await().atMost(5, SECONDS).until(() -> clientThreads.getReadyThreads() == 0);
|
||||
|
||||
// Unblock the server to read from the network, which should unblock the client.
|
||||
serverBlockLatch.countDown();
|
||||
|
||||
assertTrue(latch.await(10, SECONDS), client.dump());
|
||||
clientBlockLatch.countDown();
|
||||
}
|
||||
}
|
@ -486,7 +486,7 @@ public class ClientConnector extends ContainerLifeCycle
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Could not configure {} to {} on {}", option, value, channel);
|
||||
LOG.debug("Could not configure {} to {} on {}", option, value, channel, x);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,17 +80,5 @@
|
||||
<version>1.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.geronimo.components</groupId>
|
||||
<artifactId>geronimo-jaspi</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.geronimo.specs</groupId>
|
||||
<artifactId>geronimo-jaspic_1.0_spec</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
|
||||
|
||||
<Configure>
|
||||
<Call class="javax.security.auth.message.config.AuthConfigFactory" name="getFactory">
|
||||
<!-- Configure the AuthConfigFactory here. -->
|
||||
</Call>
|
||||
</Configure>
|
20
jetty-jaspi/src/main/config/etc/jaspi/jaspi-default.xml
Normal file
20
jetty-jaspi/src/main/config/etc/jaspi/jaspi-default.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
|
||||
|
||||
<Configure id="Server" class="org.eclipse.jetty.server.Server">
|
||||
|
||||
<!-- ===================================================================== -->
|
||||
<!-- Configure a factory for Jaspi -->
|
||||
<!-- ===================================================================== -->
|
||||
<Call class="javax.security.auth.message.config.AuthConfigFactory" name="setFactory">
|
||||
<Arg>
|
||||
<New id="jaspiAuthConfigFactory" class="org.eclipse.jetty.security.jaspi.DefaultAuthConfigFactory" />
|
||||
</Arg>
|
||||
</Call>
|
||||
<Call name="addBean">
|
||||
<Arg>
|
||||
<Ref refid="jaspiAuthConfigFactory" />
|
||||
</Arg>
|
||||
<Arg type="boolean">false</Arg>
|
||||
</Call>
|
||||
</Configure>
|
48
jetty-jaspi/src/main/config/etc/jaspi/jaspi-demo.xml
Normal file
48
jetty-jaspi/src/main/config/etc/jaspi/jaspi-demo.xml
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
|
||||
|
||||
<Configure>
|
||||
<Call class="javax.security.auth.message.config.AuthConfigFactory" name="getFactory">
|
||||
<Call name="registerConfigProvider">
|
||||
|
||||
<!-- The Jetty provided implementation of AuthConfigProvider which will wrap a ServerAuthModule. -->
|
||||
<Arg type="String">org.eclipse.jetty.security.jaspi.provider.JaspiAuthConfigProvider</Arg>
|
||||
|
||||
<!-- A Map of initialization properties. -->
|
||||
<Arg>
|
||||
<Map>
|
||||
<Entry>
|
||||
<!-- Provide the fully qualified classname of the ServerAuthModule to be used. -->
|
||||
<Item>ServerAuthModule</Item>
|
||||
<Item>org.eclipse.jetty.security.jaspi.modules.BasicAuthenticationAuthModule</Item>
|
||||
</Entry>
|
||||
<Entry>
|
||||
<!-- Realm as utilised by Jetty Security -->
|
||||
<Item>org.eclipse.jetty.security.jaspi.modules.RealmName</Item>
|
||||
<Item>Test Realm</Item>
|
||||
</Entry>
|
||||
</Map>
|
||||
</Arg>
|
||||
|
||||
<!-- Message Layer Identifier as per spec chapter 3.1 -->
|
||||
<Arg type="String">HttpServlet</Arg>
|
||||
|
||||
<!-- Application Context Identifier as per spec chapter 3.2
|
||||
|
||||
AppContextID ::= hostname blank context-path
|
||||
The algorithm applied here will use the
|
||||
_serverName on the configured JaspiAuthenticatorFactory (if set) and try to match it
|
||||
against the "server" part (in the "server /test" example below).
|
||||
Next it will try to match the ServletContext#getVirtualServerName to the "server" part.
|
||||
If neither are set, it will then try to match the first Subject's principal name, and finally fall back to
|
||||
the default value "server" if none are available.
|
||||
|
||||
The context-path should match the context path where this applies.
|
||||
-->
|
||||
<Arg type="String">server /test</Arg>
|
||||
|
||||
<!-- A friendly description of the provided auth-module. -->
|
||||
<Arg type="String">A simple provider using HTTP BASIC authentication.</Arg>
|
||||
</Call>
|
||||
</Call>
|
||||
</Configure>
|
@ -0,0 +1,16 @@
|
||||
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
|
||||
|
||||
[description]
|
||||
Provides a DefaultAuthConfigFactory for jaspi
|
||||
|
||||
[tags]
|
||||
security
|
||||
|
||||
[depend]
|
||||
security
|
||||
|
||||
[provide]
|
||||
auth-config-factory
|
||||
|
||||
[xml]
|
||||
etc/jaspi/jaspi-default.xml
|
16
jetty-jaspi/src/main/config/modules/jaspi-demo.mod
Normal file
16
jetty-jaspi/src/main/config/modules/jaspi-demo.mod
Normal file
@ -0,0 +1,16 @@
|
||||
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
|
||||
|
||||
[description]
|
||||
Enables JASPI basic authentication the /test context path.
|
||||
|
||||
[tags]
|
||||
security
|
||||
|
||||
[depend]
|
||||
jaspi
|
||||
|
||||
[xml]
|
||||
etc/jaspi/jaspi-demo.xml
|
||||
|
||||
[files]
|
||||
basehome:etc/jaspi/jaspi-demo.xml|etc/jaspi/jaspi-demo.xml
|
@ -3,9 +3,20 @@
|
||||
[description]
|
||||
Enables JASPI authentication for deployed web applications.
|
||||
|
||||
[tags]
|
||||
security
|
||||
|
||||
[depend]
|
||||
security
|
||||
auth-config-factory
|
||||
|
||||
[lib]
|
||||
lib/jetty-jaspi-${jetty.version}.jar
|
||||
lib/jaspi/*.jar
|
||||
|
||||
[xml]
|
||||
etc/jaspi/jaspi-authmoduleconfig.xml
|
||||
|
||||
[files]
|
||||
basehome:etc/jaspi/jaspi-authmoduleconfig.xml|etc/jaspi/jaspi-authmoduleconfig.xml
|
||||
|
||||
|
@ -19,8 +19,9 @@ module org.eclipse.jetty.security.jaspi
|
||||
exports org.eclipse.jetty.security.jaspi;
|
||||
exports org.eclipse.jetty.security.jaspi.callback;
|
||||
exports org.eclipse.jetty.security.jaspi.modules;
|
||||
exports org.eclipse.jetty.security.jaspi.provider;
|
||||
|
||||
requires javax.security.auth.message;
|
||||
requires transitive javax.security.auth.message;
|
||||
requires jetty.servlet.api;
|
||||
requires transitive org.eclipse.jetty.security;
|
||||
requires org.slf4j;
|
||||
|
@ -0,0 +1,249 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.jaspi;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import javax.security.auth.AuthPermission;
|
||||
import javax.security.auth.message.config.AuthConfigFactory;
|
||||
import javax.security.auth.message.config.AuthConfigProvider;
|
||||
import javax.security.auth.message.config.RegistrationListener;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A very basic {@link AuthConfigFactory} that allows for registering providers programmatically.
|
||||
*/
|
||||
public class DefaultAuthConfigFactory extends AuthConfigFactory
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultAuthConfigFactory.class);
|
||||
private final Map<String, DefaultRegistrationContext> _registrations = new ConcurrentHashMap<>();
|
||||
|
||||
public DefaultAuthConfigFactory()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthConfigProvider getConfigProvider(String layer, String appContext, RegistrationListener listener)
|
||||
{
|
||||
DefaultRegistrationContext registrationContext = _registrations.get(getKey(layer, appContext));
|
||||
if (registrationContext == null)
|
||||
registrationContext = _registrations.get(getKey(null, appContext));
|
||||
if (registrationContext == null)
|
||||
registrationContext = _registrations.get(getKey(layer, null));
|
||||
if (registrationContext == null)
|
||||
registrationContext = _registrations.get(getKey(null, null));
|
||||
if (registrationContext == null)
|
||||
return null;
|
||||
|
||||
// TODO: according to the javadoc you're supposed to register listener even if there is no context available.
|
||||
if (listener != null)
|
||||
registrationContext.addListener(listener);
|
||||
return registrationContext.getProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String registerConfigProvider(String className, Map properties, String layer, String appContext, String description)
|
||||
{
|
||||
SecurityManager sm = System.getSecurityManager();
|
||||
if (sm != null)
|
||||
sm.checkPermission(new AuthPermission("registerAuthConfigProvider"));
|
||||
|
||||
String key = getKey(layer, appContext);
|
||||
AuthConfigProvider configProvider = createConfigProvider(className, properties);
|
||||
DefaultRegistrationContext context = new DefaultRegistrationContext(configProvider, layer, appContext, description, true);
|
||||
DefaultRegistrationContext oldContext = _registrations.put(key, context);
|
||||
if (oldContext != null)
|
||||
oldContext.notifyListeners();
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String registerConfigProvider(AuthConfigProvider provider, String layer, String appContext, String description)
|
||||
{
|
||||
SecurityManager sm = System.getSecurityManager();
|
||||
if (sm != null)
|
||||
sm.checkPermission(new AuthPermission("registerAuthConfigProvider"));
|
||||
|
||||
String key = getKey(layer, appContext);
|
||||
DefaultRegistrationContext context = new DefaultRegistrationContext(provider, layer, appContext, description, false);
|
||||
DefaultRegistrationContext oldContext = _registrations.put(key, context);
|
||||
if (oldContext != null)
|
||||
oldContext.notifyListeners();
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeRegistration(String registrationID)
|
||||
{
|
||||
SecurityManager sm = System.getSecurityManager();
|
||||
if (sm != null)
|
||||
sm.checkPermission(new AuthPermission("removeAuthRegistration"));
|
||||
|
||||
DefaultRegistrationContext registrationContext = _registrations.remove(registrationID);
|
||||
if (registrationContext == null)
|
||||
return false;
|
||||
|
||||
registrationContext.notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] detachListener(RegistrationListener listener, String layer, String appContext)
|
||||
{
|
||||
SecurityManager sm = System.getSecurityManager();
|
||||
if (sm != null)
|
||||
sm.checkPermission(new AuthPermission("detachAuthListener"));
|
||||
|
||||
List<String> registrationIds = new ArrayList<>();
|
||||
for (DefaultRegistrationContext registration : _registrations.values())
|
||||
{
|
||||
if ((layer == null || layer.equals(registration.getMessageLayer())) && (appContext == null || appContext.equals(registration.getAppContext())))
|
||||
{
|
||||
if (registration.removeListener(listener))
|
||||
registrationIds.add(getKey(registration.getMessageLayer(), registration.getAppContext()));
|
||||
}
|
||||
}
|
||||
|
||||
return registrationIds.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getRegistrationIDs(AuthConfigProvider provider)
|
||||
{
|
||||
List<String> registrationIds = new ArrayList<>();
|
||||
for (DefaultRegistrationContext registration : _registrations.values())
|
||||
{
|
||||
if (provider == registration.getProvider())
|
||||
registrationIds.add(getKey(registration.getMessageLayer(), registration.getAppContext()));
|
||||
}
|
||||
|
||||
return registrationIds.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RegistrationContext getRegistrationContext(String registrationID)
|
||||
{
|
||||
return _registrations.get(registrationID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh()
|
||||
{
|
||||
SecurityManager sm = System.getSecurityManager();
|
||||
if (sm != null)
|
||||
sm.checkPermission(new AuthPermission("refreshAuth"));
|
||||
|
||||
// TODO: maybe we should re-construct providers created from classname.
|
||||
}
|
||||
|
||||
private static String getKey(String layer, String appContext)
|
||||
{
|
||||
return layer + "/" + appContext;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private AuthConfigProvider createConfigProvider(String className, Map properties)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Javadoc specifies all AuthConfigProvider implementations must have this constructor, and that
|
||||
// to construct this we must pass a null value for the factory argument of the constructor.
|
||||
return (AuthConfigProvider)Class.forName(className)
|
||||
.getConstructor(Map.class, AuthConfigFactory.class)
|
||||
.newInstance(properties, null);
|
||||
}
|
||||
catch (ReflectiveOperationException e)
|
||||
{
|
||||
throw new SecurityException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultRegistrationContext implements RegistrationContext
|
||||
{
|
||||
private final String _layer;
|
||||
private final String _appContext;
|
||||
private final boolean _persistent;
|
||||
private final AuthConfigProvider _provider;
|
||||
private final String _description;
|
||||
private final List<RegistrationListener> _listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
public DefaultRegistrationContext(AuthConfigProvider provider, String layer, String appContext, String description, boolean persistent)
|
||||
{
|
||||
_provider = provider;
|
||||
_layer = layer;
|
||||
_appContext = appContext;
|
||||
_description = description;
|
||||
_persistent = persistent;
|
||||
}
|
||||
|
||||
public AuthConfigProvider getProvider()
|
||||
{
|
||||
return _provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessageLayer()
|
||||
{
|
||||
return _layer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAppContext()
|
||||
{
|
||||
return _appContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription()
|
||||
{
|
||||
return _description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersistent()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addListener(RegistrationListener listener)
|
||||
{
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
public void notifyListeners()
|
||||
{
|
||||
for (RegistrationListener listener : _listeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
listener.notify(_layer, _appContext);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
LOG.warn("Error from RegistrationListener", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeListener(RegistrationListener listener)
|
||||
{
|
||||
return _listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ package org.eclipse.jetty.security.jaspi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.security.auth.Subject;
|
||||
@ -22,6 +23,9 @@ import javax.security.auth.message.AuthException;
|
||||
import javax.security.auth.message.AuthStatus;
|
||||
import javax.security.auth.message.callback.CallerPrincipalCallback;
|
||||
import javax.security.auth.message.callback.GroupPrincipalCallback;
|
||||
import javax.security.auth.message.config.AuthConfigFactory;
|
||||
import javax.security.auth.message.config.AuthConfigProvider;
|
||||
import javax.security.auth.message.config.RegistrationListener;
|
||||
import javax.security.auth.message.config.ServerAuthConfig;
|
||||
import javax.security.auth.message.config.ServerAuthContext;
|
||||
import javax.servlet.ServletRequest;
|
||||
@ -30,57 +34,100 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.eclipse.jetty.security.EmptyLoginService;
|
||||
import org.eclipse.jetty.security.IdentityService;
|
||||
import org.eclipse.jetty.security.LoginService;
|
||||
import org.eclipse.jetty.security.ServerAuthException;
|
||||
import org.eclipse.jetty.security.UserAuthentication;
|
||||
import org.eclipse.jetty.security.WrappedAuthConfiguration;
|
||||
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
|
||||
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
|
||||
import org.eclipse.jetty.security.authentication.SessionAuthentication;
|
||||
import org.eclipse.jetty.server.Authentication;
|
||||
import org.eclipse.jetty.server.Authentication.User;
|
||||
import org.eclipse.jetty.server.UserIdentity;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.eclipse.jetty.security.jaspi.JaspiAuthenticatorFactory.MESSAGE_LAYER;
|
||||
|
||||
/**
|
||||
* @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
|
||||
* Implementation of Jetty {@link LoginAuthenticator} that is a bridge from Javax Authentication to Jetty Security.
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public class JaspiAuthenticator extends LoginAuthenticator
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JaspiAuthenticator.class.getName());
|
||||
|
||||
private final ServerAuthConfig _authConfig;
|
||||
|
||||
private final Map _authProperties;
|
||||
|
||||
private final ServletCallbackHandler _callbackHandler;
|
||||
|
||||
private final Subject _serviceSubject;
|
||||
|
||||
private final String _appContext;
|
||||
private final boolean _allowLazyAuthentication;
|
||||
private final AuthConfigFactory _authConfigFactory = AuthConfigFactory.getFactory();
|
||||
private Map _authProperties;
|
||||
private IdentityService _identityService;
|
||||
private ServletCallbackHandler _callbackHandler;
|
||||
private ServerAuthConfig _authConfig;
|
||||
|
||||
private final IdentityService _identityService;
|
||||
|
||||
public JaspiAuthenticator(ServerAuthConfig authConfig, Map authProperties, ServletCallbackHandler callbackHandler, Subject serviceSubject,
|
||||
boolean allowLazyAuthentication, IdentityService identityService)
|
||||
public JaspiAuthenticator(Subject serviceSubject, String appContext, boolean allowLazyAuthentication)
|
||||
{
|
||||
_serviceSubject = serviceSubject;
|
||||
_appContext = appContext;
|
||||
_allowLazyAuthentication = allowLazyAuthentication;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public JaspiAuthenticator(ServerAuthConfig authConfig, Map authProperties, ServletCallbackHandler callbackHandler, Subject serviceSubject, boolean allowLazyAuthentication, IdentityService identityService)
|
||||
{
|
||||
// TODO maybe pass this in via setConfiguration ?
|
||||
if (callbackHandler == null)
|
||||
throw new NullPointerException("No CallbackHandler");
|
||||
if (authConfig == null)
|
||||
throw new NullPointerException("No AuthConfig");
|
||||
this._authConfig = authConfig;
|
||||
this._authProperties = authProperties;
|
||||
this._callbackHandler = callbackHandler;
|
||||
this._serviceSubject = serviceSubject;
|
||||
this._allowLazyAuthentication = allowLazyAuthentication;
|
||||
this._identityService = identityService;
|
||||
this._appContext = null;
|
||||
this._authConfig = authConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(AuthConfiguration configuration)
|
||||
{
|
||||
LoginService loginService = configuration.getLoginService();
|
||||
if (loginService == null)
|
||||
{
|
||||
// Add an empty login service so we can use JASPI without tying into Jetty auth mechanisms.
|
||||
configuration = new JaspiAuthConfiguration(configuration);
|
||||
loginService = configuration.getLoginService();
|
||||
}
|
||||
|
||||
super.setConfiguration(configuration);
|
||||
|
||||
// Only do this if the new constructor was used.
|
||||
if (_authConfig == null)
|
||||
{
|
||||
_identityService = configuration.getIdentityService();
|
||||
_callbackHandler = new ServletCallbackHandler(loginService);
|
||||
_authProperties = new HashMap();
|
||||
for (String key : configuration.getInitParameterNames())
|
||||
{
|
||||
_authProperties.put(key, configuration.getInitParameter(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ServerAuthConfig getAuthConfig() throws AuthException
|
||||
{
|
||||
if (_authConfig != null)
|
||||
return _authConfig;
|
||||
|
||||
RegistrationListener listener = (layer, appContext) -> _authConfig = null;
|
||||
AuthConfigProvider authConfigProvider = _authConfigFactory.getConfigProvider(MESSAGE_LAYER, _appContext, listener);
|
||||
if (authConfigProvider == null)
|
||||
{
|
||||
_authConfigFactory.detachListener(listener, MESSAGE_LAYER, _appContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
_authConfig = authConfigProvider.getServerAuthConfig(MESSAGE_LAYER, _appContext, _callbackHandler);
|
||||
return _authConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -124,8 +171,12 @@ public class JaspiAuthenticator extends LoginAuthenticator
|
||||
{
|
||||
try
|
||||
{
|
||||
String authContextId = _authConfig.getAuthContextID(messageInfo);
|
||||
ServerAuthContext authContext = _authConfig.getAuthContext(authContextId, _serviceSubject, _authProperties);
|
||||
ServerAuthConfig authConfig = getAuthConfig();
|
||||
if (authConfig == null)
|
||||
throw new ServerAuthException("No ServerAuthConfig");
|
||||
|
||||
String authContextId = authConfig.getAuthContextID(messageInfo);
|
||||
ServerAuthContext authContext = authConfig.getAuthContext(authContextId, _serviceSubject, _authProperties);
|
||||
Subject clientSubject = new Subject();
|
||||
|
||||
AuthStatus authStatus = authContext.validateRequest(messageInfo, clientSubject, _serviceSubject);
|
||||
@ -154,6 +205,8 @@ public class JaspiAuthenticator extends LoginAuthenticator
|
||||
if (principal == null)
|
||||
{
|
||||
String principalName = principalCallback.getName();
|
||||
|
||||
// TODO: if the Principal class is provided it doesn't need to be in subject, why do we enforce this here?
|
||||
Set<Principal> principals = principalCallback.getSubject().getPrincipals();
|
||||
for (Principal p : principals)
|
||||
{
|
||||
@ -214,8 +267,12 @@ public class JaspiAuthenticator extends LoginAuthenticator
|
||||
{
|
||||
try
|
||||
{
|
||||
String authContextId = _authConfig.getAuthContextID(messageInfo);
|
||||
ServerAuthContext authContext = _authConfig.getAuthContext(authContextId, _serviceSubject, _authProperties);
|
||||
ServerAuthConfig authConfig = getAuthConfig();
|
||||
if (authConfig == null)
|
||||
throw new NullPointerException("no ServerAuthConfig found for context");
|
||||
|
||||
String authContextId = authConfig.getAuthContextID(messageInfo);
|
||||
ServerAuthContext authContext = authConfig.getAuthContext(authContextId, _serviceSubject, _authProperties);
|
||||
// TODO
|
||||
// authContext.cleanSubject(messageInfo,validatedUser.getUserIdentity().getSubject());
|
||||
AuthStatus status = authContext.secureResponse(messageInfo, _serviceSubject);
|
||||
@ -226,4 +283,20 @@ public class JaspiAuthenticator extends LoginAuthenticator
|
||||
throw new ServerAuthException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class JaspiAuthConfiguration extends WrappedAuthConfiguration
|
||||
{
|
||||
private final LoginService loginService = new EmptyLoginService();
|
||||
|
||||
public JaspiAuthConfiguration(AuthConfiguration configuration)
|
||||
{
|
||||
super(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginService getLoginService()
|
||||
{
|
||||
return loginService;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,16 +14,10 @@
|
||||
package org.eclipse.jetty.security.jaspi;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.message.AuthException;
|
||||
import javax.security.auth.message.config.AuthConfigFactory;
|
||||
import javax.security.auth.message.config.AuthConfigProvider;
|
||||
import javax.security.auth.message.config.RegistrationListener;
|
||||
import javax.security.auth.message.config.ServerAuthConfig;
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
import org.eclipse.jetty.security.Authenticator;
|
||||
@ -32,14 +26,29 @@ import org.eclipse.jetty.security.DefaultAuthenticatorFactory;
|
||||
import org.eclipse.jetty.security.IdentityService;
|
||||
import org.eclipse.jetty.security.LoginService;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Javax Authentication (JASPI) Authenticator Factory.
|
||||
*
|
||||
* This is used to link a jetty-security {@link Authenticator.Factory} to a Javax Authentication {@link AuthConfigFactory}.
|
||||
* <p>
|
||||
* This should be initialized with the provided {@link DefaultAuthConfigFactory} to set up Javax Authentication {@link AuthConfigFactory} before use.
|
||||
* (A different {@link AuthConfigFactory} may also be provided using the same steps below)
|
||||
* <p>
|
||||
* To initialize either:
|
||||
* <ul>
|
||||
* <li>invoke {@link AuthConfigFactory#setFactory(AuthConfigFactory)}</li>
|
||||
* <li>Alternatively: set {@link AuthConfigFactory#DEFAULT_FACTORY_SECURITY_PROPERTY}</li>
|
||||
* </ul>
|
||||
*
|
||||
*/
|
||||
public class JaspiAuthenticatorFactory extends DefaultAuthenticatorFactory
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JaspiAuthenticatorFactory.class);
|
||||
|
||||
private static String MESSAGE_LAYER = "HTTP";
|
||||
public static final String MESSAGE_LAYER = "HttpServlet";
|
||||
|
||||
private Subject _serviceSubject;
|
||||
private String _serverName;
|
||||
@ -79,52 +88,26 @@ public class JaspiAuthenticatorFactory extends DefaultAuthenticatorFactory
|
||||
@Override
|
||||
public Authenticator getAuthenticator(Server server, ServletContext context, AuthConfiguration configuration, IdentityService identityService, LoginService loginService)
|
||||
{
|
||||
Authenticator authenticator = null;
|
||||
try
|
||||
{
|
||||
AuthConfigFactory authConfigFactory = AuthConfigFactory.getFactory();
|
||||
RegistrationListener listener = (layer, appContext) ->
|
||||
{
|
||||
};
|
||||
AuthConfigFactory factory = AuthConfigFactory.getFactory();
|
||||
if (factory == null)
|
||||
return null;
|
||||
|
||||
Subject serviceSubject = findServiceSubject(server);
|
||||
String serverName = findServerName(server);
|
||||
String serverName = findServerName(context, server);
|
||||
Subject serviceSubject = findServiceSubject(server);
|
||||
String contextPath = StringUtil.isEmpty(context.getContextPath()) ? "/" : context.getContextPath();
|
||||
String appContext = serverName + " " + contextPath;
|
||||
|
||||
String contextPath = context.getContextPath();
|
||||
if (contextPath == null || contextPath.length() == 0)
|
||||
contextPath = "/";
|
||||
String appContext = serverName + " " + contextPath;
|
||||
// We will only create the Authenticator if an AuthConfigProvider matches this context.
|
||||
if (factory.getConfigProvider(MESSAGE_LAYER, appContext, null) == null)
|
||||
return null;
|
||||
|
||||
AuthConfigProvider authConfigProvider = authConfigFactory.getConfigProvider(MESSAGE_LAYER, appContext, listener);
|
||||
|
||||
if (authConfigProvider != null)
|
||||
{
|
||||
ServletCallbackHandler servletCallbackHandler = new ServletCallbackHandler(loginService);
|
||||
ServerAuthConfig serverAuthConfig = authConfigProvider.getServerAuthConfig(MESSAGE_LAYER, appContext, servletCallbackHandler);
|
||||
if (serverAuthConfig != null)
|
||||
{
|
||||
Map map = new HashMap();
|
||||
for (String key : configuration.getInitParameterNames())
|
||||
{
|
||||
map.put(key, configuration.getInitParameter(key));
|
||||
}
|
||||
authenticator = new JaspiAuthenticator(serverAuthConfig, map, servletCallbackHandler,
|
||||
serviceSubject, true, identityService);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (AuthException e)
|
||||
{
|
||||
LOG.warn("Failed to get ServerAuthConfig", e);
|
||||
}
|
||||
return authenticator;
|
||||
return new JaspiAuthenticator(serviceSubject, appContext, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a service Subject.
|
||||
* If {@link #setServiceSubject(Subject)} has not been used to
|
||||
* set a subject, then the {@link Server#getBeans(Class)} method is
|
||||
* used to look for a Subject.
|
||||
* Find a service Subject. If {@link #setServiceSubject(Subject)} has not been
|
||||
* used to set a subject, then the {@link Server#getBeans(Class)} method is used
|
||||
* to look for a Subject.
|
||||
*
|
||||
* @param server the server to pull the Subject from
|
||||
* @return the subject
|
||||
@ -140,18 +123,24 @@ public class JaspiAuthenticatorFactory extends DefaultAuthenticatorFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a servername.
|
||||
* If {@link #setServerName(String)} has not been called, then
|
||||
* use the name of the a principal in the service subject.
|
||||
* If not found, return "server".
|
||||
* Find a servername. If {@link #setServerName(String)} has not been called,
|
||||
* then use the virtualServerName of the context.
|
||||
* If this is also null, then use the name of the a principal in the service subject.
|
||||
* If none are found, return "server".
|
||||
* @param context
|
||||
*
|
||||
* @param server the server to find the name of
|
||||
* @return the server name from the service Subject (or default value if not found in subject or principals)
|
||||
* @return the server name from the service Subject (or default value if not
|
||||
* found in subject or principals)
|
||||
*/
|
||||
protected String findServerName(Server server)
|
||||
{
|
||||
protected String findServerName(ServletContext context, Server server)
|
||||
{
|
||||
if (_serverName != null)
|
||||
return _serverName;
|
||||
|
||||
String virtualServerName = context.getVirtualServerName();
|
||||
if (virtualServerName != null)
|
||||
return virtualServerName;
|
||||
|
||||
Subject subject = findServiceSubject(server);
|
||||
if (subject != null)
|
||||
|
@ -33,14 +33,13 @@ import org.eclipse.jetty.security.jaspi.callback.CredentialValidationCallback;
|
||||
import org.eclipse.jetty.server.UserIdentity;
|
||||
|
||||
/**
|
||||
* Idiot class required by jaspi stupidity
|
||||
* This {@link CallbackHandler} will bridge {@link Callback}s to handle to the given to the Jetty {@link LoginService}.
|
||||
*/
|
||||
public class ServletCallbackHandler implements CallbackHandler
|
||||
{
|
||||
private final LoginService _loginService;
|
||||
|
||||
private final ThreadLocal<CallerPrincipalCallback> _callerPrincipals = new ThreadLocal<CallerPrincipalCallback>();
|
||||
private final ThreadLocal<GroupPrincipalCallback> _groupPrincipals = new ThreadLocal<GroupPrincipalCallback>();
|
||||
private final ThreadLocal<CallerPrincipalCallback> _callerPrincipals = new ThreadLocal<>();
|
||||
private final ThreadLocal<GroupPrincipalCallback> _groupPrincipals = new ThreadLocal<>();
|
||||
|
||||
public ServletCallbackHandler(LoginService loginService)
|
||||
{
|
||||
@ -64,6 +63,7 @@ public class ServletCallbackHandler implements CallbackHandler
|
||||
else if (callback instanceof PasswordValidationCallback)
|
||||
{
|
||||
PasswordValidationCallback passwordValidationCallback = (PasswordValidationCallback)callback;
|
||||
@SuppressWarnings("unused")
|
||||
Subject subject = passwordValidationCallback.getSubject();
|
||||
|
||||
UserIdentity user = _loginService.login(passwordValidationCallback.getUsername(), passwordValidationCallback.getPassword(), null);
|
||||
|
@ -15,11 +15,14 @@ package org.eclipse.jetty.security.jaspi;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.message.AuthException;
|
||||
import javax.security.auth.message.MessageInfo;
|
||||
import javax.security.auth.message.config.ServerAuthConfig;
|
||||
import javax.security.auth.message.config.ServerAuthContext;
|
||||
|
||||
/**
|
||||
* @deprecated use {@link org.eclipse.jetty.security.jaspi.provider.JaspiAuthConfigProvider}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class SimpleAuthConfig implements ServerAuthConfig
|
||||
{
|
||||
public static final String HTTP_SERVLET = "HttpServlet";
|
||||
@ -35,7 +38,7 @@ public class SimpleAuthConfig implements ServerAuthConfig
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties) throws AuthException
|
||||
public ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties)
|
||||
{
|
||||
return _serverAuthContext;
|
||||
}
|
||||
|
@ -39,7 +39,11 @@ import org.eclipse.jetty.security.jaspi.callback.CredentialValidationCallback;
|
||||
import org.eclipse.jetty.util.security.Credential;
|
||||
import org.eclipse.jetty.util.security.Password;
|
||||
|
||||
public class BaseAuthModule implements ServerAuthModule, ServerAuthContext
|
||||
/**
|
||||
* Simple abstract module implementing a Javax Authentication {@link ServerAuthModule} and {@link ServerAuthContext}.
|
||||
* To be used as a building block for building more sophisticated auth modules.
|
||||
*/
|
||||
public abstract class BaseAuthModule implements ServerAuthModule, ServerAuthContext
|
||||
{
|
||||
private static final Class[] SUPPORTED_MESSAGE_TYPES = new Class[]{HttpServletRequest.class, HttpServletResponse.class};
|
||||
|
||||
@ -92,12 +96,6 @@ public class BaseAuthModule implements ServerAuthModule, ServerAuthContext
|
||||
return AuthStatus.SEND_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException
|
||||
{
|
||||
return AuthStatus.SEND_FAILURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param messageInfo message info to examine for mandatory flag
|
||||
* @return whether authentication is mandatory or optional
|
||||
@ -110,9 +108,7 @@ public class BaseAuthModule implements ServerAuthModule, ServerAuthContext
|
||||
return Boolean.parseBoolean(mandatory);
|
||||
}
|
||||
|
||||
protected boolean login(Subject clientSubject, String credentials,
|
||||
String authMethod, MessageInfo messageInfo)
|
||||
throws IOException, UnsupportedCallbackException
|
||||
protected boolean login(Subject clientSubject, String credentials, String authMethod, MessageInfo messageInfo) throws IOException, UnsupportedCallbackException
|
||||
{
|
||||
credentials = credentials.substring(credentials.indexOf(' ') + 1);
|
||||
credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1);
|
||||
@ -122,10 +118,7 @@ public class BaseAuthModule implements ServerAuthModule, ServerAuthContext
|
||||
return login(clientSubject, userName, new Password(password), authMethod, messageInfo);
|
||||
}
|
||||
|
||||
protected boolean login(Subject clientSubject, String username,
|
||||
Credential credential, String authMethod,
|
||||
MessageInfo messageInfo)
|
||||
throws IOException, UnsupportedCallbackException
|
||||
protected boolean login(Subject clientSubject, String username, Credential credential, String authMethod, MessageInfo messageInfo) throws IOException, UnsupportedCallbackException
|
||||
{
|
||||
CredentialValidationCallback credValidationCallback = new CredentialValidationCallback(clientSubject, username, credential);
|
||||
callbackHandler.handle(new Callback[]{credValidationCallback});
|
||||
|
@ -11,7 +11,7 @@
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.jaspi;
|
||||
package org.eclipse.jetty.security.jaspi.modules;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
@ -22,46 +22,45 @@ import javax.security.auth.message.AuthException;
|
||||
import javax.security.auth.message.AuthStatus;
|
||||
import javax.security.auth.message.MessageInfo;
|
||||
import javax.security.auth.message.MessagePolicy;
|
||||
import javax.security.auth.message.module.ServerAuthModule;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.security.jaspi.modules.BaseAuthModule;
|
||||
import org.eclipse.jetty.util.security.Constraint;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class BasicAuthModule extends BaseAuthModule
|
||||
/**
|
||||
* A {@link ServerAuthModule} implementation of HTTP Basic Authentication.
|
||||
*/
|
||||
public class BasicAuthenticationAuthModule extends BaseAuthModule
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BasicAuthModule.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BasicAuthenticationAuthModule.class);
|
||||
|
||||
private String realmName;
|
||||
|
||||
private static final String REALM_KEY = "org.eclipse.jetty.security.jaspi.modules.RealmName";
|
||||
|
||||
public BasicAuthModule()
|
||||
public BasicAuthenticationAuthModule()
|
||||
{
|
||||
}
|
||||
|
||||
public BasicAuthModule(CallbackHandler callbackHandler, String realmName)
|
||||
public BasicAuthenticationAuthModule(CallbackHandler callbackHandler, String realmName)
|
||||
{
|
||||
super(callbackHandler);
|
||||
this.realmName = realmName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy,
|
||||
CallbackHandler handler, Map options)
|
||||
throws AuthException
|
||||
public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, CallbackHandler callbackHandler, Map options) throws AuthException
|
||||
{
|
||||
super.initialize(requestPolicy, responsePolicy, handler, options);
|
||||
super.initialize(requestPolicy, responsePolicy, callbackHandler, options);
|
||||
realmName = (String)options.get(REALM_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject,
|
||||
Subject serviceSubject)
|
||||
throws AuthException
|
||||
public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException
|
||||
{
|
||||
HttpServletRequest request = (HttpServletRequest)messageInfo.getRequestMessage();
|
||||
HttpServletResponse response = (HttpServletResponse)messageInfo.getResponseMessage();
|
||||
@ -87,11 +86,7 @@ public class BasicAuthModule extends BaseAuthModule
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return AuthStatus.SEND_CONTINUE;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new AuthException(e.getMessage());
|
||||
}
|
||||
catch (UnsupportedCallbackException e)
|
||||
catch (IOException | UnsupportedCallbackException e)
|
||||
{
|
||||
throw new AuthException(e.getMessage());
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.jaspi.modules;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class UserInfo
|
||||
{
|
||||
private final String userName;
|
||||
|
||||
private char[] password;
|
||||
|
||||
public UserInfo(String userName, char[] password)
|
||||
{
|
||||
this.userName = userName;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUserName()
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
public char[] getPassword()
|
||||
{
|
||||
return password;
|
||||
}
|
||||
|
||||
public void clearPassword()
|
||||
{
|
||||
Arrays.fill(password, (char)0);
|
||||
password = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.jaspi.provider;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.message.config.AuthConfigFactory;
|
||||
import javax.security.auth.message.config.AuthConfigProvider;
|
||||
import javax.security.auth.message.config.ClientAuthConfig;
|
||||
import javax.security.auth.message.config.ServerAuthConfig;
|
||||
import javax.security.auth.message.module.ServerAuthModule;
|
||||
|
||||
import org.eclipse.jetty.security.jaspi.JaspiAuthenticatorFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* <p>A Jetty implementation of the {@link AuthConfigProvider} to allow registration of a {@link ServerAuthModule}
|
||||
* directly without having to write a custom {@link AuthConfigProvider}.</p>
|
||||
* <p>If this is being constructed by an {@link AuthConfigFactory} after being passed in as a className, then
|
||||
* you will need to provide the property {@code ServerAuthModule} containing the fully qualified name of
|
||||
* the {@link ServerAuthModule} class you wish to use.</p>
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class JaspiAuthConfigProvider implements AuthConfigProvider
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JaspiAuthConfigProvider.class);
|
||||
private final Map providerProperties;
|
||||
private final ServerAuthModule serverAuthModule;
|
||||
|
||||
/**
|
||||
* <p>Constructor with signature and implementation that's required by API.</p>
|
||||
* <p>The property map must contain the {@code ServerAuthModule} property containing the fully qualified name of
|
||||
* the {@link ServerAuthModule} class you wish to use. If this constructor is being used for self-registration an
|
||||
* optional property of {@code appContext} can be used specify the appContext value to register the provider.</p>
|
||||
*
|
||||
* @param properties A Map of initialization properties.
|
||||
* @param factory The {@link AuthConfigFactory} to register on.
|
||||
*/
|
||||
public JaspiAuthConfigProvider(Map properties, AuthConfigFactory factory)
|
||||
{
|
||||
if (properties == null || !properties.containsKey("ServerAuthModule"))
|
||||
throw new IllegalArgumentException("Missing property 'ServerAuthModule', cannot create JaspiAuthConfigProvider");
|
||||
|
||||
this.providerProperties = Map.copyOf(properties);
|
||||
this.serverAuthModule = createServerAuthModule((String)properties.get("ServerAuthModule"));
|
||||
|
||||
// API requires self registration if factory is provided.
|
||||
if (factory != null)
|
||||
factory.registerConfigProvider(this, JaspiAuthenticatorFactory.MESSAGE_LAYER, (String)properties.get("appContext"), "Self Registration");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param className The fully qualified name of a {@link ServerAuthModule} class.
|
||||
*/
|
||||
public JaspiAuthConfigProvider(String className)
|
||||
{
|
||||
this(className, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param className The fully qualified name of a {@link ServerAuthModule} class.
|
||||
* @param properties A Map of initialization properties.
|
||||
*/
|
||||
public JaspiAuthConfigProvider(String className, Map properties)
|
||||
{
|
||||
this(createServerAuthModule(className), properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param serverAuthModule The instance of {@link ServerAuthModule} to use.
|
||||
*/
|
||||
public JaspiAuthConfigProvider(ServerAuthModule serverAuthModule)
|
||||
{
|
||||
this.serverAuthModule = Objects.requireNonNull(serverAuthModule);
|
||||
this.providerProperties = Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param serverAuthModule The instance of {@link ServerAuthModule} to use.
|
||||
* @param properties A Map of initialization properties.
|
||||
*/
|
||||
public JaspiAuthConfigProvider(ServerAuthModule serverAuthModule, Map properties)
|
||||
{
|
||||
this.serverAuthModule = Objects.requireNonNull(serverAuthModule);
|
||||
this.providerProperties = properties == null ? Collections.emptyMap() : Map.copyOf(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientAuthConfig getClientAuthConfig(String layer, String appContext, CallbackHandler handler)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerAuthConfig getServerAuthConfig(String layer, String appContext, CallbackHandler handler)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("getServerAuthConfig");
|
||||
return new SimpleAuthConfig(layer, appContext, handler, providerProperties, serverAuthModule);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh()
|
||||
{
|
||||
}
|
||||
|
||||
private static ServerAuthModule createServerAuthModule(String serverAuthModuleClassName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (ServerAuthModule)Class.forName(serverAuthModuleClassName).getDeclaredConstructor().newInstance();
|
||||
}
|
||||
catch (ReflectiveOperationException e)
|
||||
{
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.jaspi.provider;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.message.AuthException;
|
||||
import javax.security.auth.message.MessageInfo;
|
||||
import javax.security.auth.message.config.ServerAuthConfig;
|
||||
import javax.security.auth.message.config.ServerAuthContext;
|
||||
import javax.security.auth.message.module.ServerAuthModule;
|
||||
|
||||
/**
|
||||
* Simple implementation of the {@link ServerAuthConfig} interface.
|
||||
*
|
||||
* This implementation wires up the given {@link ServerAuthModule} to the appropriate Javax Authentication {@link ServerAuthContext} responsible
|
||||
* for providing it.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
class SimpleAuthConfig implements ServerAuthConfig
|
||||
{
|
||||
private final String _messageLayer;
|
||||
private final String _appContext;
|
||||
private final CallbackHandler _callbackHandler;
|
||||
private final Map _properties;
|
||||
private final ServerAuthModule _serverAuthModule;
|
||||
|
||||
public SimpleAuthConfig(String messageLayer, String appContext, CallbackHandler callbackHandler, Map properties, ServerAuthModule serverAuthModule)
|
||||
{
|
||||
_messageLayer = messageLayer;
|
||||
_appContext = appContext;
|
||||
_callbackHandler = callbackHandler;
|
||||
_properties = properties;
|
||||
_serverAuthModule = serverAuthModule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerAuthContext getAuthContext(String authContextID, Subject serviceSubject, Map properties) throws AuthException
|
||||
{
|
||||
return new SimpleServerAuthContext(_callbackHandler, _serverAuthModule, _properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAppContext()
|
||||
{
|
||||
return _appContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthContextID(MessageInfo messageInfo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessageLayer()
|
||||
{
|
||||
return _messageLayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isProtected()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh()
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.jaspi.provider;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.message.AuthException;
|
||||
import javax.security.auth.message.AuthStatus;
|
||||
import javax.security.auth.message.MessageInfo;
|
||||
import javax.security.auth.message.config.ServerAuthContext;
|
||||
import javax.security.auth.message.module.ServerAuthModule;
|
||||
|
||||
/**
|
||||
* Simple bridge implementation of the Javax Authentication {@link ServerAuthContext} interface.
|
||||
*
|
||||
* This implementation will only delegate to the provided {@link ServerAuthModule} implementation.
|
||||
*/
|
||||
class SimpleServerAuthContext implements ServerAuthContext
|
||||
{
|
||||
private final ServerAuthModule serverAuthModule;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public SimpleServerAuthContext(CallbackHandler callbackHandler, ServerAuthModule serverAuthModule, Map properties) throws AuthException
|
||||
{
|
||||
this.serverAuthModule = serverAuthModule;
|
||||
serverAuthModule.initialize(null, null, callbackHandler, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject, Subject serviceSubject) throws AuthException
|
||||
{
|
||||
return serverAuthModule.validateRequest(messageInfo, clientSubject, serviceSubject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthStatus secureResponse(MessageInfo messageInfo, Subject serviceSubject) throws AuthException
|
||||
{
|
||||
return serverAuthModule.secureResponse(messageInfo, serviceSubject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanSubject(MessageInfo messageInfo, Subject subject) throws AuthException
|
||||
{
|
||||
serverAuthModule.cleanSubject(messageInfo, subject);
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
org.eclipse.jetty.security.jaspi.JaspiAuthenticatorFactory
|
||||
org.eclipse.jetty.security.jaspi.JaspiAuthenticatorFactory
|
||||
|
@ -0,0 +1,115 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.jaspi;
|
||||
|
||||
import java.util.Map;
|
||||
import javax.security.auth.message.config.AuthConfigFactory;
|
||||
import javax.security.auth.message.config.AuthConfigProvider;
|
||||
import javax.security.auth.message.config.RegistrationListener;
|
||||
|
||||
import org.eclipse.jetty.security.jaspi.provider.JaspiAuthConfigProvider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.arrayContaining;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
public class DefaultAuthConfigFactoryTest
|
||||
{
|
||||
|
||||
private static final String MESSAGE_LAYER = "HttpServlet";
|
||||
|
||||
private final String jettyAuthConfigProvider = "org.eclipse.jetty.security.jaspi.provider.JaspiAuthConfigProvider";
|
||||
private final String appContext = "server /test";
|
||||
|
||||
private final Map<String, String> serverAuthModuleProperties = Map.of("ServerAuthModule",
|
||||
"org.eclipse.jetty.security.jaspi.modules.BasicAuthenticationAuthModule", "AppContextID", appContext,
|
||||
"org.eclipse.jetty.security.jaspi.modules.RealmName", "TestRealm");
|
||||
|
||||
private final String serverAuthModuleClassName = "org.eclipse.jetty.security.jaspi.modules.BasicAuthenticationAuthModule";
|
||||
|
||||
@Test
|
||||
public void testRegisterConfigProviderByClassName() throws Exception
|
||||
{
|
||||
AuthConfigFactory factory = new DefaultAuthConfigFactory();
|
||||
String registrationId = factory.registerConfigProvider(jettyAuthConfigProvider,
|
||||
serverAuthModuleProperties, MESSAGE_LAYER, appContext, "a test provider");
|
||||
AuthConfigProvider registeredProvider = factory.getConfigProvider(MESSAGE_LAYER, appContext, null);
|
||||
assertThat(registeredProvider, instanceOf(JaspiAuthConfigProvider.class));
|
||||
assertThat(registeredProvider.getServerAuthConfig(MESSAGE_LAYER, appContext, null), notNullValue());
|
||||
|
||||
assertThat(factory.getRegistrationContext(registrationId), notNullValue());
|
||||
assertThat(factory.getRegistrationIDs(registeredProvider), arrayContaining(registrationId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterAuthConfigProviderDirect() throws Exception
|
||||
{
|
||||
AuthConfigProvider provider = new JaspiAuthConfigProvider(
|
||||
serverAuthModuleClassName,
|
||||
serverAuthModuleProperties);
|
||||
|
||||
AuthConfigFactory factory = new DefaultAuthConfigFactory();
|
||||
String registrationId = factory.registerConfigProvider(provider, MESSAGE_LAYER, appContext, "a test provider");
|
||||
|
||||
AuthConfigProvider registeredProvider = factory.getConfigProvider(MESSAGE_LAYER, appContext, null);
|
||||
assertThat(registeredProvider, instanceOf(JaspiAuthConfigProvider.class));
|
||||
assertThat(registeredProvider.getServerAuthConfig(MESSAGE_LAYER, appContext, null), notNullValue());
|
||||
|
||||
assertThat(factory.getRegistrationContext(registrationId), notNullValue());
|
||||
assertThat(factory.getRegistrationIDs(registeredProvider), arrayContaining(registrationId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveRegistration() throws Exception
|
||||
{
|
||||
// Arrange
|
||||
AuthConfigProvider provider = new JaspiAuthConfigProvider(
|
||||
serverAuthModuleClassName,
|
||||
serverAuthModuleProperties);
|
||||
|
||||
AuthConfigFactory factory = new DefaultAuthConfigFactory();
|
||||
String registrationId = factory.registerConfigProvider(provider, MESSAGE_LAYER, appContext, "a test provider");
|
||||
|
||||
DummyRegistrationListener dummyListener = new DummyRegistrationListener();
|
||||
assertThat(factory.getConfigProvider(MESSAGE_LAYER, appContext, dummyListener), notNullValue());
|
||||
|
||||
// Act
|
||||
factory.removeRegistration(registrationId);
|
||||
|
||||
// Assert config provider removed
|
||||
assertThat(factory.getConfigProvider(MESSAGE_LAYER, appContext, null), nullValue());
|
||||
|
||||
// Assert listeners invoked
|
||||
assertThat(dummyListener.appContext, equalTo(appContext));
|
||||
assertThat(dummyListener.layer, equalTo(MESSAGE_LAYER));
|
||||
|
||||
}
|
||||
|
||||
static class DummyRegistrationListener implements RegistrationListener
|
||||
{
|
||||
String layer;
|
||||
String appContext;
|
||||
|
||||
@Override
|
||||
public void notify(String layer, String appContext)
|
||||
{
|
||||
this.layer = layer;
|
||||
this.appContext = appContext;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.security.auth.message.config.AuthConfigFactory;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@ -39,7 +40,9 @@ import org.eclipse.jetty.util.security.Constraint;
|
||||
import org.eclipse.jetty.util.security.Credential;
|
||||
import org.eclipse.jetty.util.security.Password;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@ -52,7 +55,7 @@ public class JaspiTest
|
||||
Server _server;
|
||||
LocalConnector _connector;
|
||||
|
||||
public class TestLoginService extends AbstractLoginService
|
||||
public static class TestLoginService extends AbstractLoginService
|
||||
{
|
||||
protected Map<String, UserPrincipal> _users = new HashMap<>();
|
||||
protected Map<String, List<RolePrincipal>> _roles = new HashMap<>();
|
||||
@ -86,10 +89,34 @@ public class JaspiTest
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws Exception
|
||||
{
|
||||
AuthConfigFactory factory = new DefaultAuthConfigFactory();
|
||||
|
||||
factory.registerConfigProvider("org.eclipse.jetty.security.jaspi.provider.JaspiAuthConfigProvider",
|
||||
Map.of("ServerAuthModule", "org.eclipse.jetty.security.jaspi.modules.BasicAuthenticationAuthModule",
|
||||
"AppContextID", "server /ctx",
|
||||
"org.eclipse.jetty.security.jaspi.modules.RealmName", "TestRealm"),
|
||||
"HttpServlet", "server /ctx", "a test provider");
|
||||
|
||||
factory.registerConfigProvider("org.eclipse.jetty.security.jaspi.provider.JaspiAuthConfigProvider",
|
||||
Map.of("ServerAuthModule", "org.eclipse.jetty.security.jaspi.HttpHeaderAuthModule",
|
||||
"AppContextID", "server /other"),
|
||||
"HttpServlet", "server /other", "another test provider");
|
||||
|
||||
AuthConfigFactory.setFactory(factory);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterAll() throws Exception
|
||||
{
|
||||
AuthConfigFactory.setFactory(null);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception
|
||||
{
|
||||
System.setProperty("org.apache.geronimo.jaspic.configurationFile", "src/test/resources/jaspi.xml");
|
||||
_server = new Server();
|
||||
_connector = new LocalConnector(_server);
|
||||
_server.addConnector(_connector);
|
||||
@ -98,8 +125,8 @@ public class JaspiTest
|
||||
_server.setHandler(contexts);
|
||||
|
||||
TestLoginService loginService = new TestLoginService("TestRealm");
|
||||
loginService.putUser("user", new Password("password"), new String[]{"users"});
|
||||
loginService.putUser("admin", new Password("secret"), new String[]{"users", "admins"});
|
||||
loginService.putUser("user", new Password("password"), new String[] {"users"});
|
||||
loginService.putUser("admin", new Password("secret"), new String[] {"users", "admins"});
|
||||
_server.addBean(loginService);
|
||||
|
||||
ContextHandler context = new ContextHandler();
|
||||
@ -159,9 +186,8 @@ public class JaspiTest
|
||||
@Test
|
||||
public void testConstraintWrongAuth() throws Exception
|
||||
{
|
||||
String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" +
|
||||
"Authorization: Basic " + Base64.getEncoder().encodeToString("user:wrong".getBytes(ISO_8859_1)) +
|
||||
"\n\n");
|
||||
String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" + "Authorization: Basic " +
|
||||
Base64.getEncoder().encodeToString("user:wrong".getBytes(ISO_8859_1)) + "\n\n");
|
||||
assertThat(response, startsWith("HTTP/1.1 401 Unauthorized"));
|
||||
assertThat(response, Matchers.containsString("WWW-Authenticate: basic realm=\"TestRealm\""));
|
||||
}
|
||||
@ -169,9 +195,8 @@ public class JaspiTest
|
||||
@Test
|
||||
public void testConstraintAuth() throws Exception
|
||||
{
|
||||
String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" +
|
||||
"Authorization: Basic " + Base64.getEncoder().encodeToString("user:password".getBytes(ISO_8859_1)) +
|
||||
"\n\n");
|
||||
String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" + "Authorization: Basic " +
|
||||
Base64.getEncoder().encodeToString("user:password".getBytes(ISO_8859_1)) + "\n\n");
|
||||
assertThat(response, startsWith("HTTP/1.1 200 OK"));
|
||||
}
|
||||
|
||||
@ -185,8 +210,7 @@ public class JaspiTest
|
||||
@Test
|
||||
public void testOtherAuth() throws Exception
|
||||
{
|
||||
String response = _connector.getResponse("GET /other/test HTTP/1.0\n" +
|
||||
"X-Forwarded-User: user\n\n");
|
||||
String response = _connector.getResponse("GET /other/test HTTP/1.0\n" + "X-Forwarded-User: user\n\n");
|
||||
assertThat(response, startsWith("HTTP/1.1 200 OK"));
|
||||
}
|
||||
|
||||
@ -194,7 +218,8 @@ public class JaspiTest
|
||||
{
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
response.setStatus(200);
|
||||
|
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<jaspi xmlns="http://geronimo.apache.org/xml/ns/geronimo-jaspi">
|
||||
<configProvider>
|
||||
<messageLayer>HTTP</messageLayer>
|
||||
<appContext>server /ctx</appContext>
|
||||
<description>description</description>
|
||||
<serverAuthConfig>
|
||||
<authenticationContextID>authenticationContextID1</authenticationContextID>
|
||||
<protected>true</protected>
|
||||
<serverAuthContext>
|
||||
<serverAuthModule>
|
||||
<className>org.eclipse.jetty.security.jaspi.BasicAuthModule</className>
|
||||
<options>
|
||||
org.eclipse.jetty.security.jaspi.modules.RealmName=TestRealm
|
||||
</options>
|
||||
</serverAuthModule>
|
||||
</serverAuthContext>
|
||||
</serverAuthConfig>
|
||||
<persistent>true</persistent>
|
||||
</configProvider>
|
||||
<configProvider>
|
||||
<messageLayer>HTTP</messageLayer>
|
||||
<appContext>server /other</appContext>
|
||||
<description>description</description>
|
||||
<serverAuthConfig>
|
||||
<authenticationContextID>authenticationContextID2</authenticationContextID>
|
||||
<protected>true</protected>
|
||||
<serverAuthContext>
|
||||
<serverAuthModule>
|
||||
<className>org.eclipse.jetty.security.jaspi.HttpHeaderAuthModule</className>
|
||||
<options>
|
||||
</options>
|
||||
</serverAuthModule>
|
||||
</serverAuthContext>
|
||||
</serverAuthConfig>
|
||||
<persistent>true</persistent>
|
||||
</configProvider>
|
||||
</jaspi>
|
@ -125,6 +125,11 @@
|
||||
<artifactId>maven-artifact-transfer</artifactId>
|
||||
<version>0.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.plexus</groupId>
|
||||
<artifactId>plexus-utils</artifactId>
|
||||
<version>3.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-plugin-api</artifactId>
|
||||
|
@ -28,7 +28,6 @@ import java.util.jar.Manifest;
|
||||
|
||||
import org.codehaus.plexus.util.SelectorUtils;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
import org.eclipse.jetty.util.resource.JarResource;
|
||||
import org.slf4j.Logger;
|
||||
@ -87,7 +86,7 @@ public class SelectiveJarResource extends JarResource
|
||||
{
|
||||
for (String include : _includes)
|
||||
{
|
||||
if (SelectorUtils.matchPath(include, name, _caseSensitive))
|
||||
if (SelectorUtils.matchPath(include, name, "/", _caseSensitive))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -99,7 +98,7 @@ public class SelectiveJarResource extends JarResource
|
||||
{
|
||||
for (String exclude : _excludes)
|
||||
{
|
||||
if (SelectorUtils.matchPath(exclude, name, _caseSensitive))
|
||||
if (SelectorUtils.matchPath(exclude, name, "/", _caseSensitive))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -140,8 +139,8 @@ public class SelectiveJarResource extends JarResource
|
||||
String entryName = entry.getName();
|
||||
|
||||
LOG.debug("Looking at {}", entryName);
|
||||
String dotCheck = StringUtil.replace(entryName, '\\', '/');
|
||||
dotCheck = URIUtil.canonicalPath(dotCheck);
|
||||
// make sure no access out of the root entry is present
|
||||
String dotCheck = URIUtil.canonicalPath(entryName);
|
||||
if (dotCheck == null)
|
||||
{
|
||||
LOG.info("Invalid entry: {}", entryName);
|
||||
|
@ -13,96 +13,81 @@
|
||||
|
||||
package org.eclipse.jetty.maven.plugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.resource.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class TestSelectiveJarResource
|
||||
{
|
||||
File unpackParent;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
unpackParent = MavenTestingUtils.getTargetTestingDir("selective-jar-resource");
|
||||
unpackParent.mkdirs();
|
||||
}
|
||||
|
||||
public WorkDir workDir;
|
||||
|
||||
@Test
|
||||
public void testIncludesNoExcludes() throws Exception
|
||||
{
|
||||
File unpackDir = File.createTempFile("inc", "exc", unpackParent);
|
||||
unpackDir.delete();
|
||||
unpackDir.mkdirs();
|
||||
Path unpackDir = workDir.getEmptyPathDir();
|
||||
|
||||
File testJar = MavenTestingUtils.getTestResourceFile("selective-jar-test.jar");
|
||||
try (SelectiveJarResource sjr = new SelectiveJarResource(new URL("jar:" + Resource.toURL(testJar).toString() + "!/"));)
|
||||
Path testJar = MavenTestingUtils.getTestResourcePathFile("selective-jar-test.jar");
|
||||
try (SelectiveJarResource sjr = new SelectiveJarResource(new URL("jar:" + testJar.toUri().toASCIIString() + "!/")))
|
||||
{
|
||||
sjr.setCaseSensitive(false);
|
||||
List<String> includes = new ArrayList<>();
|
||||
includes.add("**/*.html");
|
||||
sjr.setIncludes(includes);
|
||||
sjr.copyTo(unpackDir);
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("top.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("aa/a1.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("aa/a2.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("aa/deep/a3.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("bb/b1.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("bb/b2.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("cc/c1.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("cc/c2.html")));
|
||||
sjr.copyTo(unpackDir.toFile());
|
||||
assertTrue(Files.exists(unpackDir.resolve("top.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("aa/a1.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("aa/a2.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("aa/deep/a3.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("bb/b1.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("bb/b2.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("cc/c1.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("cc/c2.html")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludesNoIncludes() throws Exception
|
||||
{
|
||||
File unpackDir = File.createTempFile("exc", "inc", unpackParent);
|
||||
unpackDir.delete();
|
||||
unpackDir.mkdirs();
|
||||
File testJar = MavenTestingUtils.getTestResourceFile("selective-jar-test.jar");
|
||||
|
||||
try (SelectiveJarResource sjr = new SelectiveJarResource(new URL("jar:" + Resource.toURL(testJar).toString() + "!/"));)
|
||||
Path unpackDir = workDir.getEmptyPathDir();
|
||||
|
||||
Path testJar = MavenTestingUtils.getTestResourcePathFile("selective-jar-test.jar");
|
||||
try (SelectiveJarResource sjr = new SelectiveJarResource(new URL("jar:" + testJar.toUri().toASCIIString() + "!/")))
|
||||
{
|
||||
sjr.setCaseSensitive(false);
|
||||
List<String> excludes = new ArrayList<>();
|
||||
excludes.add("**/*");
|
||||
sjr.setExcludes(excludes);
|
||||
sjr.copyTo(unpackDir);
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("top.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("aa/a1.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("aa/a2.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("aa/deep/a3.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("bb/b1.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("bb/b2.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("cc/c1.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("cc/c2.html")));
|
||||
sjr.copyTo(unpackDir.toFile());
|
||||
assertFalse(Files.exists(unpackDir.resolve("top.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("aa/a1.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("aa/a2.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("aa/deep/a3.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("bb/b1.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("bb/b2.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("cc/c1.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("cc/c2.html")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncludesExcludes() throws Exception
|
||||
{
|
||||
File unpackDir = File.createTempFile("exc", "andinc", unpackParent);
|
||||
unpackDir.delete();
|
||||
unpackDir.mkdirs();
|
||||
File testJar = MavenTestingUtils.getTestResourceFile("selective-jar-test.jar");
|
||||
|
||||
try (SelectiveJarResource sjr = new SelectiveJarResource(new URL("jar:" + Resource.toURL(testJar).toString() + "!/"));)
|
||||
Path unpackDir = workDir.getEmptyPathDir();
|
||||
|
||||
Path testJar = MavenTestingUtils.getTestResourcePathFile("selective-jar-test.jar");
|
||||
try (SelectiveJarResource sjr = new SelectiveJarResource(new URL("jar:" + testJar.toUri().toASCIIString() + "!/")))
|
||||
{
|
||||
sjr.setCaseSensitive(false);
|
||||
List<String> excludes = new ArrayList<>();
|
||||
@ -111,15 +96,15 @@ public class TestSelectiveJarResource
|
||||
List<String> includes = new ArrayList<>();
|
||||
includes.add("bb/*");
|
||||
sjr.setIncludes(includes);
|
||||
sjr.copyTo(unpackDir);
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("top.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("aa/a1.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("aa/a2.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("aa/deep/a3.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("bb/b1.html")));
|
||||
assertTrue(Files.exists(unpackDir.toPath().resolve("bb/b2.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("cc/c1.html")));
|
||||
assertFalse(Files.exists(unpackDir.toPath().resolve("cc/c2.html")));
|
||||
sjr.copyTo(unpackDir.toFile());
|
||||
assertFalse(Files.exists(unpackDir.resolve("top.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("aa/a1.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("aa/a2.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("aa/deep/a3.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("bb/b1.html")));
|
||||
assertTrue(Files.exists(unpackDir.resolve("bb/b2.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("cc/c1.html")));
|
||||
assertFalse(Files.exists(unpackDir.resolve("cc/c2.html")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
# Jetty Logging using jetty-slf4j-impl
|
||||
#org.eclipse.jetty.maven.plugin.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.server.LEVEL=DEBUG
|
||||
#org.eclipse.jetty.http.LEVEL=DEBUG
|
@ -26,6 +26,7 @@
|
||||
<Arg><Property name="jetty.openid.provider.tokenEndpoint"/></Arg>
|
||||
<Arg><Property name="jetty.openid.clientId"/></Arg>
|
||||
<Arg><Property name="jetty.openid.clientSecret"/></Arg>
|
||||
<Arg><Property name="jetty.openid.authMethod" default="client_secret_post"/></Arg>
|
||||
<Arg><Ref refid="HttpClient"/></Arg>
|
||||
<Call name="addScopes">
|
||||
<Arg>
|
||||
|
@ -42,3 +42,6 @@ etc/jetty-openid.xml
|
||||
|
||||
## True if all certificates should be trusted by the default SslContextFactory
|
||||
# jetty.openid.sslContextFactory.trustAll=false
|
||||
|
||||
## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic).
|
||||
# jetty.openid.authMethod=client_secret_post
|
||||
|
@ -35,6 +35,7 @@ public class JwtDecoder
|
||||
* @param jwt the JWT to decode.
|
||||
* @return the map of claims encoded in the JWT.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> decode(String jwt)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
@ -54,7 +55,6 @@ public class JwtDecoder
|
||||
Object parsedJwtHeader = json.fromJSON(jwtHeaderString);
|
||||
if (!(parsedJwtHeader instanceof Map))
|
||||
throw new IllegalStateException("Invalid JWT header");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> jwtHeader = (Map<String, Object>)parsedJwtHeader;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("JWT Header: {}", jwtHeader);
|
||||
|
@ -45,6 +45,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
|
||||
private final String clientId;
|
||||
private final String clientSecret;
|
||||
private final List<String> scopes = new ArrayList<>();
|
||||
private final String authMethod;
|
||||
private String authEndpoint;
|
||||
private String tokenEndpoint;
|
||||
|
||||
@ -70,6 +71,22 @@ public class OpenIdConfiguration extends ContainerLifeCycle
|
||||
*/
|
||||
public OpenIdConfiguration(String issuer, String authorizationEndpoint, String tokenEndpoint,
|
||||
String clientId, String clientSecret, HttpClient httpClient)
|
||||
{
|
||||
this(issuer, authorizationEndpoint, tokenEndpoint, clientId, clientSecret, "client_secret_post", httpClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an OpenID configuration for a specific OIDC provider.
|
||||
* @param issuer The URL of the OpenID provider.
|
||||
* @param authorizationEndpoint the URL of the OpenID provider's authorization endpoint if configured.
|
||||
* @param tokenEndpoint the URL of the OpenID provider's token endpoint if configured.
|
||||
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
|
||||
* @param clientSecret The client secret known only by the Client and the Authorization Server.
|
||||
* @param authMethod Authentication method to use with the Token Endpoint.
|
||||
* @param httpClient The {@link HttpClient} instance to use.
|
||||
*/
|
||||
public OpenIdConfiguration(String issuer, String authorizationEndpoint, String tokenEndpoint,
|
||||
String clientId, String clientSecret, String authMethod, HttpClient httpClient)
|
||||
{
|
||||
this.issuer = issuer;
|
||||
this.clientId = clientId;
|
||||
@ -77,6 +94,7 @@ public class OpenIdConfiguration extends ContainerLifeCycle
|
||||
this.authEndpoint = authorizationEndpoint;
|
||||
this.tokenEndpoint = tokenEndpoint;
|
||||
this.httpClient = httpClient != null ? httpClient : newHttpClient();
|
||||
this.authMethod = authMethod;
|
||||
|
||||
if (this.issuer == null)
|
||||
throw new IllegalArgumentException("Issuer was not configured");
|
||||
@ -177,6 +195,11 @@ public class OpenIdConfiguration extends ContainerLifeCycle
|
||||
return tokenEndpoint;
|
||||
}
|
||||
|
||||
public String getAuthMethod()
|
||||
{
|
||||
return authMethod;
|
||||
}
|
||||
|
||||
public void addScopes(String... scopes)
|
||||
{
|
||||
if (scopes != null)
|
||||
|
@ -14,12 +14,16 @@
|
||||
package org.eclipse.jetty.security.openid;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.client.api.Authentication;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.BasicAuthentication;
|
||||
import org.eclipse.jetty.client.util.FormRequestContent;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.ajax.JSON;
|
||||
@ -45,6 +49,14 @@ public class OpenIdCredentials implements Serializable
|
||||
private String authCode;
|
||||
private Map<String, Object> response;
|
||||
private Map<String, Object> claims;
|
||||
private boolean verified = false;
|
||||
|
||||
public OpenIdCredentials(Map<String, Object> claims)
|
||||
{
|
||||
this.redirectUri = null;
|
||||
this.authCode = null;
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
public OpenIdCredentials(String authCode, String redirectUri)
|
||||
{
|
||||
@ -95,7 +107,6 @@ public class OpenIdCredentials implements Serializable
|
||||
claims = JwtDecoder.decode(idToken);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("claims {}", claims);
|
||||
validateClaims(configuration);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -103,6 +114,12 @@ public class OpenIdCredentials implements Serializable
|
||||
authCode = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!verified)
|
||||
{
|
||||
validateClaims(configuration);
|
||||
verified = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateClaims(OpenIdConfiguration configuration) throws Exception
|
||||
@ -138,10 +155,11 @@ public class OpenIdCredentials implements Serializable
|
||||
throw new AuthenticationException("Audience Claim MUST contain the client_id value");
|
||||
else if (isList)
|
||||
{
|
||||
if (!Arrays.asList((Object[])aud).contains(clientId))
|
||||
List<Object> list = Arrays.asList((Object[])aud);
|
||||
if (!list.contains(clientId))
|
||||
throw new AuthenticationException("Audience Claim MUST contain the client_id value");
|
||||
|
||||
if (claims.get("azp") == null)
|
||||
if (list.size() > 1 && claims.get("azp") == null)
|
||||
throw new AuthenticationException("A multi-audience ID token needs to contain an azp claim");
|
||||
}
|
||||
else if (!isValidType)
|
||||
@ -153,14 +171,27 @@ public class OpenIdCredentials implements Serializable
|
||||
{
|
||||
Fields fields = new Fields();
|
||||
fields.add("code", authCode);
|
||||
fields.add("client_id", configuration.getClientId());
|
||||
fields.add("client_secret", configuration.getClientSecret());
|
||||
fields.add("redirect_uri", redirectUri);
|
||||
fields.add("grant_type", "authorization_code");
|
||||
|
||||
Request request = configuration.getHttpClient().POST(configuration.getTokenEndpoint());
|
||||
switch (configuration.getAuthMethod())
|
||||
{
|
||||
case "client_secret_basic":
|
||||
URI uri = URI.create(configuration.getTokenEndpoint());
|
||||
Authentication.Result authentication = new BasicAuthentication.BasicResult(uri, configuration.getClientId(), configuration.getClientSecret());
|
||||
authentication.apply(request);
|
||||
break;
|
||||
case "client_secret_post":
|
||||
fields.add("client_id", configuration.getClientId());
|
||||
fields.add("client_secret", configuration.getClientSecret());
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(configuration.getAuthMethod());
|
||||
}
|
||||
|
||||
FormRequestContent formContent = new FormRequestContent(fields);
|
||||
Request request = configuration.getHttpClient().POST(configuration.getTokenEndpoint())
|
||||
.body(formContent)
|
||||
.timeout(10, TimeUnit.SECONDS);
|
||||
request = request.body(formContent).timeout(10, TimeUnit.SECONDS);
|
||||
ContentResponse response = request.send();
|
||||
String responseBody = response.getContentAsString();
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -0,0 +1,40 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security.openid;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
public class OpenIdCredentialsTest
|
||||
{
|
||||
@Test
|
||||
public void testSingleAudienceValueInArray() throws Exception
|
||||
{
|
||||
String issuer = "myIssuer123";
|
||||
String clientId = "myClientId456";
|
||||
OpenIdConfiguration configuration = new OpenIdConfiguration(issuer, "", "", clientId, "", new HttpClient());
|
||||
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("iss", issuer);
|
||||
claims.put("aud", new String[]{clientId});
|
||||
claims.put("exp", System.currentTimeMillis() + 5000);
|
||||
|
||||
assertDoesNotThrow(() -> new OpenIdCredentials(claims).redeemAuthCode(configuration));
|
||||
}
|
||||
}
|
@ -1290,7 +1290,7 @@ public class AsyncMiddleManServletTest
|
||||
startClient();
|
||||
|
||||
ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort())
|
||||
.timeout(5, TimeUnit.SECONDS)
|
||||
.timeout(10, TimeUnit.SECONDS)
|
||||
.send();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
|
@ -62,7 +62,7 @@ public class DefaultAuthenticatorFactory implements Authenticator.Factory
|
||||
String auth = configuration.getAuthMethod();
|
||||
Authenticator authenticator = null;
|
||||
|
||||
if (auth == null || Constraint.__BASIC_AUTH.equalsIgnoreCase(auth))
|
||||
if (Constraint.__BASIC_AUTH.equalsIgnoreCase(auth))
|
||||
authenticator = new BasicAuthenticator();
|
||||
else if (Constraint.__DIGEST_AUTH.equalsIgnoreCase(auth))
|
||||
authenticator = new DigestAuthenticator();
|
||||
|
@ -0,0 +1,58 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
import org.eclipse.jetty.server.UserIdentity;
|
||||
|
||||
/**
|
||||
* LoginService implementation which always denies any attempt to login.
|
||||
*/
|
||||
public class EmptyLoginService implements LoginService
|
||||
{
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserIdentity login(String username, Object credentials, ServletRequest request)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(UserIdentity user)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityService getIdentityService()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdentityService(IdentityService service)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(UserIdentity user)
|
||||
{
|
||||
}
|
||||
}
|
@ -293,9 +293,6 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
|
||||
return getServer().getBean(IdentityService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
protected void doStart()
|
||||
throws Exception
|
||||
@ -334,11 +331,8 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
|
||||
|
||||
if (_identityService == null)
|
||||
{
|
||||
if (_realmName != null)
|
||||
{
|
||||
setIdentityService(new DefaultIdentityService());
|
||||
manage(_identityService);
|
||||
}
|
||||
setIdentityService(new DefaultIdentityService());
|
||||
manage(_identityService);
|
||||
}
|
||||
else
|
||||
unmanage(_identityService);
|
||||
@ -352,7 +346,7 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
|
||||
throw new IllegalStateException("LoginService has different IdentityService to " + this);
|
||||
}
|
||||
|
||||
if (_authenticator == null && _identityService != null)
|
||||
if (_authenticator == null)
|
||||
{
|
||||
// If someone has set an authenticator factory only use that, otherwise try the list of discovered factories.
|
||||
if (_authenticatorFactory != null)
|
||||
@ -399,7 +393,6 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
protected void doStop() throws Exception
|
||||
{
|
||||
//if we discovered the services (rather than had them explicitly configured), remove them.
|
||||
@ -572,6 +565,11 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti
|
||||
authenticator.secureResponse(request, response, isAuthMandatory, null);
|
||||
}
|
||||
}
|
||||
else if (isAuthMandatory)
|
||||
{
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "unauthenticated");
|
||||
baseRequest.setHandled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
baseRequest.setAuthentication(authentication);
|
||||
|
@ -0,0 +1,74 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jetty.security.Authenticator.AuthConfiguration;
|
||||
|
||||
/**
|
||||
* A wrapper for {@link AuthConfiguration}. This allows you create a new AuthConfiguration which can
|
||||
* override a method to change a value from an another instance of AuthConfiguration.
|
||||
*/
|
||||
public class WrappedAuthConfiguration implements AuthConfiguration
|
||||
{
|
||||
private final AuthConfiguration _configuration;
|
||||
|
||||
public WrappedAuthConfiguration(AuthConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthMethod()
|
||||
{
|
||||
return _configuration.getAuthMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRealmName()
|
||||
{
|
||||
return _configuration.getRealmName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInitParameter(String param)
|
||||
{
|
||||
return _configuration.getInitParameter(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getInitParameterNames()
|
||||
{
|
||||
return _configuration.getInitParameterNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginService getLoginService()
|
||||
{
|
||||
return _configuration.getLoginService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityService getIdentityService()
|
||||
{
|
||||
return _configuration.getIdentityService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSessionRenewedOnAuthentication()
|
||||
{
|
||||
return _configuration.isSessionRenewedOnAuthentication();
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
|
||||
import org.eclipse.jetty.server.Authentication;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
|
||||
public class DefaultIdentityServiceTest
|
||||
{
|
||||
@Test
|
||||
public void testDefaultIdentityService() throws Exception
|
||||
{
|
||||
Server server = new Server();
|
||||
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
||||
TestAuthenticator authenticator = new TestAuthenticator();
|
||||
securityHandler.setAuthenticator(authenticator);
|
||||
|
||||
try
|
||||
{
|
||||
server.setHandler(securityHandler);
|
||||
server.start();
|
||||
|
||||
// The DefaultIdentityService should have been created by default.
|
||||
assertThat(securityHandler.getIdentityService(), instanceOf(DefaultIdentityService.class));
|
||||
assertThat(authenticator.getIdentityService(), instanceOf(DefaultIdentityService.class));
|
||||
}
|
||||
finally
|
||||
{
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public static class TestAuthenticator implements Authenticator
|
||||
{
|
||||
private IdentityService _identityService;
|
||||
|
||||
public IdentityService getIdentityService()
|
||||
{
|
||||
return _identityService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(AuthConfiguration configuration)
|
||||
{
|
||||
_identityService = configuration.getIdentityService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthMethod()
|
||||
{
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareRequest(ServletRequest request)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,6 @@ import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.condition.OS.MAC;
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class PropertyUserStoreTest
|
||||
@ -277,7 +276,6 @@ public class PropertyUserStoreTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs({MAC, WINDOWS}) // File is locked on OS, cannot change.
|
||||
public void testPropertyUserStoreLoadRemoveUser() throws Exception
|
||||
{
|
||||
testdir.ensureEmpty();
|
||||
|
@ -0,0 +1,145 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.security.authentication.DeferredAuthentication;
|
||||
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
|
||||
import org.eclipse.jetty.server.Authentication;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.util.security.Constraint;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
public class UnauthenticatedTest
|
||||
{
|
||||
private LocalConnector connector;
|
||||
private TestAuthenticator authenticator;
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() throws Exception
|
||||
{
|
||||
Server server = new Server();
|
||||
connector = new LocalConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
// Authenticator that always returns UNAUTHENTICATED.
|
||||
authenticator = new TestAuthenticator();
|
||||
|
||||
// Add a security handler which requires paths under /requireAuth to be authenticated.
|
||||
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
|
||||
Constraint requireAuthentication = new Constraint();
|
||||
requireAuthentication.setRoles(new String[]{"**"});
|
||||
requireAuthentication.setAuthenticate(true);
|
||||
ConstraintMapping constraintMapping = new ConstraintMapping();
|
||||
constraintMapping.setPathSpec("/requireAuth/*");
|
||||
constraintMapping.setConstraint(requireAuthentication);
|
||||
securityHandler.addConstraintMapping(constraintMapping);
|
||||
|
||||
securityHandler.setAuthenticator(authenticator);
|
||||
securityHandler.setHandler(new AbstractHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
response.setStatus(HttpStatus.OK_200);
|
||||
response.getWriter().println("authentication: " + baseRequest.getAuthentication());
|
||||
}
|
||||
});
|
||||
|
||||
server.setHandler(securityHandler);
|
||||
server.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnauthenticated() throws Exception
|
||||
{
|
||||
TestAuthenticator.AUTHENTICATION.set(Authentication.UNAUTHENTICATED);
|
||||
|
||||
// Request to URI which doesn't require authentication can get through even though auth is UNAUTHENTICATED.
|
||||
String response = connector.getResponse("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response, containsString("HTTP/1.1 200 OK"));
|
||||
assertThat(response, containsString("authentication: UNAUTHENTICATED"));
|
||||
|
||||
// This URI requires just that the request is authenticated.
|
||||
response = connector.getResponse("GET /requireAuth/test HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response, containsString("HTTP/1.1 401 Unauthorized"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeferredAuth() throws Exception
|
||||
{
|
||||
TestAuthenticator.AUTHENTICATION.set(new DeferredAuthentication(authenticator));
|
||||
|
||||
// Request to URI which doesn't require authentication can get through even though auth is UNAUTHENTICATED.
|
||||
String response = connector.getResponse("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response, containsString("HTTP/1.1 200 OK"));
|
||||
assertThat(response, containsString("DeferredAuthentication"));
|
||||
|
||||
// This URI requires just that the request is authenticated. But DeferredAuthentication can bypass this.
|
||||
response = connector.getResponse("GET /requireAuth/test HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assertThat(response, containsString("HTTP/1.1 200 OK"));
|
||||
assertThat(response, containsString("DeferredAuthentication"));
|
||||
}
|
||||
|
||||
public static class TestAuthenticator extends LoginAuthenticator
|
||||
{
|
||||
static AtomicReference<Authentication> AUTHENTICATION = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public void setConfiguration(AuthConfiguration configuration)
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthMethod()
|
||||
{
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareRequest(ServletRequest request)
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException
|
||||
{
|
||||
return AUTHENTICATION.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean secureResponse(ServletRequest request, ServletResponse response, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -56,6 +56,8 @@ import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.eclipse.jetty.util.thread.Invocable.InvocationType.NON_BLOCKING;
|
||||
|
||||
/**
|
||||
* HttpChannel represents a single endpoint for HTTP semantic processing.
|
||||
* The HttpChannel is both an HttpParser.RequestHandler, where it passively receives events from
|
||||
@ -540,7 +542,7 @@ public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor
|
||||
break;
|
||||
|
||||
// Set a close callback on the HttpOutput to make it an async callback
|
||||
_response.completeOutput(Callback.from(() -> _state.completed(null), _state::completed));
|
||||
_response.completeOutput(Callback.from(NON_BLOCKING, () -> _state.completed(null), _state::completed));
|
||||
|
||||
break;
|
||||
}
|
||||
|
@ -725,7 +725,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
}
|
||||
|
||||
if (content == null)
|
||||
{
|
||||
new AsyncFlush(false).iterate();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
@ -1534,7 +1536,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
{
|
||||
final boolean _last;
|
||||
|
||||
ChannelWriteCB(boolean last)
|
||||
private ChannelWriteCB(boolean last)
|
||||
{
|
||||
_last = last;
|
||||
}
|
||||
@ -1560,14 +1562,20 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
|
||||
private abstract class NestedChannelWriteCB extends ChannelWriteCB
|
||||
{
|
||||
final Callback _callback;
|
||||
private final Callback _callback;
|
||||
|
||||
NestedChannelWriteCB(Callback callback, boolean last)
|
||||
private NestedChannelWriteCB(Callback callback, boolean last)
|
||||
{
|
||||
super(last);
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return _callback.getInvocationType();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteSuccess()
|
||||
{
|
||||
@ -1602,9 +1610,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
|
||||
private class AsyncFlush extends ChannelWriteCB
|
||||
{
|
||||
volatile boolean _flushed;
|
||||
private volatile boolean _flushed;
|
||||
|
||||
AsyncFlush(boolean last)
|
||||
private AsyncFlush(boolean last)
|
||||
{
|
||||
super(last);
|
||||
}
|
||||
@ -1637,7 +1645,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
private final int _len;
|
||||
private boolean _completed;
|
||||
|
||||
AsyncWrite(byte[] b, int off, int len, boolean last)
|
||||
private AsyncWrite(byte[] b, int off, int len, boolean last)
|
||||
{
|
||||
super(last);
|
||||
_buffer = ByteBuffer.wrap(b, off, len);
|
||||
@ -1646,7 +1654,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
_slice = _len < getBufferSize() ? null : _buffer.duplicate();
|
||||
}
|
||||
|
||||
AsyncWrite(ByteBuffer buffer, boolean last)
|
||||
private AsyncWrite(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
super(last);
|
||||
_buffer = buffer;
|
||||
@ -1734,7 +1742,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
private boolean _eof;
|
||||
private boolean _closed;
|
||||
|
||||
InputStreamWritingCB(InputStream in, Callback callback)
|
||||
private InputStreamWritingCB(InputStream in, Callback callback)
|
||||
{
|
||||
super(callback, true);
|
||||
_in = in;
|
||||
@ -1810,7 +1818,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
private boolean _eof;
|
||||
private boolean _closed;
|
||||
|
||||
ReadableByteChannelWritingCB(ReadableByteChannel in, Callback callback)
|
||||
private ReadableByteChannelWritingCB(ReadableByteChannel in, Callback callback)
|
||||
{
|
||||
super(callback, true);
|
||||
_in = in;
|
||||
@ -1882,5 +1890,11 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
||||
{
|
||||
onWriteComplete(true, x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
@ -297,6 +298,16 @@ public class ResourceService
|
||||
// Send the data
|
||||
releaseContent = sendData(request, response, included, content, reqRanges);
|
||||
}
|
||||
// Can be thrown from contentFactory.getContent() call when using invalid characters
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("InvalidPathException for pathInContext: {}", pathInContext, e);
|
||||
if (included)
|
||||
throw new FileNotFoundException("!" + pathInContext);
|
||||
notFound(request, response);
|
||||
return response.isCommitted();
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
LOG.warn("Failed to serve resource: {}", pathInContext, e);
|
||||
@ -717,6 +728,12 @@ public class ResourceService
|
||||
content.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
@ -19,6 +19,7 @@ import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketOption;
|
||||
import java.net.StandardSocketOptions;
|
||||
import java.nio.channels.Channel;
|
||||
import java.nio.channels.SelectableChannel;
|
||||
@ -332,10 +333,10 @@ public class ServerConnector extends AbstractNetworkConnector
|
||||
{
|
||||
InetSocketAddress bindAddress = getHost() == null ? new InetSocketAddress(getPort()) : new InetSocketAddress(getHost(), getPort());
|
||||
serverChannel = ServerSocketChannel.open();
|
||||
setSocketOption(serverChannel, StandardSocketOptions.SO_REUSEADDR, getReuseAddress());
|
||||
setSocketOption(serverChannel, StandardSocketOptions.SO_REUSEPORT, isReusePort());
|
||||
try
|
||||
{
|
||||
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, getReuseAddress());
|
||||
serverChannel.setOption(StandardSocketOptions.SO_REUSEPORT, isReusePort());
|
||||
serverChannel.bind(bindAddress, getAcceptQueueSize());
|
||||
}
|
||||
catch (Throwable e)
|
||||
@ -348,6 +349,19 @@ public class ServerConnector extends AbstractNetworkConnector
|
||||
return serverChannel;
|
||||
}
|
||||
|
||||
private <T> void setSocketOption(ServerSocketChannel channel, SocketOption<T> option, T value)
|
||||
{
|
||||
try
|
||||
{
|
||||
channel.setOption(option, value);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Could not configure {} to {} on {}", option, value, channel, x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
|
@ -114,7 +114,9 @@ public class FileBufferedResponseHandler extends BufferedResponseHandler
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
LOG.warn("Could not delete file {}", _filePath, t);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Could not immediately delete file (delaying to jvm exit) {}", _filePath, t);
|
||||
_filePath.toFile().deleteOnExit();
|
||||
}
|
||||
_filePath = null;
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ public class GracefulStopTest
|
||||
|
||||
HttpTester.Response response = HttpTester.parseResponse(client.getInputStream());
|
||||
assertThat(response.getStatus(), is(200));
|
||||
assertThat(response.getContent(), is("read 10/10\n"));
|
||||
assertThat(response.getContent(), is("read [10/10]"));
|
||||
assertThat(response.get(HttpHeader.CONNECTION), nullValue());
|
||||
|
||||
return client;
|
||||
@ -164,7 +164,7 @@ public class GracefulStopTest
|
||||
|
||||
HttpTester.Response response = HttpTester.parseResponse(client.getInputStream());
|
||||
assertThat(response.getStatus(), is(200));
|
||||
assertThat(response.getContent(), is("read 10/10\n"));
|
||||
assertThat(response.getContent(), is("read [10/10]"));
|
||||
assertThat(response.get(HttpHeader.CONNECTION), nullValue());
|
||||
}
|
||||
|
||||
@ -224,7 +224,7 @@ public class GracefulStopTest
|
||||
assertThat(response.get(HttpHeader.CONNECTION), is("close"));
|
||||
else
|
||||
assertThat(response.get(HttpHeader.CONNECTION), nullValue());
|
||||
assertThat(response.getContent(), is("read 10/10\n"));
|
||||
assertThat(response.getContent(), is("read [10/10]"));
|
||||
}
|
||||
|
||||
void assert500Response(Socket client) throws Exception
|
||||
@ -414,7 +414,7 @@ public class GracefulStopTest
|
||||
}
|
||||
}
|
||||
|
||||
response.getWriter().printf("read %d/%d%n", c, contentLength);
|
||||
response.getWriter().printf("read [%d/%d]", c, contentLength);
|
||||
}
|
||||
catch (Throwable th)
|
||||
{
|
||||
|
@ -45,6 +45,8 @@ import org.eclipse.jetty.server.handler.HandlerList;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.anyOf;
|
||||
@ -214,6 +216,7 @@ public class ServerConnectorTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "SO_REUSEPORT not available on windows")
|
||||
public void testReusePort() throws Exception
|
||||
{
|
||||
int port;
|
||||
|
@ -30,10 +30,8 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -42,11 +40,8 @@ import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.condition.OS.MAC;
|
||||
|
||||
@Disabled
|
||||
@Tag("stress")
|
||||
@DisabledOnOs(MAC) // TODO: needs investigation
|
||||
public class StressTest
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StressTest.class);
|
||||
@ -129,6 +124,7 @@ public class StressTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("Slow")
|
||||
public void testNonPersistent() throws Throwable
|
||||
{
|
||||
doThreads(20, 20, false);
|
||||
@ -145,6 +141,7 @@ public class StressTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("Slow")
|
||||
public void testPersistent() throws Throwable
|
||||
{
|
||||
doThreads(40, 40, true);
|
||||
|
@ -45,10 +45,14 @@ import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -60,10 +64,13 @@ import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class FileBufferedResponseHandlerTest
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileBufferedResponseHandlerTest.class);
|
||||
|
||||
public WorkDir _workDir;
|
||||
|
||||
private final CountDownLatch _disposeLatch = new CountDownLatch(1);
|
||||
private Server _server;
|
||||
private LocalConnector _localConnector;
|
||||
@ -74,8 +81,7 @@ public class FileBufferedResponseHandlerTest
|
||||
@BeforeEach
|
||||
public void before() throws Exception
|
||||
{
|
||||
_testDir = MavenTestingUtils.getTargetTestingPath(FileBufferedResponseHandlerTest.class.getName());
|
||||
FS.ensureDirExists(_testDir);
|
||||
_testDir = _workDir.getEmptyPathDir();
|
||||
|
||||
_server = new Server();
|
||||
HttpConfiguration config = new HttpConfiguration();
|
||||
@ -109,8 +115,6 @@ public class FileBufferedResponseHandlerTest
|
||||
_bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
|
||||
_bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
|
||||
_server.setHandler(_bufferedHandler);
|
||||
|
||||
FS.ensureEmpty(_testDir);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@ -175,8 +179,13 @@ public class FileBufferedResponseHandlerTest
|
||||
assertThat(responseContent, containsString("Committed: false"));
|
||||
assertThat(responseContent, containsString("NumFiles: 1"));
|
||||
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -269,8 +278,13 @@ public class FileBufferedResponseHandlerTest
|
||||
assertThat(responseContent, containsString("Committed: false"));
|
||||
assertThat(responseContent, containsString("NumFiles: 1"));
|
||||
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -301,8 +315,13 @@ public class FileBufferedResponseHandlerTest
|
||||
assertThat(responseContent, not(containsString("writtenAfterClose")));
|
||||
assertThat(responseContent, containsString("NumFiles: 1"));
|
||||
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -363,8 +382,13 @@ public class FileBufferedResponseHandlerTest
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, containsString("NumFiles: 0"));
|
||||
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -400,12 +424,18 @@ public class FileBufferedResponseHandlerTest
|
||||
// Resetting the response buffer will delete the file.
|
||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||
assertThat(responseContent, not(containsString("THIS WILL BE RESET")));
|
||||
|
||||
assertThat(responseContent, containsString("NumFilesBeforeReset: 1"));
|
||||
assertThat(responseContent, containsString("NumFilesAfterReset: 0"));
|
||||
assertThat(responseContent, containsString("NumFilesAfterWrite: 1"));
|
||||
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -479,8 +509,13 @@ public class FileBufferedResponseHandlerTest
|
||||
assertThat(response.get("FileSize"), is(Long.toString(fileSize)));
|
||||
assertThat(received.get(), is(fileSize));
|
||||
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -553,9 +588,14 @@ public class FileBufferedResponseHandlerTest
|
||||
Throwable error = errorFuture.get(5, TimeUnit.SECONDS);
|
||||
assertThat(error.getMessage(), containsString("intentionally throwing from interceptor"));
|
||||
|
||||
// All files were deleted.
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
// Unable to verify file deletion on windows, as immediate delete not possible.
|
||||
// only after a GC has occurred.
|
||||
if (!OS.WINDOWS.isCurrentOs())
|
||||
{
|
||||
// All files were deleted.
|
||||
assertTrue(_disposeLatch.await(5, TimeUnit.SECONDS));
|
||||
assertThat(getNumFiles(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -17,7 +17,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
@ -26,7 +25,6 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.servlet.ServletException;
|
||||
@ -49,7 +47,6 @@ import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -59,7 +56,6 @@ import static org.hamcrest.Matchers.emptyOrNullString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
/**
|
||||
* HttpServer Tester for SSL based ServerConnector
|
||||
@ -124,43 +120,6 @@ public class ServerConnectorSslServerTest extends HttpServerTestBase
|
||||
return socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DisabledOnOs(WINDOWS) // Don't run on Windows (buggy JVM)
|
||||
public void testFullMethod() throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
super.testFullMethod();
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
// TODO This needs to be investigated #2244
|
||||
LOG.warn("Close overtook 400 response", e);
|
||||
}
|
||||
catch (SSLException e)
|
||||
{
|
||||
// TODO This needs to be investigated #2244
|
||||
if (e.getCause() instanceof SocketException)
|
||||
LOG.warn("Close overtook 400 response", e);
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@DisabledOnOs(WINDOWS) // Don't run on Windows (buggy JVM)
|
||||
public void testFullURI() throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
super.testFullURI();
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
LOG.warn("Close overtook 400 response", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testFullHeader() throws Exception
|
||||
{
|
||||
|
@ -235,8 +235,8 @@ public class SniSslConnectionFactoryTest
|
||||
assertThat(response, Matchers.containsString("Invalid SNI"));
|
||||
}
|
||||
|
||||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "See Issue #6609 - TLSv1.3 behavior differences between Linux and Windows")
|
||||
@Test
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
public void testWrongSNIRejectedConnection() throws Exception
|
||||
{
|
||||
start(ssl ->
|
||||
@ -275,8 +275,8 @@ public class SniSslConnectionFactoryTest
|
||||
assertThat(response.getStatus(), is(400));
|
||||
}
|
||||
|
||||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "See Issue #6609 - TLSv1.3 behavior differences between Linux and Windows")
|
||||
@Test
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
public void testWrongSNIRejectedFunction() throws Exception
|
||||
{
|
||||
start((ssl, customizer) ->
|
||||
@ -302,8 +302,8 @@ public class SniSslConnectionFactoryTest
|
||||
assertThat(response.getStatus(), is(400));
|
||||
}
|
||||
|
||||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "See Issue #6609 - TLSv1.3 behavior differences between Linux and Windows")
|
||||
@Test
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
public void testWrongSNIRejectedConnectionWithNonSNIKeystore() throws Exception
|
||||
{
|
||||
start(ssl ->
|
||||
|
@ -37,6 +37,7 @@ import org.eclipse.jetty.util.URIUtil;
|
||||
* will be handled by any servlets mapped to that URL.
|
||||
*
|
||||
* Requests to "/some/directory" will be redirected to "/some/directory/".
|
||||
* @deprecated no replacement is offered, use standard Servlet web.xml welcome features
|
||||
*/
|
||||
@Deprecated
|
||||
public class WelcomeFilter implements Filter
|
||||
|
@ -45,6 +45,7 @@ import org.junit.jupiter.api.Test;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ -289,7 +290,8 @@ public abstract class AbstractDoSFilterTest
|
||||
String responses = doRequests(request1 + request2 + request1 + request2 + request1, 2, 1100, 1100, last);
|
||||
|
||||
assertEquals(11, count(responses, "HTTP/1.1 200 OK"));
|
||||
assertEquals(0, count(responses, "DoSFilter: delayed"));
|
||||
// This test is system speed dependent, so allow some (20%-ish) requests to be delayed, but not more.
|
||||
assertThat("delayed count", count(responses, "DoSFilter: delayed"), lessThan(2));
|
||||
|
||||
// alternate between sessions
|
||||
responses = doRequests(request1 + request2 + request1 + request2 + request1, 2, 250, 250, last);
|
||||
|
@ -25,6 +25,7 @@ import org.eclipse.jetty.http.UriCompliance;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.webapp.WebAppContext;
|
||||
@ -80,6 +81,8 @@ public class WelcomeFilterTest
|
||||
}
|
||||
|
||||
WebAppContext context = new WebAppContext(server, directoryPath.toString(), "/");
|
||||
// Turn off memory-mapped behavior in DefaultServlet for Windows testing reasons.
|
||||
context.setInitParameter(DefaultServlet.CONTEXT_INIT + "useFileMappedBuffer", "false");
|
||||
server.setHandler(context);
|
||||
String concatPath = "/*";
|
||||
|
||||
|
@ -24,10 +24,8 @@ public class JettyLoggingServiceProvider implements SLF4JServiceProvider
|
||||
{
|
||||
/**
|
||||
* Declare the version of the SLF4J API this implementation is compiled against.
|
||||
* The value of this field is modified with each major release.
|
||||
*/
|
||||
// to avoid constant folding by the compiler, this field must *not* be final
|
||||
public static String REQUESTED_API_VERSION = "1.8.99"; // !final
|
||||
private static final String REQUESTED_API_VERSION = "2.0";
|
||||
|
||||
private JettyLoggerFactory loggerFactory;
|
||||
private BasicMarkerFactory markerFactory;
|
||||
@ -66,7 +64,7 @@ public class JettyLoggingServiceProvider implements SLF4JServiceProvider
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequesteApiVersion()
|
||||
public String getRequestedApiVersion()
|
||||
{
|
||||
return REQUESTED_API_VERSION;
|
||||
}
|
||||
|
@ -22,12 +22,12 @@ EXISTS|maindir/
|
||||
EXISTS|start.ini
|
||||
|
||||
# Output Assertions [regex!] (order is irrelevant)
|
||||
OUTPUT|INFO : mkdir ..jetty.base./start.d
|
||||
OUTPUT|INFO : extra initialized in \$\{jetty.base\}/start.d/extra.ini
|
||||
OUTPUT|INFO : mkdir ..jetty.base.[/\\]start.d
|
||||
OUTPUT|INFO : extra initialized in \$\{jetty.base\}[/\\]start.d[/\\]extra.ini
|
||||
OUTPUT|INFO : main transitively enabled, ini template available with --add-module=main
|
||||
OUTPUT|INFO : optional initialized in \$\{jetty.base\}/start.d/optional.ini
|
||||
OUTPUT|INFO : optional initialized in \$\{jetty.base\}[/\\]start.d[/\\]optional.ini
|
||||
OUTPUT|INFO : base transitively enabled
|
||||
OUTPUT|INFO : mkdir ..jetty.base./maindir
|
||||
OUTPUT|INFO : mkdir ..jetty.base.[/\\]maindir
|
||||
OUTPUT|INFO : Base directory was modified
|
||||
|
||||
|
||||
|
@ -22,12 +22,12 @@ EXISTS|maindir/
|
||||
EXISTS|start.ini
|
||||
|
||||
# Output Assertions [regex!] (order is irrelevant)
|
||||
OUTPUT|INFO : create ..jetty.base./start.ini
|
||||
OUTPUT|INFO : extra initialized in ..jetty.base./start.ini
|
||||
OUTPUT|INFO : create ..jetty.base.[/\\]start.ini
|
||||
OUTPUT|INFO : extra initialized in ..jetty.base.[/\\]start.ini
|
||||
OUTPUT|INFO : main transitively enabled, ini template available with --add-module=main
|
||||
OUTPUT|INFO : optional initialized in ..jetty.base./start.ini
|
||||
OUTPUT|INFO : optional initialized in ..jetty.base.[/\\]start.ini
|
||||
OUTPUT|INFO : base transitively enabled
|
||||
OUTPUT|INFO : mkdir ..jetty.base./maindir
|
||||
OUTPUT|INFO : mkdir ..jetty.base.[/\\]maindir
|
||||
OUTPUT|INFO : Base directory was modified
|
||||
|
||||
|
||||
|
@ -14,4 +14,4 @@ PROP|main.prop=value0
|
||||
EXISTS|maindir/
|
||||
EXISTS|start.d/main.ini
|
||||
|
||||
OUTPUT|INFO : main already enabled by \[\$\{jetty.base}[\\/]start.d/main.ini\]
|
||||
OUTPUT|INFO : main already enabled by \[\$\{jetty.base}[/\\]start.d[/\\]main.ini\]
|
||||
|
@ -17,7 +17,7 @@ PROP|optional.prop=value0
|
||||
EXISTS|maindir/
|
||||
EXISTS|start.ini
|
||||
|
||||
OUTPUT|INFO : copy ..jetty.base./start.d/main.ini into ..jetty.base./start.ini
|
||||
OUTPUT|INFO : optional initialized in ..jetty.base./start.ini
|
||||
OUTPUT|INFO : mkdir ..jetty.base./maindir
|
||||
OUTPUT|INFO : copy ..jetty.base.[/\\]start.d[/\\]main.ini into ..jetty.base.[/\\]start.ini
|
||||
OUTPUT|INFO : optional initialized in ..jetty.base.[/\\]start.ini
|
||||
OUTPUT|INFO : mkdir ..jetty.base.[/\\]maindir
|
||||
OUTPUT|INFO : Base directory was modified
|
||||
|
@ -108,13 +108,26 @@ public interface Callback extends Invocable
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callback from the passed success and failure
|
||||
* Creates a callback from the given success and failure lambdas.
|
||||
*
|
||||
* @param success Called when the callback succeeds
|
||||
* @param failure Called when the callback fails
|
||||
* @return a new Callback
|
||||
*/
|
||||
static Callback from(Runnable success, Consumer<Throwable> failure)
|
||||
{
|
||||
return from(InvocationType.BLOCKING, success, failure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a callback with the given InvocationType from the given success and failure lambdas.
|
||||
*
|
||||
* @param invocationType the Callback invocation type
|
||||
* @param success Called when the callback succeeds
|
||||
* @param failure Called when the callback fails
|
||||
* @return a new Callback
|
||||
*/
|
||||
static Callback from(InvocationType invocationType, Runnable success, Consumer<Throwable> failure)
|
||||
{
|
||||
return new Callback()
|
||||
{
|
||||
@ -129,6 +142,12 @@ public interface Callback extends Invocable
|
||||
{
|
||||
failure.accept(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType()
|
||||
{
|
||||
return invocationType;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -339,10 +358,6 @@ public interface Callback extends Invocable
|
||||
}
|
||||
}
|
||||
|
||||
interface InvocableCallback extends Invocable, Callback
|
||||
{
|
||||
}
|
||||
|
||||
static Callback combine(Callback cb1, Callback cb2)
|
||||
{
|
||||
if (cb1 == null || cb1 == cb2)
|
||||
@ -350,7 +365,7 @@ public interface Callback extends Invocable
|
||||
if (cb2 == null)
|
||||
return cb1;
|
||||
|
||||
return new InvocableCallback()
|
||||
return new Callback()
|
||||
{
|
||||
@Override
|
||||
public void succeeded()
|
||||
|
@ -2178,9 +2178,9 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
|
||||
String host = sslEngine.getPeerHost();
|
||||
if (host != null)
|
||||
{
|
||||
// TODO Must handle : somehow as java17 SNIHostName never handles: See #6624
|
||||
// Must use the byte[] constructor, because the character ':' is forbidden when
|
||||
// using the String constructor (but typically present in IPv6 addresses).
|
||||
// Since Java 17, only letter|digit|hyphen characters are allowed, even by the byte[] constructor.
|
||||
return List.of(new SNIHostName(host.getBytes(StandardCharsets.US_ASCII)));
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
package org.eclipse.jetty.util.thread;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
@ -21,6 +22,7 @@ import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jetty.util.AtomicBiInteger;
|
||||
import org.eclipse.jetty.util.ProcessorUtils;
|
||||
@ -38,14 +40,14 @@ import static org.eclipse.jetty.util.AtomicBiInteger.getLo;
|
||||
|
||||
|
||||
/**
|
||||
* An Executor using pre-allocated/reserved Threads from a wrapped Executor.
|
||||
* <p>Calls to {@link #execute(Runnable)} on a {@link ReservedThreadExecutor} will either succeed
|
||||
* with a Thread immediately being assigned the Runnable task, or fail if no Thread is
|
||||
* available.
|
||||
* <p>Threads are reserved lazily, with a new reserved threads being allocated from the
|
||||
* {@link Executor} passed to the constructor. Whenever 1 or more reserved threads have been
|
||||
* <p>A TryExecutor using pre-allocated/reserved threads from an external Executor.</p>
|
||||
* <p>Calls to {@link #tryExecute(Runnable)} on ReservedThreadExecutor will either
|
||||
* succeed with a reserved thread immediately being assigned the task, or fail if
|
||||
* no reserved thread is available.</p>
|
||||
* <p>Threads are reserved lazily, with new reserved threads being allocated from the external
|
||||
* {@link Executor} passed to the constructor. Whenever 1 or more reserved threads have been
|
||||
* idle for more than {@link #getIdleTimeoutMs()} then one reserved thread will return to
|
||||
* the executor.
|
||||
* the external Executor.</p>
|
||||
*/
|
||||
@ManagedObject("A pool for reserved threads")
|
||||
public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExecutor, Dumpable
|
||||
@ -62,7 +64,7 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExec
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "STOP!";
|
||||
return "STOP";
|
||||
}
|
||||
};
|
||||
|
||||
@ -72,7 +74,6 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExec
|
||||
private final SynchronousQueue<Runnable> _queue = new SynchronousQueue<>(false);
|
||||
private final AtomicBiInteger _count = new AtomicBiInteger(); // hi=pending; lo=size;
|
||||
private final AtomicLong _lastEmptyTime = new AtomicLong(System.nanoTime());
|
||||
|
||||
private ThreadPoolBudget.Lease _lease;
|
||||
private long _idleTimeNanos = DEFAULT_IDLE_TIMEOUT;
|
||||
|
||||
@ -175,20 +176,25 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExec
|
||||
|
||||
super.doStop();
|
||||
|
||||
// Offer STOP task to all waiting reserved threads.
|
||||
for (int i = _count.getAndSetLo(-1); i-- > 0;)
|
||||
// Mark this instance as stopped.
|
||||
int size = _count.getAndSetLo(-1);
|
||||
|
||||
// Offer the STOP task to all waiting reserved threads.
|
||||
for (int i = 0; i < size; ++i)
|
||||
{
|
||||
// yield to wait for any reserved threads that have incremented the size but not yet polled
|
||||
// Yield to wait for any reserved threads that
|
||||
// have incremented the size but not yet polled.
|
||||
Thread.yield();
|
||||
_queue.offer(STOP);
|
||||
}
|
||||
// Interrupt any reserved thread missed the offer so it doesn't wait too long.
|
||||
for (ReservedThread reserved : _threads)
|
||||
{
|
||||
Thread thread = reserved._thread;
|
||||
if (thread != null)
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
// Interrupt any reserved thread missed the offer,
|
||||
// so they do not wait for the whole idle timeout.
|
||||
_threads.stream()
|
||||
.filter(ReservedThread::isReserved)
|
||||
.map(t -> t._thread)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(Thread::interrupt);
|
||||
_threads.clear();
|
||||
_count.getAndSetHi(0);
|
||||
}
|
||||
@ -264,13 +270,16 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExec
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
Dumpable.dumpObjects(out, indent, this,
|
||||
new DumpableCollection("reserved", _threads));
|
||||
new DumpableCollection("threads",
|
||||
_threads.stream()
|
||||
.filter(ReservedThread::isReserved)
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s@%x{s=%d/%d,p=%d}",
|
||||
return String.format("%s@%x{reserved=%d/%d,pending=%d}",
|
||||
getClass().getSimpleName(),
|
||||
hashCode(),
|
||||
_count.getLo(),
|
||||
@ -293,6 +302,11 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExec
|
||||
private volatile State _state = State.PENDING;
|
||||
private volatile Thread _thread;
|
||||
|
||||
private boolean isReserved()
|
||||
{
|
||||
return _state == State.RESERVED;
|
||||
}
|
||||
|
||||
private Runnable reservedWait()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
@ -321,7 +335,6 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements TryExec
|
||||
}
|
||||
_state = size >= 0 ? State.IDLE : State.STOPPED;
|
||||
return STOP;
|
||||
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
|
@ -362,7 +362,7 @@ public class PathWatcherTest
|
||||
capture.finishedLatch.await(LONG_TIME, TimeUnit.MILLISECONDS);
|
||||
long end = System.nanoTime();
|
||||
capture.assertEvents(expected);
|
||||
assertThat(end - start, greaterThan(TimeUnit.MILLISECONDS.toNanos(2 * QUIET_TIME)));
|
||||
assertThat(end - start, greaterThan(TimeUnit.MILLISECONDS.toNanos(2L * QUIET_TIME)));
|
||||
Thread.sleep(WAIT_TIME);
|
||||
capture.assertEvents(expected);
|
||||
|
||||
|
@ -17,6 +17,7 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.ArrayList;
|
||||
@ -28,40 +29,38 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.Scanner.Notification;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class ScannerTest
|
||||
{
|
||||
static File _directory;
|
||||
static Scanner _scanner;
|
||||
static BlockingQueue<Event> _queue = new LinkedBlockingQueue<>();
|
||||
static BlockingQueue<Set<String>> _bulk = new LinkedBlockingQueue<>();
|
||||
public WorkDir workDir;
|
||||
private Path _directory;
|
||||
private Scanner _scanner;
|
||||
private BlockingQueue<Event> _queue = new LinkedBlockingQueue<>();
|
||||
private BlockingQueue<Set<String>> _bulk = new LinkedBlockingQueue<>();
|
||||
|
||||
@BeforeAll
|
||||
public static void setUpBeforeClass() throws Exception
|
||||
@BeforeEach
|
||||
public void setupScanner() throws Exception
|
||||
{
|
||||
File testDir = MavenTestingUtils.getTargetTestingDir(ScannerTest.class.getSimpleName());
|
||||
FS.ensureEmpty(testDir);
|
||||
|
||||
// Use full path, pointing to a real directory (for FileSystems that are case-insensitive, like Windows and OSX to use)
|
||||
// This is only needed for the various comparisons below to make sense.
|
||||
_directory = testDir.toPath().toRealPath().toFile();
|
||||
|
||||
_directory = workDir.getEmptyPathDir();
|
||||
_scanner = new Scanner();
|
||||
_scanner.addDirectory(_directory.toPath());
|
||||
_scanner.addDirectory(_directory);
|
||||
_scanner.setScanInterval(0);
|
||||
_scanner.setReportDirs(false);
|
||||
_scanner.setReportExistingFilesOnStartup(false);
|
||||
@ -94,11 +93,10 @@ public class ScannerTest
|
||||
assertTrue(_bulk.isEmpty());
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDownAfterClass() throws Exception
|
||||
@AfterEach
|
||||
public void cleanup() throws Exception
|
||||
{
|
||||
_scanner.stop();
|
||||
IO.delete(_directory);
|
||||
LifeCycle.stop(_scanner);
|
||||
}
|
||||
|
||||
static class Event
|
||||
@ -139,7 +137,7 @@ public class ScannerTest
|
||||
@Test
|
||||
public void testDepth() throws Exception
|
||||
{
|
||||
File root = new File(_directory, "root");
|
||||
File root = new File(_directory.toFile(), "root");
|
||||
FS.ensureDirExists(root);
|
||||
FS.touch(new File(root, "foo.foo"));
|
||||
FS.touch(new File(root, "foo2.foo"));
|
||||
@ -215,7 +213,7 @@ public class ScannerTest
|
||||
public void testPatterns() throws Exception
|
||||
{
|
||||
//test include and exclude patterns
|
||||
File root = new File(_directory, "proot");
|
||||
File root = new File(_directory.toFile(), "proot");
|
||||
FS.ensureDirExists(root);
|
||||
|
||||
File ttt = new File(root, "ttt.txt");
|
||||
@ -288,7 +286,7 @@ public class ScannerTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(WINDOWS) // TODO: needs review
|
||||
@Tag("Slow")
|
||||
public void testAddedChangeRemove() throws Exception
|
||||
{
|
||||
touch("a0");
|
||||
@ -299,7 +297,7 @@ public class ScannerTest
|
||||
|
||||
Event event = _queue.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(event, "Event should not be null");
|
||||
assertEquals(_directory + "/a0", event._filename);
|
||||
assertEquals(_directory.resolve("a0").toString(), event._filename);
|
||||
assertEquals(Notification.ADDED, event._notification);
|
||||
|
||||
// add 3 more files
|
||||
@ -323,8 +321,8 @@ public class ScannerTest
|
||||
List<Event> actualEvents = new ArrayList<>();
|
||||
_queue.drainTo(actualEvents);
|
||||
assertEquals(2, actualEvents.size());
|
||||
Event a1 = new Event(_directory + "/a1", Notification.ADDED);
|
||||
Event a3 = new Event(_directory + "/a3", Notification.REMOVED);
|
||||
Event a1 = new Event(_directory.resolve("a1").toString(), Notification.ADDED);
|
||||
Event a3 = new Event(_directory.resolve("a3").toString(), Notification.REMOVED);
|
||||
assertThat(actualEvents, Matchers.containsInAnyOrder(a1, a3));
|
||||
assertTrue(_queue.isEmpty());
|
||||
|
||||
@ -332,7 +330,7 @@ public class ScannerTest
|
||||
_scanner.scan();
|
||||
event = _queue.poll();
|
||||
assertNotNull(event);
|
||||
assertEquals(_directory + "/a2", event._filename);
|
||||
assertEquals(_directory.resolve("a2").toString(), event._filename);
|
||||
assertEquals(Notification.ADDED, event._notification);
|
||||
assertTrue(_queue.isEmpty());
|
||||
|
||||
@ -353,7 +351,7 @@ public class ScannerTest
|
||||
_scanner.scan();
|
||||
event = _queue.poll();
|
||||
assertNotNull(event);
|
||||
assertEquals(_directory + "/a1", event._filename);
|
||||
assertEquals(_directory.resolve("a1").toString(), event._filename);
|
||||
assertEquals(Notification.CHANGED, event._notification);
|
||||
assertTrue(_queue.isEmpty());
|
||||
|
||||
@ -361,7 +359,7 @@ public class ScannerTest
|
||||
_scanner.scan();
|
||||
event = _queue.poll();
|
||||
assertNotNull(event);
|
||||
assertEquals(_directory + "/a2", event._filename);
|
||||
assertEquals(_directory.resolve("a2").toString(), event._filename);
|
||||
assertEquals(Notification.CHANGED, event._notification);
|
||||
assertTrue(_queue.isEmpty());
|
||||
|
||||
@ -371,8 +369,8 @@ public class ScannerTest
|
||||
|
||||
//Immediate notification of deletes.
|
||||
_scanner.scan();
|
||||
a1 = new Event(_directory + "/a1", Notification.REMOVED);
|
||||
Event a2 = new Event(_directory + "/a2", Notification.REMOVED);
|
||||
a1 = new Event(_directory.resolve("a1").toString(), Notification.REMOVED);
|
||||
Event a2 = new Event(_directory.resolve("a2").toString(), Notification.REMOVED);
|
||||
actualEvents = new ArrayList<>();
|
||||
_queue.drainTo(actualEvents);
|
||||
assertEquals(2, actualEvents.size());
|
||||
@ -392,13 +390,12 @@ public class ScannerTest
|
||||
_scanner.scan();
|
||||
event = _queue.poll();
|
||||
assertNotNull(event);
|
||||
assertEquals(_directory + "/a2", event._filename);
|
||||
assertEquals(_directory.resolve("a2").toString(), event._filename);
|
||||
assertEquals(Notification.ADDED, event._notification);
|
||||
assertTrue(_queue.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(WINDOWS) // TODO: needs review
|
||||
public void testSizeChange() throws Exception
|
||||
{
|
||||
touch("tsc0");
|
||||
@ -408,12 +405,12 @@ public class ScannerTest
|
||||
// takes 2 scans to notice tsc0 and check that it is stable.
|
||||
Event event = _queue.poll();
|
||||
assertNotNull(event);
|
||||
assertEquals(_directory + "/tsc0", event._filename);
|
||||
assertEquals(_directory.resolve("tsc0").toString(), event._filename);
|
||||
assertEquals(Notification.ADDED, event._notification);
|
||||
|
||||
// Create a new file by writing to it.
|
||||
long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
|
||||
File file = new File(_directory, "st");
|
||||
File file = new File(_directory.toFile(), "st");
|
||||
try (OutputStream out = new FileOutputStream(file, true))
|
||||
{
|
||||
out.write('x');
|
||||
@ -439,7 +436,7 @@ public class ScannerTest
|
||||
_scanner.scan();
|
||||
event = _queue.poll();
|
||||
assertNotNull(event);
|
||||
assertEquals(_directory + "/st", event._filename);
|
||||
assertEquals(_directory.resolve("st").toString(), event._filename);
|
||||
assertEquals(Notification.ADDED, event._notification);
|
||||
|
||||
// Modify size only
|
||||
@ -456,21 +453,20 @@ public class ScannerTest
|
||||
_scanner.scan();
|
||||
event = _queue.poll();
|
||||
assertNotNull(event);
|
||||
assertEquals(_directory + "/st", event._filename);
|
||||
assertEquals(_directory.resolve("st").toString(), event._filename);
|
||||
assertEquals(Notification.CHANGED, event._notification);
|
||||
}
|
||||
}
|
||||
|
||||
private void delete(String string)
|
||||
private void delete(String string) throws IOException
|
||||
{
|
||||
File file = new File(_directory, string);
|
||||
if (file.exists())
|
||||
IO.delete(file);
|
||||
Path file = _directory.resolve(string);
|
||||
Files.deleteIfExists(file);
|
||||
}
|
||||
|
||||
private void touch(String string) throws IOException
|
||||
{
|
||||
File file = new File(_directory, string);
|
||||
File file = new File(_directory.toFile(), string);
|
||||
if (file.exists())
|
||||
file.setLastModified(TimeUnit.NANOSECONDS.toMillis(System.nanoTime()));
|
||||
else
|
||||
|
@ -46,7 +46,6 @@ import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.hamcrest.BaseMatcher;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@ -641,7 +640,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fsResourceProvider")
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testSymlink(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -649,18 +647,20 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo");
|
||||
Path bar = dir.resolve("bar");
|
||||
|
||||
boolean symlinkSupported;
|
||||
try
|
||||
{
|
||||
Files.createFile(foo);
|
||||
Files.createSymbolicLink(bar, foo);
|
||||
symlinkSupported = true;
|
||||
}
|
||||
catch (UnsupportedOperationException | FileSystemException e)
|
||||
{
|
||||
// if unable to create symlink, no point testing the rest
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported");
|
||||
symlinkSupported = false;
|
||||
}
|
||||
|
||||
assumeTrue(symlinkSupported, "Symlink not supported");
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
{
|
||||
Resource resFoo = base.addPath("foo");
|
||||
@ -683,7 +683,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = PathResource.class) // FileResource does not support this
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testNonExistantSymlink(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -692,17 +691,19 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo");
|
||||
Path bar = dir.resolve("bar");
|
||||
|
||||
boolean symlinkSupported;
|
||||
try
|
||||
{
|
||||
Files.createSymbolicLink(bar, foo);
|
||||
symlinkSupported = true;
|
||||
}
|
||||
catch (UnsupportedOperationException | FileSystemException e)
|
||||
{
|
||||
// if unable to create symlink, no point testing the rest
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported");
|
||||
symlinkSupported = false;
|
||||
}
|
||||
|
||||
assumeTrue(symlinkSupported, "Symlink not supported");
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
{
|
||||
Resource resFoo = base.addPath("foo");
|
||||
@ -835,8 +836,7 @@ public class FileSystemResourceTest
|
||||
}
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// NTFS filesystem streams are unsupported on some platforms.
|
||||
assumeTrue(true, "Not supported");
|
||||
assumeTrue(false, "NTFS simple streams not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -883,8 +883,7 @@ public class FileSystemResourceTest
|
||||
}
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// NTFS filesystem streams are unsupported on some platforms.
|
||||
assumeTrue(true, "Not supported");
|
||||
assumeTrue(false, "NTFS $DATA streams not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -929,15 +928,13 @@ public class FileSystemResourceTest
|
||||
}
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// NTFS filesystem streams are unsupported on some platforms.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "NTFS $DATA streams not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fsResourceProvider")
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testSemicolon(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -948,11 +945,9 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo;");
|
||||
Files.createFile(foo);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create file with semicolon");
|
||||
}
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
@ -964,7 +959,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fsResourceProvider")
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testSingleQuote(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -976,11 +970,9 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo' bar");
|
||||
Files.createFile(foo);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create file with single quote");
|
||||
}
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
@ -992,7 +984,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fsResourceProvider")
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testSingleBackTick(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -1004,11 +995,9 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo` bar");
|
||||
Files.createFile(foo);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create file with single back tick");
|
||||
}
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
@ -1020,7 +1009,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fsResourceProvider")
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testBrackets(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -1032,11 +1020,9 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo[1]");
|
||||
Files.createFile(foo);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create file with square brackets");
|
||||
}
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
@ -1048,7 +1034,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = PathResource.class) // FileResource does not support this
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testBraces(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -1060,11 +1045,9 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo.{bar}.txt");
|
||||
Files.createFile(foo);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create file with squiggle braces");
|
||||
}
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
@ -1076,7 +1059,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = PathResource.class) // FileResource does not support this
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testCaret(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -1088,11 +1070,9 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo^3.txt");
|
||||
Files.createFile(foo);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create file with caret");
|
||||
}
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
@ -1104,7 +1084,6 @@ public class FileSystemResourceTest
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = PathResource.class) // FileResource does not support this
|
||||
@DisabledOnOs(WINDOWS)
|
||||
public void testPipe(Class<PathResource> resourceClass) throws Exception
|
||||
{
|
||||
Path dir = workDir.getEmptyPathDir();
|
||||
@ -1116,11 +1095,9 @@ public class FileSystemResourceTest
|
||||
Path foo = dir.resolve("foo|bar.txt");
|
||||
Files.createFile(foo);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create file with pipe symbol");
|
||||
}
|
||||
|
||||
try (Resource base = newResource(resourceClass, dir.toFile()))
|
||||
@ -1477,10 +1454,7 @@ public class FileSystemResourceTest
|
||||
}
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to create file, no point testing the rest.
|
||||
// this is the path that occurs if you have a system that doesn't support UTF-8
|
||||
// directory names (or you simply don't have a Locale set properly)
|
||||
assumeTrue(true, "Not supported on this OS");
|
||||
assumeTrue(false, "Unable to create directory with utf-8 character");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Stream;
|
||||
@ -29,7 +30,6 @@ import org.eclipse.jetty.util.IO;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@ -43,6 +43,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
public class ResourceTest
|
||||
{
|
||||
@ -287,15 +288,23 @@ public class ResourceTest
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(OS.WINDOWS) // this uses forbidden characters on some Windows Environments
|
||||
public void testGlobPath() throws IOException
|
||||
{
|
||||
Path testDir = MavenTestingUtils.getTargetTestingPath("testGlobPath");
|
||||
FS.ensureEmpty(testDir);
|
||||
|
||||
String globReference = testDir.toAbsolutePath().toString() + File.separator + '*';
|
||||
Resource globResource = Resource.newResource(globReference);
|
||||
assertNotNull(globResource, "Should have produced a Resource");
|
||||
try
|
||||
{
|
||||
String globReference = testDir.toAbsolutePath() + File.separator + '*';
|
||||
Resource globResource = Resource.newResource(globReference);
|
||||
assertNotNull(globResource, "Should have produced a Resource");
|
||||
}
|
||||
catch (InvalidPathException e)
|
||||
{
|
||||
// if unable to reference the glob file, no point testing the rest.
|
||||
// this is the path that Microsoft Windows takes.
|
||||
assumeTrue(false, "Glob not supported on this OS");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -831,7 +831,7 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest
|
||||
dump = pool.dump();
|
||||
assertThat(count(dump, " - STARTED"), is(2));
|
||||
assertThat(dump, containsString(",3<=3<=4,i=2,r=2,q=0"));
|
||||
assertThat(dump, containsString("s=0/2"));
|
||||
assertThat(dump, containsString("reserved=0/2"));
|
||||
assertThat(dump, containsString("[ReservedThreadExecutor@"));
|
||||
assertThat(count(dump, " IDLE"), is(2));
|
||||
assertThat(count(dump, " WAITING"), is(1));
|
||||
@ -846,7 +846,7 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest
|
||||
dump = pool.dump();
|
||||
assertThat(count(dump, " - STARTED"), is(2));
|
||||
assertThat(dump, containsString(",3<=3<=4,i=1,r=2,q=0"));
|
||||
assertThat(dump, containsString("s=1/2"));
|
||||
assertThat(dump, containsString("reserved=1/2"));
|
||||
assertThat(dump, containsString("[ReservedThreadExecutor@"));
|
||||
assertThat(count(dump, " IDLE"), is(1));
|
||||
assertThat(count(dump, " WAITING"), is(1));
|
||||
|
@ -359,6 +359,5 @@ public class ReservedThreadExecutorTest
|
||||
|
||||
assertThat(usedReserved.get(), greaterThan(0));
|
||||
assertThat(usedReserved.get() + usedPool.get(), is(LOOPS));
|
||||
// System.err.printf("reserved=%d pool=%d total=%d%n", usedReserved.get(), usedPool.get(), LOOPS);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ module org.eclipse.jetty.webapp
|
||||
provides Configuration with
|
||||
org.eclipse.jetty.webapp.FragmentConfiguration,
|
||||
org.eclipse.jetty.webapp.JaasConfiguration,
|
||||
org.eclipse.jetty.webapp.JaspiConfiguration,
|
||||
org.eclipse.jetty.webapp.JettyWebXmlConfiguration,
|
||||
org.eclipse.jetty.webapp.JmxConfiguration,
|
||||
org.eclipse.jetty.webapp.JndiConfiguration,
|
||||
|
@ -0,0 +1,50 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.webapp;
|
||||
|
||||
import org.eclipse.jetty.util.Loader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* <p>JASPI Configuration</p>
|
||||
* <p>This configuration configures the WebAppContext server/system classes to
|
||||
* not be able to see the {@code javax.security.auth.message} package.</p>
|
||||
*/
|
||||
public class JaspiConfiguration extends AbstractConfiguration
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JaspiConfiguration.class);
|
||||
|
||||
public JaspiConfiguration()
|
||||
{
|
||||
addDependencies(WebXmlConfiguration.class, MetaInfConfiguration.class, WebInfConfiguration.class, FragmentConfiguration.class);
|
||||
addDependents(WebAppConfiguration.class);
|
||||
|
||||
hide("javax.security.auth.message.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Loader.loadClass("org.eclipse.jetty.security.jaspi.JaspiAuthenticator") != null;
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
LOG.trace("IGNORED", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
org.eclipse.jetty.webapp.FragmentConfiguration
|
||||
org.eclipse.jetty.webapp.JettyWebXmlConfiguration
|
||||
org.eclipse.jetty.webapp.JaasConfiguration
|
||||
org.eclipse.jetty.webapp.JaspiConfiguration
|
||||
org.eclipse.jetty.webapp.JmxConfiguration
|
||||
org.eclipse.jetty.webapp.JndiConfiguration
|
||||
org.eclipse.jetty.webapp.JspConfiguration
|
||||
|
@ -123,9 +123,22 @@ public class HugeResourceTest
|
||||
@AfterAll
|
||||
public static void cleanupTestFiles()
|
||||
{
|
||||
FS.ensureDeleted(staticBase);
|
||||
FS.ensureDeleted(outputDir);
|
||||
FS.ensureDeleted(multipartTempDir);
|
||||
quietlyDelete(staticBase);
|
||||
quietlyDelete(outputDir);
|
||||
quietlyDelete(multipartTempDir);
|
||||
}
|
||||
|
||||
private static void quietlyDelete(Path path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (path != null)
|
||||
FS.ensureDeleted(path);
|
||||
}
|
||||
catch (Throwable ignore)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private static void makeStaticFile(Path staticFile, long size) throws IOException
|
||||
|
@ -25,8 +25,10 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
@ -289,9 +291,12 @@ public class TempDirTest
|
||||
|
||||
/**
|
||||
* ServletContext.TEMPDIR has invalid <code>String</code> directory value (wrong permission to write into it)
|
||||
* IllegalStateException
|
||||
*
|
||||
* Note that if run in the CI environment, the test will fail, because it runs as root,
|
||||
* so we _will_ have permission to write to this directory.
|
||||
*/
|
||||
@Disabled("Jenkins will run as root so we do have permission to write to this directory.")
|
||||
@DisabledIfSystemProperty(named = "env", matches = "ci")
|
||||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Test/Temp directory is always writable")
|
||||
@Test
|
||||
public void attributeWithInvalidPermissions()
|
||||
{
|
||||
|
@ -23,10 +23,11 @@ import org.eclipse.jetty.http.UriCompliance;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
@ -35,8 +36,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class WebAppDefaultServletTest
|
||||
{
|
||||
public WorkDir workDir;
|
||||
private Server server;
|
||||
private LocalConnector connector;
|
||||
|
||||
@ -48,9 +51,7 @@ public class WebAppDefaultServletTest
|
||||
connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986);
|
||||
server.addConnector(connector);
|
||||
|
||||
Path directoryPath = MavenTestingUtils.getTargetTestingDir().toPath();
|
||||
IO.delete(directoryPath.toFile());
|
||||
Files.createDirectories(directoryPath);
|
||||
Path directoryPath = workDir.getEmptyPathDir();
|
||||
Path welcomeResource = directoryPath.resolve("index.html");
|
||||
try (OutputStream output = Files.newOutputStream(welcomeResource))
|
||||
{
|
||||
|
@ -29,6 +29,7 @@ import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
@ -90,9 +91,17 @@ public class ServerUpgradeRequest
|
||||
{
|
||||
if (cookies == null)
|
||||
{
|
||||
cookies = Arrays.stream(request.getCookies())
|
||||
.map(c -> new HttpCookie(c.getName(), c.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
Cookie[] reqCookies = request.getCookies();
|
||||
if (reqCookies != null)
|
||||
{
|
||||
cookies = Arrays.stream(reqCookies)
|
||||
.map(c -> new HttpCookie(c.getName(), c.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
else
|
||||
{
|
||||
cookies = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
@ -135,10 +144,13 @@ public class ServerUpgradeRequest
|
||||
{
|
||||
Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements())
|
||||
if (headerNames != null)
|
||||
{
|
||||
String name = headerNames.nextElement();
|
||||
headers.put(name, Collections.list(request.getHeaders(name)));
|
||||
while (headerNames.hasMoreElements())
|
||||
{
|
||||
String name = headerNames.nextElement();
|
||||
headers.put(name, Collections.list(request.getHeaders(name)));
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
@ -63,6 +63,8 @@ import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerI
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsStringIgnoringCase;
|
||||
@ -234,6 +236,7 @@ public class WebSocketOverHTTP2Test
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Issue #6660 - Windows does not throw ConnectException")
|
||||
public void testWebSocketConnectPortDoesNotExist() throws Exception
|
||||
{
|
||||
startServer();
|
||||
|
14
pom.xml
14
pom.xml
@ -20,9 +20,9 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<build-support.version>1.5</build-support.version>
|
||||
<checkstyle.version>8.37</checkstyle.version>
|
||||
<slf4j.version>2.0.0-alpha1</slf4j.version>
|
||||
<slf4j.version>2.0.0-alpha4</slf4j.version>
|
||||
<log4j2.version>2.14.0</log4j2.version>
|
||||
<logback.version>1.3.0-alpha5</logback.version>
|
||||
<logback.version>1.3.0-alpha9</logback.version>
|
||||
<disruptor.version>3.4.2</disruptor.version>
|
||||
<jetty-test-policy.version>1.2</jetty-test-policy.version>
|
||||
<servlet.api.version>4.0.6</servlet.api.version>
|
||||
@ -79,7 +79,8 @@
|
||||
<settingsPath>src/it/settings.xml</settingsPath>
|
||||
<invoker.mergeUserSettings>false</invoker.mergeUserSettings>
|
||||
<surefire.rerunFailingTestsCount>0</surefire.rerunFailingTestsCount>
|
||||
<testcontainers.version>1.15.1</testcontainers.version>
|
||||
<testcontainers.version>1.16.0</testcontainers.version>
|
||||
<jna.version>5.8.0</jna.version>
|
||||
<mariadb.version>2.7.0</mariadb.version>
|
||||
</properties>
|
||||
|
||||
@ -981,7 +982,7 @@
|
||||
<plugin>
|
||||
<groupId>org.mortbay.jetty</groupId>
|
||||
<artifactId>h2spec-maven-plugin</artifactId>
|
||||
<version>1.0.5</version>
|
||||
<version>1.0.6</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
@ -1151,7 +1152,7 @@
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>5.6.0</version>
|
||||
<version>${jna.version}</version>
|
||||
</dependency>
|
||||
<!-- Old Deps -->
|
||||
<dependency>
|
||||
@ -1458,6 +1459,9 @@
|
||||
<version>${maven.surefire.version}</version>
|
||||
<configuration>
|
||||
<excludedGroups>external, large-disk-resource</excludedGroups>
|
||||
<systemPropertyVariables>
|
||||
<env>ci</env>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
@ -191,6 +191,11 @@
|
||||
<artifactId>gcloud</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user