Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.1.x
This commit is contained in:
commit
af23f51d92
|
@ -127,7 +127,7 @@ def mavenBuild(jdk, cmdline, mvnName) {
|
|||
}
|
||||
finally
|
||||
{
|
||||
junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml', allowEmptyResults: true
|
||||
junit testResults: '**/target/surefire-reports/**/*.xml,**/target/invoker-reports/TEST*.xml', allowEmptyResults: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
README.md
14
README.md
|
@ -4,7 +4,7 @@ Eclipse Jetty is a lightweight, highly scalable, Java-based web server and Servl
|
|||
Jetty's goal is to support web protocols (HTTP/1, HTTP/2, HTTP/3, WebSocket, etc.) in a high volume low latency way that provides maximum performance while retaining the ease of use and compatibility with years of Servlet development.
|
||||
Jetty is a modern fully asynchronous web server that has a long history as a component oriented technology, and can be easily embedded into applications while still offering a solid traditional distribution for webapp deployment.
|
||||
|
||||
- https://eclipse.dev/jetty/
|
||||
- https://jetty.org
|
||||
- https://projects.eclipse.org/projects/rt.jetty
|
||||
|
||||
## Webapp Example
|
||||
|
@ -53,21 +53,21 @@ $ cd jetty.project
|
|||
$ mvn -Pfast clean install # fast build bypasses tests and other checks
|
||||
```
|
||||
|
||||
For more detailed information on building and contributing to the Jetty project, please see the [Contribution Guide](https://eclipse.dev/jetty/documentation/contribution-guide/index.html).
|
||||
For more detailed information on building and contributing to the Jetty project, please see the [Contribution Guide](https://jetty.org/docs/contribution-guide/index.html).
|
||||
|
||||
# Documentation
|
||||
|
||||
[Jetty's documentation](https://eclipse.dev/jetty/documentation) is available on the Eclipse Jetty website.
|
||||
[Jetty's documentation](https://jetty.org/docs) is available on the Eclipse Jetty website.
|
||||
|
||||
The documentation is divided into three guides, based on use case:
|
||||
|
||||
* The [Operations Guide](https://eclipse.dev/jetty/documentation/jetty-12/operations-guide/index.html) targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications.
|
||||
* The [Operations Guide](https://jetty.org/docs/jetty/12/operations-guide/index.html) targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications.
|
||||
|
||||
* The [Programming Guide](https://eclipse.dev/jetty/documentation/jetty-12/programming-guide/index.html) targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications.
|
||||
* The [Programming Guide](https://jetty.org/docs/jetty/12/programming-guide/index.html) targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications.
|
||||
|
||||
* The [Contribution Guide](https://eclipse.dev/jetty/documentation/contribution-guide/index.html) targets developers that wish to contribute to the Jetty Project with code patches or documentation improvements.
|
||||
* The [Contribution Guide](https://jetty.org/docs/contribution-guide/index.html) targets developers that wish to contribute to the Jetty Project with code patches or documentation improvements.
|
||||
|
||||
|
||||
# Commercial Support
|
||||
|
||||
Expert advice and production support of Jetty are provided by [Webtide](https://webtide.com).
|
||||
Expert advice and production support of Jetty are provided by [Webtide](https://webtide.com).
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.eclipse.jetty.http.HttpCookie;
|
|||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.Trailers;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.Context;
|
||||
|
@ -60,10 +61,11 @@ public class ServletToHandlerDocs
|
|||
// - servletRequest.getProtocol();
|
||||
String protocol = request.getConnectionMetaData().getProtocol();
|
||||
|
||||
// Gets the full request URI.
|
||||
// Gets the request URL.
|
||||
// Replaces:
|
||||
// - servletRequest.getRequestURL();
|
||||
String fullRequestURI = request.getHttpURI().asString();
|
||||
HttpURI httpURI = HttpURI.build(request.getHttpURI()).query(null);
|
||||
StringBuffer requestURL = new StringBuffer(httpURI.asString());
|
||||
|
||||
// Gets the request context.
|
||||
// Replaces:
|
||||
|
|
|
@ -47,37 +47,46 @@ Use an editor to create the file `src/main/java/org/example/HelloWorld.java` wit
|
|||
----
|
||||
package org.example;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
|
||||
public class HelloWorld extends AbstractHandler
|
||||
class HelloWorldHandler extends Handler.Abstract.NonBlocking
|
||||
{
|
||||
public void handle(String target,
|
||||
Request baseRequest,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response)
|
||||
throws IOException, ServletException
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback)
|
||||
{
|
||||
response.setContentType("text/html;charset=utf-8");
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
baseRequest.setHandled(true);
|
||||
response.getWriter().println("<h1>Hello World</h1>");
|
||||
}
|
||||
response.setStatus(200);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html; charset=UTF-8");
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
{
|
||||
Server server = new Server(8080);
|
||||
server.setHandler(new HelloWorld());
|
||||
|
||||
server.start();
|
||||
server.join();
|
||||
// Write a Hello World response.
|
||||
Content.Sink.write(response, true, """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Jetty Hello World Handler</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello World</p>
|
||||
</body>
|
||||
</html>
|
||||
""", callback);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Server server = new Server();
|
||||
Connector connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
// Set the Hello World Handler.
|
||||
server.setHandler(new HelloWorldHandler());
|
||||
|
||||
server.start();
|
||||
}
|
||||
----
|
||||
|
||||
[[creating-embedded-pom-descriptor]]
|
||||
|
@ -116,7 +125,6 @@ Use an editor to create the file `pom.xml` in the `JettyMavenHelloWorld` directo
|
|||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>1.1</version>
|
||||
<executions>
|
||||
<execution><goals><goal>java</goal></goals></execution>
|
||||
</executions>
|
||||
|
|
|
@ -208,7 +208,7 @@ public abstract class HttpSender
|
|||
return false;
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Request failure {}", exchange.getRequest(), failure);
|
||||
LOG.debug("Request failure {}, response {}", exchange.getRequest(), exchange.getResponse(), failure);
|
||||
|
||||
// Mark atomically the request as completed, with respect
|
||||
// to concurrency between request success and request failure.
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.net.URL;
|
|||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
@ -77,8 +78,8 @@ import org.slf4j.LoggerFactory;
|
|||
* <p>For XML configured contexts, the ID map will contain a reference to the {@link Server} instance called "Server" and
|
||||
* properties for the webapp file such as "jetty.webapp" and directory as "jetty.webapps".
|
||||
* The properties will be initialized with:<ul>
|
||||
* <li>The properties set on the application via {@link App#getProperties()}; otherwise:</li>
|
||||
* <li>The properties set on this provider via {@link #getProperties()}</li>
|
||||
* <li>The properties set on the application via {@link App#getProperties()}; otherwise:</li>
|
||||
* <li>The properties set on this provider via {@link #getProperties()}</li>
|
||||
* </ul>
|
||||
*/
|
||||
@ManagedObject("Provider for start-up deployment of webapps based on presence in directory")
|
||||
|
@ -243,6 +244,7 @@ public class ContextProvider extends ScanningAppProvider
|
|||
|
||||
/**
|
||||
* This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property.
|
||||
*
|
||||
* @param configurations The configuration class names as a comma separated list
|
||||
*/
|
||||
public void setConfigurationClasses(String configurations)
|
||||
|
@ -252,6 +254,7 @@ public class ContextProvider extends ScanningAppProvider
|
|||
|
||||
/**
|
||||
* This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property.
|
||||
*
|
||||
* @param configurations The configuration class names.
|
||||
*/
|
||||
public void setConfigurationClasses(String[] configurations)
|
||||
|
@ -262,8 +265,8 @@ public class ContextProvider extends ScanningAppProvider
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This is equivalent to getting the {@link Deployable#CONFIGURATION_CLASSES} property.
|
||||
*
|
||||
* @return The configuration class names.
|
||||
*/
|
||||
@ManagedAttribute("configuration classes for webapps to be processed through")
|
||||
|
@ -341,32 +344,48 @@ public class ContextProvider extends ScanningAppProvider
|
|||
|
||||
// prepare properties
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
|
||||
//add in properties from start mechanism
|
||||
properties.putAll(getProperties());
|
||||
|
||||
Object context = null;
|
||||
//check if there is a specific ContextHandler type to create set in the
|
||||
//properties associated with the webapp. If there is, we create it _before_
|
||||
//applying the environment xml file.
|
||||
String contextHandlerClassName = app.getProperties().get(Deployable.CONTEXT_HANDLER_CLASS);
|
||||
if (contextHandlerClassName != null)
|
||||
context = Class.forName(contextHandlerClassName).getDeclaredConstructor().newInstance();
|
||||
|
||||
//add in environment-specific properties
|
||||
String env = app.getEnvironmentName() == null ? "" : app.getEnvironmentName();
|
||||
Path envProperties = app.getPath().getParent().resolve(env + ".properties");
|
||||
if (Files.exists(envProperties))
|
||||
{
|
||||
try (InputStream stream = Files.newInputStream(envProperties))
|
||||
{
|
||||
Properties p = new Properties();
|
||||
p.load(stream);
|
||||
p.stringPropertyNames().forEach(k -> properties.put(k, p.getProperty(k)));
|
||||
}
|
||||
|
||||
String str = properties.get(Deployable.ENVIRONMENT_XML);
|
||||
if (!StringUtil.isEmpty(str))
|
||||
{
|
||||
Path envXmlPath = Paths.get(str);
|
||||
if (!envXmlPath.isAbsolute())
|
||||
envXmlPath = getMonitoredDirResource().getPath().getParent().resolve(envXmlPath);
|
||||
|
||||
context = applyXml(context, envXmlPath, env, properties);
|
||||
}
|
||||
}
|
||||
|
||||
//add in properties specific to the deployable
|
||||
properties.putAll(app.getProperties());
|
||||
|
||||
// Handle a context XML file
|
||||
if (FileID.isXml(path))
|
||||
{
|
||||
XmlConfiguration xmlc = new XmlConfiguration(ResourceFactory.of(this).newResource(path), null, properties)
|
||||
{
|
||||
@Override
|
||||
public void initializeDefaults(Object context)
|
||||
{
|
||||
super.initializeDefaults(context);
|
||||
ContextProvider.this.initializeContextHandler(context, path, properties);
|
||||
}
|
||||
};
|
||||
|
||||
xmlc.getIdMap().put("Environment", environment);
|
||||
xmlc.setJettyStandardIdsAndProperties(getDeploymentManager().getServer(), path);
|
||||
|
||||
// If it is a core context environment, then look for a classloader
|
||||
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null;
|
||||
if (coreContextClassLoader != null)
|
||||
Thread.currentThread().setContextClassLoader(coreContextClassLoader);
|
||||
|
||||
// Create the context by running the configuration
|
||||
Object context = xmlc.configure();
|
||||
context = applyXml(context, path, env, properties);
|
||||
|
||||
// Look for the contextHandler itself
|
||||
ContextHandler contextHandler = null;
|
||||
|
@ -382,27 +401,33 @@ public class ContextProvider extends ScanningAppProvider
|
|||
throw new IllegalStateException("Unknown context type of " + context);
|
||||
|
||||
// Set the classloader if we have a coreContextClassLoader
|
||||
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null;
|
||||
if (coreContextClassLoader != null)
|
||||
contextHandler.setClassLoader(coreContextClassLoader);
|
||||
|
||||
return contextHandler;
|
||||
}
|
||||
|
||||
// Otherwise it must be a directory or an archive
|
||||
else if (!Files.isDirectory(path) && !FileID.isWebArchive(path))
|
||||
{
|
||||
throw new IllegalStateException("unable to create ContextHandler for " + app);
|
||||
}
|
||||
|
||||
// Build the web application
|
||||
String contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass");
|
||||
if (StringUtil.isBlank(contextHandlerClassName))
|
||||
throw new IllegalStateException("No ContextHandler classname for " + app);
|
||||
Class<?> contextHandlerClass = Loader.loadClass(contextHandlerClassName);
|
||||
if (contextHandlerClass == null)
|
||||
throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app);
|
||||
// Build the web application if necessary
|
||||
if (context == null)
|
||||
{
|
||||
contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass");
|
||||
if (StringUtil.isBlank(contextHandlerClassName))
|
||||
throw new IllegalStateException("No ContextHandler classname for " + app);
|
||||
Class<?> contextHandlerClass = Loader.loadClass(contextHandlerClassName);
|
||||
if (contextHandlerClass == null)
|
||||
throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app);
|
||||
|
||||
context = contextHandlerClass.getDeclaredConstructor().newInstance();
|
||||
properties.put(Deployable.WAR, path.toString());
|
||||
}
|
||||
|
||||
Object context = contextHandlerClass.getDeclaredConstructor().newInstance();
|
||||
properties.put(Deployable.WAR, path.toString());
|
||||
return initializeContextHandler(context, path, properties);
|
||||
}
|
||||
finally
|
||||
|
@ -411,6 +436,36 @@ public class ContextProvider extends ScanningAppProvider
|
|||
}
|
||||
}
|
||||
|
||||
protected Object applyXml(Object context, Path xml, String environment, Map<String, String> properties) throws Exception
|
||||
{
|
||||
if (!FileID.isXml(xml))
|
||||
return null;
|
||||
|
||||
XmlConfiguration xmlc = new XmlConfiguration(ResourceFactory.of(this).newResource(xml), null, properties)
|
||||
{
|
||||
@Override
|
||||
public void initializeDefaults(Object context)
|
||||
{
|
||||
super.initializeDefaults(context);
|
||||
ContextProvider.this.initializeContextHandler(context, xml, properties);
|
||||
}
|
||||
};
|
||||
|
||||
xmlc.getIdMap().put("Environment", environment);
|
||||
xmlc.setJettyStandardIdsAndProperties(getDeploymentManager().getServer(), xml);
|
||||
|
||||
// If it is a core context environment, then look for a classloader
|
||||
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(xml) : null;
|
||||
if (coreContextClassLoader != null)
|
||||
Thread.currentThread().setContextClassLoader(coreContextClassLoader);
|
||||
|
||||
// Create or configure the context
|
||||
if (context == null)
|
||||
return xmlc.configure();
|
||||
|
||||
return xmlc.configure(context);
|
||||
}
|
||||
|
||||
protected ClassLoader findCoreContextClassLoader(Path path) throws IOException
|
||||
{
|
||||
Path webapps = path.getParent();
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.deploy;
|
||||
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
|
||||
public class BarContextHandler extends ContextHandler
|
||||
{
|
||||
}
|
|
@ -15,27 +15,18 @@ package org.eclipse.jetty.deploy.providers;
|
|||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.eclipse.jetty.deploy.AppProvider;
|
||||
import org.eclipse.jetty.deploy.DeploymentManager;
|
||||
import org.eclipse.jetty.deploy.BarContextHandler;
|
||||
import org.eclipse.jetty.deploy.test.XmlConfiguredJetty;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.Deployable;
|
||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenPaths;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.Scanner;
|
||||
import org.eclipse.jetty.util.component.Container;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
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;
|
||||
|
@ -43,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -77,7 +69,10 @@ public class ContextProviderStartupTest
|
|||
|
||||
// Should not throw an Exception
|
||||
jetty.load();
|
||||
}
|
||||
|
||||
public void startJetty() throws Exception
|
||||
{
|
||||
// Start it
|
||||
jetty.start();
|
||||
}
|
||||
|
@ -89,9 +84,47 @@ public class ContextProviderStartupTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testStartupContext()
|
||||
public void testStartupContext() throws Exception
|
||||
{
|
||||
startJetty();
|
||||
|
||||
// Check Server for Handlers
|
||||
jetty.assertContextHandlerExists("/bar");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartupWithRelativeEnvironmentContext() throws Exception
|
||||
{
|
||||
Path jettyBase = jetty.getJettyBasePath();
|
||||
Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = etc/core-context.xml", StandardOpenOption.CREATE_NEW);
|
||||
assertTrue(Files.exists(propsFile));
|
||||
Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING);
|
||||
jetty.copyWebapp("bar-core-context.properties", "bar.properties");
|
||||
startJetty();
|
||||
|
||||
//check environment context xml was applied to the produced context
|
||||
ContextHandler context = jetty.getContextHandler("/bar");
|
||||
assertNotNull(context);
|
||||
assertThat(context.getAttribute("somename"), equalTo("somevalue"));
|
||||
assertTrue(context instanceof BarContextHandler);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartupWithAbsoluteEnvironmentContext() throws Exception
|
||||
{
|
||||
Path jettyBase = jetty.getJettyBasePath();
|
||||
Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = " +
|
||||
MavenPaths.findTestResourceFile("etc/core-context.xml"), StandardOpenOption.CREATE_NEW);
|
||||
assertTrue(Files.exists(propsFile));
|
||||
Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING);
|
||||
jetty.copyWebapp("bar-core-context.properties", "bar-core-context.properties");
|
||||
startJetty();
|
||||
|
||||
//check environment context xml was applied to the produced context
|
||||
ContextHandler context = jetty.getContextHandler("/bar");
|
||||
assertNotNull(context);
|
||||
assertThat(context.getAttribute("somename"), equalTo("somevalue"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- // -->
|
||||
<!-- // ======================================================================== -->
|
||||
<!-- // Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -->
|
||||
<!-- // -->
|
||||
<!-- // This program and the accompanying materials are made available under the -->
|
||||
<!-- // terms of the Eclipse Public License v. 2.0 which is available at -->
|
||||
<!-- // https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -->
|
||||
<!-- // which is available at https://www.apache.org/licenses/LICENSE-2.0. -->
|
||||
<!-- // -->
|
||||
<!-- // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -->
|
||||
<!-- // ======================================================================== -->
|
||||
<!-- // -->
|
||||
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
|
||||
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
|
||||
<Set name="contextPath">/global</Set>
|
||||
<Call name="setAttribute">
|
||||
<Arg>somename</Arg>
|
||||
<Arg>somevalue</Arg>
|
||||
</Call>
|
||||
</Configure>
|
|
@ -0,0 +1,2 @@
|
|||
environment: core
|
||||
jetty.deploy.contextHandlerClass: org.eclipse.jetty.deploy.BarContextHandler
|
|
@ -169,9 +169,7 @@ public class WebAppClassLoading
|
|||
* Add a hidden (server) Class pattern to use for all WebAppContexts of a given {@link Server}.
|
||||
* @param attributes The {@link Attributes} instance to add classes to
|
||||
* @param patterns the patterns to use
|
||||
* @deprecated use {@link #addHiddenClasses(Server, String...)} instead
|
||||
*/
|
||||
@Deprecated (since = "12.0.9", forRemoval = true)
|
||||
public static void addHiddenClasses(Attributes attributes, String... patterns)
|
||||
{
|
||||
if (patterns != null && patterns.length > 0)
|
||||
|
|
|
@ -38,9 +38,16 @@ import java.util.stream.StreamSupport;
|
|||
/**
|
||||
* <p>An ordered collection of {@link HttpField}s that represent the HTTP headers
|
||||
* or HTTP trailers of an HTTP request or an HTTP response.</p>
|
||||
*
|
||||
* <p>{@link HttpFields} is immutable and typically used in server-side HTTP requests
|
||||
* and client-side HTTP responses, while {@link HttpFields.Mutable} is mutable and
|
||||
* typically used in server-side HTTP responses and client-side HTTP requests.</p>
|
||||
*
|
||||
* <p>Access is always more efficient using {@link HttpHeader} keys rather than {@link String} field names.</p>
|
||||
*
|
||||
* <p>The primary implementations of {@code HttpFields} have been optimized assuming few
|
||||
* lookup operations, thus typically if many {@link HttpField}s need to looked up, it may be
|
||||
* better to use an {@link Iterator} to find multiple fields in a single iteration.</p>
|
||||
*/
|
||||
public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
||||
{
|
||||
|
@ -350,10 +357,12 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
/**
|
||||
* <p>Returns whether this instance contains the given field name.</p>
|
||||
* <p>The comparison of field name is case-insensitive via
|
||||
* {@link HttpField#is(String)}.
|
||||
* {@link HttpField#is(String)}. If possible, it is more efficient to use
|
||||
* {@link #contains(HttpHeader)}.
|
||||
*
|
||||
* @param name the case-insensitive field name to search for
|
||||
* @return whether this instance contains the given field name
|
||||
* @see #contains(HttpHeader)
|
||||
*/
|
||||
default boolean contains(String name)
|
||||
{
|
||||
|
@ -412,7 +421,7 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
* <p>Returns the encoded value of the first field with the given field name,
|
||||
* or {@code null} if no such field is present.</p>
|
||||
* <p>The comparison of field name is case-insensitive via
|
||||
* {@link HttpField#is(String)}.</p>
|
||||
* {@link HttpField#is(String)}. If possible, it is more efficient to use {@link #get(HttpHeader)}.</p>
|
||||
* <p>In case of multi-valued fields, the returned value is the encoded
|
||||
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.</p>
|
||||
*
|
||||
|
@ -420,6 +429,7 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
* @return the raw value of the first field with the given field name,
|
||||
* or {@code null} if no such field is present
|
||||
* @see HttpField#getValue()
|
||||
* @see #get(HttpHeader)
|
||||
*/
|
||||
default String get(String name)
|
||||
{
|
||||
|
@ -594,12 +604,13 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
* <p>Returns a {@link Set} of the field names.</p>
|
||||
* <p>Case-sensitivity of the field names is preserved.</p>
|
||||
*
|
||||
* @return a {@link Set} of the field names
|
||||
* @return an immutable {@link Set} of the field names. Changes made to the
|
||||
* {@code HttpFields} after this call are not reflected in the set.
|
||||
*/
|
||||
default Set<String> getFieldNamesCollection()
|
||||
{
|
||||
Set<HttpHeader> seenByHeader = EnumSet.noneOf(HttpHeader.class);
|
||||
Set<String> seenByName = null;
|
||||
Set<String> buildByName = null;
|
||||
List<String> list = new ArrayList<>(size());
|
||||
|
||||
for (HttpField f : this)
|
||||
|
@ -607,9 +618,9 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
HttpHeader header = f.getHeader();
|
||||
if (header == null)
|
||||
{
|
||||
if (seenByName == null)
|
||||
seenByName = new TreeSet<>(String::compareToIgnoreCase);
|
||||
if (seenByName.add(f.getName()))
|
||||
if (buildByName == null)
|
||||
buildByName = new TreeSet<>(String::compareToIgnoreCase);
|
||||
if (buildByName.add(f.getName()))
|
||||
list.add(f.getName());
|
||||
}
|
||||
else if (seenByHeader.add(header))
|
||||
|
@ -618,6 +629,8 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
}
|
||||
}
|
||||
|
||||
Set<String> seenByName = buildByName;
|
||||
|
||||
// use the list to retain a rough ordering
|
||||
return new AbstractSet<>()
|
||||
{
|
||||
|
@ -632,6 +645,14 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
|||
{
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o)
|
||||
{
|
||||
if (o instanceof String s)
|
||||
return seenByName != null && seenByName.contains(s) || seenByHeader.contains(HttpHeader.CACHE.get(s));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -20,12 +20,7 @@ import java.util.Objects;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* HTTP Fields. A collection of HTTP header and or Trailer fields.
|
||||
*
|
||||
* <p>This class is not synchronized as it is expected that modifications will only be performed by a
|
||||
* single thread.
|
||||
*
|
||||
* <p>The cookie handling provided by this class is guided by the Servlet specification and RFC6265.
|
||||
* An immutable implementation of {@link HttpFields}.
|
||||
*/
|
||||
class ImmutableHttpFields implements HttpFields
|
||||
{
|
||||
|
@ -70,10 +65,9 @@ class ImmutableHttpFields implements HttpFields
|
|||
{
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof org.eclipse.jetty.http.ImmutableHttpFields))
|
||||
return false;
|
||||
|
||||
return isEqualTo((HttpFields)o);
|
||||
if (o instanceof HttpFields httpFields)
|
||||
return isEqualTo(httpFields);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -67,7 +67,7 @@ class MutableHttpFields implements HttpFields.Mutable
|
|||
*/
|
||||
protected MutableHttpFields(HttpFields fields)
|
||||
{
|
||||
if (fields instanceof ImmutableHttpFields immutable)
|
||||
if (fields instanceof org.eclipse.jetty.http.ImmutableHttpFields immutable)
|
||||
{
|
||||
_immutable = true;
|
||||
_fields = immutable._fields;
|
||||
|
@ -180,7 +180,7 @@ class MutableHttpFields implements HttpFields.Mutable
|
|||
public HttpFields asImmutable()
|
||||
{
|
||||
_immutable = true;
|
||||
return new ImmutableHttpFields(_fields, _size);
|
||||
return new org.eclipse.jetty.http.ImmutableHttpFields(_fields, _size);
|
||||
}
|
||||
|
||||
private void copyImmutable()
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.util.ListIterator;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
@ -352,16 +353,27 @@ public class HttpFieldsTest
|
|||
assertThat(header.get("EXPECT"), is("100"));
|
||||
assertThat(header.get("eXpEcT"), is("100"));
|
||||
assertThat(header.get(HttpHeader.EXPECT), is("100"));
|
||||
assertTrue(header.contains("expect"));
|
||||
assertTrue(header.contains("Expect"));
|
||||
assertTrue(header.contains("EXPECT"));
|
||||
assertTrue(header.contains("eXpEcT"));
|
||||
|
||||
assertThat(header.get("random"), is("value"));
|
||||
assertThat(header.get("Random"), is("value"));
|
||||
assertThat(header.get("RANDOM"), is("value"));
|
||||
assertThat(header.get("rAnDoM"), is("value"));
|
||||
assertThat(header.get("RaNdOm"), is("value"));
|
||||
assertTrue(header.contains("random"));
|
||||
assertTrue(header.contains("Random"));
|
||||
assertTrue(header.contains("RANDOM"));
|
||||
assertTrue(header.contains("rAnDoM"));
|
||||
assertTrue(header.contains("RaNdOm"));
|
||||
|
||||
assertThat(header.get("Accept-Charset"), is("UTF-8"));
|
||||
assertThat(header.get("accept-charset"), is("UTF-8"));
|
||||
assertThat(header.get(HttpHeader.ACCEPT_CHARSET), is("UTF-8"));
|
||||
assertTrue(header.contains("Accept-Charset"));
|
||||
assertTrue(header.contains("accept-charset"));
|
||||
|
||||
assertThat(header.getValuesList("Accept-Charset"), contains("UTF-8", "UTF-16"));
|
||||
assertThat(header.getValuesList("accept-charset"), contains("UTF-8", "UTF-16"));
|
||||
|
@ -371,9 +383,19 @@ public class HttpFieldsTest
|
|||
assertThat(header.get("Foo-Bar"), is("one"));
|
||||
assertThat(header.getValuesList("foo-bar"), contains("one", "two"));
|
||||
assertThat(header.getValuesList("Foo-Bar"), contains("one", "two"));
|
||||
assertTrue(header.contains("foo-bar"));
|
||||
assertTrue(header.contains("Foo-Bar"));
|
||||
|
||||
// We know the order of the set is deterministic
|
||||
assertThat(header.getFieldNamesCollection(), contains("expect", "RaNdOm", "Accept-Charset", "foo-bar"));
|
||||
Set<String> names = header.getFieldNamesCollection();
|
||||
assertThat(names, contains("expect", "RaNdOm", "Accept-Charset", "foo-bar"));
|
||||
assertTrue(names.contains("expect"));
|
||||
assertTrue(names.contains("Expect"));
|
||||
assertTrue(names.contains("random"));
|
||||
assertTrue(names.contains("accept-charset"));
|
||||
assertTrue(names.contains("Accept-Charset"));
|
||||
assertTrue(names.contains("foo-bar"));
|
||||
assertTrue(names.contains("Foo-Bar"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
package org.eclipse.jetty.http2.server;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
|
@ -155,7 +156,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
|
|||
@Override
|
||||
public void onReset(Stream stream, ResetFrame frame, Callback callback)
|
||||
{
|
||||
EofException failure = new EofException("Reset " + ErrorCode.toString(frame.getError(), null));
|
||||
EOFException failure = new EOFException("Reset " + ErrorCode.toString(frame.getError(), null));
|
||||
onFailure(stream, failure, callback);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
package org.eclipse.jetty.http2.server.internal;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.BiConsumer;
|
||||
|
@ -38,6 +39,7 @@ import org.eclipse.jetty.http2.frames.ResetFrame;
|
|||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.HttpStream;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
|
@ -587,7 +589,8 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
|
|||
@Override
|
||||
public Runnable onFailure(Throwable failure, Callback callback)
|
||||
{
|
||||
Runnable runnable = _httpChannel.onFailure(failure);
|
||||
boolean remote = failure instanceof EOFException;
|
||||
Runnable runnable = remote ? _httpChannel.onRemoteFailure(new EofException(failure)) : _httpChannel.onFailure(failure);
|
||||
return () ->
|
||||
{
|
||||
if (runnable != null)
|
||||
|
|
|
@ -58,7 +58,12 @@ public class AbstractTest
|
|||
|
||||
protected void start(Handler handler) throws Exception
|
||||
{
|
||||
HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(new HttpConfiguration());
|
||||
start(handler, new HttpConfiguration());
|
||||
}
|
||||
|
||||
protected void start(Handler handler, HttpConfiguration httpConfiguration) throws Exception
|
||||
{
|
||||
HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(httpConfiguration);
|
||||
connectionFactory.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||
connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||
prepareServer(connectionFactory);
|
||||
|
|
|
@ -17,22 +17,33 @@ import java.io.InterruptedIOException;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.FuturePromise;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
@ -241,6 +252,67 @@ public class AsyncIOTest extends AbstractTest
|
|||
*/
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testClientResetRemoteErrorNotification(boolean notify) throws Exception
|
||||
{
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<Response> responseRef = new AtomicReference<>();
|
||||
AtomicReference<Throwable> failureRef = new AtomicReference<>();
|
||||
HttpConfiguration httpConfiguration = new HttpConfiguration();
|
||||
httpConfiguration.setNotifyRemoteAsyncErrors(notify);
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback)
|
||||
{
|
||||
request.addFailureListener(failureRef::set);
|
||||
responseRef.set(response);
|
||||
latch.countDown();
|
||||
return true;
|
||||
}
|
||||
}, httpConfiguration);
|
||||
|
||||
Session session = newClientSession(new Session.Listener() {});
|
||||
MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
|
||||
HeadersFrame frame = new HeadersFrame(metaData, null, true);
|
||||
FuturePromise<Stream> promise = new FuturePromise<>();
|
||||
session.newStream(frame, promise, null);
|
||||
Stream stream = promise.get(5, TimeUnit.SECONDS);
|
||||
|
||||
// Wait for the server to be idle.
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
sleep(500);
|
||||
|
||||
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP);
|
||||
|
||||
if (notify)
|
||||
// Wait for the reset to be notified to the failure listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(failureRef::get, instanceOf(EofException.class));
|
||||
else
|
||||
// Wait for the reset to NOT be notified to the failure listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(failureRef::get, nullValue());
|
||||
|
||||
// Assert that writing to the response fails.
|
||||
var cb = new Callback()
|
||||
{
|
||||
private Throwable failure = null;
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x)
|
||||
{
|
||||
failure = x;
|
||||
}
|
||||
|
||||
Throwable failure()
|
||||
{
|
||||
return failure;
|
||||
}
|
||||
};
|
||||
responseRef.get().write(true, BufferUtil.EMPTY_BUFFER, cb);
|
||||
await().atMost(5, TimeUnit.SECONDS).until(cb::failure, instanceOf(EofException.class));
|
||||
}
|
||||
|
||||
private static void sleep(long ms) throws InterruptedIOException
|
||||
{
|
||||
try
|
||||
|
|
|
@ -154,58 +154,6 @@ public class AsyncServletTest extends AbstractTest
|
|||
// assertTrue(clientLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// public void testStartAsyncThenClientResetWithoutRemoteErrorNotification() throws Exception
|
||||
// {
|
||||
// HttpConfiguration httpConfiguration = new HttpConfiguration();
|
||||
// httpConfiguration.setNotifyRemoteAsyncErrors(false);
|
||||
// prepareServer(new HTTP2ServerConnectionFactory(httpConfiguration));
|
||||
// ServletContextHandler context = new ServletContextHandler(server, "/");
|
||||
// AtomicReference<AsyncContext> asyncContextRef = new AtomicReference<>();
|
||||
// CountDownLatch latch = new CountDownLatch(1);
|
||||
// context.addServlet(new ServletHolder(new HttpServlet()
|
||||
// {
|
||||
// @Override
|
||||
// protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||
// {
|
||||
// AsyncContext asyncContext = request.startAsync();
|
||||
// asyncContext.setTimeout(0);
|
||||
// asyncContextRef.set(asyncContext);
|
||||
// latch.countDown();
|
||||
// }
|
||||
// }), servletPath + "/*");
|
||||
// server.start();
|
||||
//
|
||||
// prepareClient();
|
||||
// client.start();
|
||||
// Session session = newClient(new Session.Listener() {});
|
||||
// MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
|
||||
// HeadersFrame frame = new HeadersFrame(metaData, null, true);
|
||||
// FuturePromise<Stream> promise = new FuturePromise<>();
|
||||
// session.newStream(frame, promise, null);
|
||||
// Stream stream = promise.get(5, TimeUnit.SECONDS);
|
||||
//
|
||||
// // Wait for the server to be in ASYNC_WAIT.
|
||||
// assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
// sleep(500);
|
||||
//
|
||||
// stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP);
|
||||
//
|
||||
// // Wait for the reset to be processed by the server.
|
||||
// sleep(500);
|
||||
//
|
||||
// AsyncContext asyncContext = asyncContextRef.get();
|
||||
// ServletResponse response = asyncContext.getResponse();
|
||||
// ServletOutputStream output = response.getOutputStream();
|
||||
//
|
||||
// assertThrows(IOException.class,
|
||||
// () ->
|
||||
// {
|
||||
// // Large writes or explicit flush() must
|
||||
// // fail because the stream has been reset.
|
||||
// output.flush();
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// public void testStartAsyncThenServerSessionIdleTimeout() throws Exception
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
package org.eclipse.jetty.http3;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -275,7 +274,7 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
|||
}
|
||||
}
|
||||
|
||||
private MessageParser.Result parseAndFill(boolean setFillInterest)
|
||||
private MessageParser.Result parseAndFill(boolean setFillInterest) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -336,16 +335,9 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
|||
}
|
||||
}
|
||||
|
||||
private int fill(ByteBuffer byteBuffer)
|
||||
private int fill(ByteBuffer byteBuffer) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
return getEndPoint().fill(byteBuffer);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
throw new UncheckedIOException(x.getMessage(), x);
|
||||
}
|
||||
return getEndPoint().fill(byteBuffer);
|
||||
}
|
||||
|
||||
private void processHeaders(HeadersFrame frame, boolean wasBlocked, Runnable delegate)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
package org.eclipse.jetty.http3.server.internal;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -33,6 +34,7 @@ import org.eclipse.jetty.http3.api.Stream;
|
|||
import org.eclipse.jetty.http3.frames.DataFrame;
|
||||
import org.eclipse.jetty.http3.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.HttpStream;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
|
@ -536,6 +538,8 @@ public class HttpStreamOverHTTP3 implements HttpStream
|
|||
chunk = Content.Chunk.from(failure, true);
|
||||
}
|
||||
connection.onFailure(failure);
|
||||
return httpChannel.onFailure(failure);
|
||||
|
||||
boolean remote = failure instanceof EOFException;
|
||||
return remote ? httpChannel.onRemoteFailure(new EofException(failure)) : httpChannel.onFailure(failure);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,11 @@
|
|||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.reactivestreams</groupId>
|
||||
<artifactId>reactive-streams-tck-flow</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
@ -50,6 +55,22 @@
|
|||
<argLine>@{argLine} ${jetty.surefire.argLine}
|
||||
--add-reads org.eclipse.jetty.io=org.eclipse.jetty.logging</argLine>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<!--
|
||||
surefire plugin currently does not support junit5 with testng out of the box: https://maven.apache.org/surefire/maven-surefire-plugin/examples/testng.html#running-testng-and-junit-tests
|
||||
We need to specify providers explicitly: https://maven.apache.org/surefire/maven-surefire-plugin/examples/providers.html
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.surefire</groupId>
|
||||
<artifactId>surefire-junit-platform</artifactId>
|
||||
<version>${maven.surefire.plugin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.surefire</groupId>
|
||||
<artifactId>surefire-testng</artifactId>
|
||||
<version>${maven.surefire.plugin.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
|
|
@ -13,11 +13,17 @@
|
|||
|
||||
package org.eclipse.jetty.io.content;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.util.IteratingCallback;
|
||||
import org.eclipse.jetty.util.MathUtils;
|
||||
import org.eclipse.jetty.util.thread.AutoLock;
|
||||
import org.eclipse.jetty.util.StaticException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* <p>Wraps a {@link Content.Source} as a {@link Flow.Publisher}.
|
||||
|
@ -25,135 +31,282 @@ import org.eclipse.jetty.util.thread.AutoLock;
|
|||
* read from the passed {@link Content.Source} and passed to {@link Flow.Subscriber#onNext(Object)}.
|
||||
* If no content is available, then the {@link Content.Source#demand(Runnable)} method is used to
|
||||
* ultimately call {@link Flow.Subscriber#onNext(Object)} once content is available.</p>
|
||||
* <p>{@link Content.Source} can be consumed only once and does not support multicast subscription.
|
||||
* {@link Content.Source} will be consumed fully, otherwise will be failed in case of any errors
|
||||
* to prevent resource leaks.</p>
|
||||
*/
|
||||
public class ContentSourcePublisher implements Flow.Publisher<Content.Chunk>
|
||||
{
|
||||
private final Content.Source content;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ContentSourcePublisher.class);
|
||||
|
||||
private final AtomicReference<Content.Source> content;
|
||||
|
||||
public ContentSourcePublisher(Content.Source content)
|
||||
{
|
||||
this.content = content;
|
||||
Objects.requireNonNull(content, "Content.Source must not be null");
|
||||
this.content = new AtomicReference<>(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(Flow.Subscriber<? super Content.Chunk> subscriber)
|
||||
{
|
||||
subscriber.onSubscribe(new SubscriptionImpl(content, subscriber));
|
||||
// As per rule 1.11, we have decided to support SINGLE subscriber
|
||||
// in a UNICAST configuration for this implementation. It means
|
||||
// that Content.Source can be consumed only once.
|
||||
Content.Source content = this.content.getAndSet(null);
|
||||
if (content != null)
|
||||
onSubscribe(subscriber, content);
|
||||
else
|
||||
onMultiSubscribe(subscriber);
|
||||
}
|
||||
|
||||
private static class SubscriptionImpl implements Flow.Subscription
|
||||
private void onSubscribe(Flow.Subscriber<? super Content.Chunk> subscriber, Content.Source content)
|
||||
{
|
||||
private final AutoLock lock = new AutoLock();
|
||||
private final Content.Source content;
|
||||
private final Flow.Subscriber<? super Content.Chunk> subscriber;
|
||||
private long demand;
|
||||
private boolean stalled;
|
||||
private boolean cancelled;
|
||||
private boolean terminated;
|
||||
|
||||
public SubscriptionImpl(Content.Source content, Flow.Subscriber<? super Content.Chunk> subscriber)
|
||||
// As per rule 1.9, we need to throw a `java.lang.NullPointerException`
|
||||
// if the `Subscriber` is `null`
|
||||
if (subscriber == null)
|
||||
{
|
||||
this.content = content;
|
||||
this.subscriber = subscriber;
|
||||
this.stalled = true;
|
||||
NullPointerException error = new NullPointerException("Flow.Subscriber must not be null");
|
||||
content.fail(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
ActiveSubscription subscription = new ActiveSubscription(content, subscriber);
|
||||
// As per rule 1.9, this method must return normally (i.e. not throw).
|
||||
try
|
||||
{
|
||||
subscriber.onSubscribe(subscription);
|
||||
}
|
||||
catch (Throwable err)
|
||||
{
|
||||
// As per rule 2.13, we MUST consider subscription cancelled and
|
||||
// MUST raise this error condition in a fashion that is adequate for the runtime environment.
|
||||
subscription.cancel(new SuppressedException(err));
|
||||
if (LOG.isTraceEnabled())
|
||||
LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMultiSubscribe(Flow.Subscriber<? super Content.Chunk> subscriber)
|
||||
{
|
||||
// As per rule 1.9, we need to throw a `java.lang.NullPointerException`
|
||||
// if the `Subscriber` is `null`
|
||||
if (subscriber == null)
|
||||
throw new NullPointerException("Flow.Subscriber must not be null");
|
||||
|
||||
ExhaustedSubscription subscription = new ExhaustedSubscription();
|
||||
// As per 1.9, this method must return normally (i.e. not throw).
|
||||
try
|
||||
{
|
||||
// As per rule 1.9, the only legal way to signal about Subscriber rejection
|
||||
// is by calling onError (after calling onSubscribe).
|
||||
subscriber.onSubscribe(subscription);
|
||||
subscriber.onError(new IllegalStateException("Content.Source was exhausted."));
|
||||
}
|
||||
catch (Throwable err)
|
||||
{
|
||||
// As per rule 2.13, we MUST consider subscription cancelled and
|
||||
// MUST raise this error condition in a fashion that is adequate for the runtime environment.
|
||||
if (LOG.isTraceEnabled())
|
||||
LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ExhaustedSubscription implements Flow.Subscription
|
||||
{
|
||||
@Override
|
||||
public void request(long n)
|
||||
{
|
||||
boolean process = false;
|
||||
Throwable failure = null;
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
if (cancelled || terminated)
|
||||
return;
|
||||
if (n <= 0)
|
||||
{
|
||||
terminated = true;
|
||||
failure = new IllegalArgumentException("invalid demand " + n);
|
||||
}
|
||||
demand = MathUtils.cappedAdd(demand, n);
|
||||
if (stalled)
|
||||
{
|
||||
stalled = false;
|
||||
process = true;
|
||||
}
|
||||
}
|
||||
if (failure != null)
|
||||
subscriber.onError(failure);
|
||||
else if (process)
|
||||
process();
|
||||
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel()
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
cancelled = true;
|
||||
}
|
||||
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ActiveSubscription extends IteratingCallback implements Flow.Subscription
|
||||
{
|
||||
private static final long NO_MORE_DEMAND = -1;
|
||||
private static final Throwable COMPLETED = new StaticException("Source.Content read fully");
|
||||
private final AtomicReference<Throwable> cancelled;
|
||||
private final AtomicLong demand;
|
||||
private Content.Source content;
|
||||
private Flow.Subscriber<? super Content.Chunk> subscriber;
|
||||
|
||||
public ActiveSubscription(Content.Source content, Flow.Subscriber<? super Content.Chunk> subscriber)
|
||||
{
|
||||
this.cancelled = new AtomicReference<>(null);
|
||||
this.demand = new AtomicLong(0);
|
||||
this.content = content;
|
||||
this.subscriber = subscriber;
|
||||
}
|
||||
|
||||
private void process()
|
||||
// As per rule 3.3, Subscription MUST place an upper bound on possible synchronous
|
||||
// recursion between Publisher and Subscriber
|
||||
//
|
||||
// As per rule 1.3, onSubscribe, onNext, onError and onComplete signaled to a
|
||||
// Subscriber MUST be signaled serially.
|
||||
//
|
||||
// IteratingCallback guarantee that process() method will be executed by one thread only.
|
||||
// The process() method can be only initiated from request() or cancel() demands methods.
|
||||
@Override
|
||||
protected Action process()
|
||||
{
|
||||
while (true)
|
||||
Throwable cancelled = this.cancelled.get();
|
||||
if (cancelled != null)
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
// As per rule 3.13, Subscription.cancel() MUST request the Publisher to eventually
|
||||
// drop any references to the corresponding subscriber.
|
||||
this.demand.set(NO_MORE_DEMAND);
|
||||
if (cancelled != COMPLETED)
|
||||
this.content.fail(cancelled);
|
||||
this.content = null;
|
||||
try
|
||||
{
|
||||
if (demand > 0)
|
||||
{
|
||||
--demand;
|
||||
}
|
||||
else
|
||||
{
|
||||
stalled = true;
|
||||
return;
|
||||
}
|
||||
if (cancelled == COMPLETED)
|
||||
this.subscriber.onComplete();
|
||||
else if (!(cancelled instanceof SuppressedException))
|
||||
this.subscriber.onError(cancelled);
|
||||
}
|
||||
|
||||
Content.Chunk chunk = content.read();
|
||||
|
||||
if (chunk == null)
|
||||
catch (Throwable err)
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
{
|
||||
// Restore the demand decremented above.
|
||||
++demand;
|
||||
stalled = true;
|
||||
}
|
||||
content.demand(this::process);
|
||||
return;
|
||||
if (LOG.isTraceEnabled())
|
||||
LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err);
|
||||
}
|
||||
this.subscriber = null;
|
||||
return Action.SUCCEEDED;
|
||||
}
|
||||
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
terminate();
|
||||
if (!chunk.isLast())
|
||||
content.fail(chunk.getFailure());
|
||||
subscriber.onError(chunk.getFailure());
|
||||
return;
|
||||
}
|
||||
Content.Chunk chunk = content.read();
|
||||
|
||||
subscriber.onNext(chunk);
|
||||
if (chunk == null)
|
||||
{
|
||||
content.demand(this::succeeded);
|
||||
return Action.SCHEDULED;
|
||||
}
|
||||
|
||||
if (Content.Chunk.isFailure(chunk))
|
||||
{
|
||||
cancel(chunk.getFailure());
|
||||
chunk.release();
|
||||
|
||||
if (chunk.isLast())
|
||||
{
|
||||
terminate();
|
||||
// Reactive Stream specification rule 2.9 allows Publishers to call onComplete()
|
||||
// even without demand, and Subscribers must be prepared to handle this case.
|
||||
subscriber.onComplete();
|
||||
return;
|
||||
}
|
||||
return Action.IDLE;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.subscriber.onNext(chunk);
|
||||
}
|
||||
catch (Throwable err)
|
||||
{
|
||||
cancel(new SuppressedException(err));
|
||||
if (LOG.isTraceEnabled())
|
||||
LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err);
|
||||
}
|
||||
chunk.release();
|
||||
|
||||
if (chunk.isLast())
|
||||
{
|
||||
cancel(COMPLETED);
|
||||
return Action.IDLE;
|
||||
}
|
||||
|
||||
if (demand.decrementAndGet() > 0)
|
||||
this.iterate();
|
||||
|
||||
return Action.IDLE;
|
||||
}
|
||||
|
||||
private void terminate()
|
||||
@Override
|
||||
public void request(long n)
|
||||
{
|
||||
try (AutoLock ignored = lock.lock())
|
||||
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
|
||||
if (cancelled.get() != null)
|
||||
return;
|
||||
|
||||
// As per rule 3.9, MUST signal onError with a java.lang.IllegalArgumentException if the argument is <= 0.
|
||||
if (n <= 0L)
|
||||
{
|
||||
terminated = true;
|
||||
String errorMsg = "Flow.Subscriber " + subscriber + " violated rule 3.9: non-positive requests are not allowed.";
|
||||
cancel(new IllegalArgumentException(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
// As per rule 3.17, when demand overflows `Long.MAX_VALUE`
|
||||
// we treat the signalled demand as "effectively unbounded"
|
||||
if (demand.updateAndGet(it -> it == NO_MORE_DEMAND ? it : MathUtils.cappedAdd(it, n)) != NO_MORE_DEMAND)
|
||||
this.iterate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel()
|
||||
{
|
||||
cancel(new CancelledException());
|
||||
}
|
||||
|
||||
public void cancel(Throwable cause)
|
||||
{
|
||||
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
|
||||
//
|
||||
// As per rule 3.5, this handles cancellation requests, and is idempotent, thread-safe and not
|
||||
// synchronously performing heavy computations
|
||||
if (cancelled.compareAndSet(null, cause))
|
||||
this.iterate();
|
||||
}
|
||||
|
||||
// Publisher notes
|
||||
//
|
||||
// 1.6 If a Publisher signals either onError or onComplete on a Subscriber,
|
||||
// that Subscriber’s Subscription MUST be considered cancelled.
|
||||
// 2.4 Subscriber.onComplete() and Subscriber.onError(Throwable t) MUST consider the
|
||||
// Subscription cancelled after having received the signal.
|
||||
//
|
||||
// Publisher failed -> cancel(Throwable)
|
||||
// 1.4 If a Publisher fails it MUST signal an onError.
|
||||
//
|
||||
// Publisher succeeded -> cancel(COMPLETED)
|
||||
// 1.5 If a Publisher terminates successfully (finite stream) it MUST signal an onComplete.
|
||||
|
||||
// Subscriber
|
||||
// 2.13 In the case that this rule is violated, any associated Subscription to the Subscriber
|
||||
// MUST be considered as cancelled, and the caller MUST raise this error condition in a
|
||||
// fashion that is adequate for the runtime environment.
|
||||
//
|
||||
// Subscriber.onSubscribe/onNext/onError/onComplete failed -> cancel(new Suppressed(cause))
|
||||
|
||||
// Subscription notes
|
||||
//
|
||||
// Subscription.cancel -> cancel(new Cancelled())
|
||||
// It's not clearly specified in the specification, but according to:
|
||||
// - the issue: https://github.com/reactive-streams/reactive-streams-jvm/issues/458
|
||||
// - TCK test 'untested_spec108_possiblyCanceledSubscriptionShouldNotReceiveOnErrorOrOnCompleteSignals'
|
||||
// - 1.8 If a Subscription is cancelled its Subscriber MUST eventually stop being signaled.
|
||||
//
|
||||
// Subscription.request with negative argument -> cancel(err)
|
||||
// 3.9 While the Subscription is not cancelled, Subscription.request(long n) MUST signal onError with a
|
||||
// java.lang.IllegalArgumentException if the argument is <= 0.
|
||||
}
|
||||
|
||||
private static class SuppressedException extends Exception
|
||||
{
|
||||
SuppressedException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
SuppressedException(Throwable cause)
|
||||
{
|
||||
super(cause.getMessage(), cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CancelledException extends SuppressedException
|
||||
{
|
||||
CancelledException()
|
||||
{
|
||||
super("Subscription was cancelled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.io.content;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.reactivestreams.tck.TestEnvironment;
|
||||
import org.reactivestreams.tck.flow.FlowPublisherVerification;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
@Test
|
||||
public final class ContentSourcePublisherTest extends FlowPublisherVerification<Content.Chunk>
|
||||
{
|
||||
public ContentSourcePublisherTest()
|
||||
{
|
||||
super(new TestEnvironment());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flow.Publisher<Content.Chunk> createFlowPublisher(long elements)
|
||||
{
|
||||
Content.Source source = new SyntheticContentSource(elements);
|
||||
return new ContentSourcePublisher(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flow.Publisher<Content.Chunk> createFailedFlowPublisher()
|
||||
{
|
||||
Content.Source source = new SyntheticContentSource(0);
|
||||
Flow.Publisher<Content.Chunk> publisher = new ContentSourcePublisher(source);
|
||||
// Simulate exhausted Content.Source
|
||||
publisher.subscribe(new Flow.Subscriber<>()
|
||||
{
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription)
|
||||
{
|
||||
subscription.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(Content.Chunk item)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete()
|
||||
{
|
||||
}
|
||||
});
|
||||
return publisher;
|
||||
}
|
||||
|
||||
private static final class SyntheticContentSource implements Content.Source
|
||||
{
|
||||
private final AtomicReference<State> state;
|
||||
private final long contentSize;
|
||||
|
||||
public SyntheticContentSource(long chunksToRead)
|
||||
{
|
||||
this.state = new AtomicReference<>(new State.Reading(chunksToRead));
|
||||
this.contentSize = State.Reading.chunkSize * Math.max(chunksToRead, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLength()
|
||||
{
|
||||
return contentSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Chunk read()
|
||||
{
|
||||
return state.getAndUpdate(before -> before.read()).chunk();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void demand(Runnable demandCallback)
|
||||
{
|
||||
// recursive stack overflow not necessary for this test
|
||||
demandCallback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(Throwable failure)
|
||||
{
|
||||
fail(failure, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(Throwable failure, boolean last)
|
||||
{
|
||||
state.getAndUpdate(before -> before.fail(failure, last));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rewind()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed interface State permits State.Reading, State.ReadFailed, State.ReadCompleted
|
||||
{
|
||||
Content.Chunk chunk();
|
||||
|
||||
State read();
|
||||
|
||||
State fail(Throwable failure, boolean last);
|
||||
|
||||
final class Reading implements State
|
||||
{
|
||||
public static final int chunkSize = 16;
|
||||
private static final Random random = new Random();
|
||||
|
||||
private final long chunksToRead;
|
||||
private final Content.Chunk chunk;
|
||||
|
||||
public Reading(long chunksToRead)
|
||||
{
|
||||
this.chunksToRead = chunksToRead;
|
||||
this.chunk = generateValidChunk(chunksToRead);
|
||||
}
|
||||
|
||||
public Reading(long chunksToRead, Throwable transientFailure)
|
||||
{
|
||||
this.chunksToRead = chunksToRead;
|
||||
this.chunk = generateFailureChunk(transientFailure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Chunk chunk()
|
||||
{
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read()
|
||||
{
|
||||
long leftToRead = leftToRead();
|
||||
if (leftToRead <= 0)
|
||||
return new ReadCompleted();
|
||||
return new Reading(leftToRead);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State fail(Throwable failure, boolean last)
|
||||
{
|
||||
if (last)
|
||||
return new ReadFailed(failure);
|
||||
return new Reading(chunksToRead, failure);
|
||||
}
|
||||
|
||||
private long leftToRead()
|
||||
{
|
||||
if (chunksToRead == Long.MAX_VALUE) // endless source
|
||||
return chunksToRead;
|
||||
return chunksToRead - 1;
|
||||
}
|
||||
|
||||
private static Content.Chunk generateFailureChunk(Throwable transientFailure)
|
||||
{
|
||||
return Content.Chunk.from(transientFailure, false);
|
||||
}
|
||||
|
||||
private static Content.Chunk generateValidChunk(long chunksToRead)
|
||||
{
|
||||
if (chunksToRead <= 0)
|
||||
return Content.Chunk.EOF;
|
||||
if (chunksToRead == 1)
|
||||
return Content.Chunk.from(randomPayload(), true);
|
||||
return Content.Chunk.from(randomPayload(), false);
|
||||
}
|
||||
|
||||
private static ByteBuffer randomPayload()
|
||||
{
|
||||
byte[] payload = new byte[chunkSize];
|
||||
random.nextBytes(payload);
|
||||
return ByteBuffer.wrap(payload);
|
||||
}
|
||||
}
|
||||
|
||||
final class ReadFailed implements State
|
||||
{
|
||||
private final Content.Chunk chunk;
|
||||
|
||||
public ReadFailed(Throwable failure)
|
||||
{
|
||||
this.chunk = Content.Chunk.from(failure, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content.Chunk chunk()
|
||||
{
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State fail(Throwable failure, boolean last)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
final class ReadCompleted implements State
|
||||
{
|
||||
@Override
|
||||
public Content.Chunk chunk()
|
||||
{
|
||||
return Content.Chunk.EOF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State read()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State fail(Throwable failure, boolean last)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,4 +47,4 @@ etc/jetty-openid.xml
|
|||
# jetty.openid.authenticationMethod=client_secret_post
|
||||
|
||||
## Whether the user should be logged out after the idToken expires.
|
||||
# jetty.openid.logoutWhenIdTokenIsExpired=false
|
||||
# jetty.openid.logoutWhenIdTokenIsExpired=false
|
||||
|
|
|
@ -13,10 +13,14 @@
|
|||
|
||||
package org.eclipse.jetty.quic.common;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EventListener;
|
||||
|
@ -423,6 +427,31 @@ public abstract class QuicSession extends ContainerLifeCycle
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the peer certificates chain.</p>
|
||||
* <p>Due to current Quiche C API limitations (that the Rust version does not have),
|
||||
* only the last certificate in the chain is returned.
|
||||
* This may change in the future when the C APIs are aligned to the Rust APIs.</p>
|
||||
*
|
||||
* @return the peer certificates chain (currently only the last certificate in the chain)
|
||||
*/
|
||||
public X509Certificate[] getPeerCertificates()
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] encoded = quicheConnection.getPeerCertificate();
|
||||
if (encoded == null)
|
||||
return null;
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X509");
|
||||
X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(encoded));
|
||||
return new X509Certificate[]{certificate};
|
||||
}
|
||||
catch (CertificateException x)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
|
|
|
@ -13,9 +13,11 @@
|
|||
|
||||
package org.eclipse.jetty.quic.common;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
|
@ -40,6 +42,7 @@ public class QuicStreamEndPoint extends AbstractEndPoint
|
|||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QuicStreamEndPoint.class);
|
||||
private static final ByteBuffer LAST_FLAG = ByteBuffer.allocate(0);
|
||||
private static final ByteBuffer EMPTY_WRITABLE_BUFFER = ByteBuffer.allocate(0);
|
||||
|
||||
private final QuicSession session;
|
||||
private final long streamId;
|
||||
|
@ -221,6 +224,15 @@ public class QuicStreamEndPoint extends AbstractEndPoint
|
|||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslSessionData getSslSessionData()
|
||||
{
|
||||
X509Certificate[] peerCertificates = getQuicSession().getPeerCertificates();
|
||||
if (peerCertificates == null)
|
||||
return null;
|
||||
return SslSessionData.from(null, null, null, peerCertificates);
|
||||
}
|
||||
|
||||
public void onWritable()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -255,12 +267,25 @@ public class QuicStreamEndPoint extends AbstractEndPoint
|
|||
}
|
||||
else
|
||||
{
|
||||
QuicStreamEndPoint streamEndPoint = getQuicSession().getStreamEndPoint(streamId);
|
||||
if (streamEndPoint.isStreamFinished())
|
||||
if (isStreamFinished())
|
||||
{
|
||||
EofException e = new EofException();
|
||||
streamEndPoint.getFillInterest().onFail(e);
|
||||
streamEndPoint.getQuicSession().onFailure(e);
|
||||
// Check if the stream was finished normally.
|
||||
try
|
||||
{
|
||||
fill(EMPTY_WRITABLE_BUFFER);
|
||||
}
|
||||
catch (EOFException x)
|
||||
{
|
||||
// Got reset.
|
||||
getFillInterest().onFail(x);
|
||||
getQuicSession().onFailure(x);
|
||||
}
|
||||
catch (Throwable x)
|
||||
{
|
||||
EofException e = new EofException(x);
|
||||
getFillInterest().onFail(e);
|
||||
getQuicSession().onFailure(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return interested;
|
||||
|
|
|
@ -152,6 +152,8 @@ public abstract class QuicheConnection
|
|||
|
||||
public abstract CloseInfo getLocalCloseInfo();
|
||||
|
||||
public abstract byte[] getPeerCertificate();
|
||||
|
||||
public static class CloseInfo
|
||||
{
|
||||
private final long error;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
package org.eclipse.jetty.quic.quiche.foreign;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.lang.foreign.Arena;
|
||||
import java.lang.foreign.MemorySegment;
|
||||
|
@ -518,6 +519,7 @@ public class ForeignQuicheConnection extends QuicheConnection
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPeerCertificate()
|
||||
{
|
||||
try (AutoLock ignore = lock.lock())
|
||||
|
@ -532,7 +534,7 @@ public class ForeignQuicheConnection extends QuicheConnection
|
|||
quiche_h.quiche_conn_peer_cert(quicheConn, outSegment, outLenSegment);
|
||||
|
||||
long outLen = outLenSegment.get(NativeHelper.C_LONG, 0L);
|
||||
if (outLen == 0L)
|
||||
if (outLen <= 0L)
|
||||
return null;
|
||||
byte[] out = new byte[(int)outLen];
|
||||
// dereference outSegment pointer
|
||||
|
@ -917,14 +919,19 @@ public class ForeignQuicheConnection extends QuicheConnection
|
|||
MemorySegment fin = scope.allocate(NativeHelper.C_CHAR);
|
||||
read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment, buffer.remaining(), fin);
|
||||
|
||||
int prevPosition = buffer.position();
|
||||
buffer.put(bufferSegment.asByteBuffer().limit((int)read));
|
||||
buffer.position(prevPosition);
|
||||
if (read > 0)
|
||||
{
|
||||
int prevPosition = buffer.position();
|
||||
buffer.put(bufferSegment.asByteBuffer().limit((int)read));
|
||||
buffer.position(prevPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (read == quiche_error.QUICHE_ERR_DONE)
|
||||
return isStreamFinished(streamId) ? -1 : 0;
|
||||
if (read == quiche_error.QUICHE_ERR_STREAM_RESET)
|
||||
throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
||||
if (read < 0L)
|
||||
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
||||
buffer.position((int)(buffer.position() + read));
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
package org.eclipse.jetty.quic.quiche.jna;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
|
@ -413,6 +414,7 @@ public class JnaQuicheConnection extends QuicheConnection
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPeerCertificate()
|
||||
{
|
||||
try (AutoLock ignore = lock.lock())
|
||||
|
@ -424,6 +426,8 @@ public class JnaQuicheConnection extends QuicheConnection
|
|||
size_t_pointer out_len = new size_t_pointer();
|
||||
LibQuiche.INSTANCE.quiche_conn_peer_cert(quicheConn, out, out_len);
|
||||
int len = out_len.getPointee().intValue();
|
||||
if (len <= 0)
|
||||
return null;
|
||||
return out.getValueAsBytes(len);
|
||||
}
|
||||
}
|
||||
|
@ -744,6 +748,8 @@ public class JnaQuicheConnection extends QuicheConnection
|
|||
int read = LibQuiche.INSTANCE.quiche_conn_stream_recv(quicheConn, new uint64_t(streamId), buffer, new size_t(buffer.remaining()), fin).intValue();
|
||||
if (read == quiche_error.QUICHE_ERR_DONE)
|
||||
return isStreamFinished(streamId) ? -1 : 0;
|
||||
if (read == quiche_error.QUICHE_ERR_STREAM_RESET)
|
||||
throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
||||
if (read < 0L)
|
||||
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
||||
buffer.position(buffer.position() + read);
|
||||
|
|
|
@ -56,8 +56,10 @@ public interface Deployable
|
|||
String CONFIGURATION_CLASSES = "jetty.deploy.configurationClasses";
|
||||
String CONTAINER_SCAN_JARS = "jetty.deploy.containerScanJarPattern";
|
||||
String CONTEXT_PATH = "jetty.deploy.contextPath";
|
||||
String CONTEXT_HANDLER_CLASS = "jetty.deploy.contextHandlerClass";
|
||||
String DEFAULTS_DESCRIPTOR = "jetty.deploy.defaultsDescriptor";
|
||||
String ENVIRONMENT = "environment";
|
||||
String ENVIRONMENT_XML = "jetty.deploy.environmentXml";
|
||||
String EXTRACT_WARS = "jetty.deploy.extractWars";
|
||||
String PARENT_LOADER_PRIORITY = "jetty.deploy.parentLoaderPriority";
|
||||
String SCI_EXCLUSION_PATTERN = "jetty.deploy.servletContainerInitializerExclusionPattern";
|
||||
|
|
|
@ -88,8 +88,7 @@ public interface HttpChannel extends Invocable
|
|||
|
||||
/**
|
||||
* <p>Notifies this {@code HttpChannel} that an asynchronous failure happened.</p>
|
||||
* <p>Typical failure examples could be HTTP/2 resets or
|
||||
* protocol failures (for example, invalid request bytes).</p>
|
||||
* <p>Typical failure examples could be protocol failures (for example, invalid request bytes).</p>
|
||||
*
|
||||
* @param failure the failure cause.
|
||||
* @return a {@code Runnable} that performs the failure action, or {@code null}
|
||||
|
@ -98,6 +97,18 @@ public interface HttpChannel extends Invocable
|
|||
*/
|
||||
Runnable onFailure(Throwable failure);
|
||||
|
||||
/**
|
||||
* <p>Notifies this {@code HttpChannel} that an asynchronous notification was received indicating
|
||||
* a remote failure happened.</p>
|
||||
* <p>Typical failure examples could be HTTP/2 resets.</p>
|
||||
*
|
||||
* @param failure the failure cause.
|
||||
* @return a {@code Runnable} that performs the failure action, or {@code null}
|
||||
* if no failure action needs be performed by the calling thread
|
||||
* @see Request#addFailureListener(Consumer)
|
||||
*/
|
||||
Runnable onRemoteFailure(Throwable failure);
|
||||
|
||||
/**
|
||||
* <p>Notifies this {@code HttpChannel} that an asynchronous close happened.</p>
|
||||
*
|
||||
|
|
|
@ -392,6 +392,17 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
|
||||
@Override
|
||||
public Runnable onFailure(Throwable x)
|
||||
{
|
||||
return onFailure(x, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Runnable onRemoteFailure(Throwable x)
|
||||
{
|
||||
return onFailure(x, true);
|
||||
}
|
||||
|
||||
private Runnable onFailure(Throwable x, boolean remote)
|
||||
{
|
||||
HttpStream stream;
|
||||
Runnable task;
|
||||
|
@ -437,7 +448,9 @@ public class HttpChannelState implements HttpChannel, Components
|
|||
// Notify the failure listeners only once.
|
||||
Consumer<Throwable> onFailure = _onFailure;
|
||||
_onFailure = null;
|
||||
Runnable invokeOnFailureListeners = onFailure == null ? null : () ->
|
||||
|
||||
boolean skipListeners = remote && !getHttpConfiguration().isNotifyRemoteAsyncErrors();
|
||||
Runnable invokeOnFailureListeners = onFailure == null || skipListeners ? null : () ->
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
@ -1426,9 +1426,11 @@ public class StartArgs
|
|||
{
|
||||
for (String moduleName : moduleNames)
|
||||
{
|
||||
modules.add(moduleName);
|
||||
Set<String> set = sources.computeIfAbsent(moduleName, k -> new HashSet<>());
|
||||
set.add(source);
|
||||
if (modules.add(moduleName))
|
||||
{
|
||||
Set<String> set = sources.computeIfAbsent(moduleName, k -> new HashSet<>());
|
||||
set.add(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
@ -183,4 +184,27 @@ public class MainTest
|
|||
);
|
||||
assertThat(commandLine, containsString(expectedExpansion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModulesDeclaredTwice() throws Exception
|
||||
{
|
||||
List<String> cmdLineArgs = new ArrayList<>();
|
||||
|
||||
Path homePath = MavenPaths.findTestResourceDir("dist-home");
|
||||
Path basePath = MavenPaths.findTestResourceDir("overdeclared-modules");
|
||||
cmdLineArgs.add("jetty.home=" + homePath);
|
||||
cmdLineArgs.add("user.dir=" + basePath);
|
||||
|
||||
Main main = new Main();
|
||||
|
||||
cmdLineArgs.add("--module=main");
|
||||
|
||||
// The "main" module is enabled in both ...
|
||||
// 1) overdeclared-modules/start.d/config.ini
|
||||
// 2) command-line
|
||||
// This shouldn't result in an error
|
||||
StartArgs args = main.processCommandLine(cmdLineArgs.toArray(new String[0]));
|
||||
|
||||
assertThat(args.getSelectedModules(), hasItem("main"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
--module=main
|
|
@ -0,0 +1 @@
|
|||
--modules=main
|
|
@ -18,11 +18,6 @@
|
|||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-openid</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-security</artifactId>
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/
|
||||
|
||||
[description]
|
||||
Adds openid security for EE10.
|
||||
|
||||
[environment]
|
||||
ee10
|
||||
|
||||
[depend]
|
||||
openid
|
|
@ -671,7 +671,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
|||
catch (Throwable t)
|
||||
{
|
||||
onWriteComplete(false, t);
|
||||
throw t;
|
||||
if (t instanceof IOException)
|
||||
throw t;
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
<configuration>
|
||||
<argLine>@{argLine}
|
||||
${jetty.surefire.argLine}
|
||||
--enable-native-access org.eclipse.jetty.quic.quiche.foreign</argLine>
|
||||
--enable-native-access=ALL-UNNAMED</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
|
@ -102,6 +102,14 @@ public class AbstractTest
|
|||
return transports;
|
||||
}
|
||||
|
||||
public static Collection<Transport> transportsSecure()
|
||||
{
|
||||
EnumSet<Transport> transports = EnumSet.of(Transport.HTTPS, Transport.H2, Transport.H3);
|
||||
if ("ci".equals(System.getProperty("env")))
|
||||
transports.remove(Transport.H3);
|
||||
return transports;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void prepare()
|
||||
{
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.test.client.transport;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.FuturePromise;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class Http2AsyncIOServletTest
|
||||
{
|
||||
private Server server;
|
||||
private ServerConnector connector;
|
||||
private HTTP2Client client;
|
||||
|
||||
private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory(httpConfig));
|
||||
server.addConnector(connector);
|
||||
ServletContextHandler servletContextHandler = new ServletContextHandler("/");
|
||||
servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*");
|
||||
server.setHandler(servletContextHandler);
|
||||
server.start();
|
||||
|
||||
client = new HTTP2Client();
|
||||
client.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown()
|
||||
{
|
||||
LifeCycle.stop(client);
|
||||
LifeCycle.stop(server);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception
|
||||
{
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setNotifyRemoteAsyncErrors(notify);
|
||||
|
||||
AtomicReference<AsyncEvent> errorAsyncEventRef = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
start(httpConfig, new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response)
|
||||
{
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.addListener(new AsyncListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(AsyncEvent event)
|
||||
{
|
||||
errorAsyncEventRef.set(event);
|
||||
asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartAsync(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
});
|
||||
asyncContext.setTimeout(0);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort());
|
||||
FuturePromise<Session> sessionPromise = new FuturePromise<>();
|
||||
client.connect(address, new Session.Listener() {}, sessionPromise);
|
||||
Session session = sessionPromise.get(5, TimeUnit.SECONDS);
|
||||
MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_2, HttpFields.EMPTY);
|
||||
HeadersFrame frame = new HeadersFrame(metaData, null, false);
|
||||
Stream stream = session.newStream(frame, null).get(5, TimeUnit.SECONDS);
|
||||
|
||||
// Wait for the server to be in ASYNC_WAIT.
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
Thread.sleep(500);
|
||||
|
||||
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code));
|
||||
|
||||
if (notify)
|
||||
// Wait for the reset to be notified to the async context listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(() ->
|
||||
{
|
||||
AsyncEvent asyncEvent = errorAsyncEventRef.get();
|
||||
return asyncEvent == null ? null : asyncEvent.getThrowable();
|
||||
}, instanceOf(EofException.class));
|
||||
else
|
||||
// Wait for the reset to NOT be notified to the failure listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.test.client.transport;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http3.HTTP3ErrorCode;
|
||||
import org.eclipse.jetty.http3.api.Stream;
|
||||
import org.eclipse.jetty.http3.client.HTTP3Client;
|
||||
import org.eclipse.jetty.http3.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.quic.client.ClientQuicConfiguration;
|
||||
import org.eclipse.jetty.quic.server.QuicServerConnector;
|
||||
import org.eclipse.jetty.quic.server.ServerQuicConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.toolchain.test.MavenPaths;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.eclipse.jetty.http3.api.Session.Client;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class Http3AsyncIOServletTest
|
||||
{
|
||||
public WorkDir workDir;
|
||||
|
||||
private Server server;
|
||||
private QuicServerConnector connector;
|
||||
private HTTP3Client client;
|
||||
|
||||
private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
|
||||
serverSslContextFactory.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString());
|
||||
serverSslContextFactory.setKeyStorePassword("storepwd");
|
||||
ServerQuicConfiguration serverQuicConfiguration = new ServerQuicConfiguration(serverSslContextFactory, workDir.getEmptyPathDir());
|
||||
connector = new QuicServerConnector(server, serverQuicConfiguration, new HTTP3ServerConnectionFactory(serverQuicConfiguration, httpConfig));
|
||||
server.addConnector(connector);
|
||||
ServletContextHandler servletContextHandler = new ServletContextHandler("/");
|
||||
servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*");
|
||||
server.setHandler(servletContextHandler);
|
||||
server.start();
|
||||
|
||||
client = new HTTP3Client(new ClientQuicConfiguration(new SslContextFactory.Client(true), null));
|
||||
client.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown()
|
||||
{
|
||||
LifeCycle.stop(client);
|
||||
LifeCycle.stop(server);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception
|
||||
{
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setNotifyRemoteAsyncErrors(notify);
|
||||
|
||||
AtomicReference<AsyncEvent> errorAsyncEventRef = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
start(httpConfig, new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response)
|
||||
{
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.addListener(new AsyncListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(AsyncEvent event)
|
||||
{
|
||||
errorAsyncEventRef.set(event);
|
||||
asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartAsync(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
});
|
||||
asyncContext.setTimeout(0);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort());
|
||||
Client session = client.connect(address, new Client.Listener() {}).get(5, TimeUnit.SECONDS);
|
||||
MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_3, HttpFields.EMPTY);
|
||||
HeadersFrame frame = new HeadersFrame(metaData, false);
|
||||
Stream stream = session.newRequest(frame, null).get(5, TimeUnit.SECONDS);
|
||||
|
||||
// Wait for the server to be in ASYNC_WAIT.
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
Thread.sleep(500);
|
||||
|
||||
stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new Exception());
|
||||
|
||||
if (notify)
|
||||
// Wait for the reset to be notified to the async context listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(() ->
|
||||
{
|
||||
AsyncEvent asyncEvent = errorAsyncEventRef.get();
|
||||
return asyncEvent == null ? null : asyncEvent.getThrowable();
|
||||
}, instanceOf(EofException.class));
|
||||
else
|
||||
// Wait for the reset to NOT be notified to the failure listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee10.test.client.transport;
|
||||
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
public class NeedClientAuthTest extends AbstractTest
|
||||
{
|
||||
@ParameterizedTest
|
||||
@MethodSource("transportsSecure")
|
||||
public void testNeedClientAuth(Transport transport) throws Exception
|
||||
{
|
||||
prepareServer(transport, new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response)
|
||||
{
|
||||
// Verify that the request attribute is present.
|
||||
assertNotNull(request.getAttribute(ServletContextRequest.PEER_CERTIFICATES));
|
||||
}
|
||||
});
|
||||
sslContextFactoryServer.setNeedClientAuth(true);
|
||||
server.start();
|
||||
|
||||
startClient(transport, httpClient ->
|
||||
{
|
||||
// Configure the SslContextFactory to send a certificate to the server.
|
||||
SslContextFactory.Client clientSSL = httpClient.getSslContextFactory();
|
||||
clientSSL.setKeyStorePath("src/test/resources/keystore.p12");
|
||||
clientSSL.setKeyStorePassword("storepwd");
|
||||
clientSSL.setCertAlias("mykey");
|
||||
});
|
||||
|
||||
ContentResponse response = client.newRequest(newURI(transport)).send();
|
||||
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
|
||||
<Configure id="Server" class="org.eclipse.jetty.server.Server">
|
||||
<Get id="ThreadPool" name="threadPool"/>
|
||||
<New id="HttpClient" class="org.eclipse.jetty.client.HttpClient">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.io.ClientConnector">
|
||||
<Set name="sslContextFactory">
|
||||
<New class="org.eclipse.jetty.util.ssl.SslContextFactory$Client">
|
||||
<Set name="trustAll" type="boolean">
|
||||
<Property name="jetty.openid.sslContextFactory.trustAll" default="false"/>
|
||||
</Set>
|
||||
</New>
|
||||
</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</New>
|
||||
</Arg>
|
||||
<Set name="executor"><Ref refid="ThreadPool"/></Set>
|
||||
</New>
|
||||
<Call name="addBean">
|
||||
<Arg>
|
||||
<Ref refid="BaseLoginService"/>
|
||||
</Arg>
|
||||
</Call>
|
||||
<Call name="addBean">
|
||||
<Arg>
|
||||
<New id="OpenIdConfiguration" class="org.eclipse.jetty.ee8.security.openid.OpenIdConfiguration">
|
||||
<Arg name="issuer"><Property name="jetty.openid.provider" deprecated="jetty.openid.openIdProvider"/></Arg>
|
||||
<Arg name="authorizationEndpoint"><Property name="jetty.openid.provider.authorizationEndpoint"/></Arg>
|
||||
<Arg name="tokenEndpoint"><Property name="jetty.openid.provider.tokenEndpoint"/></Arg>
|
||||
<Arg name="clientId"><Property name="jetty.openid.clientId"/></Arg>
|
||||
<Arg name="clientSecret"><Property name="jetty.openid.clientSecret"/></Arg>
|
||||
<Arg name="authMethod"><Property name="jetty.openid.authenticationMethod" deprecated="jetty.openid.authMethod" default="client_secret_post"/></Arg>
|
||||
<Arg name="httpClient"><Ref refid="HttpClient"/></Arg>
|
||||
<Set name="authenticateNewUsers">
|
||||
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
|
||||
</Set>
|
||||
<Call name="addScopes">
|
||||
<Arg>
|
||||
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
|
||||
<Arg><Property name="jetty.openid.scopes"/></Arg>
|
||||
</Call>
|
||||
</Arg>
|
||||
</Call>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
</Configure>
|
|
@ -3,46 +3,12 @@
|
|||
[description]
|
||||
Adds OpenId Connect authentication to the server.
|
||||
|
||||
[environment]
|
||||
ee8
|
||||
|
||||
[depend]
|
||||
ee8-security
|
||||
openid
|
||||
client
|
||||
|
||||
[lib]
|
||||
lib/jetty-ee8-openid-${jetty.version}.jar
|
||||
lib/jetty-util-ajax-${jetty.version}.jar
|
||||
|
||||
[files]
|
||||
basehome:modules/openid/jetty-ee8-openid-baseloginservice.xml|etc/openid-baseloginservice.xml
|
||||
|
||||
[xml]
|
||||
etc/openid-baseloginservice.xml
|
||||
etc/jetty-ee8-openid.xml
|
||||
|
||||
[ini-template]
|
||||
## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration")
|
||||
# jetty.openid.provider=https://id.example.com/
|
||||
|
||||
## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible)
|
||||
# jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization
|
||||
|
||||
## The OpenID Identity Provider's token endpoint (optional if the metadata of the OP is accessible)
|
||||
# jetty.openid.provider.tokenEndpoint=https://id.example.com/token
|
||||
|
||||
## The Client Identifier
|
||||
# jetty.openid.clientId=test1234
|
||||
|
||||
## The Client Secret
|
||||
# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P
|
||||
|
||||
## Additional Scopes to Request
|
||||
# jetty.openid.scopes=email,profile
|
||||
|
||||
## Whether to Authenticate users not found by base LoginService
|
||||
# jetty.openid.authenticateNewUsers=false
|
||||
|
||||
## 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.authenticationMethod=client_secret_post
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
|
||||
<Configure>
|
||||
<!-- Optional code to configure the base LoginService used by the OpenIdLoginService
|
||||
<New id="BaseLoginService" class="org.eclipse.jetty.security.HashLoginService">
|
||||
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>
|
||||
<Set name="hotReload">true</Set>
|
||||
</New>
|
||||
-->
|
||||
</Configure>
|
|
@ -740,7 +740,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
|||
catch (Throwable t)
|
||||
{
|
||||
onWriteComplete(false, t);
|
||||
throw t;
|
||||
if (t instanceof IOException)
|
||||
throw t;
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
|
||||
<Configure id="Server" class="org.eclipse.jetty.server.Server">
|
||||
<Get id="ThreadPool" name="threadPool"/>
|
||||
<New id="HttpClient" class="org.eclipse.jetty.client.HttpClient">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.io.ClientConnector">
|
||||
<Set name="sslContextFactory">
|
||||
<New class="org.eclipse.jetty.util.ssl.SslContextFactory$Client">
|
||||
<Set name="trustAll" type="boolean">
|
||||
<Property name="jetty.openid.sslContextFactory.trustAll" default="false"/>
|
||||
</Set>
|
||||
</New>
|
||||
</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</New>
|
||||
</Arg>
|
||||
<Set name="executor"><Ref refid="ThreadPool"/></Set>
|
||||
</New>
|
||||
<Call name="addBean">
|
||||
<Arg>
|
||||
<Ref refid="BaseLoginService"/>
|
||||
</Arg>
|
||||
</Call>
|
||||
<Call name="addBean">
|
||||
<Arg>
|
||||
<New id="OpenIdConfiguration" class="org.eclipse.jetty.security.openid.OpenIdConfiguration">
|
||||
<Arg name="issuer"><Property name="jetty.openid.provider" deprecated="jetty.openid.openIdProvider"/></Arg>
|
||||
<Arg name="authorizationEndpoint"><Property name="jetty.openid.provider.authorizationEndpoint"/></Arg>
|
||||
<Arg name="tokenEndpoint"><Property name="jetty.openid.provider.tokenEndpoint"/></Arg>
|
||||
<Arg name="clientId"><Property name="jetty.openid.clientId"/></Arg>
|
||||
<Arg name="clientSecret"><Property name="jetty.openid.clientSecret"/></Arg>
|
||||
<Arg name="authMethod"><Property name="jetty.openid.authenticationMethod" deprecated="jetty.openid.authMethod" default="client_secret_post"/></Arg>
|
||||
<Arg name="httpClient"><Ref refid="HttpClient"/></Arg>
|
||||
<Set name="authenticateNewUsers">
|
||||
<Property name="jetty.openid.authenticateNewUsers" default="false"/>
|
||||
</Set>
|
||||
<Set name="logoutWhenIdTokenIsExpired">
|
||||
<Property name="jetty.openid.logoutWhenIdTokenIsExpired" default="false"/>
|
||||
</Set>
|
||||
<Call name="addScopes">
|
||||
<Arg>
|
||||
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
|
||||
<Arg><Property name="jetty.openid.scopes"/></Arg>
|
||||
</Call>
|
||||
</Arg>
|
||||
</Call>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
</Configure>
|
|
@ -9,46 +9,6 @@ ee9
|
|||
[depend]
|
||||
ee9-security
|
||||
openid
|
||||
client
|
||||
|
||||
[lib]
|
||||
lib/jetty-ee9-openid-${jetty.version}.jar
|
||||
lib/jetty-util-ajax-${jetty.version}.jar
|
||||
|
||||
[files]
|
||||
basehome:modules/openid/jetty-ee9-openid-baseloginservice.xml|etc/openid-baseloginservice.xml
|
||||
|
||||
[xml]
|
||||
etc/openid-baseloginservice.xml
|
||||
etc/jetty-ee9-openid.xml
|
||||
|
||||
[ini-template]
|
||||
## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration")
|
||||
# jetty.openid.provider=https://id.example.com/
|
||||
|
||||
## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible)
|
||||
# jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization
|
||||
|
||||
## The OpenID Identity Provider's token endpoint (optional if the metadata of the OP is accessible)
|
||||
# jetty.openid.provider.tokenEndpoint=https://id.example.com/token
|
||||
|
||||
## The Client Identifier
|
||||
# jetty.openid.clientId=test1234
|
||||
|
||||
## The Client Secret
|
||||
# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P
|
||||
|
||||
## Additional Scopes to Request
|
||||
# jetty.openid.scopes=email,profile
|
||||
|
||||
## Whether to Authenticate users not found by base LoginService
|
||||
# jetty.openid.authenticateNewUsers=false
|
||||
|
||||
## 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.authenticationMethod=client_secret_post
|
||||
|
||||
## Whether the user should be logged out after the idToken expires.
|
||||
# jetty.openid.logoutWhenIdTokenIsExpired=false
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
|
||||
<Configure>
|
||||
<!-- Optional code to configure the base LoginService used by the OpenIdLoginService
|
||||
<New id="BaseLoginService" class="org.eclipse.jetty.security.HashLoginService">
|
||||
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>
|
||||
<Set name="hotReload">true</Set>
|
||||
</New>
|
||||
-->
|
||||
</Configure>
|
File diff suppressed because it is too large
Load Diff
|
@ -112,7 +112,7 @@
|
|||
<configuration>
|
||||
<argLine>@{argLine}
|
||||
${jetty.surefire.argLine}
|
||||
--enable-native-access org.eclipse.jetty.quic.quiche.foreign</argLine>
|
||||
--enable-native-access=ALL-UNNAMED</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.test.client.transport;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http2.ErrorCode;
|
||||
import org.eclipse.jetty.http2.api.Session;
|
||||
import org.eclipse.jetty.http2.api.Stream;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.FuturePromise;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class Http2AsyncIOServletTest
|
||||
{
|
||||
private Server server;
|
||||
private ServerConnector connector;
|
||||
private HTTP2Client client;
|
||||
|
||||
private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory(httpConfig));
|
||||
server.addConnector(connector);
|
||||
ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/");
|
||||
servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*");
|
||||
server.start();
|
||||
|
||||
client = new HTTP2Client();
|
||||
client.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown()
|
||||
{
|
||||
LifeCycle.stop(client);
|
||||
LifeCycle.stop(server);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception
|
||||
{
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setNotifyRemoteAsyncErrors(notify);
|
||||
|
||||
AtomicReference<AsyncEvent> errorAsyncEventRef = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
start(httpConfig, new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response)
|
||||
{
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.addListener(new AsyncListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(AsyncEvent event)
|
||||
{
|
||||
errorAsyncEventRef.set(event);
|
||||
asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartAsync(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
});
|
||||
asyncContext.setTimeout(0);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort());
|
||||
FuturePromise<Session> sessionPromise = new FuturePromise<>();
|
||||
client.connect(address, new Session.Listener() {}, sessionPromise);
|
||||
Session session = sessionPromise.get(5, TimeUnit.SECONDS);
|
||||
MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_2, HttpFields.EMPTY);
|
||||
HeadersFrame frame = new HeadersFrame(metaData, null, false);
|
||||
Stream stream = session.newStream(frame, null).get(5, TimeUnit.SECONDS);
|
||||
|
||||
// Wait for the server to be in ASYNC_WAIT.
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
Thread.sleep(500);
|
||||
|
||||
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code));
|
||||
|
||||
if (notify)
|
||||
// Wait for the reset to be notified to the async context listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(() ->
|
||||
{
|
||||
AsyncEvent asyncEvent = errorAsyncEventRef.get();
|
||||
return asyncEvent == null ? null : asyncEvent.getThrowable();
|
||||
}, instanceOf(EofException.class));
|
||||
else
|
||||
// Wait for the reset to NOT be notified to the failure listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.ee9.test.client.transport;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee9.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.http.MetaData;
|
||||
import org.eclipse.jetty.http3.HTTP3ErrorCode;
|
||||
import org.eclipse.jetty.http3.api.Session;
|
||||
import org.eclipse.jetty.http3.api.Stream;
|
||||
import org.eclipse.jetty.http3.client.HTTP3Client;
|
||||
import org.eclipse.jetty.http3.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.quic.client.ClientQuicConfiguration;
|
||||
import org.eclipse.jetty.quic.server.QuicServerConnector;
|
||||
import org.eclipse.jetty.quic.server.ServerQuicConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.toolchain.test.MavenPaths;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.eclipse.jetty.http3.api.Session.Client;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class Http3AsyncIOServletTest
|
||||
{
|
||||
public WorkDir workDir;
|
||||
|
||||
private Server server;
|
||||
private QuicServerConnector connector;
|
||||
private HTTP3Client client;
|
||||
|
||||
private void start(HttpConfiguration httpConfig, HttpServlet httpServlet) throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
|
||||
serverSslContextFactory.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString());
|
||||
serverSslContextFactory.setKeyStorePassword("storepwd");
|
||||
ServerQuicConfiguration serverQuicConfiguration = new ServerQuicConfiguration(serverSslContextFactory, workDir.getEmptyPathDir());
|
||||
connector = new QuicServerConnector(server, serverQuicConfiguration, new HTTP3ServerConnectionFactory(serverQuicConfiguration, httpConfig));
|
||||
server.addConnector(connector);
|
||||
ServletContextHandler servletContextHandler = new ServletContextHandler(server, "/");
|
||||
servletContextHandler.addServlet(new ServletHolder(httpServlet), "/*");
|
||||
server.start();
|
||||
|
||||
client = new HTTP3Client(new ClientQuicConfiguration(new SslContextFactory.Client(true), null));
|
||||
client.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown()
|
||||
{
|
||||
LifeCycle.stop(client);
|
||||
LifeCycle.stop(server);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testStartAsyncThenClientResetRemoteErrorNotification(boolean notify) throws Exception
|
||||
{
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setNotifyRemoteAsyncErrors(notify);
|
||||
|
||||
AtomicReference<AsyncEvent> errorAsyncEventRef = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
start(httpConfig, new HttpServlet()
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest request, HttpServletResponse response)
|
||||
{
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.addListener(new AsyncListener()
|
||||
{
|
||||
@Override
|
||||
public void onComplete(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(AsyncEvent event)
|
||||
{
|
||||
errorAsyncEventRef.set(event);
|
||||
asyncContext.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartAsync(AsyncEvent event)
|
||||
{
|
||||
}
|
||||
});
|
||||
asyncContext.setTimeout(0);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
InetSocketAddress address = new InetSocketAddress("localhost", connector.getLocalPort());
|
||||
Session.Client session = client.connect(address, new Client.Listener() {}).get(5, TimeUnit.SECONDS);
|
||||
MetaData.Request metaData = new MetaData.Request("GET", HttpURI.from("/"), HttpVersion.HTTP_3, HttpFields.EMPTY);
|
||||
HeadersFrame frame = new HeadersFrame(metaData, false);
|
||||
Stream stream = session.newRequest(frame, null).get(5, TimeUnit.SECONDS);
|
||||
|
||||
// Wait for the server to be in ASYNC_WAIT.
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
Thread.sleep(500);
|
||||
|
||||
stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new Exception());
|
||||
|
||||
if (notify)
|
||||
// Wait for the reset to be notified to the async context listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).until(() ->
|
||||
{
|
||||
AsyncEvent asyncEvent = errorAsyncEventRef.get();
|
||||
return asyncEvent == null ? null : asyncEvent.getThrowable();
|
||||
}, instanceOf(EofException.class));
|
||||
else
|
||||
// Wait for the reset to NOT be notified to the failure listener.
|
||||
await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(errorAsyncEventRef::get, nullValue());
|
||||
}
|
||||
}
|
12
pom.xml
12
pom.xml
|
@ -394,6 +394,7 @@
|
|||
<plexus-xml.version>4.0.3</plexus-xml.version>
|
||||
<project.build.outputTimestamp>2024-04-26T07:15:13Z</project.build.outputTimestamp>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<reactive-streams.version>1.0.4</reactive-streams.version>
|
||||
<settingsPath>src/it/settings.xml</settingsPath>
|
||||
<slf4j.version>2.0.12</slf4j.version>
|
||||
<spifly.version>1.3.7</spifly.version>
|
||||
|
@ -402,6 +403,7 @@
|
|||
<surefire.rerunFailingTestsCount>0</surefire.rerunFailingTestsCount>
|
||||
<swissbox.version>1.8.3</swissbox.version>
|
||||
<testcontainers.version>1.19.7</testcontainers.version>
|
||||
<testng.version>7.10.2</testng.version>
|
||||
<tinybundles.version>3.0.0</tinybundles.version>
|
||||
<versions.maven.plugin.version>2.16.2</versions.maven.plugin.version>
|
||||
<wildfly.common.version>1.7.0.Final</wildfly.common.version>
|
||||
|
@ -1249,6 +1251,11 @@
|
|||
<artifactId>osgi.core</artifactId>
|
||||
<version>${org.osgi.core.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.reactivestreams</groupId>
|
||||
<artifactId>reactive-streams-tck-flow</artifactId>
|
||||
<version>${reactive-streams.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl104-over-slf4j</artifactId>
|
||||
|
@ -1285,6 +1292,11 @@
|
|||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
<artifactId>testng</artifactId>
|
||||
<version>${testng.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.wildfly.common</groupId>
|
||||
<artifactId>wildfly-common</artifactId>
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.jmh;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Fork;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OperationsPerInvocation;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.Options;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
|
||||
@State(Scope.Thread)
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
@Fork(1)
|
||||
@Warmup(iterations = 6, time = 2000, timeUnit = TimeUnit.MILLISECONDS)
|
||||
@Measurement(iterations = 3, time = 2000, timeUnit = TimeUnit.MILLISECONDS)
|
||||
public class HashMapVsEnumMapBenchmark
|
||||
{
|
||||
private static final HttpHeader[] HEADERS = HttpHeader.values();
|
||||
private static final HttpHeader[] HEADER_NAMES =
|
||||
{
|
||||
// These will be hits
|
||||
HttpHeader.HOST,
|
||||
HttpHeader.CONTENT_TYPE,
|
||||
HttpHeader.CONTENT_LENGTH,
|
||||
HttpHeader.ACCEPT,
|
||||
|
||||
// These will be misses
|
||||
HttpHeader.TRANSFER_ENCODING,
|
||||
HttpHeader.AUTHORIZATION
|
||||
};
|
||||
|
||||
private List<HttpField> newHeaders()
|
||||
{
|
||||
List<HttpField> list = new ArrayList<>();
|
||||
list.add(new HttpField(HttpHeader.HOST, "Localhost"));
|
||||
list.add(new HttpField(HttpHeader.CONTENT_TYPE, "application/json"));
|
||||
list.add(new HttpField(HttpHeader.CONTENT_LENGTH, "123"));
|
||||
list.add(new HttpField(HttpHeader.USER_AGENT, "JMH Benchmark"));
|
||||
list.add(new HttpField(HttpHeader.ACCEPT, "application/json"));
|
||||
return list;
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@OperationsPerInvocation(5)
|
||||
public long testListLookup()
|
||||
{
|
||||
// Build the HashMap
|
||||
List<HttpField> list = newHeaders();
|
||||
|
||||
// Perform lookups
|
||||
long result = 0;
|
||||
for (HttpHeader header : HEADER_NAMES)
|
||||
{
|
||||
for (HttpField field : list)
|
||||
{
|
||||
if (field.getHeader() == header)
|
||||
{
|
||||
result ^= field.getValue().hashCode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@OperationsPerInvocation(5)
|
||||
public long testHashMapBuildAndLookup()
|
||||
{
|
||||
// Build the HashMap
|
||||
List<HttpField> list = newHeaders();
|
||||
Map<String, HttpField> hashMap = new HashMap<>();
|
||||
for (HttpField field : list)
|
||||
{
|
||||
hashMap.put(field.getName(), field);
|
||||
}
|
||||
|
||||
// Perform lookups
|
||||
long result = 0;
|
||||
for (HttpHeader header : HEADER_NAMES)
|
||||
{
|
||||
HttpField field = hashMap.get(header.asString());
|
||||
if (field != null)
|
||||
result ^= field.getValue().hashCode();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@OperationsPerInvocation(5)
|
||||
public long testEnumMapBuildAndLookup()
|
||||
{
|
||||
// Build the EnumMap
|
||||
Map<HttpHeader, HttpField> enumMap = new EnumMap<>(HttpHeader.class);
|
||||
|
||||
List<HttpField> list = newHeaders();
|
||||
for (HttpField field : list)
|
||||
{
|
||||
enumMap.put(field.getHeader(), field);
|
||||
}
|
||||
|
||||
// Perform lookups
|
||||
long result = 0;
|
||||
for (HttpHeader header : HEADERS)
|
||||
{
|
||||
HttpField field = enumMap.get(header);
|
||||
if (field != null)
|
||||
result ^= field.getValue().hashCode();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException
|
||||
{
|
||||
Options opt = new OptionsBuilder()
|
||||
.include(HashMapVsEnumMapBenchmark.class.getSimpleName())
|
||||
// .addProfiler(GCProfiler.class)
|
||||
.forks(1)
|
||||
.build();
|
||||
|
||||
new Runner(opt).run();
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
<module>jetty-jmh</module>
|
||||
<module>jetty-test-multipart</module>
|
||||
<module>jetty-test-session-common</module>
|
||||
<module>test-cross-context-dispatch</module>
|
||||
<module>test-distribution</module>
|
||||
<module>test-integration</module>
|
||||
<module>test-jpms</module>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>test-cross-context-dispatch</artifactId>
|
||||
<version>12.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>ccd-common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Tests :: Cross Context Dispatch :: Common</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-session</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-slf4j-impl</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,215 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
|
||||
public class DispatchPlan
|
||||
{
|
||||
private final Deque<Step> steps = new LinkedBlockingDeque<>();
|
||||
private final List<String> events = new ArrayList<>();
|
||||
private final List<String> expectedEvents = new ArrayList<>();
|
||||
private final List<Property> expectedProperties = new ArrayList<>();
|
||||
private final List<String> expectedOutput = new ArrayList<>();
|
||||
private HttpRequest requestStep;
|
||||
private String id;
|
||||
private String expectedContentType;
|
||||
// if true, assert that all Session.id seen in request attributes are the same id.
|
||||
private boolean expectedSessionIds;
|
||||
|
||||
public DispatchPlan()
|
||||
{
|
||||
}
|
||||
|
||||
public static DispatchPlan read(Path inputText) throws IOException
|
||||
{
|
||||
DispatchPlan plan = new DispatchPlan();
|
||||
|
||||
plan.id = inputText.getFileName().toString();
|
||||
|
||||
for (String line : Files.readAllLines(inputText, StandardCharsets.UTF_8))
|
||||
{
|
||||
if (line.startsWith("#"))
|
||||
continue; // skip
|
||||
if (line.startsWith("REQUEST|"))
|
||||
{
|
||||
plan.setRequestStep(HttpRequest.parse(line));
|
||||
}
|
||||
else if (line.startsWith("STEP|"))
|
||||
{
|
||||
plan.addStep(Step.parse(line));
|
||||
}
|
||||
else if (line.startsWith("EXPECTED_CONTENT_TYPE|"))
|
||||
{
|
||||
plan.setExpectedContentType(dropType(line));
|
||||
}
|
||||
else if (line.startsWith("EXPECTED_EVENT|"))
|
||||
{
|
||||
plan.addExpectedEvent(dropType(line));
|
||||
}
|
||||
else if (line.startsWith("EXPECTED_PROP|"))
|
||||
{
|
||||
plan.addExpectedProperty(Property.parse(line));
|
||||
}
|
||||
else if (line.startsWith("EXPECTED_OUTPUT|"))
|
||||
{
|
||||
plan.addExpectedOutput(dropType(line));
|
||||
}
|
||||
else if (line.startsWith("EXPECTED_SESSION_IDS|"))
|
||||
{
|
||||
plan.setExpectedSessionIds(Boolean.parseBoolean(dropType(line)));
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
private static String dropType(String line)
|
||||
{
|
||||
int idx = line.indexOf("|");
|
||||
return line.substring(idx + 1);
|
||||
}
|
||||
|
||||
public void addEvent(String format, Object... args)
|
||||
{
|
||||
events.add(String.format(format, args));
|
||||
}
|
||||
|
||||
public void addExpectedEvent(String event)
|
||||
{
|
||||
expectedEvents.add(event);
|
||||
}
|
||||
|
||||
public void addExpectedOutput(String output)
|
||||
{
|
||||
expectedOutput.add(output);
|
||||
}
|
||||
|
||||
public void addExpectedProperty(String name, String value)
|
||||
{
|
||||
expectedProperties.add(new Property(name, value));
|
||||
}
|
||||
|
||||
public void addExpectedProperty(Property property)
|
||||
{
|
||||
expectedProperties.add(property);
|
||||
}
|
||||
|
||||
public void addStep(Step step)
|
||||
{
|
||||
steps.add(step);
|
||||
}
|
||||
|
||||
public List<String> getEvents()
|
||||
{
|
||||
return events;
|
||||
}
|
||||
|
||||
public String getExpectedContentType()
|
||||
{
|
||||
return expectedContentType;
|
||||
}
|
||||
|
||||
public void setExpectedContentType(String expectedContentType)
|
||||
{
|
||||
this.expectedContentType = expectedContentType;
|
||||
}
|
||||
|
||||
public List<String> getExpectedEvents()
|
||||
{
|
||||
return expectedEvents;
|
||||
}
|
||||
|
||||
public void setExpectedEvents(String[] events)
|
||||
{
|
||||
expectedEvents.clear();
|
||||
expectedEvents.addAll(List.of(events));
|
||||
}
|
||||
|
||||
public List<String> getExpectedOutput()
|
||||
{
|
||||
return expectedOutput;
|
||||
}
|
||||
|
||||
public void setExpectedOutput(String[] output)
|
||||
{
|
||||
expectedOutput.clear();
|
||||
expectedOutput.addAll(List.of(output));
|
||||
}
|
||||
|
||||
public List<Property> getExpectedProperties()
|
||||
{
|
||||
return expectedProperties;
|
||||
}
|
||||
|
||||
public void setExpectedProperties(Property[] properties)
|
||||
{
|
||||
expectedProperties.clear();
|
||||
expectedProperties.addAll(List.of(properties));
|
||||
}
|
||||
|
||||
public HttpRequest getRequestStep()
|
||||
{
|
||||
return requestStep;
|
||||
}
|
||||
|
||||
public void setRequestStep(HttpRequest requestStep)
|
||||
{
|
||||
this.requestStep = requestStep;
|
||||
}
|
||||
|
||||
public Deque<Step> getSteps()
|
||||
{
|
||||
return steps;
|
||||
}
|
||||
|
||||
public void setSteps(Step[] stepArr)
|
||||
{
|
||||
steps.clear();
|
||||
for (Step step: stepArr)
|
||||
steps.add(step);
|
||||
}
|
||||
|
||||
public boolean isExpectedSessionIds()
|
||||
{
|
||||
return expectedSessionIds;
|
||||
}
|
||||
|
||||
public void setExpectedSessionIds(boolean expectedSessionIds)
|
||||
{
|
||||
this.expectedSessionIds = expectedSessionIds;
|
||||
}
|
||||
|
||||
public String id()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public Step popStep()
|
||||
{
|
||||
return steps.pollFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "DispatchPlan[id=" + id + "]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DispatchPlanHandler extends Handler.Wrapper
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DispatchPlanHandler.class);
|
||||
private Path plansDir;
|
||||
|
||||
public Path getPlansDir()
|
||||
{
|
||||
return plansDir;
|
||||
}
|
||||
|
||||
public void setPlansDir(Path plansDir)
|
||||
{
|
||||
this.plansDir = plansDir;
|
||||
}
|
||||
|
||||
public void setPlansDir(String plansDir)
|
||||
{
|
||||
this.setPlansDir(Path.of(plansDir));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
DispatchPlan dispatchPlan = (DispatchPlan)request.getAttribute(DispatchPlan.class.getName());
|
||||
|
||||
if (dispatchPlan == null)
|
||||
{
|
||||
String planName = request.getHeaders().get("X-DispatchPlan");
|
||||
if (planName != null)
|
||||
{
|
||||
Path planPath = plansDir.resolve(planName);
|
||||
if (!Files.isRegularFile(planPath))
|
||||
{
|
||||
callback.failed(new IOException("Unable to find: " + planPath));
|
||||
}
|
||||
dispatchPlan = DispatchPlan.read(planPath);
|
||||
dispatchPlan.addEvent("Initial plan: %s", planName);
|
||||
request.setAttribute(DispatchPlan.class.getName(), dispatchPlan);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Missing Request Header [X-DispatchPlan], skipping DispatchPlan behaviors for this request: {}", request.getHttpURI().toURI());
|
||||
}
|
||||
}
|
||||
|
||||
if (dispatchPlan != null)
|
||||
dispatchPlan.addEvent("DispatchPlanHandler.handle() method=%s path-query=%s", request.getMethod(), request.getHttpURI().getPathQuery());
|
||||
|
||||
return super.handle(request, response, callback);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
public enum DispatchType
|
||||
{
|
||||
INCLUDE,
|
||||
FORWARD;
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpRequest implements Step
|
||||
{
|
||||
private String method;
|
||||
private String requestPath;
|
||||
private String body;
|
||||
private Map<String, String> headers;
|
||||
|
||||
public static HttpRequest parse(String line)
|
||||
{
|
||||
String[] parts = line.split("\\|");
|
||||
HttpRequest request = new HttpRequest();
|
||||
request.setMethod(parts[1]);
|
||||
request.setRequestPath(parts[2]);
|
||||
if (parts.length > 4)
|
||||
request.setBody(parts[3]);
|
||||
return request;
|
||||
}
|
||||
|
||||
public String getMethod()
|
||||
{
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(String method)
|
||||
{
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getRequestPath()
|
||||
{
|
||||
return requestPath;
|
||||
}
|
||||
|
||||
public void setRequestPath(String requestPath)
|
||||
{
|
||||
this.requestPath = requestPath;
|
||||
}
|
||||
|
||||
public String getBody()
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setBody(String body)
|
||||
{
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders()
|
||||
{
|
||||
return headers;
|
||||
}
|
||||
|
||||
public void setHeaders(Map<String, String> headers)
|
||||
{
|
||||
this.headers = headers;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.eclipse.jetty.session.DefaultSessionCache;
|
||||
import org.eclipse.jetty.session.ManagedSession;
|
||||
import org.eclipse.jetty.session.NullSessionDataStore;
|
||||
import org.eclipse.jetty.session.SessionData;
|
||||
import org.eclipse.jetty.session.SessionManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PlanSessionCache extends DefaultSessionCache
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PlanSessionCache.class);
|
||||
private final Path outputFile;
|
||||
|
||||
public PlanSessionCache(SessionManager manager)
|
||||
{
|
||||
super(manager);
|
||||
outputFile = Path.of(System.getProperty("jetty.base"), "work/session.log");
|
||||
setSessionDataStore(new NullSessionDataStore());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedSession newSession(SessionData data)
|
||||
{
|
||||
logEvent("newSession()", data);
|
||||
return super.newSession(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit(ManagedSession session) throws Exception
|
||||
{
|
||||
logEvent("commit()", session);
|
||||
super.commit(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release(ManagedSession session) throws Exception
|
||||
{
|
||||
logEvent("release()", session);
|
||||
super.release(session);
|
||||
}
|
||||
|
||||
private void logEvent(String eventType, SessionData data)
|
||||
{
|
||||
String name = "SessionCache.event." + eventType;
|
||||
String value = "";
|
||||
if (data != null)
|
||||
{
|
||||
value = String.format("id=%s|contextPath=%s", data.getId(), data.getContextPath());
|
||||
}
|
||||
logAttribute(name, value);
|
||||
}
|
||||
|
||||
private void logEvent(String eventType, ManagedSession session)
|
||||
{
|
||||
String name = "SessionCache.event." + eventType;
|
||||
String value = "";
|
||||
if (session != null)
|
||||
{
|
||||
value = String.format("id=%s", session.getId());
|
||||
SessionData data = session.getSessionData();
|
||||
if (data != null)
|
||||
{
|
||||
value = String.format("id=%s|contextPath=%s", data.getId(), data.getContextPath());
|
||||
}
|
||||
}
|
||||
logAttribute(name, value);
|
||||
}
|
||||
|
||||
private void logAttribute(String name, String value)
|
||||
{
|
||||
String line = name + "=" + value;
|
||||
if (LOG.isInfoEnabled())
|
||||
LOG.info(line);
|
||||
|
||||
try
|
||||
{
|
||||
Files.writeString(outputFile, line + "\n", StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
LOG.warn("Unable to write to " + outputFile, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
public class Property
|
||||
{
|
||||
private String name;
|
||||
private String value;
|
||||
|
||||
public Property()
|
||||
{
|
||||
}
|
||||
|
||||
public Property(String name, String value)
|
||||
{
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static Property parse(String line)
|
||||
{
|
||||
String[] parts = line.split("\\|");
|
||||
String name = parts[1];
|
||||
String value = null;
|
||||
if (parts.length > 2)
|
||||
value = parts[2];
|
||||
return new Property(name, value);
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "Property{" +
|
||||
"name='" + name + '\'' +
|
||||
", value='" + value + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
public interface Step
|
||||
{
|
||||
static Step parse(String line)
|
||||
{
|
||||
String[] parts = line.split("\\|");
|
||||
switch (parts[1])
|
||||
{
|
||||
case "CONTEXT_FORWARD" ->
|
||||
{
|
||||
ContextRedispatch step = new ContextRedispatch();
|
||||
step.setDispatchType(DispatchType.FORWARD);
|
||||
step.setContextPath(parts[2]);
|
||||
step.setDispatchPath(parts[3]);
|
||||
return step;
|
||||
}
|
||||
case "CONTEXT_INCLUDE" ->
|
||||
{
|
||||
ContextRedispatch step = new ContextRedispatch();
|
||||
step.setDispatchType(DispatchType.INCLUDE);
|
||||
step.setContextPath(parts[2]);
|
||||
step.setDispatchPath(parts[3]);
|
||||
return step;
|
||||
}
|
||||
case "REQUEST_FORWARD" ->
|
||||
{
|
||||
RequestDispatch step = new RequestDispatch();
|
||||
step.setDispatchType(DispatchType.FORWARD);
|
||||
step.setDispatchPath(parts[2]);
|
||||
return step;
|
||||
}
|
||||
case "REQUEST_INCLUDE" ->
|
||||
{
|
||||
RequestDispatch step = new RequestDispatch();
|
||||
step.setDispatchType(DispatchType.INCLUDE);
|
||||
step.setDispatchPath(parts[2]);
|
||||
return step;
|
||||
}
|
||||
case "GET_HTTP_SESSION_ATTRIBUTE" ->
|
||||
{
|
||||
GetHttpSession step = new GetHttpSession();
|
||||
step.setName(parts[2]);
|
||||
return step;
|
||||
}
|
||||
case "SET_HTTP_SESSION_ATTRIBUTE" ->
|
||||
{
|
||||
String name = parts[2];
|
||||
String value = parts[3];
|
||||
Property prop = new Property(name, value);
|
||||
HttpSessionSetAttribute step = new HttpSessionSetAttribute(prop);
|
||||
return step;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Unknown STEP type [" + parts[1] + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Will cause an Attribute to be set on the HttpSession via {@code HttpSession.setAttribute(String, Object)}
|
||||
*/
|
||||
class HttpSessionSetAttribute implements Step
|
||||
{
|
||||
private Property property;
|
||||
|
||||
public HttpSessionSetAttribute(Property property)
|
||||
{
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
public Property getProperty()
|
||||
{
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will cause the HttpSession to be fetched via {@code HttpServletRequest#getHttpSession(false)}
|
||||
* and report the state of the HttpSession in the events (even if null).
|
||||
*/
|
||||
class GetHttpSession implements Step
|
||||
{
|
||||
private String name;
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a Redispatch with FORWARD or INCLUDE types using the {@code ServletContext}.
|
||||
* Uses the {@code ServletContext.getContext(contextPath)} to obtain the
|
||||
* {@code ServletContext} to then use {@code ServletContext.getRequestDispatcher(dispatchPath)}
|
||||
* against, which then results in a {@code RequestDispatcher.include} or {@code RequestDispatcher.forward}
|
||||
* call.
|
||||
*/
|
||||
class ContextRedispatch implements Step
|
||||
{
|
||||
private DispatchType dispatchType;
|
||||
private String contextPath;
|
||||
private String dispatchPath;
|
||||
|
||||
public DispatchType getDispatchType()
|
||||
{
|
||||
return dispatchType;
|
||||
}
|
||||
|
||||
public void setDispatchType(DispatchType dispatchType)
|
||||
{
|
||||
this.dispatchType = dispatchType;
|
||||
}
|
||||
|
||||
public String getContextPath()
|
||||
{
|
||||
return contextPath;
|
||||
}
|
||||
|
||||
public void setContextPath(String contextPath)
|
||||
{
|
||||
this.contextPath = contextPath;
|
||||
}
|
||||
|
||||
public String getDispatchPath()
|
||||
{
|
||||
return dispatchPath;
|
||||
}
|
||||
|
||||
public void setDispatchPath(String dispatchPath)
|
||||
{
|
||||
this.dispatchPath = dispatchPath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a Redispatch with FORWARD or INCLUDE types using the {@code HttpServletRequest}.
|
||||
* Uses the {@code HttpServletRequest.getRequestDispatcher(dispatchPath)} which then
|
||||
* results in a {@code RequestDispatcher.include} or {@code RequestDispatcher.forward}
|
||||
* call.
|
||||
*/
|
||||
class RequestDispatch implements Step
|
||||
{
|
||||
private DispatchType dispatchType;
|
||||
private String dispatchPath;
|
||||
|
||||
public DispatchType getDispatchType()
|
||||
{
|
||||
return dispatchType;
|
||||
}
|
||||
|
||||
public void setDispatchType(DispatchType dispatchType)
|
||||
{
|
||||
this.dispatchType = dispatchType;
|
||||
}
|
||||
|
||||
public String getDispatchPath()
|
||||
{
|
||||
return dispatchPath;
|
||||
}
|
||||
|
||||
public void setDispatchPath(String dispatchPath)
|
||||
{
|
||||
this.dispatchPath = dispatchPath;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.eclipse.jetty.toolchain.test.MavenPaths;
|
||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class DispatchPlanLoadTest
|
||||
{
|
||||
@Test
|
||||
public void testRead() throws IOException
|
||||
{
|
||||
Path planPath = MavenPaths.findTestResourceFile("forward-include-dump.txt");
|
||||
DispatchPlan plan = DispatchPlan.read(planPath);
|
||||
assertNotNull(plan);
|
||||
|
||||
assertThat("plan.id", plan.id(), is(planPath.getFileName().toString()));
|
||||
|
||||
HttpRequest httpRequestStep = plan.getRequestStep();
|
||||
assertThat(httpRequestStep.getMethod(), is("GET"));
|
||||
assertThat(httpRequestStep.getRequestPath(), is("/ccd-ee10/redispatch/ee10"));
|
||||
assertThat(httpRequestStep.getBody(), is(nullValue()));
|
||||
|
||||
assertEquals(3, plan.getSteps().size());
|
||||
|
||||
Step step = plan.popStep();
|
||||
assertThat(step, instanceOf(Step.ContextRedispatch.class));
|
||||
Step.ContextRedispatch contextRedispatchStep = (Step.ContextRedispatch)step;
|
||||
assertThat(contextRedispatchStep.getDispatchType(), is(DispatchType.FORWARD));
|
||||
assertThat(contextRedispatchStep.getContextPath(), is("/ccd-ee8"));
|
||||
assertThat(contextRedispatchStep.getDispatchPath(), is("/redispatch/ee8"));
|
||||
|
||||
step = plan.popStep();
|
||||
assertThat(step, instanceOf(Step.ContextRedispatch.class));
|
||||
contextRedispatchStep = (Step.ContextRedispatch)step;
|
||||
assertThat(contextRedispatchStep.getDispatchType(), is(DispatchType.FORWARD));
|
||||
assertThat(contextRedispatchStep.getContextPath(), is("/ccd-ee9"));
|
||||
assertThat(contextRedispatchStep.getDispatchPath(), is("/redispatch/ee9"));
|
||||
|
||||
step = plan.popStep();
|
||||
assertThat(step, instanceOf(Step.RequestDispatch.class));
|
||||
Step.RequestDispatch requestRedispatchStep = (Step.RequestDispatch)step;
|
||||
assertThat(requestRedispatchStep.getDispatchType(), is(DispatchType.INCLUDE));
|
||||
assertThat(requestRedispatchStep.getDispatchPath(), is("/dump/ee9"));
|
||||
|
||||
assertThat(plan.getExpectedContentType(), is("text/x-java-properties; charset=utf-8"));
|
||||
|
||||
assertThat("Expected Events", plan.getExpectedEvents().size(), is(6));
|
||||
assertThat("Expected Output", plan.getExpectedOutput().size(), is(1));
|
||||
assertThat("Expected Properties", plan.getExpectedProperties().size(), is(18));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
REQUEST|GET|/ccd-ee10/redispatch/ee10
|
||||
STEP|CONTEXT_FORWARD|/ccd-ee8|/redispatch/ee8
|
||||
STEP|CONTEXT_FORWARD|/ccd-ee9|/redispatch/ee9
|
||||
STEP|REQUEST_INCLUDE|/dump/ee9
|
||||
EXPECTED_CONTENT_TYPE|text/x-java-properties; charset=utf-8
|
||||
EXPECTED_EVENT|Initial plan: ee10-forward-to-ee8-include-ee9-dump.json
|
||||
EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee9/redispatch/ee9
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee9/redispatch/ee9
|
||||
EXPECTED_PROP|request.dispatcherType|INCLUDE
|
||||
EXPECTED_PROP|request.requestURI|/ccd-ee9/redispatch/ee9
|
||||
EXPECTED_PROP|attr[jakarta.servlet.forward.context_path]|/ccd-ee8
|
||||
EXPECTED_PROP|attr[jakarta.servlet.forward.path_info]|/ee8
|
||||
EXPECTED_PROP|attr[jakarta.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_PROP|attr[jakarta.servlet.forward.servlet_path]|/redispatch
|
||||
EXPECTED_PROP|attr[jakarta.servlet.include.context_path]/ccd-ee9
|
||||
EXPECTED_PROP|attr[jakarta.servlet.include.path_info]|/ee9
|
||||
EXPECTED_PROP|attr[jakarta.servlet.include.request_uri]|/ccd-ee9/dump/ee9
|
||||
EXPECTED_PROP|attr[jakarta.servlet.include.servlet_path]/dump
|
||||
EXPECTED_PROP|attr[javax.servlet.include.context_path]|<null>
|
||||
EXPECTED_PROP|attr[javax.servlet.include.path_info]|<null>
|
||||
EXPECTED_PROP|attr[javax.servlet.include.request_uri]|<null>
|
||||
EXPECTED_PROP|attr[javax.servlet.include.servlet_path]|<null>
|
||||
EXPECTED_PROP|attr[javax.servlet.forward.context_path]|/ccd-ee8
|
||||
EXPECTED_PROP|attr[javax.servlet.forward.path_info]|/ee8
|
||||
EXPECTED_PROP|attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_PROP|attr[javax.servlet.forward.servlet_path]|/redispatch
|
||||
EXPECTED_OUTPUT|foo-bar
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>test-cross-context-dispatch</artifactId>
|
||||
<version>12.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>ccd-ee10-webapp</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>Tests :: Cross Context Dispatch :: ee10 WebApp</name>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.ee10</groupId>
|
||||
<artifactId>jetty-ee10-bom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>6.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>ccd-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee10;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.eclipse.jetty.tests.ccd.common.DispatchPlan;
|
||||
import org.eclipse.jetty.tests.ccd.common.Property;
|
||||
import org.eclipse.jetty.tests.ccd.common.Step;
|
||||
|
||||
public class CCDServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName());
|
||||
|
||||
if (dispatchPlan == null)
|
||||
throw new ServletException("Unable to find DispatchPlan");
|
||||
|
||||
dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s",
|
||||
this.getClass().getName(),
|
||||
req.getDispatcherType(), req.getMethod(), req.getRequestURI());
|
||||
|
||||
Step step;
|
||||
|
||||
while ((step = dispatchPlan.popStep()) != null)
|
||||
{
|
||||
if (step instanceof Step.ContextRedispatch contextRedispatchStep)
|
||||
{
|
||||
ServletContext otherContext = getServletContext().getContext(contextRedispatchStep.getContextPath());
|
||||
if (otherContext == null)
|
||||
throw new NullPointerException("ServletContext.getContext(\"" + contextRedispatchStep.getContextPath() + "\") returned null");
|
||||
RequestDispatcher dispatcher = otherContext.getRequestDispatcher(contextRedispatchStep.getDispatchPath());
|
||||
if (dispatcher == null)
|
||||
throw new NullPointerException("ServletContext.getRequestDispatcher(\"" + contextRedispatchStep.getDispatchPath() + "\") returned null");
|
||||
switch (contextRedispatchStep.getDispatchType())
|
||||
{
|
||||
case FORWARD -> dispatcher.forward(req, resp);
|
||||
case INCLUDE -> dispatcher.include(req, resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (step instanceof Step.RequestDispatch requestDispatchStep)
|
||||
{
|
||||
RequestDispatcher dispatcher = req.getRequestDispatcher(requestDispatchStep.getDispatchPath());
|
||||
if (dispatcher == null)
|
||||
throw new NullPointerException("HttpServletRequest.getRequestDispatcher(\"" + requestDispatchStep.getDispatchPath() + "\") returned null");
|
||||
switch (requestDispatchStep.getDispatchType())
|
||||
{
|
||||
case FORWARD -> dispatcher.forward(req, resp);
|
||||
case INCLUDE -> dispatcher.include(req, resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (step instanceof Step.GetHttpSession getHttpSessionTask)
|
||||
{
|
||||
HttpSession session = req.getSession(false);
|
||||
if (session == null)
|
||||
{
|
||||
dispatchPlan.addEvent("%s.service() HttpSession is null",
|
||||
this.getClass().getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
String name = getHttpSessionTask.getName();
|
||||
Object value = session.getAttribute(name);
|
||||
dispatchPlan.addEvent("%s.service() HttpSession exists: [%s]=[%s]",
|
||||
this.getClass().getName(),
|
||||
name,
|
||||
Objects.toString(value)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
else if (step instanceof Step.HttpSessionSetAttribute sessionSetAttribute)
|
||||
{
|
||||
HttpSession session = req.getSession(true);
|
||||
req.setAttribute("session[" + req.getRequestURI() + "].id", session.getId());
|
||||
Property prop = sessionSetAttribute.getProperty();
|
||||
session.setAttribute(prop.getName(), prop.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException("Unable to execute task " + step + " in " + this.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee10;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.eclipse.jetty.tests.ccd.common.DispatchPlan;
|
||||
|
||||
public class DumpServlet extends HttpServlet
|
||||
{
|
||||
private static final String NULL = "<null>";
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException
|
||||
{
|
||||
DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName());
|
||||
|
||||
if (dispatchPlan != null)
|
||||
{
|
||||
dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s",
|
||||
this.getClass().getName(),
|
||||
req.getDispatcherType(), req.getMethod(), req.getRequestURI());
|
||||
}
|
||||
|
||||
Properties props = new Properties();
|
||||
props.setProperty("requestType", req.getClass().getName());
|
||||
props.setProperty("responseType", resp.getClass().getName());
|
||||
|
||||
props.setProperty("request.authType", Objects.toString(req.getAuthType(), NULL));
|
||||
props.setProperty("request.characterEncoding", Objects.toString(req.getCharacterEncoding(), NULL));
|
||||
props.setProperty("request.contentLength", Long.toString(req.getContentLengthLong()));
|
||||
props.setProperty("request.contentType", Objects.toString(req.getContentType(), NULL));
|
||||
props.setProperty("request.contextPath", Objects.toString(req.getContextPath(), NULL));
|
||||
props.setProperty("request.dispatcherType", Objects.toString(req.getDispatcherType(), NULL));
|
||||
props.setProperty("request.localAddr", Objects.toString(req.getLocalAddr(), NULL));
|
||||
props.setProperty("request.localName", Objects.toString(req.getLocalName(), NULL));
|
||||
props.setProperty("request.localPort", Integer.toString(req.getLocalPort()));
|
||||
props.setProperty("request.locale", Objects.toString(req.getLocale(), NULL));
|
||||
props.setProperty("request.method", Objects.toString(req.getMethod(), NULL));
|
||||
props.setProperty("request.pathInfo", Objects.toString(req.getPathInfo(), NULL));
|
||||
props.setProperty("request.pathTranslated", Objects.toString(req.getPathTranslated(), NULL));
|
||||
props.setProperty("request.protocol", Objects.toString(req.getProtocol(), NULL));
|
||||
props.setProperty("request.queryString", Objects.toString(req.getQueryString(), NULL));
|
||||
props.setProperty("request.remoteAddr", Objects.toString(req.getRemoteAddr(), NULL));
|
||||
props.setProperty("request.remoteHost", Objects.toString(req.getRemoteHost(), NULL));
|
||||
props.setProperty("request.remotePort", Integer.toString(req.getRemotePort()));
|
||||
props.setProperty("request.remoteUser", Objects.toString(req.getRemoteUser(), NULL));
|
||||
props.setProperty("request.requestedSessionId", Objects.toString(req.getRequestedSessionId(), NULL));
|
||||
props.setProperty("request.requestURI", Objects.toString(req.getRequestURI(), NULL));
|
||||
props.setProperty("request.requestURL", Objects.toString(req.getRequestURL(), NULL));
|
||||
props.setProperty("request.serverPort", Integer.toString(req.getServerPort()));
|
||||
props.setProperty("request.servletPath", Objects.toString(req.getServletPath(), NULL));
|
||||
|
||||
props.setProperty("request.session.exists", "false");
|
||||
HttpSession httpSession = req.getSession(false);
|
||||
if (httpSession != null)
|
||||
{
|
||||
props.setProperty("request.session.exists", "true");
|
||||
List<String> attrNames = Collections.list(httpSession.getAttributeNames());
|
||||
attrNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
Object attrVal = httpSession.getAttribute(name);
|
||||
props.setProperty("session[" + name + "]", Objects.toString(attrVal, NULL));
|
||||
});
|
||||
}
|
||||
|
||||
addAttributes(props, "req", req::getAttributeNames, req::getAttribute);
|
||||
addAttributes(props, "context",
|
||||
() -> getServletContext().getAttributeNames(),
|
||||
(name) -> getServletContext().getAttribute(name));
|
||||
|
||||
List<String> headerNames = Collections.list(req.getHeaderNames());
|
||||
headerNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
String headerVal = req.getHeader(name);
|
||||
props.setProperty("header[" + name + "]", Objects.toString(headerVal, NULL));
|
||||
});
|
||||
|
||||
if (dispatchPlan != null)
|
||||
{
|
||||
int eventCount = dispatchPlan.getEvents().size();
|
||||
props.setProperty("dispatchPlan.events.count", Integer.toString(dispatchPlan.getEvents().size()));
|
||||
for (int i = 0; i < eventCount; i++)
|
||||
{
|
||||
props.setProperty("dispatchPlan.event[" + i + "]", dispatchPlan.getEvents().get(i));
|
||||
}
|
||||
}
|
||||
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.setCharacterEncoding("utf-8");
|
||||
resp.setContentType("text/x-java-properties");
|
||||
PrintWriter out = resp.getWriter();
|
||||
props.store(out, "From " + this.getClass().getName());
|
||||
}
|
||||
|
||||
private void addAttributes(Properties props,
|
||||
String prefix,
|
||||
Supplier<Enumeration<String>> getNamesSupplier,
|
||||
Function<String, Object> getAttributeFunction)
|
||||
{
|
||||
List<String> attrNames = Collections.list(getNamesSupplier.get());
|
||||
attrNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
Object attrVal = getAttributeFunction.apply(name);
|
||||
props.setProperty(prefix + ".attr[" + name + "]", Objects.toString(attrVal, NULL));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee10;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public class ForwardServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String forwardTo = req.getHeader("X-ForwardTo");
|
||||
Objects.requireNonNull(forwardTo);
|
||||
RequestDispatcher requestDispatcher = req.getRequestDispatcher(forwardTo);
|
||||
requestDispatcher.forward(req, resp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee10;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* A servlet filter that will harshly change the return value of
|
||||
* {@link HttpServletRequest#getRequestURI()} to something that does
|
||||
* not satisfy the Servlet spec URI invariant {@code request URI == context path + servlet path + path info}
|
||||
*/
|
||||
public class InternalRequestURIFilter implements Filter
|
||||
{
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
|
||||
{
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
|
||||
InternalRequestURIWrapper requestURIWrapper = new InternalRequestURIWrapper(httpServletRequest);
|
||||
chain.doFilter(requestURIWrapper, httpServletResponse);
|
||||
}
|
||||
|
||||
private static class InternalRequestURIWrapper extends HttpServletRequestWrapper
|
||||
{
|
||||
public InternalRequestURIWrapper(HttpServletRequest request)
|
||||
{
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestURI()
|
||||
{
|
||||
return "/internal/";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
|
||||
version="6.0">
|
||||
|
||||
<display-name>ccd-ee10</display-name>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>ccd</servlet-name>
|
||||
<servlet-class>org.eclipse.jetty.tests.ccd.ee10.CCDServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>dump</servlet-name>
|
||||
<servlet-class>org.eclipse.jetty.tests.ccd.ee10.DumpServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>ccd</servlet-name>
|
||||
<url-pattern>/redispatch/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>dump</servlet-name>
|
||||
<url-pattern>/dump/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
</web-app>
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>test-cross-context-dispatch</artifactId>
|
||||
<version>12.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>ccd-ee8-webapp</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>Tests :: Cross Context Dispatch :: ee8 WebApp</name>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.ee8</groupId>
|
||||
<artifactId>jetty-ee8-bom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>4.0.4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>ccd-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee8;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.eclipse.jetty.tests.ccd.common.DispatchPlan;
|
||||
import org.eclipse.jetty.tests.ccd.common.Property;
|
||||
import org.eclipse.jetty.tests.ccd.common.Step;
|
||||
|
||||
public class CCDServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName());
|
||||
|
||||
if (dispatchPlan == null)
|
||||
throw new ServletException("Unable to find DispatchPlan");
|
||||
|
||||
dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s",
|
||||
this.getClass().getName(),
|
||||
req.getDispatcherType(), req.getMethod(), req.getRequestURI());
|
||||
|
||||
Step step;
|
||||
|
||||
while ((step = dispatchPlan.popStep()) != null)
|
||||
{
|
||||
if (step instanceof Step.ContextRedispatch contextRedispatchStep)
|
||||
{
|
||||
ServletContext otherContext = getServletContext().getContext(contextRedispatchStep.getContextPath());
|
||||
if (otherContext == null)
|
||||
throw new NullPointerException("ServletContext.getContext(\"" + contextRedispatchStep.getContextPath() + "\") returned null");
|
||||
RequestDispatcher dispatcher = otherContext.getRequestDispatcher(contextRedispatchStep.getDispatchPath());
|
||||
if (dispatcher == null)
|
||||
throw new NullPointerException("ServletContext.getRequestDispatcher(\"" + contextRedispatchStep.getDispatchPath() + "\") returned null");
|
||||
switch (contextRedispatchStep.getDispatchType())
|
||||
{
|
||||
case FORWARD -> dispatcher.forward(req, resp);
|
||||
case INCLUDE -> dispatcher.include(req, resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (step instanceof Step.RequestDispatch requestDispatchStep)
|
||||
{
|
||||
RequestDispatcher dispatcher = req.getRequestDispatcher(requestDispatchStep.getDispatchPath());
|
||||
if (dispatcher == null)
|
||||
throw new NullPointerException("HttpServletRequest.getRequestDispatcher(\"" + requestDispatchStep.getDispatchPath() + "\") returned null");
|
||||
switch (requestDispatchStep.getDispatchType())
|
||||
{
|
||||
case FORWARD -> dispatcher.forward(req, resp);
|
||||
case INCLUDE -> dispatcher.include(req, resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (step instanceof Step.GetHttpSession getHttpSessionTask)
|
||||
{
|
||||
HttpSession session = req.getSession(false);
|
||||
if (session == null)
|
||||
{
|
||||
dispatchPlan.addEvent("%s.service() HttpSession is null",
|
||||
this.getClass().getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
String name = getHttpSessionTask.getName();
|
||||
Object value = session.getAttribute(name);
|
||||
dispatchPlan.addEvent("%s.service() HttpSession exists: [%s]=[%s]",
|
||||
this.getClass().getName(),
|
||||
name,
|
||||
Objects.toString(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (step instanceof Step.HttpSessionSetAttribute sessionSetAttribute)
|
||||
{
|
||||
HttpSession session = req.getSession(true);
|
||||
req.setAttribute("session[" + req.getRequestURI() + "].id", session.getId());
|
||||
Property prop = sessionSetAttribute.getProperty();
|
||||
session.setAttribute(prop.getName(), prop.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException("Unable to execute task " + step + " in " + this.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee8;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.eclipse.jetty.tests.ccd.common.DispatchPlan;
|
||||
|
||||
public class DumpServlet extends HttpServlet
|
||||
{
|
||||
private static final String NULL = "<null>";
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException
|
||||
{
|
||||
DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName());
|
||||
|
||||
if (dispatchPlan != null)
|
||||
{
|
||||
dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s",
|
||||
this.getClass().getName(),
|
||||
req.getDispatcherType(), req.getMethod(), req.getRequestURI());
|
||||
}
|
||||
|
||||
Properties props = new Properties();
|
||||
props.setProperty("requestType", req.getClass().getName());
|
||||
props.setProperty("responseType", resp.getClass().getName());
|
||||
|
||||
props.setProperty("request.authType", Objects.toString(req.getAuthType(), NULL));
|
||||
props.setProperty("request.characterEncoding", Objects.toString(req.getCharacterEncoding(), NULL));
|
||||
props.setProperty("request.contentLength", Long.toString(req.getContentLengthLong()));
|
||||
props.setProperty("request.contentType", Objects.toString(req.getContentType(), NULL));
|
||||
props.setProperty("request.contextPath", Objects.toString(req.getContextPath(), NULL));
|
||||
props.setProperty("request.dispatcherType", Objects.toString(req.getDispatcherType(), NULL));
|
||||
props.setProperty("request.localAddr", Objects.toString(req.getLocalAddr(), NULL));
|
||||
props.setProperty("request.localName", Objects.toString(req.getLocalName(), NULL));
|
||||
props.setProperty("request.localPort", Integer.toString(req.getLocalPort()));
|
||||
props.setProperty("request.locale", Objects.toString(req.getLocale(), NULL));
|
||||
props.setProperty("request.method", Objects.toString(req.getMethod(), NULL));
|
||||
props.setProperty("request.pathInfo", Objects.toString(req.getPathInfo(), NULL));
|
||||
props.setProperty("request.pathTranslated", Objects.toString(req.getPathTranslated(), NULL));
|
||||
props.setProperty("request.protocol", Objects.toString(req.getProtocol(), NULL));
|
||||
props.setProperty("request.queryString", Objects.toString(req.getQueryString(), NULL));
|
||||
props.setProperty("request.remoteAddr", Objects.toString(req.getRemoteAddr(), NULL));
|
||||
props.setProperty("request.remoteHost", Objects.toString(req.getRemoteHost(), NULL));
|
||||
props.setProperty("request.remotePort", Integer.toString(req.getRemotePort()));
|
||||
props.setProperty("request.remoteUser", Objects.toString(req.getRemoteUser(), NULL));
|
||||
props.setProperty("request.requestedSessionId", Objects.toString(req.getRequestedSessionId(), NULL));
|
||||
props.setProperty("request.requestURI", Objects.toString(req.getRequestURI(), NULL));
|
||||
props.setProperty("request.requestURL", Objects.toString(req.getRequestURL(), NULL));
|
||||
props.setProperty("request.serverPort", Integer.toString(req.getServerPort()));
|
||||
props.setProperty("request.servletPath", Objects.toString(req.getServletPath(), NULL));
|
||||
|
||||
props.setProperty("request.session.exists", "false");
|
||||
HttpSession httpSession = req.getSession(false);
|
||||
if (httpSession != null)
|
||||
{
|
||||
props.setProperty("request.session.exists", "true");
|
||||
List<String> attrNames = Collections.list(httpSession.getAttributeNames());
|
||||
attrNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
Object attrVal = httpSession.getAttribute(name);
|
||||
props.setProperty("session[" + name + "]", Objects.toString(attrVal, NULL));
|
||||
});
|
||||
}
|
||||
|
||||
addAttributes(props, "req", req::getAttributeNames, req::getAttribute);
|
||||
addAttributes(props, "context",
|
||||
() -> getServletContext().getAttributeNames(),
|
||||
(name) -> getServletContext().getAttribute(name));
|
||||
|
||||
List<String> headerNames = Collections.list(req.getHeaderNames());
|
||||
headerNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
String headerVal = req.getHeader(name);
|
||||
props.setProperty("header[" + name + "]", Objects.toString(headerVal, NULL));
|
||||
});
|
||||
|
||||
if (dispatchPlan != null)
|
||||
{
|
||||
int eventCount = dispatchPlan.getEvents().size();
|
||||
props.setProperty("dispatchPlan.events.count", Integer.toString(dispatchPlan.getEvents().size()));
|
||||
for (int i = 0; i < eventCount; i++)
|
||||
{
|
||||
props.setProperty("dispatchPlan.event[" + i + "]", dispatchPlan.getEvents().get(i));
|
||||
}
|
||||
}
|
||||
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.setCharacterEncoding("utf-8");
|
||||
resp.setContentType("text/x-java-properties");
|
||||
PrintWriter out = resp.getWriter();
|
||||
props.store(out, "From " + this.getClass().getName());
|
||||
}
|
||||
|
||||
private void addAttributes(Properties props,
|
||||
String prefix,
|
||||
Supplier<Enumeration<String>> getNamesSupplier,
|
||||
Function<String, Object> getAttributeFunction)
|
||||
{
|
||||
List<String> attrNames = Collections.list(getNamesSupplier.get());
|
||||
attrNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
Object attrVal = getAttributeFunction.apply(name);
|
||||
props.setProperty(prefix + ".attr[" + name + "]", Objects.toString(attrVal, NULL));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee8;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
public class ForwardServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String forwardTo = req.getHeader("X-ForwardTo");
|
||||
Objects.requireNonNull(forwardTo);
|
||||
RequestDispatcher requestDispatcher = req.getRequestDispatcher(forwardTo);
|
||||
requestDispatcher.forward(req, resp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee8;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* A servlet filter that will harshly change the return value of
|
||||
* {@link HttpServletRequest#getRequestURI()} to something that does
|
||||
* not satisfy the Servlet spec URI invariant {@code request URI == context path + servlet path + path info}
|
||||
*/
|
||||
public class InternalRequestURIFilter implements Filter
|
||||
{
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
|
||||
{
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
|
||||
InternalRequestURIWrapper requestURIWrapper = new InternalRequestURIWrapper(httpServletRequest);
|
||||
chain.doFilter(requestURIWrapper, httpServletResponse);
|
||||
}
|
||||
|
||||
private static class InternalRequestURIWrapper extends HttpServletRequestWrapper
|
||||
{
|
||||
public InternalRequestURIWrapper(HttpServletRequest request)
|
||||
{
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestURI()
|
||||
{
|
||||
return "/internal/";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
|
||||
version="4.0">
|
||||
|
||||
<display-name>ccd-ee8</display-name>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>ccd</servlet-name>
|
||||
<servlet-class>org.eclipse.jetty.tests.ccd.ee8.CCDServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>forwardto</servlet-name>
|
||||
<servlet-class>org.eclipse.jetty.tests.ccd.ee8.ForwardServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>dump</servlet-name>
|
||||
<servlet-class>org.eclipse.jetty.tests.ccd.ee8.DumpServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>ccd</servlet-name>
|
||||
<url-pattern>/redispatch/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>forwardto</servlet-name>
|
||||
<url-pattern>/forwardto/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>dump</servlet-name>
|
||||
<url-pattern>/dump/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
</web-app>
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>test-cross-context-dispatch</artifactId>
|
||||
<version>12.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>ccd-ee9-webapp</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>Tests :: Cross Context Dispatch :: ee9 WebApp</name>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.ee9</groupId>
|
||||
<artifactId>jetty-ee9-bom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>ccd-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee9;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.eclipse.jetty.tests.ccd.common.DispatchPlan;
|
||||
import org.eclipse.jetty.tests.ccd.common.Property;
|
||||
import org.eclipse.jetty.tests.ccd.common.Step;
|
||||
|
||||
public class CCDServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName());
|
||||
|
||||
if (dispatchPlan == null)
|
||||
throw new ServletException("Unable to find DispatchPlan");
|
||||
|
||||
dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s",
|
||||
this.getClass().getName(),
|
||||
req.getDispatcherType(), req.getMethod(), req.getRequestURI());
|
||||
|
||||
Step step;
|
||||
|
||||
while ((step = dispatchPlan.popStep()) != null)
|
||||
{
|
||||
if (step instanceof Step.ContextRedispatch contextRedispatchStep)
|
||||
{
|
||||
ServletContext otherContext = getServletContext().getContext(contextRedispatchStep.getContextPath());
|
||||
if (otherContext == null)
|
||||
throw new NullPointerException("ServletContext.getContext(\"" + contextRedispatchStep.getContextPath() + "\") returned null");
|
||||
RequestDispatcher dispatcher = otherContext.getRequestDispatcher(contextRedispatchStep.getDispatchPath());
|
||||
if (dispatcher == null)
|
||||
throw new NullPointerException("ServletContext.getRequestDispatcher(\"" + contextRedispatchStep.getDispatchPath() + "\") returned null");
|
||||
switch (contextRedispatchStep.getDispatchType())
|
||||
{
|
||||
case FORWARD -> dispatcher.forward(req, resp);
|
||||
case INCLUDE -> dispatcher.include(req, resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (step instanceof Step.RequestDispatch requestDispatchStep)
|
||||
{
|
||||
RequestDispatcher dispatcher = req.getRequestDispatcher(requestDispatchStep.getDispatchPath());
|
||||
if (dispatcher == null)
|
||||
throw new NullPointerException("HttpServletRequest.getRequestDispatcher(\"" + requestDispatchStep.getDispatchPath() + "\") returned null");
|
||||
switch (requestDispatchStep.getDispatchType())
|
||||
{
|
||||
case FORWARD -> dispatcher.forward(req, resp);
|
||||
case INCLUDE -> dispatcher.include(req, resp);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (step instanceof Step.GetHttpSession getHttpSessionTask)
|
||||
{
|
||||
HttpSession session = req.getSession(false);
|
||||
if (session == null)
|
||||
{
|
||||
dispatchPlan.addEvent("%s.service() HttpSession is null",
|
||||
this.getClass().getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
String name = getHttpSessionTask.getName();
|
||||
Object value = session.getAttribute(name);
|
||||
dispatchPlan.addEvent("%s.service() HttpSession exists: [%s]=[%s]",
|
||||
this.getClass().getName(),
|
||||
name,
|
||||
Objects.toString(value)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
else if (step instanceof Step.HttpSessionSetAttribute sessionSetAttribute)
|
||||
{
|
||||
HttpSession session = req.getSession(true);
|
||||
req.setAttribute("session[" + req.getRequestURI() + "].id", session.getId());
|
||||
Property prop = sessionSetAttribute.getProperty();
|
||||
session.setAttribute(prop.getName(), prop.getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException("Unable to execute task " + step + " in " + this.getClass().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee9;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.eclipse.jetty.tests.ccd.common.DispatchPlan;
|
||||
|
||||
public class DumpServlet extends HttpServlet
|
||||
{
|
||||
private static final String NULL = "<null>";
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException
|
||||
{
|
||||
DispatchPlan dispatchPlan = (DispatchPlan)req.getAttribute(DispatchPlan.class.getName());
|
||||
|
||||
if (dispatchPlan != null)
|
||||
{
|
||||
dispatchPlan.addEvent("%s.service() dispatcherType=%s method=%s requestUri=%s",
|
||||
this.getClass().getName(),
|
||||
req.getDispatcherType(), req.getMethod(), req.getRequestURI());
|
||||
}
|
||||
|
||||
Properties props = new Properties();
|
||||
props.setProperty("requestType", req.getClass().getName());
|
||||
props.setProperty("responseType", resp.getClass().getName());
|
||||
|
||||
props.setProperty("request.authType", Objects.toString(req.getAuthType(), NULL));
|
||||
props.setProperty("request.characterEncoding", Objects.toString(req.getCharacterEncoding(), NULL));
|
||||
props.setProperty("request.contentLength", Long.toString(req.getContentLengthLong()));
|
||||
props.setProperty("request.contentType", Objects.toString(req.getContentType(), NULL));
|
||||
props.setProperty("request.contextPath", Objects.toString(req.getContextPath(), NULL));
|
||||
props.setProperty("request.dispatcherType", Objects.toString(req.getDispatcherType(), NULL));
|
||||
props.setProperty("request.localAddr", Objects.toString(req.getLocalAddr(), NULL));
|
||||
props.setProperty("request.localName", Objects.toString(req.getLocalName(), NULL));
|
||||
props.setProperty("request.localPort", Integer.toString(req.getLocalPort()));
|
||||
props.setProperty("request.locale", Objects.toString(req.getLocale(), NULL));
|
||||
props.setProperty("request.method", Objects.toString(req.getMethod(), NULL));
|
||||
props.setProperty("request.pathInfo", Objects.toString(req.getPathInfo(), NULL));
|
||||
props.setProperty("request.pathTranslated", Objects.toString(req.getPathTranslated(), NULL));
|
||||
props.setProperty("request.protocol", Objects.toString(req.getProtocol(), NULL));
|
||||
props.setProperty("request.queryString", Objects.toString(req.getQueryString(), NULL));
|
||||
props.setProperty("request.remoteAddr", Objects.toString(req.getRemoteAddr(), NULL));
|
||||
props.setProperty("request.remoteHost", Objects.toString(req.getRemoteHost(), NULL));
|
||||
props.setProperty("request.remotePort", Integer.toString(req.getRemotePort()));
|
||||
props.setProperty("request.remoteUser", Objects.toString(req.getRemoteUser(), NULL));
|
||||
props.setProperty("request.requestedSessionId", Objects.toString(req.getRequestedSessionId(), NULL));
|
||||
props.setProperty("request.requestURI", Objects.toString(req.getRequestURI(), NULL));
|
||||
props.setProperty("request.requestURL", Objects.toString(req.getRequestURL(), NULL));
|
||||
props.setProperty("request.serverPort", Integer.toString(req.getServerPort()));
|
||||
props.setProperty("request.servletPath", Objects.toString(req.getServletPath(), NULL));
|
||||
|
||||
props.setProperty("request.session.exists", "false");
|
||||
HttpSession httpSession = req.getSession(false);
|
||||
if (httpSession != null)
|
||||
{
|
||||
props.setProperty("request.session.exists", "true");
|
||||
List<String> attrNames = Collections.list(httpSession.getAttributeNames());
|
||||
attrNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
Object attrVal = httpSession.getAttribute(name);
|
||||
props.setProperty("session[" + name + "]", Objects.toString(attrVal, NULL));
|
||||
});
|
||||
}
|
||||
|
||||
addAttributes(props, "req", req::getAttributeNames, req::getAttribute);
|
||||
addAttributes(props, "context",
|
||||
() -> getServletContext().getAttributeNames(),
|
||||
(name) -> getServletContext().getAttribute(name));
|
||||
|
||||
List<String> headerNames = Collections.list(req.getHeaderNames());
|
||||
headerNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
String headerVal = req.getHeader(name);
|
||||
props.setProperty("header[" + name + "]", Objects.toString(headerVal, NULL));
|
||||
});
|
||||
|
||||
if (dispatchPlan != null)
|
||||
{
|
||||
int eventCount = dispatchPlan.getEvents().size();
|
||||
props.setProperty("dispatchPlan.events.count", Integer.toString(dispatchPlan.getEvents().size()));
|
||||
for (int i = 0; i < eventCount; i++)
|
||||
{
|
||||
props.setProperty("dispatchPlan.event[" + i + "]", dispatchPlan.getEvents().get(i));
|
||||
}
|
||||
}
|
||||
|
||||
resp.setStatus(HttpServletResponse.SC_OK);
|
||||
resp.setCharacterEncoding("utf-8");
|
||||
resp.setContentType("text/x-java-properties");
|
||||
PrintWriter out = resp.getWriter();
|
||||
props.store(out, "From " + this.getClass().getName());
|
||||
}
|
||||
|
||||
private void addAttributes(Properties props,
|
||||
String prefix,
|
||||
Supplier<Enumeration<String>> getNamesSupplier,
|
||||
Function<String, Object> getAttributeFunction)
|
||||
{
|
||||
List<String> attrNames = Collections.list(getNamesSupplier.get());
|
||||
attrNames
|
||||
.forEach((name) ->
|
||||
{
|
||||
Object attrVal = getAttributeFunction.apply(name);
|
||||
props.setProperty(prefix + ".attr[" + name + "]", Objects.toString(attrVal, NULL));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee9;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public class ForwardServlet extends HttpServlet
|
||||
{
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||
{
|
||||
String forwardTo = req.getHeader("X-ForwardTo");
|
||||
Objects.requireNonNull(forwardTo);
|
||||
RequestDispatcher requestDispatcher = req.getRequestDispatcher(forwardTo);
|
||||
requestDispatcher.forward(req, resp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.ccd.ee9;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* A servlet filter that will harshly change the return value of
|
||||
* {@link HttpServletRequest#getRequestURI()} to something that does
|
||||
* not satisfy the Servlet spec URI invariant {@code request URI == context path + servlet path + path info}
|
||||
*/
|
||||
public class InternalRequestURIFilter implements Filter
|
||||
{
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
|
||||
{
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
|
||||
InternalRequestURIWrapper requestURIWrapper = new InternalRequestURIWrapper(httpServletRequest);
|
||||
chain.doFilter(requestURIWrapper, httpServletResponse);
|
||||
}
|
||||
|
||||
private static class InternalRequestURIWrapper extends HttpServletRequestWrapper
|
||||
{
|
||||
public InternalRequestURIWrapper(HttpServletRequest request)
|
||||
{
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestURI()
|
||||
{
|
||||
return "/internal/";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
|
||||
version="5.0">
|
||||
|
||||
<display-name>ccd-ee9</display-name>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>ccd</servlet-name>
|
||||
<servlet-class>org.eclipse.jetty.tests.ccd.ee9.CCDServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>dump</servlet-name>
|
||||
<servlet-class>org.eclipse.jetty.tests.ccd.ee9.DumpServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>ccd</servlet-name>
|
||||
<url-pattern>/redispatch/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>dump</servlet-name>
|
||||
<url-pattern>/dump/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
</web-app>
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>test-cross-context-dispatch</artifactId>
|
||||
<version>12.0.11-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>ccd-tests</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Tests :: Cross Context Dispatch :: Tests</name>
|
||||
|
||||
<properties>
|
||||
|
||||
<!-- <junit.jupiter.execution.parallel.enabled>false</junit.jupiter.execution.parallel.enabled>-->
|
||||
<junit.jupiter.execution.parallel.config.fixed.parallelism>1</junit.jupiter.execution.parallel.config.fixed.parallelism>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-bom</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests.ccd</groupId>
|
||||
<artifactId>ccd-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-client</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-deploy</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-slf4j-impl</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.tests</groupId>
|
||||
<artifactId>jetty-testers</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<mavenRepoPath>${session.repositorySession.localRepository.basedir.absolutePath}</mavenRepoPath>
|
||||
<jettyVersion>${project.version}</jettyVersion>
|
||||
<distribution.debug.port>$(distribution.debug.port}</distribution.debug.port>
|
||||
<home.start.timeout>${home.start.timeout}</home.start.timeout>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,216 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.redispatch;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.tests.testers.JettyHomeTester;
|
||||
import org.eclipse.jetty.tests.testers.Tester;
|
||||
import org.eclipse.jetty.toolchain.test.FS;
|
||||
import org.eclipse.jetty.toolchain.test.MavenPaths;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public abstract class AbstractRedispatchTest
|
||||
{
|
||||
protected static final int START_TIMEOUT = Integer.getInteger("home.start.timeout", 30);
|
||||
protected static final List<String> ENVIRONMENTS = List.of("ee8", "ee9", "ee10");
|
||||
|
||||
static String toResponseDetails(ContentResponse response)
|
||||
{
|
||||
return new ResponseDetails(response).get();
|
||||
}
|
||||
|
||||
static class InitializedJettyBase
|
||||
{
|
||||
public Path jettyBase;
|
||||
public JettyHomeTester distribution;
|
||||
public int httpPort;
|
||||
|
||||
public InitializedJettyBase(TestInfo testInfo) throws Exception
|
||||
{
|
||||
Path testsDir = MavenPaths.targetTests();
|
||||
String cleanBaseName = toCleanDirectoryName(testInfo);
|
||||
jettyBase = testsDir.resolve(cleanBaseName);
|
||||
FS.ensureEmpty(jettyBase);
|
||||
String jettyVersion = System.getProperty("jettyVersion");
|
||||
distribution = JettyHomeTester.Builder.newInstance()
|
||||
.jettyVersion(jettyVersion)
|
||||
.jettyBase(jettyBase)
|
||||
.build();
|
||||
|
||||
httpPort = Tester.freePort();
|
||||
|
||||
List<String> configList = new ArrayList<>();
|
||||
configList.add("--add-modules=http,resources");
|
||||
for (String env : ENVIRONMENTS)
|
||||
{
|
||||
configList.add("--add-modules=" + env + "-deploy," + env + "-webapp");
|
||||
}
|
||||
|
||||
try (JettyHomeTester.Run runConfig = distribution.start(configList))
|
||||
{
|
||||
assertTrue(runConfig.awaitFor(START_TIMEOUT, TimeUnit.SECONDS));
|
||||
assertEquals(0, runConfig.getExitValue());
|
||||
|
||||
Path libDir = jettyBase.resolve("lib");
|
||||
FS.ensureDirExists(libDir);
|
||||
Path etcDir = jettyBase.resolve("etc");
|
||||
FS.ensureDirExists(etcDir);
|
||||
Path modulesDir = jettyBase.resolve("modules");
|
||||
FS.ensureDirExists(modulesDir);
|
||||
Path startDir = jettyBase.resolve("start.d");
|
||||
FS.ensureDirExists(startDir);
|
||||
|
||||
// Configure the DispatchPlanHandler
|
||||
Path ccdJar = distribution.resolveArtifact("org.eclipse.jetty.tests.ccd:ccd-common:jar:" + jettyVersion);
|
||||
Files.copy(ccdJar, libDir.resolve(ccdJar.getFileName()));
|
||||
|
||||
Path installDispatchPlanXml = MavenPaths.findTestResourceFile("install-ccd-handler.xml");
|
||||
Files.copy(installDispatchPlanXml, etcDir.resolve(installDispatchPlanXml.getFileName()));
|
||||
|
||||
String module = """
|
||||
[depend]
|
||||
server
|
||||
|
||||
[lib]
|
||||
lib/jetty-util-ajax-$J.jar
|
||||
lib/ccd-common-$J.jar
|
||||
|
||||
[xml]
|
||||
etc/install-ccd-handler.xml
|
||||
|
||||
[ini]
|
||||
jetty.webapp.addProtectedClasses+=,org.eclipse.jetty.tests.ccd.common.
|
||||
jetty.webapp.addHiddenClasses+=,-org.eclipse.jetty.tests.ccd.common.
|
||||
""".replace("$J", jettyVersion);
|
||||
Files.writeString(modulesDir.resolve("ccd.mod"), module, StandardCharsets.UTF_8);
|
||||
|
||||
// -- Error Handler
|
||||
Path errorHandlerXml = MavenPaths.findTestResourceFile("error-handler.xml");
|
||||
Files.copy(errorHandlerXml, etcDir.resolve("error-handler.xml"));
|
||||
String errorHandlerIni = """
|
||||
etc/error-handler.xml
|
||||
""";
|
||||
Files.writeString(startDir.resolve("error-handler.ini"), errorHandlerIni);
|
||||
|
||||
// -- Plans Dir
|
||||
Path plansDir = MavenPaths.findTestResourceDir("plans");
|
||||
|
||||
Path ccdIni = startDir.resolve("ccd.ini");
|
||||
String ini = """
|
||||
--module=ccd
|
||||
ccd-plans-dir=$D
|
||||
""".replace("$D", plansDir.toString());
|
||||
Files.writeString(ccdIni, ini, StandardCharsets.UTF_8);
|
||||
|
||||
// -- Add the test wars
|
||||
for (String env : ENVIRONMENTS)
|
||||
{
|
||||
Path war = distribution.resolveArtifact("org.eclipse.jetty.tests.ccd:ccd-" + env + "-webapp:war:" + jettyVersion);
|
||||
distribution.installWar(war, "ccd-" + env);
|
||||
Path warProperties = jettyBase.resolve("webapps/ccd-" + env + ".properties");
|
||||
Files.writeString(warProperties, "environment: " + env, StandardCharsets.UTF_8);
|
||||
|
||||
Path webappXmlSrc = MavenPaths.findTestResourceFile("webapp-xmls/ccd-" + env + ".xml");
|
||||
Path webappXmlDest = jettyBase.resolve("webapps/ccd-" + env + ".xml");
|
||||
Files.copy(webappXmlSrc, webappXmlDest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a name that can be used as a Jetty Base home directory in a safe way.
|
||||
*
|
||||
* Note: unlike the WorkDir object, this strips out {@code [} and {@code ]} characters
|
||||
* and also makes any non-alpha-numeric character just {@code _}, which results in
|
||||
* a happy {@code ${jetty.base}} and {@code start.jar}.
|
||||
*
|
||||
* Failure to use this method can result in start.jar behaving in unintended ways
|
||||
* when it goes through the Java -> Runtime.exec -> OS behaviors.
|
||||
*
|
||||
* This change also makes the created directory named {@code target/tests/<method-name>.<display-name>}
|
||||
* live and suitable for execution via a console without accidental shell interpretation of special
|
||||
* characters in the directory name (that can result from characters like "[]" used in a directory name)
|
||||
*
|
||||
* @param testInfo the TestInfo to use to generate directory name from.
|
||||
* @return the safe to use directory name.
|
||||
*/
|
||||
public static String toCleanDirectoryName(TestInfo testInfo)
|
||||
{
|
||||
StringBuilder name = new StringBuilder();
|
||||
if (testInfo.getTestMethod().isPresent())
|
||||
{
|
||||
name.append(testInfo.getTestMethod().get().getName());
|
||||
name.append(".");
|
||||
}
|
||||
for (char c: testInfo.getDisplayName().toCharArray())
|
||||
{
|
||||
if (Character.isLetterOrDigit(c) || c == '.' || c == '-')
|
||||
name.append(c);
|
||||
else if (c != '[' && c != ']')
|
||||
name.append("_");
|
||||
}
|
||||
return name.toString();
|
||||
}
|
||||
}
|
||||
|
||||
protected HttpClient client;
|
||||
|
||||
@BeforeEach
|
||||
public void startClient() throws Exception
|
||||
{
|
||||
client = new HttpClient();
|
||||
client.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void stopClient()
|
||||
{
|
||||
LifeCycle.stop(client);
|
||||
}
|
||||
|
||||
public static void dumpProperties(Properties props)
|
||||
{
|
||||
props.stringPropertyNames().stream()
|
||||
.sorted()
|
||||
.forEach((name) ->
|
||||
System.out.printf(" %s=%s%n", name, props.getProperty(name)));
|
||||
}
|
||||
|
||||
public static void assertProperty(Properties props, String name, Matcher<String> valueMatcher)
|
||||
{
|
||||
assertThat("Property [" + name + "]", props.getProperty(name), valueMatcher);
|
||||
}
|
||||
|
||||
public static void assertProperty(String id, Properties props, String name, Matcher<String> valueMatcher)
|
||||
{
|
||||
assertThat("id[" + id + "] property[" + name + "]", props.getProperty(name), valueMatcher);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.redispatch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.tests.ccd.common.DispatchPlan;
|
||||
import org.eclipse.jetty.tests.ccd.common.HttpRequest;
|
||||
import org.eclipse.jetty.tests.ccd.common.Property;
|
||||
import org.eclipse.jetty.tests.testers.JettyHomeTester;
|
||||
import org.eclipse.jetty.toolchain.test.MavenPaths;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class RedispatchPlansTests extends AbstractRedispatchTest
|
||||
{
|
||||
private InitializedJettyBase jettyBase;
|
||||
private JettyHomeTester.Run runStart;
|
||||
|
||||
@BeforeEach
|
||||
public void startJettyBase(TestInfo testInfo) throws Exception
|
||||
{
|
||||
jettyBase = new InitializedJettyBase(testInfo);
|
||||
|
||||
String[] argsStart = {
|
||||
"jetty.http.port=" + jettyBase.httpPort
|
||||
};
|
||||
|
||||
runStart = jettyBase.distribution.start(argsStart);
|
||||
|
||||
assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void stopJettyBase()
|
||||
{
|
||||
if (runStart.getProcess().isAlive())
|
||||
runStart.close();
|
||||
}
|
||||
|
||||
public static Stream<Arguments> dispatchPlans() throws IOException
|
||||
{
|
||||
List<Arguments> plans = new ArrayList<>();
|
||||
|
||||
List<String> disabledTests = new ArrayList<>();
|
||||
disabledTests.add("ee10-session-ee8-ee9-ee8.txt"); // causes an ISE
|
||||
|
||||
Path testPlansDir = MavenPaths.findTestResourceDir("plans");
|
||||
try (Stream<Path> plansStream = Files.list(testPlansDir))
|
||||
{
|
||||
List<Path> testPlans = plansStream
|
||||
.filter(Files::isRegularFile)
|
||||
.filter((file) -> file.getFileName().toString().endsWith(".txt"))
|
||||
.filter((file) -> !disabledTests.contains(file.getFileName().toString()))
|
||||
.toList();
|
||||
|
||||
for (Path plansText : testPlans)
|
||||
{
|
||||
plans.add(Arguments.of(DispatchPlan.read(plansText)));
|
||||
}
|
||||
}
|
||||
|
||||
return plans.stream();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("dispatchPlans")
|
||||
public void testRedispatch(DispatchPlan dispatchPlan) throws Exception
|
||||
{
|
||||
HttpRequest requestStep = dispatchPlan.getRequestStep();
|
||||
assertNotNull(requestStep);
|
||||
ContentResponse response = client.newRequest("localhost", jettyBase.httpPort)
|
||||
.method(requestStep.getMethod())
|
||||
.headers((headers) ->
|
||||
headers.put("X-DispatchPlan", dispatchPlan.id()))
|
||||
.path(requestStep.getRequestPath())
|
||||
.send();
|
||||
String responseDetails = toResponseDetails(response);
|
||||
assertThat(responseDetails, response.getStatus(), is(HttpStatus.OK_200));
|
||||
|
||||
Properties responseProps = new Properties();
|
||||
try (StringReader stringReader = new StringReader(response.getContentAsString()))
|
||||
{
|
||||
responseProps.load(stringReader);
|
||||
}
|
||||
|
||||
dumpProperties(responseProps);
|
||||
|
||||
int expectedEventCount = dispatchPlan.getExpectedEvents().size();
|
||||
assertThat(responseProps.getProperty("dispatchPlan.events.count"), is(Integer.toString(expectedEventCount)));
|
||||
for (int i = 0; i < expectedEventCount; i++)
|
||||
{
|
||||
assertThat("id[" + dispatchPlan.id() + "] event[" + i + "]", responseProps.getProperty("dispatchPlan.event[" + i + "]"), is(dispatchPlan.getExpectedEvents().get(i)));
|
||||
}
|
||||
|
||||
if (dispatchPlan.getExpectedContentType() != null)
|
||||
{
|
||||
assertThat("Expected ContentType", response.getHeaders().get(HttpHeader.CONTENT_TYPE), is(dispatchPlan.getExpectedContentType()));
|
||||
}
|
||||
|
||||
for (Property expectedProperty : dispatchPlan.getExpectedProperties())
|
||||
{
|
||||
assertProperty(dispatchPlan.id(), responseProps, expectedProperty.getName(), is(expectedProperty.getValue()));
|
||||
}
|
||||
|
||||
// Ensure that all seen session ids are the same.
|
||||
if (dispatchPlan.isExpectedSessionIds())
|
||||
{
|
||||
// Verify that Request Attributes for Session.id are in agreement
|
||||
List<String> attrNames = responseProps.keySet().stream()
|
||||
.map(Object::toString)
|
||||
.filter((name) -> name.startsWith("req.attr[session["))
|
||||
.toList();
|
||||
|
||||
if (attrNames.size() > 1)
|
||||
{
|
||||
String expectedId = responseProps.getProperty(attrNames.get(0));
|
||||
for (String name : attrNames)
|
||||
{
|
||||
assertEquals(expectedId, responseProps.getProperty(name));
|
||||
}
|
||||
}
|
||||
|
||||
// stop the forked running server.
|
||||
// we need to verify the session behaviors, and can only do that on a stopped server.
|
||||
runStart.close();
|
||||
|
||||
// Verify that Context Attributes for Session.id are in agreement
|
||||
// And that all ids have had their .commit() and .release() methods called.
|
||||
Path sessionLog = jettyBase.jettyBase.resolve("work/session.log");
|
||||
assertTrue(Files.isRegularFile(sessionLog), "Missing " + sessionLog);
|
||||
|
||||
List<String> logEntries = Files.readAllLines(sessionLog);
|
||||
List<String> newSessions = logEntries.stream()
|
||||
.filter(line -> line.contains("SessionCache.event.newSession()"))
|
||||
.map(line -> line.substring(line.indexOf("=") + 1))
|
||||
.toList();
|
||||
// we should have the commit() and release() for each new Session.
|
||||
for (String sessionId : newSessions)
|
||||
{
|
||||
assertThat(logEntries, hasItem("SessionCache.event.commit()=" + sessionId));
|
||||
assertThat(logEntries, hasItem("SessionCache.event.release()=" + sessionId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.redispatch;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.tests.testers.JettyHomeTester;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class RedispatchTests extends AbstractRedispatchTest
|
||||
{
|
||||
private JettyHomeTester.Run runStart;
|
||||
|
||||
@AfterEach
|
||||
public void stopRun()
|
||||
{
|
||||
runStart.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test ee8 behavior if an HttpServletRequestWrapper messes with the
|
||||
* {@code getRequestURI()} method.
|
||||
* see {@code org.eclipse.jetty.tests.ccd.ee8.InternalRequestURIFilter}
|
||||
*/
|
||||
@Test
|
||||
public void testEe8FilterWithAwkwardRequestURI(TestInfo testInfo) throws Exception
|
||||
{
|
||||
InitializedJettyBase jettyBase = new InitializedJettyBase(testInfo);
|
||||
|
||||
// Now add the filter to the webapp xml init
|
||||
String xml = """
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
|
||||
<Configure class="org.eclipse.jetty.ee8.webapp.WebAppContext">
|
||||
<Set name="contextPath">/ccd-ee8</Set>
|
||||
<Set name="war"><Property name="jetty.webapps" default="." />/ccd-ee8</Set>
|
||||
<Set name="crossContextDispatchSupported">true</Set>
|
||||
<Call name="addFilter">
|
||||
<Arg type="String">org.eclipse.jetty.tests.ccd.ee8.InternalRequestURIFilter</Arg>
|
||||
<Arg type="String">/*</Arg>
|
||||
<Arg>
|
||||
<Call class="java.util.EnumSet" name="of">
|
||||
<Arg><Get class="javax.servlet.DispatcherType" name="REQUEST"/></Arg>
|
||||
<Arg><Get class="javax.servlet.DispatcherType" name="FORWARD"/></Arg>
|
||||
<Arg><Get class="javax.servlet.DispatcherType" name="INCLUDE"/></Arg>
|
||||
</Call>
|
||||
</Arg>
|
||||
</Call>
|
||||
</Configure>
|
||||
""";
|
||||
// Note: the InternalRequestURIFilter messes with the requestURI
|
||||
Files.writeString(jettyBase.jettyBase.resolve("webapps/ccd-ee8.xml"), xml, StandardCharsets.UTF_8);
|
||||
|
||||
// Start Jetty instance
|
||||
String[] argsStart = {
|
||||
"jetty.http.port=" + jettyBase.httpPort
|
||||
};
|
||||
|
||||
runStart = jettyBase.distribution.start(argsStart);
|
||||
assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
|
||||
|
||||
ContentResponse response = client.newRequest("localhost", jettyBase.httpPort)
|
||||
.method(HttpMethod.GET)
|
||||
.headers((headers) ->
|
||||
headers.put("X-ForwardTo", "/dump/ee8")
|
||||
)
|
||||
.path("/ccd-ee8/forwardto/ee8")
|
||||
.send();
|
||||
|
||||
String responseDetails = toResponseDetails(response);
|
||||
assertThat(responseDetails, response.getStatus(), is(HttpStatus.OK_200));
|
||||
|
||||
Properties responseProps = new Properties();
|
||||
try (StringReader stringReader = new StringReader(response.getContentAsString()))
|
||||
{
|
||||
responseProps.load(stringReader);
|
||||
}
|
||||
|
||||
dumpProperties(responseProps);
|
||||
|
||||
assertProperty(responseProps, "request.dispatcherType", is("FORWARD"));
|
||||
assertProperty(responseProps, "request.requestURI", is("/internal/")); // the key change to look for
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.tests.redispatch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
|
||||
class ResponseDetails implements Supplier<String>
|
||||
{
|
||||
private final ContentResponse response;
|
||||
|
||||
public ResponseDetails(ContentResponse response)
|
||||
{
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get()
|
||||
{
|
||||
try (StringWriter str = new StringWriter();
|
||||
PrintWriter out = new PrintWriter(str))
|
||||
{
|
||||
out.println(response.toString());
|
||||
out.println(response.getHeaders().toString());
|
||||
out.println(response.getContentAsString());
|
||||
out.flush();
|
||||
return str.toString();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException("Unable to produce Response details", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?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">
|
||||
<Set name="errorHandler">
|
||||
<New class="org.eclipse.jetty.server.handler.ErrorHandler">
|
||||
<Set name="showStacks">true</Set>
|
||||
</New>
|
||||
</Set>
|
||||
</Configure>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure_10_0.dtd">
|
||||
|
||||
<Configure id="Server" class="org.eclipse.jetty.server.Server">
|
||||
<Call name="insertHandler">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.tests.ccd.common.DispatchPlanHandler">
|
||||
<Set name="plansDir"><Property name="ccd-plans-dir"/></Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
</Configure>
|
|
@ -0,0 +1,12 @@
|
|||
REQUEST|GET|/ccd-ee10/redispatch/ee10
|
||||
STEP|CONTEXT_FORWARD|/ccd-ee10|/dump/ee10
|
||||
EXPECTED_EVENT|Initial plan: context-ee10-forward-dump.txt
|
||||
EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.DumpServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee10/dump/ee10
|
||||
EXPECTED_PROP|request.dispatcherType|FORWARD
|
||||
EXPECTED_PROP|request.requestURI|/ccd-ee10/dump/ee10
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.context_path]|/ccd-ee10
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.path_info]|/ee10
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.request_uri]|/ccd-ee10/redispatch/ee10
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.servlet_path]|/redispatch
|
|
@ -0,0 +1,12 @@
|
|||
REQUEST|GET|/ccd-ee8/redispatch/ee8
|
||||
STEP|CONTEXT_FORWARD|/ccd-ee8|/dump/ee8
|
||||
EXPECTED_EVENT|Initial plan: context-ee8-forward-dump.txt
|
||||
EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.DumpServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/dump/ee8
|
||||
EXPECTED_PROP|request.dispatcherType|FORWARD
|
||||
EXPECTED_PROP|request.requestURI|/ccd-ee8/dump/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.context_path]|/ccd-ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.path_info]|/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.servlet_path]|/redispatch
|
|
@ -0,0 +1,14 @@
|
|||
REQUEST|GET|/ccd-ee8/redispatch/ee8
|
||||
STEP|CONTEXT_INCLUDE|/ccd-ee8|/dump/ee8
|
||||
EXPECTED_EVENT|Initial plan: context-ee8-include-dump.txt
|
||||
EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_PROP|request.dispatcherType|INCLUDE
|
||||
EXPECTED_PROP|request.contextPath|/ccd-ee8
|
||||
EXPECTED_PROP|request.pathInfo|/ee8
|
||||
EXPECTED_PROP|request.requestURI|/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.context_path]|/ccd-ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.path_info]|/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.request_uri]|/ccd-ee8/dump/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.servlet_path]|/dump
|
|
@ -0,0 +1,30 @@
|
|||
REQUEST|GET|/ccd-ee10/redispatch/ee10
|
||||
STEP|SET_HTTP_SESSION_ATTRIBUTE|test-name-10|test-value-ee10
|
||||
STEP|CONTEXT_FORWARD|/ccd-ee8|/redispatch/ee8
|
||||
STEP|CONTEXT_FORWARD|/ccd-ee9|/redispatch/ee9
|
||||
STEP|REQUEST_INCLUDE|/dump/ee9
|
||||
EXPECTED_EVENT|Initial plan: ee10-forward-to-ee8-include-ee9-dump.txt
|
||||
EXPECTED_EVENT|DispatchPlanHandler.handle() method=GET path-query=/ccd-ee10/redispatch/ee10
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee10.CCDServlet.service() dispatcherType=REQUEST method=GET requestUri=/ccd-ee10/redispatch/ee10
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee8.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.CCDServlet.service() dispatcherType=FORWARD method=GET requestUri=/ccd-ee9/redispatch/ee9
|
||||
EXPECTED_EVENT|org.eclipse.jetty.tests.ccd.ee9.DumpServlet.service() dispatcherType=INCLUDE method=GET requestUri=/ccd-ee9/redispatch/ee9
|
||||
EXPECTED_PROP|request.dispatcherType|INCLUDE
|
||||
EXPECTED_PROP|request.requestURI|/ccd-ee9/redispatch/ee9
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.context_path]|/ccd-ee8
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.path_info]|/ee8
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.forward.servlet_path]|/redispatch
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.include.context_path]/ccd-ee9
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.include.path_info]|/ee9
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.include.request_uri]|/ccd-ee9/dump/ee9
|
||||
EXPECTED_PROP|req.attr[jakarta.servlet.include.servlet_path]/dump
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.context_path]|<null>
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.path_info]|<null>
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.request_uri]|<null>
|
||||
EXPECTED_PROP|req.attr[javax.servlet.include.servlet_path]|<null>
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.context_path]|/ccd-ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.path_info]|/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.request_uri]|/ccd-ee8/redispatch/ee8
|
||||
EXPECTED_PROP|req.attr[javax.servlet.forward.servlet_path]|/redispatch
|
||||
EXPECTED_SESSION_IDS|true
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue