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

This commit is contained in:
Lachlan Roberts 2023-06-06 15:21:45 +10:00
commit 3fd9ac2875
41 changed files with 1536 additions and 83 deletions

View File

@ -14,8 +14,8 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- versions for these plugins are not based on parent pom -->
<maven.remote-resources.plugin.version>3.0.0</maven.remote-resources.plugin.version>
<maven.surefire.plugin.version>3.0.0</maven.surefire.plugin.version>
<maven.remote-resources.plugin.version>3.1.0</maven.remote-resources.plugin.version>
<maven.surefire.plugin.version>3.1.0</maven.surefire.plugin.version>
<maven.deploy.skip>true</maven.deploy.skip>
<maven.javadoc.skip>true</maven.javadoc.skip>
<skipTests>true</skipTests>

View File

@ -35,10 +35,10 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -39,6 +39,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
@ -148,6 +151,20 @@ public class LowLevelQuicheClientCertTest
assertThat(fed, is(expectedSize));
}
private void drainServerToFeedClient(Map.Entry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> entry, int expectedSizeLowerBound, int expectedSizeUpperBound) throws IOException
{
ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey();
ForeignIncubatorQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(both(greaterThanOrEqualTo(expectedSizeLowerBound)).and(lessThanOrEqualTo(expectedSizeUpperBound))));
buffer.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(both(greaterThanOrEqualTo(expectedSizeLowerBound)).and(lessThanOrEqualTo(expectedSizeUpperBound))));
}
private void drainClientToFeedServer(Map.Entry<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> entry, int expectedSize) throws IOException
{
ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey();
@ -218,7 +235,7 @@ public class LowLevelQuicheClientCertTest
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
// 2nd round (needed b/c of client cert)
drainServerToFeedClient(entry, 71);
drainServerToFeedClient(entry, 71, 72);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));

View File

@ -38,6 +38,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
@ -147,6 +150,20 @@ public class LowLevelQuicheClientCertTest
assertThat(fed, is(expectedSize));
}
private void drainServerToFeedClient(Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry, int expectedSizeLowerBound, int expectedSizeUpperBound) throws IOException
{
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(both(greaterThanOrEqualTo(expectedSizeLowerBound)).and(lessThanOrEqualTo(expectedSizeUpperBound))));
buffer.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress);
assertThat(fed, is(both(greaterThanOrEqualTo(expectedSizeLowerBound)).and(lessThanOrEqualTo(expectedSizeUpperBound))));
}
private void drainClientToFeedServer(Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry, int expectedSize) throws IOException
{
JnaQuicheConnection clientQuicheConnection = entry.getKey();
@ -217,7 +234,7 @@ public class LowLevelQuicheClientCertTest
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
// 2nd round (needed b/c of client cert)
drainServerToFeedClient(entry, 72);
drainServerToFeedClient(entry, 71, 72);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));

View File

@ -172,19 +172,28 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
public ContextHandler(String contextPath)
{
this(null, contextPath);
_context = newContext();
if (contextPath != null)
setContextPath(contextPath);
if (File.separatorChar == '/')
addAliasCheck(new SymlinkAllowedResourceAliasChecker(this));
// If the current classloader (or the one that loaded this class) is different
// from the Server classloader, then use that as the initial classloader for the context.
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null)
classLoader = this.getClass().getClassLoader();
if (classLoader != Server.class.getClassLoader())
_classLoader = classLoader;
}
@Deprecated
public ContextHandler(Handler.Container parent, String contextPath)
{
_context = newContext();
if (contextPath != null)
setContextPath(contextPath);
this(contextPath);
Container.setAsParent(parent, this);
if (File.separatorChar == '/')
addAliasCheck(new SymlinkAllowedResourceAliasChecker(this));
}
@Override

View File

@ -55,6 +55,11 @@
<artifactId>jetty-ee10-cdi</artifactId>
<version>12.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-fcgi-proxy</artifactId>
<version>12.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-glassfish-jstl</artifactId>

View File

@ -0,0 +1,48 @@
<?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">
<parent>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-ee10-fcgi-proxy</artifactId>
<name>EE10 :: FCGI Proxy</name>
<properties>
<bundle-symbolic-name>${project.groupId}.fcgi.proxy</bundle-symbolic-name>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-proxy</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
[description]
Enables support for EE10 FastCGI proxying.
[environment]
ee10
[tags]
fcgi
proxy
[depends]
fcgi
[lib]
lib/jetty-ee10-fcgi-proxy-${jetty.version}.jar
lib/jetty-ee10-proxy-${jetty.version}.jar

View File

@ -0,0 +1,23 @@
//
// ========================================================================
// 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
// ========================================================================
//
module org.eclipse.jetty.ee10.fcgi.proxy
{
requires transitive org.eclipse.jetty.ee10.proxy;
requires transitive org.eclipse.jetty.fcgi.client;
requires transitive org.eclipse.jetty.server;
requires static jakarta.servlet;
exports org.eclipse.jetty.ee10.fcgi.proxy;
}

View File

@ -0,0 +1,304 @@
//
// ========================================================================
// 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.fcgi.proxy;
import java.net.URI;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.ee10.proxy.AsyncProxyServlet;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ProcessorUtils;
/**
* Specific implementation of {@link org.eclipse.jetty.ee10.proxy.AsyncProxyServlet.Transparent} for FastCGI.
* <p>
* This servlet accepts an HTTP request and transforms it into a FastCGI request
* that is sent to the FastCGI server specified in the {@code proxyTo}
* init-param.
* <p>
* This servlet accepts these additional {@code init-param}s:
* <ul>
* <li>{@code scriptRoot}, mandatory, that must be set to the directory where
* the application that must be served via FastCGI is installed and corresponds to
* the FastCGI DOCUMENT_ROOT parameter</li>
* <li>{@code scriptPattern}, optional, defaults to {@code (.+?\.php)},
* that specifies a regular expression with at least 1 and at most 2 groups that specify
* respectively:
* <ul>
* <li>the FastCGI SCRIPT_NAME parameter</li>
* <li>the FastCGI PATH_INFO parameter</li>
* </ul></li>
* <li>{@code fastCGI.HTTPS}, optional, defaults to false, that specifies whether
* to force the FastCGI {@code HTTPS} parameter to the value {@code on}</li>
* <li>{@code fastCGI.envNames}, optional, a comma separated list of environment variable
* names read via {@link System#getenv(String)} that are forwarded as FastCGI parameters.</li>
* <li>{@code unixDomainPath}, optional, that specifies the Unix-Domain path the FastCGI
* server listens to.</li>
* </ul>
*
* @see TryFilesFilter
*/
public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent
{
public static final String SCRIPT_ROOT_INIT_PARAM = "scriptRoot";
public static final String SCRIPT_PATTERN_INIT_PARAM = "scriptPattern";
public static final String ORIGINAL_URI_ATTRIBUTE_INIT_PARAM = "originalURIAttribute";
public static final String ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM = "originalQueryAttribute";
public static final String FASTCGI_HTTPS_INIT_PARAM = "fastCGI.HTTPS";
public static final String FASTCGI_ENV_NAMES_INIT_PARAM = "fastCGI.envNames";
private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remoteAddr";
private static final String REMOTE_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remotePort";
private static final String SERVER_NAME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverName";
private static final String SERVER_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverAddr";
private static final String SERVER_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverPort";
private static final String SCHEME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".scheme";
private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestURI";
private static final String REQUEST_QUERY_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestQuery";
private Pattern scriptPattern;
private String originalURIAttribute;
private String originalQueryAttribute;
private boolean fcgiHTTPS;
private Set<String> fcgiEnvNames;
@Override
public void init() throws ServletException
{
super.init();
String value = getInitParameter(SCRIPT_PATTERN_INIT_PARAM);
if (value == null)
value = "(.+?\\.php)";
scriptPattern = Pattern.compile(value);
originalURIAttribute = getInitParameter(ORIGINAL_URI_ATTRIBUTE_INIT_PARAM);
originalQueryAttribute = getInitParameter(ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM);
fcgiHTTPS = Boolean.parseBoolean(getInitParameter(FASTCGI_HTTPS_INIT_PARAM));
fcgiEnvNames = Collections.emptySet();
String envNames = getInitParameter(FASTCGI_ENV_NAMES_INIT_PARAM);
if (envNames != null)
{
fcgiEnvNames = Stream.of(envNames.split(","))
.map(String::trim)
.collect(Collectors.toSet());
}
}
@Override
protected HttpClient newHttpClient()
{
ServletConfig config = getServletConfig();
String scriptRoot = config.getInitParameter(SCRIPT_ROOT_INIT_PARAM);
if (scriptRoot == null)
throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured");
ClientConnector connector;
String unixDomainPath = config.getInitParameter("unixDomainPath");
if (unixDomainPath != null)
{
connector = ClientConnector.forUnixDomain(Path.of(unixDomainPath));
}
else
{
int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2);
String value = config.getInitParameter("selectors");
if (value != null)
selectors = Integer.parseInt(value);
connector = new ClientConnector();
connector.setSelectors(selectors);
}
return new HttpClient(new ProxyHttpClientTransportOverFCGI(connector, scriptRoot));
}
@Override
protected void sendProxyRequest(HttpServletRequest request, HttpServletResponse proxyResponse, Request proxyRequest)
{
proxyRequest.attribute(REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr());
proxyRequest.attribute(REMOTE_PORT_ATTRIBUTE, String.valueOf(request.getRemotePort()));
proxyRequest.attribute(SERVER_NAME_ATTRIBUTE, request.getServerName());
proxyRequest.attribute(SERVER_ADDR_ATTRIBUTE, request.getLocalAddr());
proxyRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(request.getLocalPort()));
proxyRequest.attribute(SCHEME_ATTRIBUTE, request.getScheme());
// Has the original URI been rewritten ?
String originalURI = null;
String originalQuery = null;
if (originalURIAttribute != null)
originalURI = (String)request.getAttribute(originalURIAttribute);
if (originalURI != null && originalQueryAttribute != null)
{
originalQuery = (String)request.getAttribute(originalQueryAttribute);
if (originalQuery != null)
originalURI += "?" + originalQuery;
}
if (originalURI == null)
{
// If we are forwarded or included, retain the original request URI.
String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
if (originalPath == null)
{
originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
}
if (originalPath != null)
{
originalURI = originalPath;
if (originalQuery != null)
originalURI += "?" + originalQuery;
}
}
if (originalURI != null)
proxyRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI);
if (originalQuery != null)
proxyRequest.attribute(REQUEST_QUERY_ATTRIBUTE, originalQuery);
// If the Host header is missing, add it.
if (!proxyRequest.getHeaders().contains(HttpHeader.HOST))
{
String server = request.getServerName();
int port = request.getServerPort();
if (port != HttpScheme.getDefaultPort(request.getScheme()))
server += ":" + port;
String host = server;
proxyRequest.headers(headers -> headers
.put(HttpHeader.HOST, host)
.put(HttpHeader.X_FORWARDED_HOST, host));
}
// PHP does not like multiple Cookie headers, coalesce into one.
List<String> cookies = proxyRequest.getHeaders().getValuesList(HttpHeader.COOKIE);
if (cookies.size() > 1)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < cookies.size(); ++i)
{
if (i > 0)
builder.append("; ");
String cookie = cookies.get(i);
builder.append(cookie);
}
proxyRequest.headers(headers -> headers.put(HttpHeader.COOKIE, builder.toString()));
}
super.sendProxyRequest(request, proxyResponse, proxyRequest);
}
protected void customizeFastCGIHeaders(Request proxyRequest, HttpFields.Mutable fastCGIHeaders)
{
for (String envName : fcgiEnvNames)
{
String envValue = System.getenv(envName);
if (envValue != null)
fastCGIHeaders.put(envName, envValue);
}
fastCGIHeaders.remove("HTTP_PROXY");
fastCGIHeaders.put(FCGI.Headers.REMOTE_ADDR, (String)proxyRequest.getAttributes().get(REMOTE_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.REMOTE_PORT, (String)proxyRequest.getAttributes().get(REMOTE_PORT_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_NAME, (String)proxyRequest.getAttributes().get(SERVER_NAME_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_ADDR, (String)proxyRequest.getAttributes().get(SERVER_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_PORT, (String)proxyRequest.getAttributes().get(SERVER_PORT_ATTRIBUTE));
if (fcgiHTTPS || HttpScheme.HTTPS.is((String)proxyRequest.getAttributes().get(SCHEME_ATTRIBUTE)))
fastCGIHeaders.put(FCGI.Headers.HTTPS, "on");
URI proxyRequestURI = proxyRequest.getURI();
String rawPath = proxyRequestURI == null ? proxyRequest.getPath() : proxyRequestURI.getRawPath();
String rawQuery = proxyRequestURI == null ? null : proxyRequestURI.getRawQuery();
String requestURI = (String)proxyRequest.getAttributes().get(REQUEST_URI_ATTRIBUTE);
if (requestURI == null)
{
requestURI = rawPath;
if (rawQuery != null)
requestURI += "?" + rawQuery;
}
fastCGIHeaders.put(FCGI.Headers.REQUEST_URI, requestURI);
String requestQuery = (String)proxyRequest.getAttributes().get(REQUEST_QUERY_ATTRIBUTE);
if (requestQuery != null)
fastCGIHeaders.put(FCGI.Headers.QUERY_STRING, requestQuery);
String scriptName = rawPath;
Matcher matcher = scriptPattern.matcher(rawPath);
if (matcher.matches())
{
// Expect at least one group in the regular expression.
scriptName = matcher.group(1);
// If there is a second group, map it to PATH_INFO.
if (matcher.groupCount() > 1)
fastCGIHeaders.put(FCGI.Headers.PATH_INFO, matcher.group(2));
}
fastCGIHeaders.put(FCGI.Headers.SCRIPT_NAME, scriptName);
String root = fastCGIHeaders.get(FCGI.Headers.DOCUMENT_ROOT);
fastCGIHeaders.put(FCGI.Headers.SCRIPT_FILENAME, root + scriptName);
}
private class ProxyHttpClientTransportOverFCGI extends HttpClientTransportOverFCGI
{
private ProxyHttpClientTransportOverFCGI(ClientConnector connector, String scriptRoot)
{
super(connector, scriptRoot);
}
@Override
public void customize(Request request, HttpFields.Mutable fastCGIHeaders)
{
super.customize(request, fastCGIHeaders);
customizeFastCGIHeaders(request, fastCGIHeaders);
if (_log.isDebugEnabled())
{
TreeMap<String, String> fcgi = new TreeMap<>();
for (HttpField field : fastCGIHeaders)
{
fcgi.put(field.getName(), field.getValue());
}
String eol = System.lineSeparator();
_log.debug("FastCGI variables {}{}", eol, fcgi.entrySet().stream()
.map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(eol)));
}
}
}
}

View File

@ -0,0 +1,138 @@
//
// ========================================================================
// 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.fcgi.proxy;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.util.StringUtil;
/**
* Inspired by nginx's try_files functionality.
* <p>
* This filter accepts the {@code files} init-param as a list of space-separated
* file URIs. The special token {@code $path} represents the current request URL's
* path (the portion after the context path).
* <p>
* Typical example of how this filter can be configured is the following:
* <pre>
* &lt;filter&gt;
* &lt;filter-name&gt;try_files&lt;/filter-name&gt;
* &lt;filter-class&gt;org.eclipse.jetty.fcgi.server.proxy.TryFilesFilter&lt;/filter-class&gt;
* &lt;init-param&gt;
* &lt;param-name&gt;files&lt;/param-name&gt;
* &lt;param-value&gt;/maintenance.html $path /index.php?p=$path&lt;/param-value&gt;
* &lt;/init-param&gt;
* &lt;/filter&gt;
* </pre>
* For a request such as {@code /context/path/to/resource.ext}, this filter will
* try to serve the {@code /maintenance.html} file if it finds it; failing that,
* it will try to serve the {@code /path/to/resource.ext} file if it finds it;
* failing that it will forward the request to {@code /index.php?p=/path/to/resource.ext}.
* The last file URI specified in the list is therefore the "fallback" to which the request
* is forwarded to in case no previous files can be found.
* <p>
* The files are resolved using {@link ServletContext#getResource(String)} to make sure
* that only files visible to the application are served.
*
* @see FastCGIProxyServlet
*/
public class TryFilesFilter implements Filter
{
public static final String FILES_INIT_PARAM = "files";
private String[] files;
@Override
public void init(FilterConfig config) throws ServletException
{
String param = config.getInitParameter(FILES_INIT_PARAM);
if (param == null)
throw new ServletException(String.format("Missing mandatory parameter '%s'", FILES_INIT_PARAM));
files = param.split(" ");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
for (int i = 0; i < files.length - 1; ++i)
{
String file = files[i];
String resolved = resolve(httpRequest, file);
URL url = request.getServletContext().getResource(resolved);
if (url == null)
continue;
if (Files.isReadable(toPath(url)))
{
chain.doFilter(httpRequest, httpResponse);
return;
}
}
// The last one is the fallback
fallback(httpRequest, httpResponse, chain, files[files.length - 1]);
}
private Path toPath(URL url) throws IOException
{
try
{
return Paths.get(url.toURI());
}
catch (URISyntaxException x)
{
throw new IOException(x);
}
}
protected void fallback(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String fallback) throws IOException, ServletException
{
String resolved = resolve(request, fallback);
request.getServletContext().getRequestDispatcher(resolved).forward(request, response);
}
private String resolve(HttpServletRequest request, String value)
{
String path = request.getServletPath();
String info = request.getPathInfo();
if (info != null)
path += info;
if (!path.startsWith("/"))
path = "/" + path;
return StringUtil.replace(value, "$path", path);
}
@Override
public void destroy()
{
}
}

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.ee10.fcgi.proxy;
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
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.client.HttpClient;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TryFilesFilterTest
{
private Server server;
private ServerConnector connector;
private ServerConnector sslConnector;
private HttpClient client;
private String forwardPath;
public void prepare(HttpServlet servlet) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
serverSslContextFactory.setKeyStorePassword("storepwd");
sslConnector = new ServerConnector(server, serverSslContextFactory);
server.addConnector(sslConnector);
ServletContextHandler context = new ServletContextHandler(server, "/");
FilterHolder filterHolder = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
forwardPath = "/index.php";
filterHolder.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path " + forwardPath + "?p=$path");
context.addServlet(new ServletHolder(servlet), "/*");
ClientConnector clientConnector = new ClientConnector();
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(true);
// clientSslContextFactory.setEndpointIdentificationAlgorithm(null);
// clientSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
// clientSslContextFactory.setKeyStorePassword("storepwd");
clientConnector.setSslContextFactory(clientSslContextFactory);
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector));
server.addBean(client);
server.start();
}
@AfterEach
public void dispose() throws Exception
{
server.stop();
}
@Test
public void testHTTPSRequestIsForwarded() throws Exception
{
final String path = "/one/";
prepare(new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
{
assertTrue("https".equalsIgnoreCase(req.getScheme()));
assertTrue(req.isSecure());
assertEquals(forwardPath, req.getRequestURI());
assertTrue(req.getQueryString().endsWith(path));
}
});
ContentResponse response = client.newRequest("localhost", sslConnector.getLocalPort())
.scheme("https")
.path(path)
.send();
assertEquals(200, response.getStatus());
}
}

View File

@ -0,0 +1,2 @@
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.fcgi.proxy.LEVEL=DEBUG

View File

@ -488,6 +488,11 @@
<artifactId>jetty-ee10-jndi</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-fcgi-proxy</artifactId>
<optional>true</optional>
</dependency>
<!-- Demo EE10 Apps -->
<dependency>
<groupId>org.eclipse.jetty.ee10.demos</groupId>

View File

@ -131,7 +131,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
private Throwable _unavailableException;
private Map<String, String> _resourceAliases;
private boolean _ownClassLoader = false;
private ClassLoader _initialClassLoader;
private boolean _configurationDiscovered = true;
private boolean _allowDuplicateFragmentNames = false;
private boolean _throwUnavailableOnStartupException = false;
@ -461,13 +461,9 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
}
// Configure classloader
_ownClassLoader = false;
if (getClassLoader() == null)
{
WebAppClassLoader classLoader = new WebAppClassLoader(this);
setClassLoader(classLoader);
_ownClassLoader = true;
}
_initialClassLoader = getClassLoader();
if (!(_initialClassLoader instanceof WebAppClassLoader))
setClassLoader(new WebAppClassLoader(_initialClassLoader, this));
if (LOG.isDebugEnabled())
{
@ -1258,13 +1254,13 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
}
finally
{
if (_ownClassLoader)
if (!(_initialClassLoader instanceof WebAppClassLoader))
{
ClassLoader loader = getClassLoader();
if (loader instanceof URLClassLoader)
((URLClassLoader)loader).close();
setClassLoader(null);
}
setClassLoader(_initialClassLoader);
_unavailableException = null;
}

View File

@ -64,6 +64,7 @@
<module>jetty-ee10-examples</module>
<module>jetty-ee10-bom</module>
<module>jetty-ee10-home</module>
<module>jetty-ee10-fcgi-proxy</module>
</modules>
<dependencyManagement>
@ -84,6 +85,11 @@
<artifactId>jetty-ee10-cdi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-fcgi-proxy</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-glassfish-jstl</artifactId>

View File

@ -55,6 +55,11 @@
<artifactId>jetty-ee9-cdi</artifactId>
<version>12.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-fcgi-proxy</artifactId>
<version>12.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-glassfish-jstl</artifactId>

View File

@ -197,5 +197,10 @@
<artifactId>jetty-ee9-websocket-jakarta-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -27,12 +27,7 @@
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>compile</scope>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>

View File

@ -0,0 +1,48 @@
<?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">
<parent>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-ee9-fcgi-proxy</artifactId>
<name>EE9 :: FCGI Proxy</name>
<properties>
<bundle-symbolic-name>${project.groupId}.fcgi.proxy</bundle-symbolic-name>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-proxy</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,16 @@
[description]
Enables support for EE9 FastCGI proxying.
[environment]
ee9
[tags]
fcgi
proxy
[depends]
fcgi
[lib]
lib/jetty-ee9-fcgi-proxy-${jetty.version}.jar
lib/jetty-ee9-proxy-${jetty.version}.jar

View File

@ -0,0 +1,23 @@
//
// ========================================================================
// 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
// ========================================================================
//
module org.eclipse.jetty.ee9.fcgi.proxy
{
requires transitive org.eclipse.jetty.ee9.proxy;
requires transitive org.eclipse.jetty.fcgi.client;
requires transitive org.eclipse.jetty.server;
requires static jetty.servlet.api;
exports org.eclipse.jetty.ee9.fcgi.proxy;
}

View File

@ -0,0 +1,304 @@
//
// ========================================================================
// 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.fcgi.proxy;
import java.net.URI;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.ee9.proxy.AsyncProxyServlet;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ProcessorUtils;
/**
* Specific implementation of {@link org.eclipse.jetty.ee9.proxy.AsyncProxyServlet.Transparent} for FastCGI.
* <p>
* This servlet accepts an HTTP request and transforms it into a FastCGI request
* that is sent to the FastCGI server specified in the {@code proxyTo}
* init-param.
* <p>
* This servlet accepts these additional {@code init-param}s:
* <ul>
* <li>{@code scriptRoot}, mandatory, that must be set to the directory where
* the application that must be served via FastCGI is installed and corresponds to
* the FastCGI DOCUMENT_ROOT parameter</li>
* <li>{@code scriptPattern}, optional, defaults to {@code (.+?\.php)},
* that specifies a regular expression with at least 1 and at most 2 groups that specify
* respectively:
* <ul>
* <li>the FastCGI SCRIPT_NAME parameter</li>
* <li>the FastCGI PATH_INFO parameter</li>
* </ul></li>
* <li>{@code fastCGI.HTTPS}, optional, defaults to false, that specifies whether
* to force the FastCGI {@code HTTPS} parameter to the value {@code on}</li>
* <li>{@code fastCGI.envNames}, optional, a comma separated list of environment variable
* names read via {@link System#getenv(String)} that are forwarded as FastCGI parameters.</li>
* <li>{@code unixDomainPath}, optional, that specifies the Unix-Domain path the FastCGI
* server listens to.</li>
* </ul>
*
* @see TryFilesFilter
*/
public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent
{
public static final String SCRIPT_ROOT_INIT_PARAM = "scriptRoot";
public static final String SCRIPT_PATTERN_INIT_PARAM = "scriptPattern";
public static final String ORIGINAL_URI_ATTRIBUTE_INIT_PARAM = "originalURIAttribute";
public static final String ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM = "originalQueryAttribute";
public static final String FASTCGI_HTTPS_INIT_PARAM = "fastCGI.HTTPS";
public static final String FASTCGI_ENV_NAMES_INIT_PARAM = "fastCGI.envNames";
private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remoteAddr";
private static final String REMOTE_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remotePort";
private static final String SERVER_NAME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverName";
private static final String SERVER_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverAddr";
private static final String SERVER_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverPort";
private static final String SCHEME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".scheme";
private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestURI";
private static final String REQUEST_QUERY_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestQuery";
private Pattern scriptPattern;
private String originalURIAttribute;
private String originalQueryAttribute;
private boolean fcgiHTTPS;
private Set<String> fcgiEnvNames;
@Override
public void init() throws ServletException
{
super.init();
String value = getInitParameter(SCRIPT_PATTERN_INIT_PARAM);
if (value == null)
value = "(.+?\\.php)";
scriptPattern = Pattern.compile(value);
originalURIAttribute = getInitParameter(ORIGINAL_URI_ATTRIBUTE_INIT_PARAM);
originalQueryAttribute = getInitParameter(ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM);
fcgiHTTPS = Boolean.parseBoolean(getInitParameter(FASTCGI_HTTPS_INIT_PARAM));
fcgiEnvNames = Collections.emptySet();
String envNames = getInitParameter(FASTCGI_ENV_NAMES_INIT_PARAM);
if (envNames != null)
{
fcgiEnvNames = Stream.of(envNames.split(","))
.map(String::trim)
.collect(Collectors.toSet());
}
}
@Override
protected HttpClient newHttpClient()
{
ServletConfig config = getServletConfig();
String scriptRoot = config.getInitParameter(SCRIPT_ROOT_INIT_PARAM);
if (scriptRoot == null)
throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured");
ClientConnector connector;
String unixDomainPath = config.getInitParameter("unixDomainPath");
if (unixDomainPath != null)
{
connector = ClientConnector.forUnixDomain(Path.of(unixDomainPath));
}
else
{
int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2);
String value = config.getInitParameter("selectors");
if (value != null)
selectors = Integer.parseInt(value);
connector = new ClientConnector();
connector.setSelectors(selectors);
}
return new HttpClient(new ProxyHttpClientTransportOverFCGI(connector, scriptRoot));
}
@Override
protected void sendProxyRequest(HttpServletRequest request, HttpServletResponse proxyResponse, Request proxyRequest)
{
proxyRequest.attribute(REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr());
proxyRequest.attribute(REMOTE_PORT_ATTRIBUTE, String.valueOf(request.getRemotePort()));
proxyRequest.attribute(SERVER_NAME_ATTRIBUTE, request.getServerName());
proxyRequest.attribute(SERVER_ADDR_ATTRIBUTE, request.getLocalAddr());
proxyRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(request.getLocalPort()));
proxyRequest.attribute(SCHEME_ATTRIBUTE, request.getScheme());
// Has the original URI been rewritten ?
String originalURI = null;
String originalQuery = null;
if (originalURIAttribute != null)
originalURI = (String)request.getAttribute(originalURIAttribute);
if (originalURI != null && originalQueryAttribute != null)
{
originalQuery = (String)request.getAttribute(originalQueryAttribute);
if (originalQuery != null)
originalURI += "?" + originalQuery;
}
if (originalURI == null)
{
// If we are forwarded or included, retain the original request URI.
String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
if (originalPath == null)
{
originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
}
if (originalPath != null)
{
originalURI = originalPath;
if (originalQuery != null)
originalURI += "?" + originalQuery;
}
}
if (originalURI != null)
proxyRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI);
if (originalQuery != null)
proxyRequest.attribute(REQUEST_QUERY_ATTRIBUTE, originalQuery);
// If the Host header is missing, add it.
if (!proxyRequest.getHeaders().contains(HttpHeader.HOST))
{
String server = request.getServerName();
int port = request.getServerPort();
if (port != HttpScheme.getDefaultPort(request.getScheme()))
server += ":" + port;
String host = server;
proxyRequest.headers(headers -> headers
.put(HttpHeader.HOST, host)
.put(HttpHeader.X_FORWARDED_HOST, host));
}
// PHP does not like multiple Cookie headers, coalesce into one.
List<String> cookies = proxyRequest.getHeaders().getValuesList(HttpHeader.COOKIE);
if (cookies.size() > 1)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < cookies.size(); ++i)
{
if (i > 0)
builder.append("; ");
String cookie = cookies.get(i);
builder.append(cookie);
}
proxyRequest.headers(headers -> headers.put(HttpHeader.COOKIE, builder.toString()));
}
super.sendProxyRequest(request, proxyResponse, proxyRequest);
}
protected void customizeFastCGIHeaders(Request proxyRequest, HttpFields.Mutable fastCGIHeaders)
{
for (String envName : fcgiEnvNames)
{
String envValue = System.getenv(envName);
if (envValue != null)
fastCGIHeaders.put(envName, envValue);
}
fastCGIHeaders.remove("HTTP_PROXY");
fastCGIHeaders.put(FCGI.Headers.REMOTE_ADDR, (String)proxyRequest.getAttributes().get(REMOTE_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.REMOTE_PORT, (String)proxyRequest.getAttributes().get(REMOTE_PORT_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_NAME, (String)proxyRequest.getAttributes().get(SERVER_NAME_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_ADDR, (String)proxyRequest.getAttributes().get(SERVER_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_PORT, (String)proxyRequest.getAttributes().get(SERVER_PORT_ATTRIBUTE));
if (fcgiHTTPS || HttpScheme.HTTPS.is((String)proxyRequest.getAttributes().get(SCHEME_ATTRIBUTE)))
fastCGIHeaders.put(FCGI.Headers.HTTPS, "on");
URI proxyRequestURI = proxyRequest.getURI();
String rawPath = proxyRequestURI == null ? proxyRequest.getPath() : proxyRequestURI.getRawPath();
String rawQuery = proxyRequestURI == null ? null : proxyRequestURI.getRawQuery();
String requestURI = (String)proxyRequest.getAttributes().get(REQUEST_URI_ATTRIBUTE);
if (requestURI == null)
{
requestURI = rawPath;
if (rawQuery != null)
requestURI += "?" + rawQuery;
}
fastCGIHeaders.put(FCGI.Headers.REQUEST_URI, requestURI);
String requestQuery = (String)proxyRequest.getAttributes().get(REQUEST_QUERY_ATTRIBUTE);
if (requestQuery != null)
fastCGIHeaders.put(FCGI.Headers.QUERY_STRING, requestQuery);
String scriptName = rawPath;
Matcher matcher = scriptPattern.matcher(rawPath);
if (matcher.matches())
{
// Expect at least one group in the regular expression.
scriptName = matcher.group(1);
// If there is a second group, map it to PATH_INFO.
if (matcher.groupCount() > 1)
fastCGIHeaders.put(FCGI.Headers.PATH_INFO, matcher.group(2));
}
fastCGIHeaders.put(FCGI.Headers.SCRIPT_NAME, scriptName);
String root = fastCGIHeaders.get(FCGI.Headers.DOCUMENT_ROOT);
fastCGIHeaders.put(FCGI.Headers.SCRIPT_FILENAME, root + scriptName);
}
private class ProxyHttpClientTransportOverFCGI extends HttpClientTransportOverFCGI
{
private ProxyHttpClientTransportOverFCGI(ClientConnector connector, String scriptRoot)
{
super(connector, scriptRoot);
}
@Override
public void customize(Request request, HttpFields.Mutable fastCGIHeaders)
{
super.customize(request, fastCGIHeaders);
customizeFastCGIHeaders(request, fastCGIHeaders);
if (_log.isDebugEnabled())
{
TreeMap<String, String> fcgi = new TreeMap<>();
for (HttpField field : fastCGIHeaders)
{
fcgi.put(field.getName(), field.getValue());
}
String eol = System.lineSeparator();
_log.debug("FastCGI variables {}{}", eol, fcgi.entrySet().stream()
.map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(eol)));
}
}
}
}

View File

@ -0,0 +1,138 @@
//
// ========================================================================
// 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.fcgi.proxy;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.util.StringUtil;
/**
* Inspired by nginx's try_files functionality.
* <p>
* This filter accepts the {@code files} init-param as a list of space-separated
* file URIs. The special token {@code $path} represents the current request URL's
* path (the portion after the context path).
* <p>
* Typical example of how this filter can be configured is the following:
* <pre>
* &lt;filter&gt;
* &lt;filter-name&gt;try_files&lt;/filter-name&gt;
* &lt;filter-class&gt;org.eclipse.jetty.fcgi.server.proxy.TryFilesFilter&lt;/filter-class&gt;
* &lt;init-param&gt;
* &lt;param-name&gt;files&lt;/param-name&gt;
* &lt;param-value&gt;/maintenance.html $path /index.php?p=$path&lt;/param-value&gt;
* &lt;/init-param&gt;
* &lt;/filter&gt;
* </pre>
* For a request such as {@code /context/path/to/resource.ext}, this filter will
* try to serve the {@code /maintenance.html} file if it finds it; failing that,
* it will try to serve the {@code /path/to/resource.ext} file if it finds it;
* failing that it will forward the request to {@code /index.php?p=/path/to/resource.ext}.
* The last file URI specified in the list is therefore the "fallback" to which the request
* is forwarded to in case no previous files can be found.
* <p>
* The files are resolved using {@link ServletContext#getResource(String)} to make sure
* that only files visible to the application are served.
*
* @see FastCGIProxyServlet
*/
public class TryFilesFilter implements Filter
{
public static final String FILES_INIT_PARAM = "files";
private String[] files;
@Override
public void init(FilterConfig config) throws ServletException
{
String param = config.getInitParameter(FILES_INIT_PARAM);
if (param == null)
throw new ServletException(String.format("Missing mandatory parameter '%s'", FILES_INIT_PARAM));
files = param.split(" ");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
for (int i = 0; i < files.length - 1; ++i)
{
String file = files[i];
String resolved = resolve(httpRequest, file);
URL url = request.getServletContext().getResource(resolved);
if (url == null)
continue;
if (Files.isReadable(toPath(url)))
{
chain.doFilter(httpRequest, httpResponse);
return;
}
}
// The last one is the fallback
fallback(httpRequest, httpResponse, chain, files[files.length - 1]);
}
private Path toPath(URL url) throws IOException
{
try
{
return Paths.get(url.toURI());
}
catch (URISyntaxException x)
{
throw new IOException(x);
}
}
protected void fallback(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String fallback) throws IOException, ServletException
{
String resolved = resolve(request, fallback);
request.getServletContext().getRequestDispatcher(resolved).forward(request, response);
}
private String resolve(HttpServletRequest request, String value)
{
String path = request.getServletPath();
String info = request.getPathInfo();
if (info != null)
path += info;
if (!path.startsWith("/"))
path = "/" + path;
return StringUtil.replace(value, "$path", path);
}
@Override
public void destroy()
{
}
}

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.ee9.fcgi.proxy;
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
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.client.HttpClient;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.ee9.servlet.FilterHolder;
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
import org.eclipse.jetty.ee9.servlet.ServletHolder;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TryFilesFilterTest
{
private Server server;
private ServerConnector connector;
private ServerConnector sslConnector;
private HttpClient client;
private String forwardPath;
public void prepare(HttpServlet servlet) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
serverSslContextFactory.setKeyStorePassword("storepwd");
sslConnector = new ServerConnector(server, serverSslContextFactory);
server.addConnector(sslConnector);
ServletContextHandler context = new ServletContextHandler(server, "/");
FilterHolder filterHolder = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
forwardPath = "/index.php";
filterHolder.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path " + forwardPath + "?p=$path");
context.addServlet(new ServletHolder(servlet), "/*");
ClientConnector clientConnector = new ClientConnector();
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(true);
// clientSslContextFactory.setEndpointIdentificationAlgorithm(null);
// clientSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
// clientSslContextFactory.setKeyStorePassword("storepwd");
clientConnector.setSslContextFactory(clientSslContextFactory);
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector));
server.addBean(client);
server.start();
}
@AfterEach
public void dispose() throws Exception
{
server.stop();
}
@Test
public void testHTTPSRequestIsForwarded() throws Exception
{
final String path = "/one/";
prepare(new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
{
assertTrue("https".equalsIgnoreCase(req.getScheme()));
assertTrue(req.isSecure());
assertEquals(forwardPath, req.getRequestURI());
assertTrue(req.getQueryString().endsWith(path));
}
});
ContentResponse response = client.newRequest("localhost", sslConnector.getLocalPort())
.scheme("https")
.path(path)
.send();
assertEquals(200, response.getStatus());
}
}

View File

@ -0,0 +1,2 @@
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.fcgi.proxy.LEVEL=DEBUG

View File

@ -472,6 +472,11 @@
<artifactId>jetty-ee9-jndi</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-fcgi-proxy</artifactId>
<optional>true</optional>
</dependency>
<!-- Demo ee9 Apps -->
<dependency>
<groupId>org.eclipse.jetty.ee9.demos</groupId>

View File

@ -105,6 +105,10 @@
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -27,11 +27,6 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<finalName>jetty-ee9-jmx-webapp</finalName>

View File

@ -13,15 +13,6 @@
<name>EE9 :: Tests :: OpenId WebApp</name>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>

View File

@ -15,12 +15,7 @@
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>compile</scope>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>

View File

@ -15,12 +15,7 @@
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>compile</scope>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>

View File

@ -18,9 +18,8 @@
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>compile</scope>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>

View File

@ -135,7 +135,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
private Throwable _unavailableException;
private Map<String, String> _resourceAliases;
private boolean _ownClassLoader = false;
private ClassLoader _initialClassLoader;
private boolean _configurationDiscovered = true;
private boolean _allowDuplicateFragmentNames = false;
private boolean _throwUnavailableOnStartupException = false;
@ -471,13 +471,9 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
}
// Configure classloader
_ownClassLoader = false;
if (getClassLoader() == null)
{
WebAppClassLoader classLoader = new WebAppClassLoader(this);
setClassLoader(classLoader);
_ownClassLoader = true;
}
_initialClassLoader = getClassLoader();
if (!(_initialClassLoader instanceof WebAppClassLoader))
setClassLoader(new WebAppClassLoader(_initialClassLoader, this));
if (LOG.isDebugEnabled())
{
@ -1300,13 +1296,13 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
}
finally
{
if (_ownClassLoader)
if (!(_initialClassLoader instanceof WebAppClassLoader))
{
ClassLoader loader = getClassLoader();
if (loader instanceof URLClassLoader)
((URLClassLoader)loader).close();
setClassLoader(null);
}
setClassLoader(_initialClassLoader);
_unavailableException = null;
}

View File

@ -71,6 +71,7 @@
<module>jetty-ee9-tests</module>
<module>jetty-ee9-bom</module>
<module>jetty-ee9-home</module>
<module>jetty-ee9-fcgi-proxy</module>
</modules>
<dependencyManagement>
@ -92,6 +93,11 @@
<artifactId>jetty-ee9-cdi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-fcgi-proxy</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-glassfish-jstl</artifactId>

View File

@ -94,5 +94,10 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

32
pom.xml
View File

@ -36,7 +36,7 @@
<apache.avro.version>1.11.1</apache.avro.version>
<apache.httpclient.version>4.5.14</apache.httpclient.version>
<apache.httpcore.version>4.4.16</apache.httpcore.version>
<asciidoctorj-diagram.version>2.2.7</asciidoctorj-diagram.version>
<asciidoctorj-diagram.version>2.2.8</asciidoctorj-diagram.version>
<asciidoctorj.version>2.5.7</asciidoctorj.version>
<mina.core.version>2.2.1</mina.core.version>
<asm.version>9.5</asm.version>
@ -46,7 +46,7 @@
<checkstyle.version>10.6.0</checkstyle.version>
<commons-codec.version>1.15</commons-codec.version>
<commons.compress.version>1.23.0</commons.compress.version>
<commons.io.version>2.11.0</commons.io.version>
<commons.io.version>2.12.0</commons.io.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<conscrypt.version>2.5.2</conscrypt.version>
<disruptor.version>3.4.2</disruptor.version>
@ -97,7 +97,7 @@
<jmh.version>1.36</jmh.version>
<jna.version>5.13.0</jna.version>
<json-simple.version>1.1.1</json-simple.version>
<json-smart.version>2.4.10</json-smart.version>
<json-smart.version>2.4.11</json-smart.version>
<junit.version>5.9.3</junit.version>
<jsp.impl.version>10.0.14</jsp.impl.version>
<kerb-simplekdc.version>2.0.3</kerb-simplekdc.version>
@ -107,7 +107,7 @@
<mariadb.docker.version>10.3.6</mariadb.docker.version>
<maven.deps.version>3.8.7</maven.deps.version>
<maven-artifact-transfer.version>0.13.1</maven-artifact-transfer.version>
<maven.resolver.version>1.9.8</maven.resolver.version>
<maven.resolver.version>1.9.10</maven.resolver.version>
<maven.version>3.9.0</maven.version>
<mongodb.version>3.12.11</mongodb.version>
<openpojo.version>0.9.1</openpojo.version>
@ -142,13 +142,13 @@
<springboot.version>2.1.1.RELEASE</springboot.version>
<taglibs-standard-impl.version>1.2.5</taglibs-standard-impl.version>
<taglibs-standard-spec.version>1.2.5</taglibs-standard-spec.version>
<testcontainers.version>1.18.0</testcontainers.version>
<testcontainers.version>1.18.3</testcontainers.version>
<wildfly.common.version>1.6.0.Final</wildfly.common.version>
<wildfly.elytron.version>2.1.0.Final</wildfly.elytron.version>
<wildfly.elytron.version>2.2.0.Final</wildfly.elytron.version>
<xmemcached.version>2.4.7</xmemcached.version>
<!-- some maven plugins versions -->
<asciidoctor.maven.plugin.version>2.2.3</asciidoctor.maven.plugin.version>
<asciidoctor.maven.plugin.version>2.2.4</asciidoctor.maven.plugin.version>
<build-helper.maven.plugin.version>3.3.0</build-helper.maven.plugin.version>
<buildnumber.maven.plugin.version>3.0.0</buildnumber.maven.plugin.version>
<depends.maven.plugin.version>1.4.0</depends.maven.plugin.version>
@ -159,29 +159,29 @@
<jetty-version.maven.plugin.version>2.7</jetty-version.maven.plugin.version>
<license.maven.plugin.version>4.1</license.maven.plugin.version>
<maven.antrun.plugin.version>3.1.0</maven.antrun.plugin.version>
<maven.assembly.plugin.version>3.5.0</maven.assembly.plugin.version>
<maven.assembly.plugin.version>3.6.0</maven.assembly.plugin.version>
<maven.bundle.plugin.version>5.1.8</maven.bundle.plugin.version>
<maven.clean.plugin.version>3.2.0</maven.clean.plugin.version>
<maven.checkstyle.plugin.version>3.2.2</maven.checkstyle.plugin.version>
<maven.checkstyle.plugin.version>3.3.0</maven.checkstyle.plugin.version>
<maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
<maven.dependency.plugin.version>3.5.0</maven.dependency.plugin.version>
<maven.dependency.plugin.version>3.6.0</maven.dependency.plugin.version>
<maven.deploy.plugin.version>3.1.1</maven.deploy.plugin.version>
<maven.enforcer.plugin.version>3.3.0</maven.enforcer.plugin.version>
<maven.exec.plugin.version>3.1.0</maven.exec.plugin.version>
<maven.gpg.plugin.version>3.0.1</maven.gpg.plugin.version>
<maven.gpg.plugin.version>3.1.0</maven.gpg.plugin.version>
<maven.install.plugin.version>3.1.1</maven.install.plugin.version>
<maven.invoker.plugin.version>3.5.1</maven.invoker.plugin.version>
<groovy.version>4.0.6</groovy.version>
<maven.jar.plugin.version>3.3.0</maven.jar.plugin.version>
<maven.javadoc.plugin.version>3.5.0</maven.javadoc.plugin.version>
<maven.plugin-tools.version>3.8.2</maven.plugin-tools.version>
<maven-plugin.plugin.version>3.8.2</maven-plugin.plugin.version>
<maven.plugin-tools.version>3.9.0</maven.plugin-tools.version>
<maven-plugin.plugin.version>3.9.0</maven-plugin.plugin.version>
<maven.release.plugin.version>3.0.0</maven.release.plugin.version>
<maven.remote-resources-plugin.version>3.0.0</maven.remote-resources-plugin.version>
<maven.remote-resources-plugin.version>3.1.0</maven.remote-resources-plugin.version>
<maven.resources.plugin.version>3.3.1</maven.resources.plugin.version>
<maven.shade.plugin.version>3.4.1</maven.shade.plugin.version>
<maven.surefire.plugin.version>3.0.0</maven.surefire.plugin.version>
<maven.source.plugin.version>3.2.1</maven.source.plugin.version>
<maven.surefire.plugin.version>3.1.0</maven.surefire.plugin.version>
<maven.source.plugin.version>3.3.0</maven.source.plugin.version>
<maven.war.plugin.version>3.3.2</maven.war.plugin.version>
<spotbugs.maven.plugin.version>4.7.2.0</spotbugs.maven.plugin.version>
<versions.maven.plugin.version>2.14.2</versions.maven.plugin.version>

View File

@ -157,7 +157,7 @@ public class DistributionTests extends AbstractJettyHomeTest
@ParameterizedTest
@ValueSource(strings = {"ee9", "ee10"})
public void testSimpleWebAppWithJSPandJSTL(String env) throws Exception
public void testSimpleWebAppWithJSPAndJSTL(String env) throws Exception
{
Path jettyBase = newTestJettyBaseDirectory();
String jettyVersion = System.getProperty("jettyVersion");
@ -1301,6 +1301,131 @@ public class DistributionTests extends AbstractJettyHomeTest
}
}
@ParameterizedTest
@ValueSource(strings = {"ee9", "ee10"})
public void testEEFastCGIProxying(String env) throws Exception
{
Path jettyBase = newTestJettyBaseDirectory();
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.jettyBase(jettyBase)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
String mods = String.join(",",
"resources", "http", "fcgi", "core-deploy",
toEnvironment("deploy", env),
toEnvironment("fcgi-proxy", env)
);
try (JettyHomeTester.Run run1 = distribution.start(List.of("--add-modules=" + mods)))
{
assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS));
assertEquals(0, run1.getExitValue());
Path jettyLogging = distribution.getJettyBase().resolve("resources/jetty-logging.properties");
String loggingConfig = """
org.eclipse.jetty.LEVEL=DEBUG
""";
Files.writeString(jettyLogging, loggingConfig, StandardOpenOption.TRUNCATE_EXISTING);
// Add a FastCGI connector to simulate, for example, php-fpm.
int fcgiPort = distribution.freePort();
Path jettyBaseEtc = jettyBase.resolve("etc");
Files.createDirectories(jettyBaseEtc);
Path fcgiConnectorXML = jettyBaseEtc.resolve("fcgi-connector.xml");
Files.writeString(fcgiConnectorXML, """
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure id="Server">
<Call name="addConnector">
<Arg>
<New id="fcgiConnector" class="org.eclipse.jetty.server.ServerConnector">
<Arg><Ref refid="Server" /></Arg>
<Arg type="int">1</Arg>
<Arg type="int">1</Arg>
<Arg>
<Array type="org.eclipse.jetty.server.ConnectionFactory">
<Item>
<New class="org.eclipse.jetty.fcgi.server.ServerFCGIConnectionFactory">
<Arg><Ref refid="httpConfig" /></Arg>
</New>
</Item>
</Array>
</Arg>
<Set name="port">$P</Set>
</New>
</Arg>
</Call>
</Configure>
""".replace("$P", String.valueOf(fcgiPort)), StandardOpenOption.CREATE);
// Deploy a Jetty context XML file that is only necessary for the test,
// as it simulates, for example, what the php-fpm server would return.
Path jettyBaseWork = jettyBase.resolve("work");
Path phpXML = jettyBase.resolve("webapps").resolve("php.xml");
Files.writeString(phpXML, """
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
<Set name="contextPath">/php</Set>
<Set name="baseResourceAsPath">
<Call class="java.nio.file.Path" name="of">
<Arg>$R</Arg>
</Call>
</Set>
<Set name="handler">
<New class="org.eclipse.jetty.server.handler.ResourceHandler" />
</Set>
</Configure>
""".replace("$R", jettyBaseWork.toAbsolutePath().toString()), StandardOpenOption.CREATE);
// Save a file in $JETTY_BASE/work so that it can be requested.
String testFileContent = "hello";
Files.writeString(jettyBaseWork.resolve("test.txt"), testFileContent, StandardOpenOption.CREATE);
// Deploy a Jetty context XML file that sets up the FastCGIProxyServlet.
// Converts URIs from http://host:<httpPort>/proxy/foo to http://host:<fcgiPort>/php/foo.
Path proxyXML = jettyBase.resolve("webapps").resolve("proxy.xml");
Files.writeString(proxyXML, """
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<Configure class="org.eclipse.jetty.$ENV.servlet.ServletContextHandler">
<Set name="contextPath">/proxy</Set>
<Call name="addServlet">
<Arg>org.eclipse.jetty.$ENV.fcgi.proxy.FastCGIProxyServlet</Arg>
<Arg>*.txt</Arg>
<Call name="setInitParameter">
<Arg>proxyTo</Arg>
<Arg>http://localhost:$P/php</Arg>
</Call>
<Call name="setInitParameter">
<Arg>scriptRoot</Arg>
<Arg>/var/wordpress</Arg>
</Call>
</Call>
</Configure>
""".replace("$ENV", env).replace("$P", String.valueOf(fcgiPort)), StandardOpenOption.CREATE);
Path proxyProps = jettyBase.resolve("webapps").resolve("proxy.properties");
Files.writeString(proxyProps, """
environment=$ENV
""".replace("$ENV", env), StandardOpenOption.CREATE);
int httpPort = distribution.freePort();
try (JettyHomeTester.Run run2 = distribution.start("jetty.http.port=" + httpPort, "etc/fcgi-connector.xml"))
{
assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
startHttpClient();
// Make a request to the /proxy context on the httpPort; it should be converted to FastCGI
// and reverse proxied to the simulated php-fpm /php context on the fcgiPort.
ContentResponse response = client.GET("http://localhost:" + httpPort + "/proxy/test.txt");
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.getContentAsString(), is(testFileContent));
}
}
}
@Test
@DisabledForJreRange(max = JRE.JAVA_18)
@Tag("flaky")

View File

@ -17,6 +17,7 @@
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>