Merge branch 'jetty-12.0.x' of github.com:eclipse/jetty.project into jetty-12.0.x

This commit is contained in:
Joakim Erdfelt 2022-10-17 18:32:35 -05:00
commit 586d0fd097
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
25 changed files with 1033 additions and 885 deletions

View File

@ -0,0 +1,63 @@
<?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.fcgi</groupId>
<artifactId>jetty-fcgi</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-fcgi-proxy</artifactId>
<name>Jetty Core :: FastCGI :: Proxy</name>
<properties>
<bundle-symbolic-name>${project.groupId}.proxy</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
@{argLine}
${jetty.surefire.argLine}
--add-exports org.eclipse.jetty.fcgi.client/org.eclipse.jetty.fcgi.generator=ALL-UNNAMED
--add-exports org.eclipse.jetty.fcgi.client/org.eclipse.jetty.fcgi.parser=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-client</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-unixdomain-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

@ -0,0 +1,13 @@
[description]
Enables support for HTTP to FastCGI proxying.
[tags]
fcgi
proxy
[depend]
proxy
[lib]
lib/fcgi/jetty-fcgi-client-${jetty.version}.jar
lib/fcgi/jetty-fcgi-proxy-${jetty.version}.jar

View File

@ -11,9 +11,11 @@
// ========================================================================
//
module jetty.ee9.fcgi.server.proxy {
requires transitive org.eclipse.jetty.ee9.proxy;
requires transitive org.eclipse.jetty.fcgi.server;
module org.eclipse.jetty.fcgi.proxy
{
requires org.slf4j;
requires transitive org.eclipse.jetty.fcgi.client;
requires transitive org.eclipse.jetty.proxy;
exports org.eclipse.jetty.ee9.fcgi.server.proxy;
exports org.eclipse.jetty.fcgi.proxy;
}

View File

@ -0,0 +1,418 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.fcgi.proxy;
import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.client.http.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.http.HttpURI;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.proxy.ProxyHandler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.TryPathsHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>Specific implementation of {@link ProxyHandler.Reverse} for FastCGI.</p>
* <p>This handler accepts an HTTP request and transforms it into a FastCGI
* request that is sent to the FastCGI server, and viceversa for the response.</p>
*
* @see TryPathsHandler
*/
public class FastCGIProxyHandler extends ProxyHandler.Reverse
{
private static final Logger LOG = LoggerFactory.getLogger(FastCGIProxyHandler.class);
private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".remoteAddr";
private static final String REMOTE_PORT_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".remotePort";
private static final String SERVER_NAME_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".serverName";
private static final String SERVER_ADDR_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".serverAddr";
private static final String SERVER_PORT_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".serverPort";
private static final String SCHEME_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".scheme";
private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".requestURI";
private static final String REQUEST_QUERY_ATTRIBUTE = FastCGIProxyHandler.class.getName() + ".requestQuery";
private final String scriptRoot;
private Pattern scriptPattern;
private String originalPathAttribute;
private String originalQueryAttribute;
private boolean fcgiSecure;
private Set<String> fcgiEnvNames;
private Path unixDomainPath;
/**
* <p>Creates a new instance that rewrites the {@code HttpURI}
* with the given pattern and replacement strings, using
* {@link String#replaceAll(String, String)}.</p>
*
* @param uriPattern the regex pattern to use to match the incoming URI
* @param uriReplacement the replacement string to use to rewrite the incoming URI
* @param scriptRoot the root directory path of the FastCGI files
* @see ProxyHandler.Reverse#Reverse(String, String)
*/
public FastCGIProxyHandler(String uriPattern, String uriReplacement, String scriptRoot)
{
super(uriPattern, uriReplacement);
this.scriptRoot = Objects.requireNonNull(scriptRoot);
}
/**
* <p>Creates a new instance with the given {@code HttpURI} rewriter
* function.</p>
* <p>The {@code HttpURI} rewriter function should return the URI
* of the FastCGI server.</p>
* <p>The {@code scriptRoot} path must be set to the directory where the application
* that must be served via FastCGI is installed and corresponds to
* the FastCGI {@code DOCUMENT_ROOT} parameter.</p>
*
* @param httpURIRewriter a function that returns the URI of the FastCGI server
* @param scriptRoot the root directory path of the FastCGI files
*/
public FastCGIProxyHandler(Function<Request, HttpURI> httpURIRewriter, String scriptRoot)
{
super(httpURIRewriter);
this.scriptRoot = Objects.requireNonNull(scriptRoot);
}
/**
* @return the root directory path of the FastCGI files
*/
public String getScriptRoot()
{
return scriptRoot;
}
/**
* @return the regular expression that extracts the
* {@code SCRIPT_NAME} and the {@code PATH_INFO} FastCGI parameters
*/
public Pattern getScriptPattern()
{
return scriptPattern;
}
/**
* <p>Sets a regular expression with at least 1 and at most 2 groups
* that specify respectively:</p>
* <ul>
* <li>the FastCGI {@code SCRIPT_NAME} parameter</li>
* <li>the FastCGI {@code PATH_INFO} parameter</li>
* </ul>
*
* @param scriptPattern the regular expression that extracts the
* {@code SCRIPT_NAME} and the {@code PATH_INFO} FastCGI parameters
*/
public void setScriptPattern(Pattern scriptPattern)
{
this.scriptPattern = scriptPattern;
}
/**
* @return the attribute name of the original client-to-proxy
* request path
*/
public String getOriginalPathAttribute()
{
return originalPathAttribute;
}
/**
* <p>Sets the client-to-proxy request attribute name to use to
* retrieve the original request path.</p>
* <p>For example, the request URI may be rewritten by a previous
* handler that might save the original request path in a request
* attribute.</p>
*
* @param originalPathAttribute the attribute name of the original
* client-to-proxy request path
*/
public void setOriginalPathAttribute(String originalPathAttribute)
{
this.originalPathAttribute = originalPathAttribute;
}
/**
* @return the attribute name of the original client-to-proxy
* request query
*/
public String getOriginalQueryAttribute()
{
return originalQueryAttribute;
}
/**
* <p>Sets the client-to-proxy request attribute name to use to
* retrieve the original request query.</p>
* <p>For example, the request URI may be rewritten by a previous
* handler that might save the original request query in a request
* attribute.</p>
*
* @param originalQueryAttribute the attribute name of the original
* client-to-proxy request query
*/
public void setOriginalQueryAttribute(String originalQueryAttribute)
{
this.originalQueryAttribute = originalQueryAttribute;
}
/**
* @return whether to forward the {@code HTTPS} FastCGI
* parameter in the FastCGI request
*/
public boolean isFastCGISecure()
{
return fcgiSecure;
}
/**
* <p>Sets whether to forward the {@code HTTPS} FastCGI parameter
* in the FastCGI request to the FastCGI server.</p>
*
* @param fcgiSecure whether to forward the {@code HTTPS} FastCGI
* parameter in the FastCGI request
*/
public void setFastCGISecure(boolean fcgiSecure)
{
this.fcgiSecure = fcgiSecure;
}
/**
* @return the names of the environment variables forwarded
* in the FastCGI request
*/
public Set<String> getFastCGIEnvNames()
{
return fcgiEnvNames;
}
/**
* <p>Sets the names of environment variables that will forwarded,
* along with their value retrieved via {@link System#getenv(String)},
* in the FastCGI request to the FastCGI server.</p>
*
* @param fcgiEnvNames the names of the environment variables
* forwarded in the FastCGI request
* @see System#getenv(String)
*/
public void setFastCGIEnvNames(Set<String> fcgiEnvNames)
{
this.fcgiEnvNames = fcgiEnvNames;
}
/**
* @return the Unix-Domain path the FastCGI server listens to,
* or {@code null} if the FastCGI server listens over network
*/
public Path getUnixDomainPath()
{
return unixDomainPath;
}
/**
* <p>Sets the Unix-Domain path the FastCGI server listens to.</p>
* <p>If the FastCGI server listens over the network (not over a
* Unix-Domain path), then the FastCGI server host and port must
* be specified by the {@code HttpURI} rewrite function passed
* to the constructor.</p>
*
* @param unixDomainPath the Unix-Domain path the FastCGI server listens to
*/
public void setUnixDomainPath(Path unixDomainPath)
{
this.unixDomainPath = unixDomainPath;
}
@Override
protected void doStart() throws Exception
{
super.doStart();
if (scriptPattern == null)
scriptPattern = Pattern.compile("(.+?\\.php)");
if (fcgiEnvNames == null)
fcgiEnvNames = Set.of();
}
@Override
protected HttpClient newHttpClient()
{
ClientConnector clientConnector;
Path unixDomainPath = getUnixDomainPath();
if (unixDomainPath != null)
clientConnector = ClientConnector.forUnixDomain(unixDomainPath);
else
clientConnector = new ClientConnector();
QueuedThreadPool proxyClientThreads = new QueuedThreadPool();
proxyClientThreads.setName("proxy-client");
clientConnector.setExecutor(proxyClientThreads);
return new HttpClient(new ProxyHttpClientTransportOverFCGI(clientConnector, getScriptRoot()));
}
@Override
protected void sendProxyToServerRequest(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, Response proxyToClientResponse, Callback proxyToClientCallback)
{
proxyToServerRequest.attribute(REMOTE_ADDR_ATTRIBUTE, Request.getRemoteAddr(clientToProxyRequest));
proxyToServerRequest.attribute(REMOTE_PORT_ATTRIBUTE, String.valueOf(Request.getRemotePort(clientToProxyRequest)));
String serverName = Request.getServerName(clientToProxyRequest);
proxyToServerRequest.attribute(SERVER_NAME_ATTRIBUTE, serverName);
proxyToServerRequest.attribute(SERVER_ADDR_ATTRIBUTE, Request.getLocalAddr(clientToProxyRequest));
int serverPort = Request.getServerPort(clientToProxyRequest);
proxyToServerRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(serverPort));
String scheme = clientToProxyRequest.getHttpURI().getScheme();
proxyToServerRequest.attribute(SCHEME_ATTRIBUTE, scheme);
// Has the original URI been rewritten?
String originalURI = null;
String originalQuery = null;
String originalPathAttribute = getOriginalPathAttribute();
if (originalPathAttribute != null)
originalURI = (String)clientToProxyRequest.getAttribute(originalPathAttribute);
if (originalURI != null)
{
String originalQueryAttribute = getOriginalQueryAttribute();
if (originalQueryAttribute != null)
{
originalQuery = (String)clientToProxyRequest.getAttribute(originalQueryAttribute);
if (originalQuery != null)
originalURI += "?" + originalQuery;
}
}
if (originalURI != null)
proxyToServerRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI);
if (originalQuery != null)
proxyToServerRequest.attribute(REQUEST_QUERY_ATTRIBUTE, originalQuery);
// If the Host header is missing, add it.
if (!proxyToServerRequest.getHeaders().contains(HttpHeader.HOST))
{
if (!getHttpClient().isDefaultPort(scheme, serverPort))
serverName += ":" + serverPort;
String host = serverName;
proxyToServerRequest.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 = proxyToServerRequest.getHeaders().getValuesList(HttpHeader.COOKIE);
if (cookies.size() > 1)
{
String allCookies = String.join("; ", cookies);
proxyToServerRequest.headers(headers -> headers.put(HttpHeader.COOKIE, allCookies));
}
super.sendProxyToServerRequest(clientToProxyRequest, proxyToServerRequest, proxyToClientResponse, proxyToClientCallback);
}
protected void customizeFastCGIHeaders(org.eclipse.jetty.client.api.Request proxyToServerRequest, HttpFields.Mutable fastCGIHeaders)
{
for (String envName : getFastCGIEnvNames())
{
String envValue = System.getenv(envName);
if (envValue != null)
fastCGIHeaders.put(envName, envValue);
}
fastCGIHeaders.remove("HTTP_PROXY");
fastCGIHeaders.put(FCGI.Headers.REMOTE_ADDR, (String)proxyToServerRequest.getAttributes().get(REMOTE_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.REMOTE_PORT, (String)proxyToServerRequest.getAttributes().get(REMOTE_PORT_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_NAME, (String)proxyToServerRequest.getAttributes().get(SERVER_NAME_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_ADDR, (String)proxyToServerRequest.getAttributes().get(SERVER_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_PORT, (String)proxyToServerRequest.getAttributes().get(SERVER_PORT_ATTRIBUTE));
if (isFastCGISecure() || HttpScheme.HTTPS.is((String)proxyToServerRequest.getAttributes().get(SCHEME_ATTRIBUTE)))
fastCGIHeaders.put(FCGI.Headers.HTTPS, "on");
URI proxyRequestURI = proxyToServerRequest.getURI();
String rawPath = proxyRequestURI == null ? proxyToServerRequest.getPath() : proxyRequestURI.getRawPath();
String rawQuery = proxyRequestURI == null ? null : proxyRequestURI.getRawQuery();
String requestURI = (String)proxyToServerRequest.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)proxyToServerRequest.getAttributes().get(REQUEST_QUERY_ATTRIBUTE);
if (requestQuery != null)
fastCGIHeaders.put(FCGI.Headers.QUERY_STRING, requestQuery);
String scriptName = rawPath;
Matcher matcher = getScriptPattern().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(org.eclipse.jetty.client.api.Request proxyToServerRequest, HttpFields.Mutable fastCGIHeaders)
{
super.customize(proxyToServerRequest, fastCGIHeaders);
customizeFastCGIHeaders(proxyToServerRequest, 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

@ -11,105 +11,98 @@
// ========================================================================
//
package org.eclipse.jetty.fcgi.server.proxy;
package org.eclipse.jetty.fcgi.proxy;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.ee9.fcgi.server.proxy.FastCGIProxyServlet;
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
import org.eclipse.jetty.ee9.servlet.ServletHolder;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.server.ServerFCGIConnectionFactory;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Connector;
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.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FastCGIProxyServletTest
public class FastCGIProxyHandlerTest
{
private final Map<String, String> fcgiParams = new HashMap<>();
private Server server;
private ServerConnector httpConnector;
private Connector fcgiConnector;
private ServletContextHandler context;
private ServerConnector proxyConnector;
private Connector serverConnector;
private ContextHandler proxyContext;
private HttpClient client;
private Path unixDomainPath;
private FastCGIProxyHandler fcgiHandler;
public void prepare(boolean sendStatus200, HttpServlet servlet) throws Exception
public void start(boolean sendStatus200, Handler handler) throws Exception
{
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
httpConnector = new ServerConnector(server);
server.addConnector(httpConnector);
proxyConnector = new ServerConnector(server, 1, 1);
server.addConnector(proxyConnector);
ServerFCGIConnectionFactory fcgi = new ServerFCGIConnectionFactory(new HttpConfiguration(), sendStatus200);
if (unixDomainPath == null)
{
fcgiConnector = new ServerConnector(server, fcgi);
serverConnector = new ServerConnector(server, 1, 1, fcgi);
}
else
{
UnixDomainServerConnector connector = new UnixDomainServerConnector(server, fcgi);
UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, fcgi);
connector.setUnixDomainPath(unixDomainPath);
fcgiConnector = connector;
serverConnector = connector;
}
server.addConnector(fcgiConnector);
server.addConnector(serverConnector);
String contextPath = "/";
context = new ServletContextHandler(server, contextPath);
proxyContext = new ContextHandler("/ctx");
String servletPath = "/script";
FastCGIProxyServlet fcgiServlet = new FastCGIProxyServlet()
String appContextPath = "/app";
fcgiHandler = new FastCGIProxyHandler(request ->
{
@Override
protected String rewriteTarget(HttpServletRequest request)
{
String uri = "http://localhost";
if (unixDomainPath == null)
uri += ":" + ((ServerConnector)fcgiConnector).getLocalPort();
return uri + servletPath + request.getServletPath();
}
};
ServletHolder fcgiServletHolder = new ServletHolder(fcgiServlet);
fcgiServletHolder.setName("fcgi");
fcgiServletHolder.setInitParameter(FastCGIProxyServlet.SCRIPT_ROOT_INIT_PARAM, "/scriptRoot");
fcgiServletHolder.setInitParameter("proxyTo", "http://localhost");
fcgiServletHolder.setInitParameter(FastCGIProxyServlet.SCRIPT_PATTERN_INIT_PARAM, "(.+?\\.php)");
fcgiParams.forEach(fcgiServletHolder::setInitParameter);
context.addServlet(fcgiServletHolder, "*.php");
HttpURI httpURI = request.getHttpURI();
HttpURI.Mutable newHttpURI = HttpURI.build(httpURI)
.path(appContextPath + request.getPathInContext());
newHttpURI.port(unixDomainPath == null ? ((ServerConnector)serverConnector).getLocalPort() : 0);
return newHttpURI;
}, "/scriptRoot");
fcgiHandler.setUnixDomainPath(unixDomainPath);
proxyContext.setHandler(fcgiHandler);
context.addServlet(new ServletHolder(servlet), servletPath + "/*");
ContextHandler appContext = new ContextHandler("/app");
appContext.setHandler(handler);
ContextHandlerCollection contexts = new ContextHandlerCollection(proxyContext, appContext);
server.setHandler(contexts);
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
@ -121,9 +114,9 @@ public class FastCGIProxyServletTest
}
@AfterEach
public void dispose() throws Exception
public void dispose()
{
server.stop();
LifeCycle.stop(server);
}
@ParameterizedTest(name = "[{index}] sendStatus200={0}")
@ -153,18 +146,19 @@ public class FastCGIProxyServletTest
new Random().nextBytes(data);
String path = "/foo/index.php";
prepare(sendStatus200, new HttpServlet()
start(sendStatus200, new Handler.Processor()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
public void process(Request request, Response response, Callback callback)
{
assertTrue(request.getRequestURI().endsWith(path));
response.setContentLength(data.length);
response.getOutputStream().write(data);
assertNotEquals(proxyContext.getContextPath(), request.getContext().getContextPath());
assertEquals(path, request.getPathInContext());
response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, data.length);
response.write(true, ByteBuffer.wrap(data), callback);
}
});
Request request = client.newRequest("localhost", httpConnector.getLocalPort())
var request = client.newRequest("localhost", proxyConnector.getLocalPort())
.onResponseContentAsync((response, content, callback) ->
{
try
@ -178,7 +172,7 @@ public class FastCGIProxyServletTest
callback.failed(x);
}
})
.path(path);
.path(proxyContext.getContextPath() + path);
FutureResponseListener listener = new FutureResponseListener(request, length);
request.send(listener);
@ -197,65 +191,64 @@ public class FastCGIProxyServletTest
String remotePath = "/remote/index.php";
String pathAttribute = "_path_attribute";
String queryAttribute = "_query_attribute";
fcgiParams.put(FastCGIProxyServlet.ORIGINAL_URI_ATTRIBUTE_INIT_PARAM, pathAttribute);
fcgiParams.put(FastCGIProxyServlet.ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM, queryAttribute);
prepare(sendStatus200, new HttpServlet()
start(sendStatus200, new Handler.Processor()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
public void process(Request request, Response response, Callback callback)
{
assertThat((String)request.getAttribute(FCGI.Headers.REQUEST_URI), Matchers.startsWith(originalPath));
assertThat((String)request.getAttribute(FCGI.Headers.REQUEST_URI), startsWith(originalPath));
assertEquals(originalQuery, request.getAttribute(FCGI.Headers.QUERY_STRING));
assertThat(request.getRequestURI(), Matchers.endsWith(remotePath));
assertThat(request.getPathInContext(), endsWith(remotePath));
callback.succeeded();
}
});
context.stop();
context.insertHandler(new HandlerWrapper()
fcgiHandler.setOriginalPathAttribute(pathAttribute);
fcgiHandler.setOriginalQueryAttribute(queryAttribute);
proxyContext.stop();
proxyContext.insertHandler(new Handler.Wrapper()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
public Request.Processor handle(Request request) throws Exception
{
if (target.startsWith("/remote/"))
if (request.getPathInContext().startsWith("/remote/"))
{
request.setAttribute(pathAttribute, originalPath);
request.setAttribute(queryAttribute, originalQuery);
}
super.handle(target, baseRequest, request, response);
return super.handle(request);
}
});
context.start();
proxyContext.start();
ContentResponse response = client.newRequest("localhost", httpConnector.getLocalPort())
.path(remotePath)
ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort())
.path(proxyContext.getContextPath() + remotePath)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
}
@Test
@EnabledForJreRange(min = JRE.JAVA_16)
public void testUnixDomain() throws Exception
{
int maxUnixDomainPathLength = 108;
Path path = Files.createTempFile("unix", ".sock");
if (path.normalize().toAbsolutePath().toString().length() > maxUnixDomainPathLength)
if (path.normalize().toAbsolutePath().toString().length() > UnixDomainServerConnector.MAX_UNIX_DOMAIN_PATH_LENGTH)
path = Files.createTempFile(Path.of("/tmp"), "unix", ".sock");
assertTrue(Files.deleteIfExists(path));
unixDomainPath = path;
fcgiParams.put("unixDomainPath", path.toString());
byte[] content = new byte[512];
new Random().nextBytes(content);
prepare(true, new HttpServlet()
start(true, new Handler.Processor()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
public void process(Request request, Response response, Callback callback)
{
response.getOutputStream().write(content);
response.write(true, ByteBuffer.wrap(content), callback);
}
});
ContentResponse response = client.newRequest("localhost", httpConnector.getLocalPort())
.path("/index.php")
ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort())
.path(proxyContext.getContextPath() + "/index.php")
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());

View File

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

View File

@ -19,11 +19,6 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!--<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>
<optional>true</optional>
</dependency>-->
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-client</artifactId>

View File

@ -1,16 +1,13 @@
# DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Adds the FastCGI implementation to the classpath.
Enables support for the FastCGI protocol.
[tags]
fcgi
[depend]
servlet
client
server
[lib]
lib/jetty-proxy-${jetty.version}.jar
lib/fcgi/*.jar
[ini-template]
## For configuration of FastCGI contexts, see
## https://www.eclipse.org/jetty/documentation/current/fastcgi.html
lib/fcgi/jetty-fcgi-client-${jetty.version}.jar
lib/fcgi/jetty-fcgi-server-${jetty.version}.jar

View File

@ -45,6 +45,7 @@ public class HttpStreamOverFCGI implements HttpStream
private static final Logger LOG = LoggerFactory.getLogger(HttpStreamOverFCGI.class);
private final Callback _demandCallback = new DemandCallback();
private final HttpFields.Mutable _allHeaders = HttpFields.build();
private final HttpFields.Mutable _headers = HttpFields.build();
private final ServerFCGIConnection _connection;
private final ServerGenerator _generator;
@ -91,6 +92,7 @@ public class HttpStreamOverFCGI implements HttpStream
{
String name = field.getName();
String value = field.getValue();
_allHeaders.put(field);
if (FCGI.Headers.REQUEST_METHOD.equalsIgnoreCase(name))
_method = value;
else if (FCGI.Headers.DOCUMENT_URI.equalsIgnoreCase(name))
@ -109,7 +111,7 @@ public class HttpStreamOverFCGI implements HttpStream
// TODO https?
MetaData.Request request = new MetaData.Request(_method, HttpScheme.HTTP.asString(), hostPort, pathQuery, HttpVersion.fromString(_version), _headers, Long.MIN_VALUE);
Runnable task = _httpChannel.onRequest(request);
_headers.forEach(field -> _httpChannel.getRequest().setAttribute(field.getName(), field.getValue()));
_allHeaders.forEach(field -> _httpChannel.getRequest().setAttribute(field.getName(), field.getValue()));
// TODO: here we just execute the task.
// However, we should really return all the way back to onFillable()
// and feed the Runnable to an ExecutionStrategy.

View File

@ -15,6 +15,7 @@
<modules>
<module>jetty-fcgi-client</module>
<module>jetty-fcgi-server</module>
<module>jetty-fcgi-proxy</module>
</modules>
<dependencies>

View File

@ -157,6 +157,7 @@ public class HttpTester
r.setMethod(HttpMethod.GET.asString());
r.setURI("/");
r.setVersion(HttpVersion.HTTP_1_1);
r.setHeader("Host", "localhost");
return r;
}
@ -225,6 +226,11 @@ public class HttpTester
return parseMessage(responseStream, parser) ? r : null;
}
public static Response parseResponse(ReadableByteChannel channel) throws IOException
{
return parseResponse(from(channel));
}
public static Response parseResponse(Input in) throws IOException
{
Response r;

View File

@ -0,0 +1,12 @@
[description]
Enables support for HTTP proxying.
[tags]
proxy
[depend]
client
server
[lib]
lib/jetty-proxy-${jetty.version}.jar

View File

@ -517,6 +517,37 @@ public abstract class ProxyHandler extends Handler.Processor
{
private final Function<Request, HttpURI> httpURIRewriter;
/**
* <p>Convenience constructor that provides a rewrite function
* using {@link String#replaceAll(String, String)}.</p>
* <p>As a simple example, given the URI pattern of:</p>
* <p>{@code (https?)://([a-z]+):([0-9]+)/([^/]+)/(.*)}</p>
* <p>and given a replacement string of:</p>
* <p>{@code $1://$2:9000/proxy/$5}</p>
* <p>an incoming {@code HttpURI} of:</p>
* <p>{@code http://host:8080/ctx/path}</p>
* <p>will be rewritten as:</p>
* <p>{@code http://host:9000/proxy/path}</p>
*
* @param uriPattern the regex pattern to use to match the incoming URI
* @param uriReplacement the replacement string to use to rewrite the incoming URI
*/
public Reverse(String uriPattern, String uriReplacement)
{
this(request ->
{
String uri = request.getHttpURI().toString();
return HttpURI.build(uri.replaceAll(uriPattern, uriReplacement));
});
}
/**
* <p>Creates a new instance with the given {@code HttpURI} rewrite function.</p>
* <p>The rewrite functions rewrites the client-to-proxy request URI into the
* proxy-to-server request URI.</p>
*
* @param httpURIRewriter a function that returns the URI of the server
*/
public Reverse(Function<Request, HttpURI> httpURIRewriter)
{
this.httpURIRewriter = Objects.requireNonNull(httpURIRewriter);
@ -529,11 +560,11 @@ public abstract class ProxyHandler extends Handler.Processor
/**
* {@inheritDoc}
* <p>Applications that use this class must provide a rewrite function
* to the constructor.</p>
* <p>Applications that use this class typically provide a rewrite
* function to the constructor.</p>
* <p>The rewrite function rewrites the client-to-proxy request URI,
* for example {@code http://example.com/path}, to the proxy-to-server
* request URI, for example {@code http://backend1:8080/path}.</p>
* for example {@code http://example.com/app/path}, to the proxy-to-server
* request URI, for example {@code http://backend1:8080/legacy/path}.</p>
*
* @param clientToProxyRequest the client-to-proxy request
* @return the proxy-to-server request URI.

View File

@ -0,0 +1,100 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.handler;
import java.util.List;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.resource.Resource;
/**
* <p>Inspired by nginx's {@code try_files} functionality.</p>
* <p> This handler can be configured with a list of URI paths.
* The special token {@code $path} represents the current request URI
* path (the portion after the context path).</p>
* <p>Typical example of how this handler can be configured is the following:</p>
* <pre>{@code
* TryPathsHandler tryPaths = new TryPathsHandler();
* tryPaths.setPaths("/maintenance.html", "$path", "/index.php?p=$path");
* }</pre>
* <p>For a request such as {@code /context/path/to/resource.ext}, this
* handler 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} to the next handler.</p>
* <p>The last URI path specified in the list is therefore the "fallback" to
* which the request is forwarded to in case no previous files can be found.</p>
* <p>The file paths are resolved against {@link Context#getBaseResource()}
* to make sure that only files visible to the application are served.</p>
*/
public class TryPathsHandler extends Handler.Wrapper
{
private List<String> paths;
public void setPaths(List<String> paths)
{
this.paths = paths;
}
@Override
public Request.Processor handle(Request request) throws Exception
{
String interpolated = interpolate(request, "$path");
Resource rootResource = request.getContext().getBaseResource();
if (rootResource != null)
{
for (String path : paths)
{
interpolated = interpolate(request, path);
Resource resource = rootResource.resolve(interpolated);
if (resource != null && resource.exists())
break;
}
}
Request.WrapperProcessor result = new Request.WrapperProcessor(new TryPathsRequest(request, interpolated));
return result.wrapProcessor(super.handle(result));
}
private Request.Processor fallback(Request request) throws Exception
{
String fallback = paths.isEmpty() ? "$path" : paths.get(paths.size() - 1);
String interpolated = interpolate(request, fallback);
return super.handle(new TryPathsRequest(request, interpolated));
}
private String interpolate(Request request, String value)
{
String path = request.getPathInContext();
return value.replace("$path", path);
}
private static class TryPathsRequest extends Request.Wrapper
{
private final String pathInContext;
public TryPathsRequest(Request wrapped, String pathInContext)
{
super(wrapped);
this.pathInContext = pathInContext;
}
@Override
public String getPathInContext()
{
return pathInContext;
}
}
}

View File

@ -0,0 +1,178 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.handler;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import javax.net.ssl.SSLSocket;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
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.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
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 TryPathsHandlerTest
{
private Server server;
private SslContextFactory.Server sslContextFactory;
private ServerConnector connector;
private ServerConnector sslConnector;
private Path rootPath;
private String contextPath;
private void start(List<String> paths, Handler handler) throws Exception
{
server = new Server();
connector = new ServerConnector(server, 1, 1);
server.addConnector(connector);
sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
sslConnector = new ServerConnector(server, 1, 1, sslContextFactory);
server.addConnector(sslConnector);
contextPath = "/ctx";
ContextHandler context = new ContextHandler(contextPath);
rootPath = Files.createDirectories(MavenTestingUtils.getTargetTestingPath(getClass().getSimpleName()));
FS.cleanDirectory(rootPath);
context.setBaseResource(rootPath);
server.setHandler(context);
TryPathsHandler tryPaths = new TryPathsHandler();
context.setHandler(tryPaths);
tryPaths.setPaths(paths);
ResourceHandler resourceHandler = new ResourceHandler();
tryPaths.setHandler(resourceHandler);
resourceHandler.setHandler(handler);
server.start();
}
@AfterEach
public void dispose()
{
LifeCycle.stop(server);
}
@Test
public void testTryPaths() throws Exception
{
start(List.of("/maintenance.txt", "$path", "/forward?p=$path"), new Handler.Processor()
{
@Override
public void process(Request request, Response response, Callback callback)
{
assertThat(request.getPathInContext(), equalTo("/forward?p=/last"));
response.setStatus(HttpStatus.NO_CONTENT_204);
callback.succeeded();
}
});
try (SocketChannel channel = SocketChannel.open())
{
channel.connect(new InetSocketAddress("localhost", connector.getLocalPort()));
// Make a first request without existing file paths.
HttpTester.Request request = HttpTester.newRequest();
request.setURI(contextPath + "/last");
channel.write(request.generate());
HttpTester.Response response = HttpTester.parseResponse(channel);
assertNotNull(response);
assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
// Create the specific file that is requested.
String path = "idx.txt";
Files.writeString(rootPath.resolve(path), "hello", StandardOpenOption.CREATE);
// Make a second request with the specific file.
request = HttpTester.newRequest();
request.setURI(contextPath + "/" + path);
channel.write(request.generate());
response = HttpTester.parseResponse(channel);
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertEquals("hello", response.getContent());
// Create the "maintenance" file, it should be served first.
path = "maintenance.txt";
Files.writeString(rootPath.resolve(path), "maintenance", StandardOpenOption.CREATE);
// Make a second request with any path, we should get the maintenance file.
request = HttpTester.newRequest();
request.setURI(contextPath + "/whatever");
channel.write(request.generate());
response = HttpTester.parseResponse(channel);
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertEquals("maintenance", response.getContent());
}
}
@Test
public void testSecureRequestIsForwarded() throws Exception
{
String path = "/secure";
start(List.of("$path"), new Handler.Processor()
{
@Override
public void process(Request request, Response response, Callback callback)
{
HttpURI httpURI = request.getHttpURI();
assertEquals("https", httpURI.getScheme());
assertTrue(request.isSecure());
assertEquals(path, request.getPathInContext());
callback.succeeded();
}
});
try (SSLSocket sslSocket = sslContextFactory.newSslSocket())
{
sslSocket.connect(new InetSocketAddress("localhost", sslConnector.getLocalPort()));
HttpTester.Request request = HttpTester.newRequest();
request.setURI(contextPath + path);
OutputStream output = sslSocket.getOutputStream();
output.write(BufferUtil.toArray(request.generate()));
output.flush();
HttpTester.Response response = HttpTester.parseResponse(sslSocket.getInputStream());
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
}

View File

@ -1,51 +0,0 @@
<?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-server-proxy</artifactId>
<name>EE9 :: Jetty :: FastCGI :: Proxy</name>
<description>Jetty FastCGI proxy support</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-java-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-unixdomain-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-server</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,304 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.server.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.api.Request;
import org.eclipse.jetty.ee9.proxy.AsyncProxyServlet;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.client.http.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 (!getHttpClient().isDefaultPort(request.getScheme(), port))
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

@ -1,138 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.server.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

@ -1,86 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.fcgi.server.proxy;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.ee9.fcgi.server.proxy.FastCGIProxyServlet;
import org.eclipse.jetty.ee9.servlet.DefaultServlet;
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
import org.eclipse.jetty.ee9.servlet.ServletHolder;
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
public class DrupalHTTP2FastCGIProxyServer
{
public static void main(String[] args) throws Exception
{
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setCipherComparator(new HTTP2Cipher.CipherComparator());
Server server = new Server();
// HTTP(S) Configuration
HttpConfiguration config = new HttpConfiguration();
HttpConfiguration httpsConfig = new HttpConfiguration(config);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
// HTTP2 factory
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig);
ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
alpn.setDefaultProtocol(h2.getProtocol());
// SSL Factory
SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol());
// HTTP2 Connector
ServerConnector http2Connector =
new ServerConnector(server, ssl, alpn, h2, new HttpConnectionFactory(httpsConfig));
http2Connector.setPort(8443);
http2Connector.setIdleTimeout(15000);
server.addConnector(http2Connector);
// Drupal seems to only work on the root context,
// at least out of the box without additional plugins
String root = "/home/simon/programs/drupal-7.23";
ServletContextHandler context = new ServletContextHandler(server, "/");
context.setResourceBase(root);
context.setWelcomeFiles(new String[]{"index.php"});
// Serve static resources
ServletHolder defaultServlet = new ServletHolder(DefaultServlet.class);
defaultServlet.setName("default");
context.addServlet(defaultServlet, "/");
// FastCGI
ServletHolder fcgiServlet = new ServletHolder(FastCGIProxyServlet.class);
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_ROOT_INIT_PARAM, root);
fcgiServlet.setInitParameter("proxyTo", "http://localhost:9000");
fcgiServlet.setInitParameter("prefix", "/");
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_PATTERN_INIT_PARAM, "(.+\\.php)");
context.addServlet(fcgiServlet, "*.php");
server.start();
}
}

View File

@ -1,108 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.fcgi.server.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.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.ee9.fcgi.server.proxy.TryFilesFilter;
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();
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

@ -1,92 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 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.fcgi.server.proxy;
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.ee9.fcgi.server.proxy.FastCGIProxyServlet;
import org.eclipse.jetty.ee9.fcgi.server.proxy.TryFilesFilter;
import org.eclipse.jetty.ee9.servlet.DefaultServlet;
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.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
public class WordPressHTTP2FastCGIProxyServer
{
public static void main(String[] args) throws Exception
{
int tlsPort = 8443;
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setCipherComparator(new HTTP2Cipher.CipherComparator());
Server server = new Server();
// HTTP(S) Configuration
HttpConfiguration httpConfig = new HttpConfiguration();
HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
// HTTP2 factory
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig);
ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
alpn.setDefaultProtocol(h2.getProtocol());
// SSL Factory
SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol());
// HTTP2 Connector
ServerConnector http2Connector =
new ServerConnector(server, ssl, alpn, h2, new HttpConnectionFactory(httpsConfig));
http2Connector.setPort(tlsPort);
http2Connector.setIdleTimeout(15000);
server.addConnector(http2Connector);
String root = "/home/simon/programs/wordpress-3.7.1";
ServletContextHandler context = new ServletContextHandler(server, "/wp");
context.setResourceBase(root);
context.setWelcomeFiles(new String[]{"index.php"});
// Serve static resources
ServletHolder defaultServlet = new ServletHolder("default", DefaultServlet.class);
context.addServlet(defaultServlet, "/");
FilterHolder tryFilesFilter = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// tryFilesFilter.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path $path/index.php"); // Permalink /?p=123
tryFilesFilter.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path /index.php?p=$path"); // Permalink /%year%/%monthnum%/%postname%
// FastCGI
ServletHolder fcgiServlet = context.addServlet(FastCGIProxyServlet.class, "*.php");
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_ROOT_INIT_PARAM, root);
fcgiServlet.setInitParameter("proxyTo", "http://localhost:9000");
fcgiServlet.setInitParameter("prefix", "/");
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_PATTERN_INIT_PARAM, "(.+?\\.php)");
server.start();
}
}

View File

@ -56,7 +56,6 @@
<module>jetty-ee9-annotations</module>
<!-- <module>jetty-ee9-ant</module>-->
<module>jetty-ee9-cdi</module>
<!-- <module>jetty-ee9-fcgi-server-proxy</module>-->
<module>jetty-ee9-jaas</module>
<module>jetty-ee9-jaspi</module>
<module>jetty-ee9-jndi</module>

View File

@ -523,6 +523,11 @@
<artifactId>jetty-unixdomain-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-proxy</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-server</artifactId>

View File

@ -1337,6 +1337,11 @@
<artifactId>jetty-fcgi-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-proxy</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.gcloud</groupId>
<artifactId>jetty-gcloud-session-manager</artifactId>

View File

@ -1174,4 +1174,107 @@ public class DistributionTests extends AbstractJettyHomeTest
}
}
}
@Test
public void testFastCGIProxying() throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
List<String> args1 = List.of("--add-modules=resources,http,fcgi,fcgi-proxy,core-deploy");
try (JettyHomeTester.Run run1 = distribution.start(args1))
{
assertTrue(run1.awaitFor(START_TIMEOUT, TimeUnit.SECONDS));
assertEquals(0, run1.getExitValue());
// Add a FastCGI connector to simulate, for example, php-fpm.
int fcgiPort = distribution.freePort();
Path jettyBase = distribution.getJettyBase();
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="baseResource">
<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 FastCGIProxyHandler.
// Converts URIs from http://host:<httpPort>/proxy/foo to http://host:<fcgiPort>/app/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.server.handler.ContextHandler">
<Set name="contextPath">/proxy</Set>
<Set name="handler">
<New class="org.eclipse.jetty.fcgi.proxy.FastCGIProxyHandler">
<Arg>(https?)://([^:]+):(\\d+)/([^/]+)/(.*)</Arg>
<Arg>$1://$2:$P/php/$5</Arg>
<Arg>/var/wordpress</Arg>
</New>
</Set>
</Configure>
""".replace("$P", String.valueOf(fcgiPort)), 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@", 10, 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));
}
}
}
}