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
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
README.md
12
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'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.
|
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
|
- https://projects.eclipse.org/projects/rt.jetty
|
||||||
|
|
||||||
## Webapp Example
|
## Webapp Example
|
||||||
|
@ -53,19 +53,19 @@ $ cd jetty.project
|
||||||
$ mvn -Pfast clean install # fast build bypasses tests and other checks
|
$ 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
|
# 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 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
|
# Commercial Support
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.eclipse.jetty.http.HttpCookie;
|
||||||
import org.eclipse.jetty.http.HttpFields;
|
import org.eclipse.jetty.http.HttpFields;
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.eclipse.jetty.http.HttpURI;
|
||||||
import org.eclipse.jetty.http.Trailers;
|
import org.eclipse.jetty.http.Trailers;
|
||||||
import org.eclipse.jetty.io.Content;
|
import org.eclipse.jetty.io.Content;
|
||||||
import org.eclipse.jetty.server.Context;
|
import org.eclipse.jetty.server.Context;
|
||||||
|
@ -60,10 +61,11 @@ public class ServletToHandlerDocs
|
||||||
// - servletRequest.getProtocol();
|
// - servletRequest.getProtocol();
|
||||||
String protocol = request.getConnectionMetaData().getProtocol();
|
String protocol = request.getConnectionMetaData().getProtocol();
|
||||||
|
|
||||||
// Gets the full request URI.
|
// Gets the request URL.
|
||||||
// Replaces:
|
// Replaces:
|
||||||
// - servletRequest.getRequestURL();
|
// - 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.
|
// Gets the request context.
|
||||||
// Replaces:
|
// Replaces:
|
||||||
|
|
|
@ -47,37 +47,46 @@ Use an editor to create the file `src/main/java/org/example/HelloWorld.java` wit
|
||||||
----
|
----
|
||||||
package org.example;
|
package org.example;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import org.eclipse.jetty.io.Content;
|
||||||
import jakarta.servlet.ServletException;
|
import org.eclipse.jetty.server.Handler;
|
||||||
import java.io.IOException;
|
|
||||||
import org.eclipse.jetty.server.Server;
|
|
||||||
import org.eclipse.jetty.server.Request;
|
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,
|
@Override
|
||||||
Request baseRequest,
|
public boolean handle(Request request, Response response, Callback callback)
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response)
|
|
||||||
throws IOException, ServletException
|
|
||||||
{
|
{
|
||||||
response.setContentType("text/html;charset=utf-8");
|
response.setStatus(200);
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html; charset=UTF-8");
|
||||||
baseRequest.setHandled(true);
|
|
||||||
response.getWriter().println("<h1>Hello World</h1>");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception
|
// Write a Hello World response.
|
||||||
{
|
Content.Sink.write(response, true, """
|
||||||
Server server = new Server(8080);
|
<!DOCTYPE html>
|
||||||
server.setHandler(new HelloWorld());
|
<html>
|
||||||
|
<head>
|
||||||
server.start();
|
<title>Jetty Hello World Handler</title>
|
||||||
server.join();
|
</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]]
|
[[creating-embedded-pom-descriptor]]
|
||||||
|
@ -116,7 +125,6 @@ Use an editor to create the file `pom.xml` in the `JettyMavenHelloWorld` directo
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.codehaus.mojo</groupId>
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
<artifactId>exec-maven-plugin</artifactId>
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
<version>1.1</version>
|
|
||||||
<executions>
|
<executions>
|
||||||
<execution><goals><goal>java</goal></goals></execution>
|
<execution><goals><goal>java</goal></goals></execution>
|
||||||
</executions>
|
</executions>
|
||||||
|
|
|
@ -208,7 +208,7 @@ public abstract class HttpSender
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
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
|
// Mark atomically the request as completed, with respect
|
||||||
// to concurrency between request success and request failure.
|
// to concurrency between request success and request failure.
|
||||||
|
|
|
@ -21,6 +21,7 @@ import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -243,6 +244,7 @@ public class ContextProvider extends ScanningAppProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property.
|
* This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property.
|
||||||
|
*
|
||||||
* @param configurations The configuration class names as a comma separated list
|
* @param configurations The configuration class names as a comma separated list
|
||||||
*/
|
*/
|
||||||
public void setConfigurationClasses(String configurations)
|
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.
|
* This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property.
|
||||||
|
*
|
||||||
* @param configurations The configuration class names.
|
* @param configurations The configuration class names.
|
||||||
*/
|
*/
|
||||||
public void setConfigurationClasses(String[] configurations)
|
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.
|
* This is equivalent to getting the {@link Deployable#CONFIGURATION_CLASSES} property.
|
||||||
|
*
|
||||||
* @return The configuration class names.
|
* @return The configuration class names.
|
||||||
*/
|
*/
|
||||||
@ManagedAttribute("configuration classes for webapps to be processed through")
|
@ManagedAttribute("configuration classes for webapps to be processed through")
|
||||||
|
@ -341,32 +344,48 @@ public class ContextProvider extends ScanningAppProvider
|
||||||
|
|
||||||
// prepare properties
|
// prepare properties
|
||||||
Map<String, String> properties = new HashMap<>();
|
Map<String, String> properties = new HashMap<>();
|
||||||
|
|
||||||
|
//add in properties from start mechanism
|
||||||
properties.putAll(getProperties());
|
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());
|
properties.putAll(app.getProperties());
|
||||||
|
|
||||||
// Handle a context XML file
|
// Handle a context XML file
|
||||||
if (FileID.isXml(path))
|
if (FileID.isXml(path))
|
||||||
{
|
{
|
||||||
XmlConfiguration xmlc = new XmlConfiguration(ResourceFactory.of(this).newResource(path), null, properties)
|
context = applyXml(context, path, env, 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();
|
|
||||||
|
|
||||||
// Look for the contextHandler itself
|
// Look for the contextHandler itself
|
||||||
ContextHandler contextHandler = null;
|
ContextHandler contextHandler = null;
|
||||||
|
@ -382,27 +401,33 @@ public class ContextProvider extends ScanningAppProvider
|
||||||
throw new IllegalStateException("Unknown context type of " + context);
|
throw new IllegalStateException("Unknown context type of " + context);
|
||||||
|
|
||||||
// Set the classloader if we have a coreContextClassLoader
|
// Set the classloader if we have a coreContextClassLoader
|
||||||
|
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null;
|
||||||
if (coreContextClassLoader != null)
|
if (coreContextClassLoader != null)
|
||||||
contextHandler.setClassLoader(coreContextClassLoader);
|
contextHandler.setClassLoader(coreContextClassLoader);
|
||||||
|
|
||||||
return contextHandler;
|
return contextHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise it must be a directory or an archive
|
// Otherwise it must be a directory or an archive
|
||||||
else if (!Files.isDirectory(path) && !FileID.isWebArchive(path))
|
else if (!Files.isDirectory(path) && !FileID.isWebArchive(path))
|
||||||
{
|
{
|
||||||
throw new IllegalStateException("unable to create ContextHandler for " + app);
|
throw new IllegalStateException("unable to create ContextHandler for " + app);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the web application
|
// Build the web application if necessary
|
||||||
String contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass");
|
if (context == null)
|
||||||
|
{
|
||||||
|
contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass");
|
||||||
if (StringUtil.isBlank(contextHandlerClassName))
|
if (StringUtil.isBlank(contextHandlerClassName))
|
||||||
throw new IllegalStateException("No ContextHandler classname for " + app);
|
throw new IllegalStateException("No ContextHandler classname for " + app);
|
||||||
Class<?> contextHandlerClass = Loader.loadClass(contextHandlerClassName);
|
Class<?> contextHandlerClass = Loader.loadClass(contextHandlerClassName);
|
||||||
if (contextHandlerClass == null)
|
if (contextHandlerClass == null)
|
||||||
throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app);
|
throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app);
|
||||||
|
|
||||||
Object context = contextHandlerClass.getDeclaredConstructor().newInstance();
|
context = contextHandlerClass.getDeclaredConstructor().newInstance();
|
||||||
properties.put(Deployable.WAR, path.toString());
|
properties.put(Deployable.WAR, path.toString());
|
||||||
|
}
|
||||||
|
|
||||||
return initializeContextHandler(context, path, properties);
|
return initializeContextHandler(context, path, properties);
|
||||||
}
|
}
|
||||||
finally
|
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
|
protected ClassLoader findCoreContextClassLoader(Path path) throws IOException
|
||||||
{
|
{
|
||||||
Path webapps = path.getParent();
|
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.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.HashMap;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.LinkedBlockingDeque;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.deploy.AppProvider;
|
import org.eclipse.jetty.deploy.BarContextHandler;
|
||||||
import org.eclipse.jetty.deploy.DeploymentManager;
|
|
||||||
import org.eclipse.jetty.deploy.test.XmlConfiguredJetty;
|
import org.eclipse.jetty.deploy.test.XmlConfiguredJetty;
|
||||||
import org.eclipse.jetty.server.Handler;
|
import org.eclipse.jetty.server.Deployable;
|
||||||
import org.eclipse.jetty.server.Server;
|
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||||
import org.eclipse.jetty.toolchain.test.FS;
|
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.WorkDir;
|
||||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
|
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.component.LifeCycle;
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.awaitility.Awaitility.await;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
@ -77,7 +69,10 @@ public class ContextProviderStartupTest
|
||||||
|
|
||||||
// Should not throw an Exception
|
// Should not throw an Exception
|
||||||
jetty.load();
|
jetty.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startJetty() throws Exception
|
||||||
|
{
|
||||||
// Start it
|
// Start it
|
||||||
jetty.start();
|
jetty.start();
|
||||||
}
|
}
|
||||||
|
@ -89,9 +84,47 @@ public class ContextProviderStartupTest
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testStartupContext()
|
public void testStartupContext() throws Exception
|
||||||
{
|
{
|
||||||
|
startJetty();
|
||||||
|
|
||||||
// Check Server for Handlers
|
// Check Server for Handlers
|
||||||
jetty.assertContextHandlerExists("/bar");
|
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}.
|
* 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 attributes The {@link Attributes} instance to add classes to
|
||||||
* @param patterns the patterns to use
|
* @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)
|
public static void addHiddenClasses(Attributes attributes, String... patterns)
|
||||||
{
|
{
|
||||||
if (patterns != null && patterns.length > 0)
|
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
|
* <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>
|
* 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
|
* <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
|
* 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>
|
* 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>
|
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>Returns whether this instance contains the given field name.</p>
|
||||||
* <p>The comparison of field name is case-insensitive via
|
* <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
|
* @param name the case-insensitive field name to search for
|
||||||
* @return whether this instance contains the given field name
|
* @return whether this instance contains the given field name
|
||||||
|
* @see #contains(HttpHeader)
|
||||||
*/
|
*/
|
||||||
default boolean contains(String name)
|
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,
|
* <p>Returns the encoded value of the first field with the given field name,
|
||||||
* or {@code null} if no such field is present.</p>
|
* or {@code null} if no such field is present.</p>
|
||||||
* <p>The comparison of field name is case-insensitive via
|
* <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
|
* <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>
|
* 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,
|
* @return the raw value of the first field with the given field name,
|
||||||
* or {@code null} if no such field is present
|
* or {@code null} if no such field is present
|
||||||
* @see HttpField#getValue()
|
* @see HttpField#getValue()
|
||||||
|
* @see #get(HttpHeader)
|
||||||
*/
|
*/
|
||||||
default String get(String name)
|
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>Returns a {@link Set} of the field names.</p>
|
||||||
* <p>Case-sensitivity of the field names is preserved.</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()
|
default Set<String> getFieldNamesCollection()
|
||||||
{
|
{
|
||||||
Set<HttpHeader> seenByHeader = EnumSet.noneOf(HttpHeader.class);
|
Set<HttpHeader> seenByHeader = EnumSet.noneOf(HttpHeader.class);
|
||||||
Set<String> seenByName = null;
|
Set<String> buildByName = null;
|
||||||
List<String> list = new ArrayList<>(size());
|
List<String> list = new ArrayList<>(size());
|
||||||
|
|
||||||
for (HttpField f : this)
|
for (HttpField f : this)
|
||||||
|
@ -607,9 +618,9 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
||||||
HttpHeader header = f.getHeader();
|
HttpHeader header = f.getHeader();
|
||||||
if (header == null)
|
if (header == null)
|
||||||
{
|
{
|
||||||
if (seenByName == null)
|
if (buildByName == null)
|
||||||
seenByName = new TreeSet<>(String::compareToIgnoreCase);
|
buildByName = new TreeSet<>(String::compareToIgnoreCase);
|
||||||
if (seenByName.add(f.getName()))
|
if (buildByName.add(f.getName()))
|
||||||
list.add(f.getName());
|
list.add(f.getName());
|
||||||
}
|
}
|
||||||
else if (seenByHeader.add(header))
|
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
|
// use the list to retain a rough ordering
|
||||||
return new AbstractSet<>()
|
return new AbstractSet<>()
|
||||||
{
|
{
|
||||||
|
@ -632,6 +645,14 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
|
||||||
{
|
{
|
||||||
return list.size();
|
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;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP Fields. A collection of HTTP header and or Trailer fields.
|
* An immutable implementation of {@link HttpFields}.
|
||||||
*
|
|
||||||
* <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.
|
|
||||||
*/
|
*/
|
||||||
class ImmutableHttpFields implements HttpFields
|
class ImmutableHttpFields implements HttpFields
|
||||||
{
|
{
|
||||||
|
@ -70,10 +65,9 @@ class ImmutableHttpFields implements HttpFields
|
||||||
{
|
{
|
||||||
if (this == o)
|
if (this == o)
|
||||||
return true;
|
return true;
|
||||||
if (!(o instanceof org.eclipse.jetty.http.ImmutableHttpFields))
|
if (o instanceof HttpFields httpFields)
|
||||||
|
return isEqualTo(httpFields);
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return isEqualTo((HttpFields)o);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -67,7 +67,7 @@ class MutableHttpFields implements HttpFields.Mutable
|
||||||
*/
|
*/
|
||||||
protected MutableHttpFields(HttpFields fields)
|
protected MutableHttpFields(HttpFields fields)
|
||||||
{
|
{
|
||||||
if (fields instanceof ImmutableHttpFields immutable)
|
if (fields instanceof org.eclipse.jetty.http.ImmutableHttpFields immutable)
|
||||||
{
|
{
|
||||||
_immutable = true;
|
_immutable = true;
|
||||||
_fields = immutable._fields;
|
_fields = immutable._fields;
|
||||||
|
@ -180,7 +180,7 @@ class MutableHttpFields implements HttpFields.Mutable
|
||||||
public HttpFields asImmutable()
|
public HttpFields asImmutable()
|
||||||
{
|
{
|
||||||
_immutable = true;
|
_immutable = true;
|
||||||
return new ImmutableHttpFields(_fields, _size);
|
return new org.eclipse.jetty.http.ImmutableHttpFields(_fields, _size);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyImmutable()
|
private void copyImmutable()
|
||||||
|
|
|
@ -25,6 +25,7 @@ import java.util.ListIterator;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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("eXpEcT"), is("100"));
|
assertThat(header.get("eXpEcT"), is("100"));
|
||||||
assertThat(header.get(HttpHeader.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"));
|
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("accept-charset"), is("UTF-8"));
|
assertThat(header.get("accept-charset"), is("UTF-8"));
|
||||||
assertThat(header.get(HttpHeader.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"));
|
||||||
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.get("Foo-Bar"), is("one"));
|
||||||
assertThat(header.getValuesList("foo-bar"), contains("one", "two"));
|
assertThat(header.getValuesList("foo-bar"), contains("one", "two"));
|
||||||
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
|
// 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
|
@ParameterizedTest
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.http2.server;
|
package org.eclipse.jetty.http2.server;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
@ -155,7 +156,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
|
||||||
@Override
|
@Override
|
||||||
public void onReset(Stream stream, ResetFrame frame, Callback callback)
|
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);
|
onFailure(stream, failure, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.http2.server.internal;
|
package org.eclipse.jetty.http2.server.internal;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.BiConsumer;
|
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.Connection;
|
||||||
import org.eclipse.jetty.io.Content;
|
import org.eclipse.jetty.io.Content;
|
||||||
import org.eclipse.jetty.io.EndPoint;
|
import org.eclipse.jetty.io.EndPoint;
|
||||||
|
import org.eclipse.jetty.io.EofException;
|
||||||
import org.eclipse.jetty.server.HttpChannel;
|
import org.eclipse.jetty.server.HttpChannel;
|
||||||
import org.eclipse.jetty.server.HttpStream;
|
import org.eclipse.jetty.server.HttpStream;
|
||||||
import org.eclipse.jetty.server.Request;
|
import org.eclipse.jetty.server.Request;
|
||||||
|
@ -587,7 +589,8 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
|
||||||
@Override
|
@Override
|
||||||
public Runnable onFailure(Throwable failure, Callback callback)
|
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 () ->
|
return () ->
|
||||||
{
|
{
|
||||||
if (runnable != null)
|
if (runnable != null)
|
||||||
|
|
|
@ -58,7 +58,12 @@ public class AbstractTest
|
||||||
|
|
||||||
protected void start(Handler handler) throws Exception
|
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.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||||
connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||||
prepareServer(connectionFactory);
|
prepareServer(connectionFactory);
|
||||||
|
|
|
@ -17,22 +17,33 @@ import java.io.InterruptedIOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpFields;
|
import org.eclipse.jetty.http.HttpFields;
|
||||||
import org.eclipse.jetty.http.MetaData;
|
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.Session;
|
||||||
import org.eclipse.jetty.http2.api.Stream;
|
import org.eclipse.jetty.http2.api.Stream;
|
||||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
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.Content;
|
||||||
|
import org.eclipse.jetty.io.EofException;
|
||||||
import org.eclipse.jetty.server.Handler;
|
import org.eclipse.jetty.server.Handler;
|
||||||
|
import org.eclipse.jetty.server.HttpConfiguration;
|
||||||
import org.eclipse.jetty.server.Request;
|
import org.eclipse.jetty.server.Request;
|
||||||
import org.eclipse.jetty.server.Response;
|
import org.eclipse.jetty.server.Response;
|
||||||
|
import org.eclipse.jetty.util.BufferUtil;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.FuturePromise;
|
import org.eclipse.jetty.util.FuturePromise;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
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.assertTrue;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
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
|
private static void sleep(long ms) throws InterruptedIOException
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -154,58 +154,6 @@ public class AsyncServletTest extends AbstractTest
|
||||||
// assertTrue(clientLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
|
// 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
|
// @Test
|
||||||
// public void testStartAsyncThenServerSessionIdleTimeout() throws Exception
|
// public void testStartAsyncThenServerSessionIdleTimeout() throws Exception
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
package org.eclipse.jetty.http3;
|
package org.eclipse.jetty.http3;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeoutException;
|
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
|
try
|
||||||
{
|
{
|
||||||
|
@ -336,17 +335,10 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int fill(ByteBuffer byteBuffer)
|
private int fill(ByteBuffer byteBuffer) throws IOException
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
return getEndPoint().fill(byteBuffer);
|
return getEndPoint().fill(byteBuffer);
|
||||||
}
|
}
|
||||||
catch (IOException x)
|
|
||||||
{
|
|
||||||
throw new UncheckedIOException(x.getMessage(), x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processHeaders(HeadersFrame frame, boolean wasBlocked, Runnable delegate)
|
private void processHeaders(HeadersFrame frame, boolean wasBlocked, Runnable delegate)
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.http3.server.internal;
|
package org.eclipse.jetty.http3.server.internal;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeoutException;
|
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.DataFrame;
|
||||||
import org.eclipse.jetty.http3.frames.HeadersFrame;
|
import org.eclipse.jetty.http3.frames.HeadersFrame;
|
||||||
import org.eclipse.jetty.io.Content;
|
import org.eclipse.jetty.io.Content;
|
||||||
|
import org.eclipse.jetty.io.EofException;
|
||||||
import org.eclipse.jetty.server.HttpChannel;
|
import org.eclipse.jetty.server.HttpChannel;
|
||||||
import org.eclipse.jetty.server.HttpStream;
|
import org.eclipse.jetty.server.HttpStream;
|
||||||
import org.eclipse.jetty.server.Request;
|
import org.eclipse.jetty.server.Request;
|
||||||
|
@ -536,6 +538,8 @@ public class HttpStreamOverHTTP3 implements HttpStream
|
||||||
chunk = Content.Chunk.from(failure, true);
|
chunk = Content.Chunk.from(failure, true);
|
||||||
}
|
}
|
||||||
connection.onFailure(failure);
|
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>
|
<artifactId>jetty-test-helper</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.reactivestreams</groupId>
|
||||||
|
<artifactId>reactive-streams-tck-flow</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
@ -50,6 +55,22 @@
|
||||||
<argLine>@{argLine} ${jetty.surefire.argLine}
|
<argLine>@{argLine} ${jetty.surefire.argLine}
|
||||||
--add-reads org.eclipse.jetty.io=org.eclipse.jetty.logging</argLine>
|
--add-reads org.eclipse.jetty.io=org.eclipse.jetty.logging</argLine>
|
||||||
</configuration>
|
</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>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
|
@ -13,11 +13,17 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.io.content;
|
package org.eclipse.jetty.io.content;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Flow;
|
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.io.Content;
|
||||||
|
import org.eclipse.jetty.util.IteratingCallback;
|
||||||
import org.eclipse.jetty.util.MathUtils;
|
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}.
|
* <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)}.
|
* 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
|
* 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>
|
* 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>
|
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)
|
public ContentSourcePublisher(Content.Source content)
|
||||||
{
|
{
|
||||||
this.content = content;
|
Objects.requireNonNull(content, "Content.Source must not be null");
|
||||||
|
this.content = new AtomicReference<>(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void subscribe(Flow.Subscriber<? super Content.Chunk> subscriber)
|
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();
|
// As per rule 1.9, we need to throw a `java.lang.NullPointerException`
|
||||||
private final Content.Source content;
|
// if the `Subscriber` is `null`
|
||||||
private final Flow.Subscriber<? super Content.Chunk> subscriber;
|
if (subscriber == null)
|
||||||
private long demand;
|
|
||||||
private boolean stalled;
|
|
||||||
private boolean cancelled;
|
|
||||||
private boolean terminated;
|
|
||||||
|
|
||||||
public SubscriptionImpl(Content.Source content, Flow.Subscriber<? super Content.Chunk> subscriber)
|
|
||||||
{
|
{
|
||||||
this.content = content;
|
NullPointerException error = new NullPointerException("Flow.Subscriber must not be null");
|
||||||
this.subscriber = subscriber;
|
content.fail(error);
|
||||||
this.stalled = true;
|
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
|
@Override
|
||||||
public void request(long n)
|
public void request(long n)
|
||||||
{
|
{
|
||||||
boolean process = false;
|
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cancel()
|
public void cancel()
|
||||||
{
|
{
|
||||||
try (AutoLock ignored = lock.lock())
|
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
|
||||||
{
|
|
||||||
cancelled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void process()
|
private static final class ActiveSubscription extends IteratingCallback implements Flow.Subscription
|
||||||
{
|
{
|
||||||
while (true)
|
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)
|
||||||
{
|
{
|
||||||
try (AutoLock ignored = lock.lock())
|
this.cancelled = new AtomicReference<>(null);
|
||||||
{
|
this.demand = new AtomicLong(0);
|
||||||
if (demand > 0)
|
this.content = content;
|
||||||
{
|
this.subscriber = subscriber;
|
||||||
--demand;
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// 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()
|
||||||
{
|
{
|
||||||
stalled = true;
|
Throwable cancelled = this.cancelled.get();
|
||||||
return;
|
if (cancelled != null)
|
||||||
|
{
|
||||||
|
// 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 (cancelled == COMPLETED)
|
||||||
|
this.subscriber.onComplete();
|
||||||
|
else if (!(cancelled instanceof SuppressedException))
|
||||||
|
this.subscriber.onError(cancelled);
|
||||||
}
|
}
|
||||||
|
catch (Throwable err)
|
||||||
|
{
|
||||||
|
if (LOG.isTraceEnabled())
|
||||||
|
LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err);
|
||||||
|
}
|
||||||
|
this.subscriber = null;
|
||||||
|
return Action.SUCCEEDED;
|
||||||
}
|
}
|
||||||
|
|
||||||
Content.Chunk chunk = content.read();
|
Content.Chunk chunk = content.read();
|
||||||
|
|
||||||
if (chunk == null)
|
if (chunk == null)
|
||||||
{
|
{
|
||||||
try (AutoLock ignored = lock.lock())
|
content.demand(this::succeeded);
|
||||||
{
|
return Action.SCHEDULED;
|
||||||
// Restore the demand decremented above.
|
|
||||||
++demand;
|
|
||||||
stalled = true;
|
|
||||||
}
|
|
||||||
content.demand(this::process);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Content.Chunk.isFailure(chunk))
|
if (Content.Chunk.isFailure(chunk))
|
||||||
{
|
{
|
||||||
terminate();
|
cancel(chunk.getFailure());
|
||||||
if (!chunk.isLast())
|
chunk.release();
|
||||||
content.fail(chunk.getFailure());
|
return Action.IDLE;
|
||||||
subscriber.onError(chunk.getFailure());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber.onNext(chunk);
|
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();
|
chunk.release();
|
||||||
|
|
||||||
if (chunk.isLast())
|
if (chunk.isLast())
|
||||||
{
|
{
|
||||||
terminate();
|
cancel(COMPLETED);
|
||||||
// Reactive Stream specification rule 2.9 allows Publishers to call onComplete()
|
return Action.IDLE;
|
||||||
// even without demand, and Subscribers must be prepared to handle this case.
|
}
|
||||||
subscriber.onComplete();
|
|
||||||
|
if (demand.decrementAndGet() > 0)
|
||||||
|
this.iterate();
|
||||||
|
|
||||||
|
return Action.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void request(long n)
|
||||||
|
{
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
String errorMsg = "Flow.Subscriber " + subscriber + " violated rule 3.9: non-positive requests are not allowed.";
|
||||||
|
cancel(new IllegalArgumentException(errorMsg));
|
||||||
return;
|
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 void terminate()
|
private static class CancelledException extends SuppressedException
|
||||||
{
|
{
|
||||||
try (AutoLock ignored = lock.lock())
|
CancelledException()
|
||||||
{
|
{
|
||||||
terminated = true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,10 +13,14 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.quic.common;
|
package org.eclipse.jetty.quic.common;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.ClosedChannelException;
|
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.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.EventListener;
|
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
|
@Override
|
||||||
public void dump(Appendable out, String indent) throws IOException
|
public void dump(Appendable out, String indent) throws IOException
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,9 +13,11 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.quic.common;
|
package org.eclipse.jetty.quic.common;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.IntStream;
|
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 Logger LOG = LoggerFactory.getLogger(QuicStreamEndPoint.class);
|
||||||
private static final ByteBuffer LAST_FLAG = ByteBuffer.allocate(0);
|
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 QuicSession session;
|
||||||
private final long streamId;
|
private final long streamId;
|
||||||
|
@ -221,6 +224,15 @@ public class QuicStreamEndPoint extends AbstractEndPoint
|
||||||
return session;
|
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()
|
public void onWritable()
|
||||||
{
|
{
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
|
@ -255,12 +267,25 @@ public class QuicStreamEndPoint extends AbstractEndPoint
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
QuicStreamEndPoint streamEndPoint = getQuicSession().getStreamEndPoint(streamId);
|
if (isStreamFinished())
|
||||||
if (streamEndPoint.isStreamFinished())
|
|
||||||
{
|
{
|
||||||
EofException e = new EofException();
|
// Check if the stream was finished normally.
|
||||||
streamEndPoint.getFillInterest().onFail(e);
|
try
|
||||||
streamEndPoint.getQuicSession().onFailure(e);
|
{
|
||||||
|
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;
|
return interested;
|
||||||
|
|
|
@ -152,6 +152,8 @@ public abstract class QuicheConnection
|
||||||
|
|
||||||
public abstract CloseInfo getLocalCloseInfo();
|
public abstract CloseInfo getLocalCloseInfo();
|
||||||
|
|
||||||
|
public abstract byte[] getPeerCertificate();
|
||||||
|
|
||||||
public static class CloseInfo
|
public static class CloseInfo
|
||||||
{
|
{
|
||||||
private final long error;
|
private final long error;
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
package org.eclipse.jetty.quic.quiche.foreign;
|
package org.eclipse.jetty.quic.quiche.foreign;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.foreign.Arena;
|
import java.lang.foreign.Arena;
|
||||||
import java.lang.foreign.MemorySegment;
|
import java.lang.foreign.MemorySegment;
|
||||||
|
@ -518,6 +519,7 @@ public class ForeignQuicheConnection extends QuicheConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public byte[] getPeerCertificate()
|
public byte[] getPeerCertificate()
|
||||||
{
|
{
|
||||||
try (AutoLock ignore = lock.lock())
|
try (AutoLock ignore = lock.lock())
|
||||||
|
@ -532,7 +534,7 @@ public class ForeignQuicheConnection extends QuicheConnection
|
||||||
quiche_h.quiche_conn_peer_cert(quicheConn, outSegment, outLenSegment);
|
quiche_h.quiche_conn_peer_cert(quicheConn, outSegment, outLenSegment);
|
||||||
|
|
||||||
long outLen = outLenSegment.get(NativeHelper.C_LONG, 0L);
|
long outLen = outLenSegment.get(NativeHelper.C_LONG, 0L);
|
||||||
if (outLen == 0L)
|
if (outLen <= 0L)
|
||||||
return null;
|
return null;
|
||||||
byte[] out = new byte[(int)outLen];
|
byte[] out = new byte[(int)outLen];
|
||||||
// dereference outSegment pointer
|
// dereference outSegment pointer
|
||||||
|
@ -917,14 +919,19 @@ public class ForeignQuicheConnection extends QuicheConnection
|
||||||
MemorySegment fin = scope.allocate(NativeHelper.C_CHAR);
|
MemorySegment fin = scope.allocate(NativeHelper.C_CHAR);
|
||||||
read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment, buffer.remaining(), fin);
|
read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment, buffer.remaining(), fin);
|
||||||
|
|
||||||
|
if (read > 0)
|
||||||
|
{
|
||||||
int prevPosition = buffer.position();
|
int prevPosition = buffer.position();
|
||||||
buffer.put(bufferSegment.asByteBuffer().limit((int)read));
|
buffer.put(bufferSegment.asByteBuffer().limit((int)read));
|
||||||
buffer.position(prevPosition);
|
buffer.position(prevPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (read == quiche_error.QUICHE_ERR_DONE)
|
if (read == quiche_error.QUICHE_ERR_DONE)
|
||||||
return isStreamFinished(streamId) ? -1 : 0;
|
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)
|
if (read < 0L)
|
||||||
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
||||||
buffer.position((int)(buffer.position() + read));
|
buffer.position((int)(buffer.position() + read));
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
package org.eclipse.jetty.quic.quiche.jna;
|
package org.eclipse.jetty.quic.quiche.jna;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
|
@ -413,6 +414,7 @@ public class JnaQuicheConnection extends QuicheConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public byte[] getPeerCertificate()
|
public byte[] getPeerCertificate()
|
||||||
{
|
{
|
||||||
try (AutoLock ignore = lock.lock())
|
try (AutoLock ignore = lock.lock())
|
||||||
|
@ -424,6 +426,8 @@ public class JnaQuicheConnection extends QuicheConnection
|
||||||
size_t_pointer out_len = new size_t_pointer();
|
size_t_pointer out_len = new size_t_pointer();
|
||||||
LibQuiche.INSTANCE.quiche_conn_peer_cert(quicheConn, out, out_len);
|
LibQuiche.INSTANCE.quiche_conn_peer_cert(quicheConn, out, out_len);
|
||||||
int len = out_len.getPointee().intValue();
|
int len = out_len.getPointee().intValue();
|
||||||
|
if (len <= 0)
|
||||||
|
return null;
|
||||||
return out.getValueAsBytes(len);
|
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();
|
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)
|
if (read == quiche_error.QUICHE_ERR_DONE)
|
||||||
return isStreamFinished(streamId) ? -1 : 0;
|
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)
|
if (read < 0L)
|
||||||
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
|
||||||
buffer.position(buffer.position() + read);
|
buffer.position(buffer.position() + read);
|
||||||
|
|
|
@ -56,8 +56,10 @@ public interface Deployable
|
||||||
String CONFIGURATION_CLASSES = "jetty.deploy.configurationClasses";
|
String CONFIGURATION_CLASSES = "jetty.deploy.configurationClasses";
|
||||||
String CONTAINER_SCAN_JARS = "jetty.deploy.containerScanJarPattern";
|
String CONTAINER_SCAN_JARS = "jetty.deploy.containerScanJarPattern";
|
||||||
String CONTEXT_PATH = "jetty.deploy.contextPath";
|
String CONTEXT_PATH = "jetty.deploy.contextPath";
|
||||||
|
String CONTEXT_HANDLER_CLASS = "jetty.deploy.contextHandlerClass";
|
||||||
String DEFAULTS_DESCRIPTOR = "jetty.deploy.defaultsDescriptor";
|
String DEFAULTS_DESCRIPTOR = "jetty.deploy.defaultsDescriptor";
|
||||||
String ENVIRONMENT = "environment";
|
String ENVIRONMENT = "environment";
|
||||||
|
String ENVIRONMENT_XML = "jetty.deploy.environmentXml";
|
||||||
String EXTRACT_WARS = "jetty.deploy.extractWars";
|
String EXTRACT_WARS = "jetty.deploy.extractWars";
|
||||||
String PARENT_LOADER_PRIORITY = "jetty.deploy.parentLoaderPriority";
|
String PARENT_LOADER_PRIORITY = "jetty.deploy.parentLoaderPriority";
|
||||||
String SCI_EXCLUSION_PATTERN = "jetty.deploy.servletContainerInitializerExclusionPattern";
|
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>Notifies this {@code HttpChannel} that an asynchronous failure happened.</p>
|
||||||
* <p>Typical failure examples could be HTTP/2 resets or
|
* <p>Typical failure examples could be protocol failures (for example, invalid request bytes).</p>
|
||||||
* protocol failures (for example, invalid request bytes).</p>
|
|
||||||
*
|
*
|
||||||
* @param failure the failure cause.
|
* @param failure the failure cause.
|
||||||
* @return a {@code Runnable} that performs the failure action, or {@code null}
|
* @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);
|
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>
|
* <p>Notifies this {@code HttpChannel} that an asynchronous close happened.</p>
|
||||||
*
|
*
|
||||||
|
|
|
@ -392,6 +392,17 @@ public class HttpChannelState implements HttpChannel, Components
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Runnable onFailure(Throwable x)
|
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;
|
HttpStream stream;
|
||||||
Runnable task;
|
Runnable task;
|
||||||
|
@ -437,7 +448,9 @@ public class HttpChannelState implements HttpChannel, Components
|
||||||
// Notify the failure listeners only once.
|
// Notify the failure listeners only once.
|
||||||
Consumer<Throwable> onFailure = _onFailure;
|
Consumer<Throwable> onFailure = _onFailure;
|
||||||
_onFailure = null;
|
_onFailure = null;
|
||||||
Runnable invokeOnFailureListeners = onFailure == null ? null : () ->
|
|
||||||
|
boolean skipListeners = remote && !getHttpConfiguration().isNotifyRemoteAsyncErrors();
|
||||||
|
Runnable invokeOnFailureListeners = onFailure == null || skipListeners ? null : () ->
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -1426,11 +1426,13 @@ public class StartArgs
|
||||||
{
|
{
|
||||||
for (String moduleName : moduleNames)
|
for (String moduleName : moduleNames)
|
||||||
{
|
{
|
||||||
modules.add(moduleName);
|
if (modules.add(moduleName))
|
||||||
|
{
|
||||||
Set<String> set = sources.computeIfAbsent(moduleName, k -> new HashSet<>());
|
Set<String> set = sources.computeIfAbsent(moduleName, k -> new HashSet<>());
|
||||||
set.add(source);
|
set.add(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setAllModules(Modules allModules)
|
public void setAllModules(Modules allModules)
|
||||||
{
|
{
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
@ -183,4 +184,27 @@ public class MainTest
|
||||||
);
|
);
|
||||||
assertThat(commandLine, containsString(expectedExpansion));
|
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>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
|
||||||
<artifactId>jetty-openid</artifactId>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-security</artifactId>
|
<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)
|
catch (Throwable t)
|
||||||
{
|
{
|
||||||
onWriteComplete(false, t);
|
onWriteComplete(false, t);
|
||||||
|
if (t instanceof IOException)
|
||||||
throw t;
|
throw t;
|
||||||
|
throw new IOException(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@
|
||||||
<configuration>
|
<configuration>
|
||||||
<argLine>@{argLine}
|
<argLine>@{argLine}
|
||||||
${jetty.surefire.argLine}
|
${jetty.surefire.argLine}
|
||||||
--enable-native-access org.eclipse.jetty.quic.quiche.foreign</argLine>
|
--enable-native-access=ALL-UNNAMED</argLine>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|
|
@ -102,6 +102,14 @@ public class AbstractTest
|
||||||
return transports;
|
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
|
@BeforeEach
|
||||||
public void prepare()
|
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]
|
[description]
|
||||||
Adds OpenId Connect authentication to the server.
|
Adds OpenId Connect authentication to the server.
|
||||||
|
|
||||||
|
[environment]
|
||||||
|
ee8
|
||||||
|
|
||||||
[depend]
|
[depend]
|
||||||
ee8-security
|
ee8-security
|
||||||
openid
|
openid
|
||||||
client
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
lib/jetty-ee8-openid-${jetty.version}.jar
|
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)
|
catch (Throwable t)
|
||||||
{
|
{
|
||||||
onWriteComplete(false, t);
|
onWriteComplete(false, t);
|
||||||
|
if (t instanceof IOException)
|
||||||
throw t;
|
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]
|
[depend]
|
||||||
ee9-security
|
ee9-security
|
||||||
openid
|
openid
|
||||||
client
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
lib/jetty-ee9-openid-${jetty.version}.jar
|
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>
|
<configuration>
|
||||||
<argLine>@{argLine}
|
<argLine>@{argLine}
|
||||||
${jetty.surefire.argLine}
|
${jetty.surefire.argLine}
|
||||||
--enable-native-access org.eclipse.jetty.quic.quiche.foreign</argLine>
|
--enable-native-access=ALL-UNNAMED</argLine>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</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>
|
<plexus-xml.version>4.0.3</plexus-xml.version>
|
||||||
<project.build.outputTimestamp>2024-04-26T07:15:13Z</project.build.outputTimestamp>
|
<project.build.outputTimestamp>2024-04-26T07:15:13Z</project.build.outputTimestamp>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<reactive-streams.version>1.0.4</reactive-streams.version>
|
||||||
<settingsPath>src/it/settings.xml</settingsPath>
|
<settingsPath>src/it/settings.xml</settingsPath>
|
||||||
<slf4j.version>2.0.12</slf4j.version>
|
<slf4j.version>2.0.12</slf4j.version>
|
||||||
<spifly.version>1.3.7</spifly.version>
|
<spifly.version>1.3.7</spifly.version>
|
||||||
|
@ -402,6 +403,7 @@
|
||||||
<surefire.rerunFailingTestsCount>0</surefire.rerunFailingTestsCount>
|
<surefire.rerunFailingTestsCount>0</surefire.rerunFailingTestsCount>
|
||||||
<swissbox.version>1.8.3</swissbox.version>
|
<swissbox.version>1.8.3</swissbox.version>
|
||||||
<testcontainers.version>1.19.7</testcontainers.version>
|
<testcontainers.version>1.19.7</testcontainers.version>
|
||||||
|
<testng.version>7.10.2</testng.version>
|
||||||
<tinybundles.version>3.0.0</tinybundles.version>
|
<tinybundles.version>3.0.0</tinybundles.version>
|
||||||
<versions.maven.plugin.version>2.16.2</versions.maven.plugin.version>
|
<versions.maven.plugin.version>2.16.2</versions.maven.plugin.version>
|
||||||
<wildfly.common.version>1.7.0.Final</wildfly.common.version>
|
<wildfly.common.version>1.7.0.Final</wildfly.common.version>
|
||||||
|
@ -1249,6 +1251,11 @@
|
||||||
<artifactId>osgi.core</artifactId>
|
<artifactId>osgi.core</artifactId>
|
||||||
<version>${org.osgi.core.version}</version>
|
<version>${org.osgi.core.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.reactivestreams</groupId>
|
||||||
|
<artifactId>reactive-streams-tck-flow</artifactId>
|
||||||
|
<version>${reactive-streams.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>jcl104-over-slf4j</artifactId>
|
<artifactId>jcl104-over-slf4j</artifactId>
|
||||||
|
@ -1285,6 +1292,11 @@
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.testng</groupId>
|
||||||
|
<artifactId>testng</artifactId>
|
||||||
|
<version>${testng.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.wildfly.common</groupId>
|
<groupId>org.wildfly.common</groupId>
|
||||||
<artifactId>wildfly-common</artifactId>
|
<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-jmh</module>
|
||||||
<module>jetty-test-multipart</module>
|
<module>jetty-test-multipart</module>
|
||||||
<module>jetty-test-session-common</module>
|
<module>jetty-test-session-common</module>
|
||||||
|
<module>test-cross-context-dispatch</module>
|
||||||
<module>test-distribution</module>
|
<module>test-distribution</module>
|
||||||
<module>test-integration</module>
|
<module>test-integration</module>
|
||||||
<module>test-jpms</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
|
|
@ -0,0 +1,12 @@
|
||||||
|
REQUEST|GET|/ccd-ee10/redispatch/ee10
|
||||||
|
STEP|REQUEST_FORWARD|/dump/ee10
|
||||||
|
EXPECTED_EVENT|Initial plan: ee10-request-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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue