Moved FastCGIProxyServlet in ee9 to FastCGIProxyHandler in core. (#8718)
Moved FastCGIProxyServlet in ee9 to FastCGIProxyHandler in core. Moved TryFilesFilter in ee9 to TryPathsHandler in core. Update Jetty modules to properly setup the class-path in case of proxy usage. Fixes Surefire configuration to run the tests. The problem is that fcgi-proxy depends on fcgi-client, so both will be put on the module-path. However, fcgi-server is used for the tests, it is in the class-path, but depends on fcgi-client that is on the module-path. Therefore, when a fcgi-server class tries to access a fcgi-client class, JPMS throws because the fcgi-client module does not export to the unnamed module. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
e8b818db15
commit
0d0790834d
|
@ -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>
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<modules>
|
||||
<module>jetty-fcgi-client</module>
|
||||
<module>jetty-fcgi-server</module>
|
||||
<module>jetty-fcgi-proxy</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
[description]
|
||||
Enables support for HTTP proxying.
|
||||
|
||||
[tags]
|
||||
proxy
|
||||
|
||||
[depend]
|
||||
client
|
||||
server
|
||||
|
||||
[lib]
|
||||
lib/jetty-proxy-${jetty.version}.jar
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
* <filter>
|
||||
* <filter-name>try_files</filter-name>
|
||||
* <filter-class>org.eclipse.jetty.fcgi.server.proxy.TryFilesFilter</filter-class>
|
||||
* <init-param>
|
||||
* <param-name>files</param-name>
|
||||
* <param-value>/maintenance.html $path /index.php?p=$path</param-value>
|
||||
* </init-param>
|
||||
* </filter>
|
||||
* </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()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue