Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.1.x

This commit is contained in:
Joakim Erdfelt 2024-06-19 16:18:15 -05:00
commit af23f51d92
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
111 changed files with 5791 additions and 928 deletions

2
Jenkinsfile vendored
View File

@ -127,7 +127,7 @@ def mavenBuild(jdk, cmdline, mvnName) {
}
finally
{
junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml', allowEmptyResults: true
junit testResults: '**/target/surefire-reports/**/*.xml,**/target/invoker-reports/TEST*.xml', allowEmptyResults: true
}
}
}

View File

@ -4,7 +4,7 @@ Eclipse Jetty is a lightweight, highly scalable, Java-based web server and Servl
Jetty's goal is to support web protocols (HTTP/1, HTTP/2, HTTP/3, WebSocket, etc.) in a high volume low latency way that provides maximum performance while retaining the ease of use and compatibility with years of Servlet development.
Jetty is a modern fully asynchronous web server that has a long history as a component oriented technology, and can be easily embedded into applications while still offering a solid traditional distribution for webapp deployment.
- https://eclipse.dev/jetty/
- https://jetty.org
- https://projects.eclipse.org/projects/rt.jetty
## Webapp Example
@ -53,19 +53,19 @@ $ cd jetty.project
$ mvn -Pfast clean install # fast build bypasses tests and other checks
```
For more detailed information on building and contributing to the Jetty project, please see the [Contribution Guide](https://eclipse.dev/jetty/documentation/contribution-guide/index.html).
For more detailed information on building and contributing to the Jetty project, please see the [Contribution Guide](https://jetty.org/docs/contribution-guide/index.html).
# Documentation
[Jetty's documentation](https://eclipse.dev/jetty/documentation) is available on the Eclipse Jetty website.
[Jetty's documentation](https://jetty.org/docs) is available on the Eclipse Jetty website.
The documentation is divided into three guides, based on use case:
* The [Operations Guide](https://eclipse.dev/jetty/documentation/jetty-12/operations-guide/index.html) targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications.
* The [Operations Guide](https://jetty.org/docs/jetty/12/operations-guide/index.html) targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications.
* The [Programming Guide](https://eclipse.dev/jetty/documentation/jetty-12/programming-guide/index.html) targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications.
* The [Programming Guide](https://jetty.org/docs/jetty/12/programming-guide/index.html) targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications.
* The [Contribution Guide](https://eclipse.dev/jetty/documentation/contribution-guide/index.html) targets developers that wish to contribute to the Jetty Project with code patches or documentation improvements.
* The [Contribution Guide](https://jetty.org/docs/contribution-guide/index.html) targets developers that wish to contribute to the Jetty Project with code patches or documentation improvements.
# Commercial Support

View File

@ -27,6 +27,7 @@ import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Context;
@ -60,10 +61,11 @@ public class ServletToHandlerDocs
// - servletRequest.getProtocol();
String protocol = request.getConnectionMetaData().getProtocol();
// Gets the full request URI.
// Gets the request URL.
// Replaces:
// - servletRequest.getRequestURL();
String fullRequestURI = request.getHttpURI().asString();
HttpURI httpURI = HttpURI.build(request.getHttpURI()).query(null);
StringBuffer requestURL = new StringBuffer(httpURI.asString());
// Gets the request context.
// Replaces:

View File

@ -47,36 +47,45 @@ Use an editor to create the file `src/main/java/org/example/HelloWorld.java` wit
----
package org.example;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.ServletException;
import java.io.IOException;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
public class HelloWorld extends AbstractHandler
class HelloWorldHandler extends Handler.Abstract.NonBlocking
{
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
@Override
public boolean handle(Request request, Response response, Callback callback)
{
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
baseRequest.setHandled(true);
response.getWriter().println("<h1>Hello World</h1>");
response.setStatus(200);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html; charset=UTF-8");
// Write a Hello World response.
Content.Sink.write(response, true, """
<!DOCTYPE html>
<html>
<head>
<title>Jetty Hello World Handler</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
""", callback);
return true;
}
}
public static void main(String[] args) throws Exception
{
Server server = new Server(8080);
server.setHandler(new HelloWorld());
Server server = new Server();
Connector connector = new ServerConnector(server);
server.addConnector(connector);
// Set the Hello World Handler.
server.setHandler(new HelloWorldHandler());
server.start();
server.join();
}
}
----
@ -116,7 +125,6 @@ Use an editor to create the file `pom.xml` in the `JettyMavenHelloWorld` directo
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.1</version>
<executions>
<execution><goals><goal>java</goal></goals></execution>
</executions>

View File

@ -208,7 +208,7 @@ public abstract class HttpSender
return false;
if (LOG.isDebugEnabled())
LOG.debug("Request failure {}", exchange.getRequest(), failure);
LOG.debug("Request failure {}, response {}", exchange.getRequest(), exchange.getResponse(), failure);
// Mark atomically the request as completed, with respect
// to concurrency between request success and request failure.

View File

@ -21,6 +21,7 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -243,6 +244,7 @@ public class ContextProvider extends ScanningAppProvider
/**
* This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property.
*
* @param configurations The configuration class names as a comma separated list
*/
public void setConfigurationClasses(String configurations)
@ -252,6 +254,7 @@ public class ContextProvider extends ScanningAppProvider
/**
* This is equivalent to setting the {@link Deployable#CONFIGURATION_CLASSES} property.
*
* @param configurations The configuration class names.
*/
public void setConfigurationClasses(String[] configurations)
@ -262,8 +265,8 @@ public class ContextProvider extends ScanningAppProvider
}
/**
*
* This is equivalent to getting the {@link Deployable#CONFIGURATION_CLASSES} property.
*
* @return The configuration class names.
*/
@ManagedAttribute("configuration classes for webapps to be processed through")
@ -341,32 +344,48 @@ public class ContextProvider extends ScanningAppProvider
// prepare properties
Map<String, String> properties = new HashMap<>();
//add in properties from start mechanism
properties.putAll(getProperties());
Object context = null;
//check if there is a specific ContextHandler type to create set in the
//properties associated with the webapp. If there is, we create it _before_
//applying the environment xml file.
String contextHandlerClassName = app.getProperties().get(Deployable.CONTEXT_HANDLER_CLASS);
if (contextHandlerClassName != null)
context = Class.forName(contextHandlerClassName).getDeclaredConstructor().newInstance();
//add in environment-specific properties
String env = app.getEnvironmentName() == null ? "" : app.getEnvironmentName();
Path envProperties = app.getPath().getParent().resolve(env + ".properties");
if (Files.exists(envProperties))
{
try (InputStream stream = Files.newInputStream(envProperties))
{
Properties p = new Properties();
p.load(stream);
p.stringPropertyNames().forEach(k -> properties.put(k, p.getProperty(k)));
}
String str = properties.get(Deployable.ENVIRONMENT_XML);
if (!StringUtil.isEmpty(str))
{
Path envXmlPath = Paths.get(str);
if (!envXmlPath.isAbsolute())
envXmlPath = getMonitoredDirResource().getPath().getParent().resolve(envXmlPath);
context = applyXml(context, envXmlPath, env, properties);
}
}
//add in properties specific to the deployable
properties.putAll(app.getProperties());
// Handle a context XML file
if (FileID.isXml(path))
{
XmlConfiguration xmlc = new XmlConfiguration(ResourceFactory.of(this).newResource(path), null, properties)
{
@Override
public void initializeDefaults(Object context)
{
super.initializeDefaults(context);
ContextProvider.this.initializeContextHandler(context, path, properties);
}
};
xmlc.getIdMap().put("Environment", environment);
xmlc.setJettyStandardIdsAndProperties(getDeploymentManager().getServer(), path);
// If it is a core context environment, then look for a classloader
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null;
if (coreContextClassLoader != null)
Thread.currentThread().setContextClassLoader(coreContextClassLoader);
// Create the context by running the configuration
Object context = xmlc.configure();
context = applyXml(context, path, env, properties);
// Look for the contextHandler itself
ContextHandler contextHandler = null;
@ -382,27 +401,33 @@ public class ContextProvider extends ScanningAppProvider
throw new IllegalStateException("Unknown context type of " + context);
// Set the classloader if we have a coreContextClassLoader
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(path) : null;
if (coreContextClassLoader != null)
contextHandler.setClassLoader(coreContextClassLoader);
return contextHandler;
}
// Otherwise it must be a directory or an archive
else if (!Files.isDirectory(path) && !FileID.isWebArchive(path))
{
throw new IllegalStateException("unable to create ContextHandler for " + app);
}
// Build the web application
String contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass");
// Build the web application if necessary
if (context == null)
{
contextHandlerClassName = (String)environment.getAttribute("contextHandlerClass");
if (StringUtil.isBlank(contextHandlerClassName))
throw new IllegalStateException("No ContextHandler classname for " + app);
Class<?> contextHandlerClass = Loader.loadClass(contextHandlerClassName);
if (contextHandlerClass == null)
throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app);
Object context = contextHandlerClass.getDeclaredConstructor().newInstance();
context = contextHandlerClass.getDeclaredConstructor().newInstance();
properties.put(Deployable.WAR, path.toString());
}
return initializeContextHandler(context, path, properties);
}
finally
@ -411,6 +436,36 @@ public class ContextProvider extends ScanningAppProvider
}
}
protected Object applyXml(Object context, Path xml, String environment, Map<String, String> properties) throws Exception
{
if (!FileID.isXml(xml))
return null;
XmlConfiguration xmlc = new XmlConfiguration(ResourceFactory.of(this).newResource(xml), null, properties)
{
@Override
public void initializeDefaults(Object context)
{
super.initializeDefaults(context);
ContextProvider.this.initializeContextHandler(context, xml, properties);
}
};
xmlc.getIdMap().put("Environment", environment);
xmlc.setJettyStandardIdsAndProperties(getDeploymentManager().getServer(), xml);
// If it is a core context environment, then look for a classloader
ClassLoader coreContextClassLoader = Environment.CORE.equals(environment) ? findCoreContextClassLoader(xml) : null;
if (coreContextClassLoader != null)
Thread.currentThread().setContextClassLoader(coreContextClassLoader);
// Create or configure the context
if (context == null)
return xmlc.configure();
return xmlc.configure(context);
}
protected ClassLoader findCoreContextClassLoader(Path path) throws IOException
{
Path webapps = path.getParent();

View File

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

View File

@ -15,27 +15,18 @@ package org.eclipse.jetty.deploy.providers;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import org.eclipse.jetty.deploy.AppProvider;
import org.eclipse.jetty.deploy.DeploymentManager;
import org.eclipse.jetty.deploy.BarContextHandler;
import org.eclipse.jetty.deploy.test.XmlConfiguredJetty;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Deployable;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.component.Container;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -43,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -77,7 +69,10 @@ public class ContextProviderStartupTest
// Should not throw an Exception
jetty.load();
}
public void startJetty() throws Exception
{
// Start it
jetty.start();
}
@ -89,9 +84,47 @@ public class ContextProviderStartupTest
}
@Test
public void testStartupContext()
public void testStartupContext() throws Exception
{
startJetty();
// Check Server for Handlers
jetty.assertContextHandlerExists("/bar");
}
@Test
public void testStartupWithRelativeEnvironmentContext() throws Exception
{
Path jettyBase = jetty.getJettyBasePath();
Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = etc/core-context.xml", StandardOpenOption.CREATE_NEW);
assertTrue(Files.exists(propsFile));
Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING);
jetty.copyWebapp("bar-core-context.properties", "bar.properties");
startJetty();
//check environment context xml was applied to the produced context
ContextHandler context = jetty.getContextHandler("/bar");
assertNotNull(context);
assertThat(context.getAttribute("somename"), equalTo("somevalue"));
assertTrue(context instanceof BarContextHandler);
}
@Test
public void testStartupWithAbsoluteEnvironmentContext() throws Exception
{
Path jettyBase = jetty.getJettyBasePath();
Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = " +
MavenPaths.findTestResourceFile("etc/core-context.xml"), StandardOpenOption.CREATE_NEW);
assertTrue(Files.exists(propsFile));
Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING);
jetty.copyWebapp("bar-core-context.properties", "bar-core-context.properties");
startJetty();
//check environment context xml was applied to the produced context
ContextHandler context = jetty.getContextHandler("/bar");
assertNotNull(context);
assertThat(context.getAttribute("somename"), equalTo("somevalue"));
}
}

View File

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

View File

@ -0,0 +1,2 @@
environment: core
jetty.deploy.contextHandlerClass: org.eclipse.jetty.deploy.BarContextHandler

View File

@ -169,9 +169,7 @@ public class WebAppClassLoading
* Add a hidden (server) Class pattern to use for all WebAppContexts of a given {@link Server}.
* @param attributes The {@link Attributes} instance to add classes to
* @param patterns the patterns to use
* @deprecated use {@link #addHiddenClasses(Server, String...)} instead
*/
@Deprecated (since = "12.0.9", forRemoval = true)
public static void addHiddenClasses(Attributes attributes, String... patterns)
{
if (patterns != null && patterns.length > 0)

View File

@ -38,9 +38,16 @@ import java.util.stream.StreamSupport;
/**
* <p>An ordered collection of {@link HttpField}s that represent the HTTP headers
* or HTTP trailers of an HTTP request or an HTTP response.</p>
*
* <p>{@link HttpFields} is immutable and typically used in server-side HTTP requests
* and client-side HTTP responses, while {@link HttpFields.Mutable} is mutable and
* typically used in server-side HTTP responses and client-side HTTP requests.</p>
*
* <p>Access is always more efficient using {@link HttpHeader} keys rather than {@link String} field names.</p>
*
* <p>The primary implementations of {@code HttpFields} have been optimized assuming few
* lookup operations, thus typically if many {@link HttpField}s need to looked up, it may be
* better to use an {@link Iterator} to find multiple fields in a single iteration.</p>
*/
public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
{
@ -350,10 +357,12 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
/**
* <p>Returns whether this instance contains the given field name.</p>
* <p>The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.
* {@link HttpField#is(String)}. If possible, it is more efficient to use
* {@link #contains(HttpHeader)}.
*
* @param name the case-insensitive field name to search for
* @return whether this instance contains the given field name
* @see #contains(HttpHeader)
*/
default boolean contains(String name)
{
@ -412,7 +421,7 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
* <p>Returns the encoded value of the first field with the given field name,
* or {@code null} if no such field is present.</p>
* <p>The comparison of field name is case-insensitive via
* {@link HttpField#is(String)}.</p>
* {@link HttpField#is(String)}. If possible, it is more efficient to use {@link #get(HttpHeader)}.</p>
* <p>In case of multi-valued fields, the returned value is the encoded
* value, including commas and quotes, as returned by {@link HttpField#getValue()}.</p>
*
@ -420,6 +429,7 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
* @return the raw value of the first field with the given field name,
* or {@code null} if no such field is present
* @see HttpField#getValue()
* @see #get(HttpHeader)
*/
default String get(String name)
{
@ -594,12 +604,13 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
* <p>Returns a {@link Set} of the field names.</p>
* <p>Case-sensitivity of the field names is preserved.</p>
*
* @return a {@link Set} of the field names
* @return an immutable {@link Set} of the field names. Changes made to the
* {@code HttpFields} after this call are not reflected in the set.
*/
default Set<String> getFieldNamesCollection()
{
Set<HttpHeader> seenByHeader = EnumSet.noneOf(HttpHeader.class);
Set<String> seenByName = null;
Set<String> buildByName = null;
List<String> list = new ArrayList<>(size());
for (HttpField f : this)
@ -607,9 +618,9 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
HttpHeader header = f.getHeader();
if (header == null)
{
if (seenByName == null)
seenByName = new TreeSet<>(String::compareToIgnoreCase);
if (seenByName.add(f.getName()))
if (buildByName == null)
buildByName = new TreeSet<>(String::compareToIgnoreCase);
if (buildByName.add(f.getName()))
list.add(f.getName());
}
else if (seenByHeader.add(header))
@ -618,6 +629,8 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
}
}
Set<String> seenByName = buildByName;
// use the list to retain a rough ordering
return new AbstractSet<>()
{
@ -632,6 +645,14 @@ public interface HttpFields extends Iterable<HttpField>, Supplier<HttpFields>
{
return list.size();
}
@Override
public boolean contains(Object o)
{
if (o instanceof String s)
return seenByName != null && seenByName.contains(s) || seenByHeader.contains(HttpHeader.CACHE.get(s));
return false;
}
};
}

View File

@ -20,12 +20,7 @@ import java.util.Objects;
import java.util.stream.Stream;
/**
* HTTP Fields. A collection of HTTP header and or Trailer fields.
*
* <p>This class is not synchronized as it is expected that modifications will only be performed by a
* single thread.
*
* <p>The cookie handling provided by this class is guided by the Servlet specification and RFC6265.
* An immutable implementation of {@link HttpFields}.
*/
class ImmutableHttpFields implements HttpFields
{
@ -70,10 +65,9 @@ class ImmutableHttpFields implements HttpFields
{
if (this == o)
return true;
if (!(o instanceof org.eclipse.jetty.http.ImmutableHttpFields))
if (o instanceof HttpFields httpFields)
return isEqualTo(httpFields);
return false;
return isEqualTo((HttpFields)o);
}
@Override

View File

@ -67,7 +67,7 @@ class MutableHttpFields implements HttpFields.Mutable
*/
protected MutableHttpFields(HttpFields fields)
{
if (fields instanceof ImmutableHttpFields immutable)
if (fields instanceof org.eclipse.jetty.http.ImmutableHttpFields immutable)
{
_immutable = true;
_fields = immutable._fields;
@ -180,7 +180,7 @@ class MutableHttpFields implements HttpFields.Mutable
public HttpFields asImmutable()
{
_immutable = true;
return new ImmutableHttpFields(_fields, _size);
return new org.eclipse.jetty.http.ImmutableHttpFields(_fields, _size);
}
private void copyImmutable()

View File

@ -25,6 +25,7 @@ import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -352,16 +353,27 @@ public class HttpFieldsTest
assertThat(header.get("EXPECT"), is("100"));
assertThat(header.get("eXpEcT"), is("100"));
assertThat(header.get(HttpHeader.EXPECT), is("100"));
assertTrue(header.contains("expect"));
assertTrue(header.contains("Expect"));
assertTrue(header.contains("EXPECT"));
assertTrue(header.contains("eXpEcT"));
assertThat(header.get("random"), is("value"));
assertThat(header.get("Random"), is("value"));
assertThat(header.get("RANDOM"), is("value"));
assertThat(header.get("rAnDoM"), is("value"));
assertThat(header.get("RaNdOm"), is("value"));
assertTrue(header.contains("random"));
assertTrue(header.contains("Random"));
assertTrue(header.contains("RANDOM"));
assertTrue(header.contains("rAnDoM"));
assertTrue(header.contains("RaNdOm"));
assertThat(header.get("Accept-Charset"), is("UTF-8"));
assertThat(header.get("accept-charset"), is("UTF-8"));
assertThat(header.get(HttpHeader.ACCEPT_CHARSET), is("UTF-8"));
assertTrue(header.contains("Accept-Charset"));
assertTrue(header.contains("accept-charset"));
assertThat(header.getValuesList("Accept-Charset"), contains("UTF-8", "UTF-16"));
assertThat(header.getValuesList("accept-charset"), contains("UTF-8", "UTF-16"));
@ -371,9 +383,19 @@ public class HttpFieldsTest
assertThat(header.get("Foo-Bar"), is("one"));
assertThat(header.getValuesList("foo-bar"), contains("one", "two"));
assertThat(header.getValuesList("Foo-Bar"), contains("one", "two"));
assertTrue(header.contains("foo-bar"));
assertTrue(header.contains("Foo-Bar"));
// We know the order of the set is deterministic
assertThat(header.getFieldNamesCollection(), contains("expect", "RaNdOm", "Accept-Charset", "foo-bar"));
Set<String> names = header.getFieldNamesCollection();
assertThat(names, contains("expect", "RaNdOm", "Accept-Charset", "foo-bar"));
assertTrue(names.contains("expect"));
assertTrue(names.contains("Expect"));
assertTrue(names.contains("random"));
assertTrue(names.contains("accept-charset"));
assertTrue(names.contains("Accept-Charset"));
assertTrue(names.contains("foo-bar"));
assertTrue(names.contains("Foo-Bar"));
}
@ParameterizedTest

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.http2.server;
import java.io.EOFException;
import java.util.Map;
import java.util.concurrent.TimeoutException;
@ -155,7 +156,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
@Override
public void onReset(Stream stream, ResetFrame frame, Callback callback)
{
EofException failure = new EofException("Reset " + ErrorCode.toString(frame.getError(), null));
EOFException failure = new EOFException("Reset " + ErrorCode.toString(frame.getError(), null));
onFailure(stream, failure, callback);
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.http2.server.internal;
import java.io.EOFException;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
@ -38,6 +39,7 @@ import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Request;
@ -587,7 +589,8 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
@Override
public Runnable onFailure(Throwable failure, Callback callback)
{
Runnable runnable = _httpChannel.onFailure(failure);
boolean remote = failure instanceof EOFException;
Runnable runnable = remote ? _httpChannel.onRemoteFailure(new EofException(failure)) : _httpChannel.onFailure(failure);
return () ->
{
if (runnable != null)

View File

@ -58,7 +58,12 @@ public class AbstractTest
protected void start(Handler handler) throws Exception
{
HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(new HttpConfiguration());
start(handler, new HttpConfiguration());
}
protected void start(Handler handler, HttpConfiguration httpConfiguration) throws Exception
{
HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(httpConfiguration);
connectionFactory.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
prepareServer(connectionFactory);

View File

@ -17,22 +17,33 @@ import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ -241,6 +252,67 @@ public class AsyncIOTest extends AbstractTest
*/
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testClientResetRemoteErrorNotification(boolean notify) throws Exception
{
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Response> responseRef = new AtomicReference<>();
AtomicReference<Throwable> failureRef = new AtomicReference<>();
HttpConfiguration httpConfiguration = new HttpConfiguration();
httpConfiguration.setNotifyRemoteAsyncErrors(notify);
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
request.addFailureListener(failureRef::set);
responseRef.set(response);
latch.countDown();
return true;
}
}, httpConfiguration);
Session session = newClientSession(new Session.Listener() {});
MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
HeadersFrame frame = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
session.newStream(frame, promise, null);
Stream stream = promise.get(5, TimeUnit.SECONDS);
// Wait for the server to be idle.
assertTrue(latch.await(5, TimeUnit.SECONDS));
sleep(500);
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP);
if (notify)
// Wait for the reset to be notified to the failure listener.
await().atMost(5, TimeUnit.SECONDS).until(failureRef::get, instanceOf(EofException.class));
else
// Wait for the reset to NOT be notified to the failure listener.
await().atMost(5, TimeUnit.SECONDS).during(1, TimeUnit.SECONDS).until(failureRef::get, nullValue());
// Assert that writing to the response fails.
var cb = new Callback()
{
private Throwable failure = null;
@Override
public void failed(Throwable x)
{
failure = x;
}
Throwable failure()
{
return failure;
}
};
responseRef.get().write(true, BufferUtil.EMPTY_BUFFER, cb);
await().atMost(5, TimeUnit.SECONDS).until(cb::failure, instanceOf(EofException.class));
}
private static void sleep(long ms) throws InterruptedIOException
{
try

View File

@ -154,58 +154,6 @@ public class AsyncServletTest extends AbstractTest
// assertTrue(clientLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
// }
//
// @Test
// public void testStartAsyncThenClientResetWithoutRemoteErrorNotification() throws Exception
// {
// HttpConfiguration httpConfiguration = new HttpConfiguration();
// httpConfiguration.setNotifyRemoteAsyncErrors(false);
// prepareServer(new HTTP2ServerConnectionFactory(httpConfiguration));
// ServletContextHandler context = new ServletContextHandler(server, "/");
// AtomicReference<AsyncContext> asyncContextRef = new AtomicReference<>();
// CountDownLatch latch = new CountDownLatch(1);
// context.addServlet(new ServletHolder(new HttpServlet()
// {
// @Override
// protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
// {
// AsyncContext asyncContext = request.startAsync();
// asyncContext.setTimeout(0);
// asyncContextRef.set(asyncContext);
// latch.countDown();
// }
// }), servletPath + "/*");
// server.start();
//
// prepareClient();
// client.start();
// Session session = newClient(new Session.Listener() {});
// MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
// HeadersFrame frame = new HeadersFrame(metaData, null, true);
// FuturePromise<Stream> promise = new FuturePromise<>();
// session.newStream(frame, promise, null);
// Stream stream = promise.get(5, TimeUnit.SECONDS);
//
// // Wait for the server to be in ASYNC_WAIT.
// assertTrue(latch.await(5, TimeUnit.SECONDS));
// sleep(500);
//
// stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP);
//
// // Wait for the reset to be processed by the server.
// sleep(500);
//
// AsyncContext asyncContext = asyncContextRef.get();
// ServletResponse response = asyncContext.getResponse();
// ServletOutputStream output = response.getOutputStream();
//
// assertThrows(IOException.class,
// () ->
// {
// // Large writes or explicit flush() must
// // fail because the stream has been reset.
// output.flush();
// });
// }
//
// @Test
// public void testStartAsyncThenServerSessionIdleTimeout() throws Exception

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.http3;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
@ -275,7 +274,7 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
}
}
private MessageParser.Result parseAndFill(boolean setFillInterest)
private MessageParser.Result parseAndFill(boolean setFillInterest) throws IOException
{
try
{
@ -336,17 +335,10 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
}
}
private int fill(ByteBuffer byteBuffer)
{
try
private int fill(ByteBuffer byteBuffer) throws IOException
{
return getEndPoint().fill(byteBuffer);
}
catch (IOException x)
{
throw new UncheckedIOException(x.getMessage(), x);
}
}
private void processHeaders(HeadersFrame frame, boolean wasBlocked, Runnable delegate)
{

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.http3.server.internal;
import java.io.EOFException;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
@ -33,6 +34,7 @@ import org.eclipse.jetty.http3.api.Stream;
import org.eclipse.jetty.http3.frames.DataFrame;
import org.eclipse.jetty.http3.frames.HeadersFrame;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Request;
@ -536,6 +538,8 @@ public class HttpStreamOverHTTP3 implements HttpStream
chunk = Content.Chunk.from(failure, true);
}
connection.onFailure(failure);
return httpChannel.onFailure(failure);
boolean remote = failure instanceof EOFException;
return remote ? httpChannel.onRemoteFailure(new EofException(failure)) : httpChannel.onFailure(failure);
}
}

View File

@ -41,6 +41,11 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams-tck-flow</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
@ -50,6 +55,22 @@
<argLine>@{argLine} ${jetty.surefire.argLine}
--add-reads org.eclipse.jetty.io=org.eclipse.jetty.logging</argLine>
</configuration>
<dependencies>
<!--
surefire plugin currently does not support junit5 with testng out of the box: https://maven.apache.org/surefire/maven-surefire-plugin/examples/testng.html#running-testng-and-junit-tests
We need to specify providers explicitly: https://maven.apache.org/surefire/maven-surefire-plugin/examples/providers.html
-->
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>${maven.surefire.plugin.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-testng</artifactId>
<version>${maven.surefire.plugin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

View File

@ -13,11 +13,17 @@
package org.eclipse.jetty.io.content;
import java.util.Objects;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.MathUtils;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.StaticException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>Wraps a {@link Content.Source} as a {@link Flow.Publisher}.
@ -25,135 +31,282 @@ import org.eclipse.jetty.util.thread.AutoLock;
* read from the passed {@link Content.Source} and passed to {@link Flow.Subscriber#onNext(Object)}.
* If no content is available, then the {@link Content.Source#demand(Runnable)} method is used to
* ultimately call {@link Flow.Subscriber#onNext(Object)} once content is available.</p>
* <p>{@link Content.Source} can be consumed only once and does not support multicast subscription.
* {@link Content.Source} will be consumed fully, otherwise will be failed in case of any errors
* to prevent resource leaks.</p>
*/
public class ContentSourcePublisher implements Flow.Publisher<Content.Chunk>
{
private final Content.Source content;
private static final Logger LOG = LoggerFactory.getLogger(ContentSourcePublisher.class);
private final AtomicReference<Content.Source> content;
public ContentSourcePublisher(Content.Source content)
{
this.content = content;
Objects.requireNonNull(content, "Content.Source must not be null");
this.content = new AtomicReference<>(content);
}
@Override
public void subscribe(Flow.Subscriber<? super Content.Chunk> subscriber)
{
subscriber.onSubscribe(new SubscriptionImpl(content, subscriber));
// As per rule 1.11, we have decided to support SINGLE subscriber
// in a UNICAST configuration for this implementation. It means
// that Content.Source can be consumed only once.
Content.Source content = this.content.getAndSet(null);
if (content != null)
onSubscribe(subscriber, content);
else
onMultiSubscribe(subscriber);
}
private static class SubscriptionImpl implements Flow.Subscription
private void onSubscribe(Flow.Subscriber<? super Content.Chunk> subscriber, Content.Source content)
{
private final AutoLock lock = new AutoLock();
private final Content.Source content;
private final Flow.Subscriber<? super Content.Chunk> subscriber;
private long demand;
private boolean stalled;
private boolean cancelled;
private boolean terminated;
public SubscriptionImpl(Content.Source content, Flow.Subscriber<? super Content.Chunk> subscriber)
// As per rule 1.9, we need to throw a `java.lang.NullPointerException`
// if the `Subscriber` is `null`
if (subscriber == null)
{
this.content = content;
this.subscriber = subscriber;
this.stalled = true;
NullPointerException error = new NullPointerException("Flow.Subscriber must not be null");
content.fail(error);
throw error;
}
ActiveSubscription subscription = new ActiveSubscription(content, subscriber);
// As per rule 1.9, this method must return normally (i.e. not throw).
try
{
subscriber.onSubscribe(subscription);
}
catch (Throwable err)
{
// As per rule 2.13, we MUST consider subscription cancelled and
// MUST raise this error condition in a fashion that is adequate for the runtime environment.
subscription.cancel(new SuppressedException(err));
if (LOG.isTraceEnabled())
LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err);
}
}
private void onMultiSubscribe(Flow.Subscriber<? super Content.Chunk> subscriber)
{
// As per rule 1.9, we need to throw a `java.lang.NullPointerException`
// if the `Subscriber` is `null`
if (subscriber == null)
throw new NullPointerException("Flow.Subscriber must not be null");
ExhaustedSubscription subscription = new ExhaustedSubscription();
// As per 1.9, this method must return normally (i.e. not throw).
try
{
// As per rule 1.9, the only legal way to signal about Subscriber rejection
// is by calling onError (after calling onSubscribe).
subscriber.onSubscribe(subscription);
subscriber.onError(new IllegalStateException("Content.Source was exhausted."));
}
catch (Throwable err)
{
// As per rule 2.13, we MUST consider subscription cancelled and
// MUST raise this error condition in a fashion that is adequate for the runtime environment.
if (LOG.isTraceEnabled())
LOG.trace("Flow.Subscriber " + subscriber + " violated rule 2.13", err);
}
}
private static final class ExhaustedSubscription implements Flow.Subscription
{
@Override
public void request(long n)
{
boolean process = false;
Throwable failure = null;
try (AutoLock ignored = lock.lock())
{
if (cancelled || terminated)
return;
if (n <= 0)
{
terminated = true;
failure = new IllegalArgumentException("invalid demand " + n);
}
demand = MathUtils.cappedAdd(demand, n);
if (stalled)
{
stalled = false;
process = true;
}
}
if (failure != null)
subscriber.onError(failure);
else if (process)
process();
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
}
@Override
public void cancel()
{
try (AutoLock ignored = lock.lock())
{
cancelled = true;
// As per rules 3.6 and 3.7, after the Subscription is cancelled all operations MUST be NOPs.
}
}
private 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())
{
if (demand > 0)
{
--demand;
this.cancelled = new AtomicReference<>(null);
this.demand = new AtomicLong(0);
this.content = content;
this.subscriber = subscriber;
}
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;
return;
Throwable cancelled = this.cancelled.get();
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();
if (chunk == null)
{
try (AutoLock ignored = lock.lock())
{
// Restore the demand decremented above.
++demand;
stalled = true;
}
content.demand(this::process);
return;
content.demand(this::succeeded);
return Action.SCHEDULED;
}
if (Content.Chunk.isFailure(chunk))
{
terminate();
if (!chunk.isLast())
content.fail(chunk.getFailure());
subscriber.onError(chunk.getFailure());
return;
cancel(chunk.getFailure());
chunk.release();
return Action.IDLE;
}
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();
if (chunk.isLast())
{
terminate();
// Reactive Stream specification rule 2.9 allows Publishers to call onComplete()
// even without demand, and Subscribers must be prepared to handle this case.
subscriber.onComplete();
cancel(COMPLETED);
return Action.IDLE;
}
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;
}
// 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 Subscribers 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");
}
}
}

View File

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

View File

@ -13,10 +13,14 @@
package org.eclipse.jetty.quic.common;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.EventListener;
@ -423,6 +427,31 @@ public abstract class QuicSession extends ContainerLifeCycle
}
}
/**
* <p>Returns the peer certificates chain.</p>
* <p>Due to current Quiche C API limitations (that the Rust version does not have),
* only the last certificate in the chain is returned.
* This may change in the future when the C APIs are aligned to the Rust APIs.</p>
*
* @return the peer certificates chain (currently only the last certificate in the chain)
*/
public X509Certificate[] getPeerCertificates()
{
try
{
byte[] encoded = quicheConnection.getPeerCertificate();
if (encoded == null)
return null;
CertificateFactory factory = CertificateFactory.getInstance("X509");
X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(encoded));
return new X509Certificate[]{certificate};
}
catch (CertificateException x)
{
return null;
}
}
@Override
public void dump(Appendable out, String indent) throws IOException
{

View File

@ -13,9 +13,11 @@
package org.eclipse.jetty.quic.common;
import java.io.EOFException;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.stream.IntStream;
@ -40,6 +42,7 @@ public class QuicStreamEndPoint extends AbstractEndPoint
{
private static final Logger LOG = LoggerFactory.getLogger(QuicStreamEndPoint.class);
private static final ByteBuffer LAST_FLAG = ByteBuffer.allocate(0);
private static final ByteBuffer EMPTY_WRITABLE_BUFFER = ByteBuffer.allocate(0);
private final QuicSession session;
private final long streamId;
@ -221,6 +224,15 @@ public class QuicStreamEndPoint extends AbstractEndPoint
return session;
}
@Override
public SslSessionData getSslSessionData()
{
X509Certificate[] peerCertificates = getQuicSession().getPeerCertificates();
if (peerCertificates == null)
return null;
return SslSessionData.from(null, null, null, peerCertificates);
}
public void onWritable()
{
if (LOG.isDebugEnabled())
@ -255,12 +267,25 @@ public class QuicStreamEndPoint extends AbstractEndPoint
}
else
{
QuicStreamEndPoint streamEndPoint = getQuicSession().getStreamEndPoint(streamId);
if (streamEndPoint.isStreamFinished())
if (isStreamFinished())
{
EofException e = new EofException();
streamEndPoint.getFillInterest().onFail(e);
streamEndPoint.getQuicSession().onFailure(e);
// Check if the stream was finished normally.
try
{
fill(EMPTY_WRITABLE_BUFFER);
}
catch (EOFException x)
{
// Got reset.
getFillInterest().onFail(x);
getQuicSession().onFailure(x);
}
catch (Throwable x)
{
EofException e = new EofException(x);
getFillInterest().onFail(e);
getQuicSession().onFailure(e);
}
}
}
return interested;

View File

@ -152,6 +152,8 @@ public abstract class QuicheConnection
public abstract CloseInfo getLocalCloseInfo();
public abstract byte[] getPeerCertificate();
public static class CloseInfo
{
private final long error;

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.quic.quiche.foreign;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
@ -518,6 +519,7 @@ public class ForeignQuicheConnection extends QuicheConnection
}
}
@Override
public byte[] getPeerCertificate()
{
try (AutoLock ignore = lock.lock())
@ -532,7 +534,7 @@ public class ForeignQuicheConnection extends QuicheConnection
quiche_h.quiche_conn_peer_cert(quicheConn, outSegment, outLenSegment);
long outLen = outLenSegment.get(NativeHelper.C_LONG, 0L);
if (outLen == 0L)
if (outLen <= 0L)
return null;
byte[] out = new byte[(int)outLen];
// dereference outSegment pointer
@ -917,14 +919,19 @@ public class ForeignQuicheConnection extends QuicheConnection
MemorySegment fin = scope.allocate(NativeHelper.C_CHAR);
read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment, buffer.remaining(), fin);
if (read > 0)
{
int prevPosition = buffer.position();
buffer.put(bufferSegment.asByteBuffer().limit((int)read));
buffer.position(prevPosition);
}
}
}
if (read == quiche_error.QUICHE_ERR_DONE)
return isStreamFinished(streamId) ? -1 : 0;
if (read == quiche_error.QUICHE_ERR_STREAM_RESET)
throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
if (read < 0L)
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
buffer.position((int)(buffer.position() + read));

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.quic.quiche.jna;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@ -413,6 +414,7 @@ public class JnaQuicheConnection extends QuicheConnection
}
}
@Override
public byte[] getPeerCertificate()
{
try (AutoLock ignore = lock.lock())
@ -424,6 +426,8 @@ public class JnaQuicheConnection extends QuicheConnection
size_t_pointer out_len = new size_t_pointer();
LibQuiche.INSTANCE.quiche_conn_peer_cert(quicheConn, out, out_len);
int len = out_len.getPointee().intValue();
if (len <= 0)
return null;
return out.getValueAsBytes(len);
}
}
@ -744,6 +748,8 @@ public class JnaQuicheConnection extends QuicheConnection
int read = LibQuiche.INSTANCE.quiche_conn_stream_recv(quicheConn, new uint64_t(streamId), buffer, new size_t(buffer.remaining()), fin).intValue();
if (read == quiche_error.QUICHE_ERR_DONE)
return isStreamFinished(streamId) ? -1 : 0;
if (read == quiche_error.QUICHE_ERR_STREAM_RESET)
throw new EOFException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
if (read < 0L)
throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read));
buffer.position(buffer.position() + read);

View File

@ -56,8 +56,10 @@ public interface Deployable
String CONFIGURATION_CLASSES = "jetty.deploy.configurationClasses";
String CONTAINER_SCAN_JARS = "jetty.deploy.containerScanJarPattern";
String CONTEXT_PATH = "jetty.deploy.contextPath";
String CONTEXT_HANDLER_CLASS = "jetty.deploy.contextHandlerClass";
String DEFAULTS_DESCRIPTOR = "jetty.deploy.defaultsDescriptor";
String ENVIRONMENT = "environment";
String ENVIRONMENT_XML = "jetty.deploy.environmentXml";
String EXTRACT_WARS = "jetty.deploy.extractWars";
String PARENT_LOADER_PRIORITY = "jetty.deploy.parentLoaderPriority";
String SCI_EXCLUSION_PATTERN = "jetty.deploy.servletContainerInitializerExclusionPattern";

View File

@ -88,8 +88,7 @@ public interface HttpChannel extends Invocable
/**
* <p>Notifies this {@code HttpChannel} that an asynchronous failure happened.</p>
* <p>Typical failure examples could be HTTP/2 resets or
* protocol failures (for example, invalid request bytes).</p>
* <p>Typical failure examples could be protocol failures (for example, invalid request bytes).</p>
*
* @param failure the failure cause.
* @return a {@code Runnable} that performs the failure action, or {@code null}
@ -98,6 +97,18 @@ public interface HttpChannel extends Invocable
*/
Runnable onFailure(Throwable failure);
/**
* <p>Notifies this {@code HttpChannel} that an asynchronous notification was received indicating
* a remote failure happened.</p>
* <p>Typical failure examples could be HTTP/2 resets.</p>
*
* @param failure the failure cause.
* @return a {@code Runnable} that performs the failure action, or {@code null}
* if no failure action needs be performed by the calling thread
* @see Request#addFailureListener(Consumer)
*/
Runnable onRemoteFailure(Throwable failure);
/**
* <p>Notifies this {@code HttpChannel} that an asynchronous close happened.</p>
*

View File

@ -392,6 +392,17 @@ public class HttpChannelState implements HttpChannel, Components
@Override
public Runnable onFailure(Throwable x)
{
return onFailure(x, false);
}
@Override
public Runnable onRemoteFailure(Throwable x)
{
return onFailure(x, true);
}
private Runnable onFailure(Throwable x, boolean remote)
{
HttpStream stream;
Runnable task;
@ -437,7 +448,9 @@ public class HttpChannelState implements HttpChannel, Components
// Notify the failure listeners only once.
Consumer<Throwable> onFailure = _onFailure;
_onFailure = null;
Runnable invokeOnFailureListeners = onFailure == null ? null : () ->
boolean skipListeners = remote && !getHttpConfiguration().isNotifyRemoteAsyncErrors();
Runnable invokeOnFailureListeners = onFailure == null || skipListeners ? null : () ->
{
try
{

View File

@ -1426,11 +1426,13 @@ public class StartArgs
{
for (String moduleName : moduleNames)
{
modules.add(moduleName);
if (modules.add(moduleName))
{
Set<String> set = sources.computeIfAbsent(moduleName, k -> new HashSet<>());
set.add(source);
}
}
}
public void setAllModules(Modules allModules)
{

View File

@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -183,4 +184,27 @@ public class MainTest
);
assertThat(commandLine, containsString(expectedExpansion));
}
@Test
public void testModulesDeclaredTwice() throws Exception
{
List<String> cmdLineArgs = new ArrayList<>();
Path homePath = MavenPaths.findTestResourceDir("dist-home");
Path basePath = MavenPaths.findTestResourceDir("overdeclared-modules");
cmdLineArgs.add("jetty.home=" + homePath);
cmdLineArgs.add("user.dir=" + basePath);
Main main = new Main();
cmdLineArgs.add("--module=main");
// The "main" module is enabled in both ...
// 1) overdeclared-modules/start.d/config.ini
// 2) command-line
// This shouldn't result in an error
StartArgs args = main.processCommandLine(cmdLineArgs.toArray(new String[0]));
assertThat(args.getSelectedModules(), hasItem("main"));
}
}

View File

@ -1 +0,0 @@
--module=main

View File

@ -0,0 +1 @@
--modules=main

View File

@ -18,11 +18,6 @@
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-openid</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>

View File

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

View File

@ -671,7 +671,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
catch (Throwable t)
{
onWriteComplete(false, t);
if (t instanceof IOException)
throw t;
throw new IOException(t);
}
}
}

View File

@ -112,7 +112,7 @@
<configuration>
<argLine>@{argLine}
${jetty.surefire.argLine}
--enable-native-access org.eclipse.jetty.quic.quiche.foreign</argLine>
--enable-native-access=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>

View File

@ -102,6 +102,14 @@ public class AbstractTest
return transports;
}
public static Collection<Transport> transportsSecure()
{
EnumSet<Transport> transports = EnumSet.of(Transport.HTTPS, Transport.H2, Transport.H3);
if ("ci".equals(System.getProperty("env")))
transports.remove(Transport.H3);
return transports;
}
@BeforeEach
public void prepare()
{

View File

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

View File

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

View File

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

View File

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

View File

@ -3,46 +3,12 @@
[description]
Adds OpenId Connect authentication to the server.
[environment]
ee8
[depend]
ee8-security
openid
client
[lib]
lib/jetty-ee8-openid-${jetty.version}.jar
lib/jetty-util-ajax-${jetty.version}.jar
[files]
basehome:modules/openid/jetty-ee8-openid-baseloginservice.xml|etc/openid-baseloginservice.xml
[xml]
etc/openid-baseloginservice.xml
etc/jetty-ee8-openid.xml
[ini-template]
## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration")
# jetty.openid.provider=https://id.example.com/
## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible)
# jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization
## The OpenID Identity Provider's token endpoint (optional if the metadata of the OP is accessible)
# jetty.openid.provider.tokenEndpoint=https://id.example.com/token
## The Client Identifier
# jetty.openid.clientId=test1234
## The Client Secret
# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P
## Additional Scopes to Request
# jetty.openid.scopes=email,profile
## Whether to Authenticate users not found by base LoginService
# jetty.openid.authenticateNewUsers=false
## True if all certificates should be trusted by the default SslContextFactory
# jetty.openid.sslContextFactory.trustAll=false
## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic).
# jetty.openid.authenticationMethod=client_secret_post

View File

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

View File

@ -740,7 +740,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
catch (Throwable t)
{
onWriteComplete(false, t);
if (t instanceof IOException)
throw t;
throw new IOException(t);
}
}
}

View File

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

View File

@ -9,46 +9,6 @@ ee9
[depend]
ee9-security
openid
client
[lib]
lib/jetty-ee9-openid-${jetty.version}.jar
lib/jetty-util-ajax-${jetty.version}.jar
[files]
basehome:modules/openid/jetty-ee9-openid-baseloginservice.xml|etc/openid-baseloginservice.xml
[xml]
etc/openid-baseloginservice.xml
etc/jetty-ee9-openid.xml
[ini-template]
## The OpenID Identity Provider's issuer ID (the entire URL *before* ".well-known/openid-configuration")
# jetty.openid.provider=https://id.example.com/
## The OpenID Identity Provider's authorization endpoint (optional if the metadata of the OP is accessible)
# jetty.openid.provider.authorizationEndpoint=https://id.example.com/authorization
## The OpenID Identity Provider's token endpoint (optional if the metadata of the OP is accessible)
# jetty.openid.provider.tokenEndpoint=https://id.example.com/token
## The Client Identifier
# jetty.openid.clientId=test1234
## The Client Secret
# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P
## Additional Scopes to Request
# jetty.openid.scopes=email,profile
## Whether to Authenticate users not found by base LoginService
# jetty.openid.authenticateNewUsers=false
## True if all certificates should be trusted by the default SslContextFactory
# jetty.openid.sslContextFactory.trustAll=false
## What authentication method to use with the Token Endpoint (client_secret_post, client_secret_basic).
# jetty.openid.authenticationMethod=client_secret_post
## Whether the user should be logged out after the idToken expires.
# jetty.openid.logoutWhenIdTokenIsExpired=false

View File

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

View File

@ -112,7 +112,7 @@
<configuration>
<argLine>@{argLine}
${jetty.surefire.argLine}
--enable-native-access org.eclipse.jetty.quic.quiche.foreign</argLine>
--enable-native-access=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>

View File

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

View File

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

@ -394,6 +394,7 @@
<plexus-xml.version>4.0.3</plexus-xml.version>
<project.build.outputTimestamp>2024-04-26T07:15:13Z</project.build.outputTimestamp>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<reactive-streams.version>1.0.4</reactive-streams.version>
<settingsPath>src/it/settings.xml</settingsPath>
<slf4j.version>2.0.12</slf4j.version>
<spifly.version>1.3.7</spifly.version>
@ -402,6 +403,7 @@
<surefire.rerunFailingTestsCount>0</surefire.rerunFailingTestsCount>
<swissbox.version>1.8.3</swissbox.version>
<testcontainers.version>1.19.7</testcontainers.version>
<testng.version>7.10.2</testng.version>
<tinybundles.version>3.0.0</tinybundles.version>
<versions.maven.plugin.version>2.16.2</versions.maven.plugin.version>
<wildfly.common.version>1.7.0.Final</wildfly.common.version>
@ -1249,6 +1251,11 @@
<artifactId>osgi.core</artifactId>
<version>${org.osgi.core.version}</version>
</dependency>
<dependency>
<groupId>org.reactivestreams</groupId>
<artifactId>reactive-streams-tck-flow</artifactId>
<version>${reactive-streams.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl104-over-slf4j</artifactId>
@ -1285,6 +1292,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.common</groupId>
<artifactId>wildfly-common</artifactId>

View File

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

View File

@ -15,6 +15,7 @@
<module>jetty-jmh</module>
<module>jetty-test-multipart</module>
<module>jetty-test-session-common</module>
<module>test-cross-context-dispatch</module>
<module>test-distribution</module>
<module>test-integration</module>
<module>test-jpms</module>

View File

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

View File

@ -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 + "]";
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 + '\'' +
'}';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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