org.eclipse.jetty.fcgi.proxy.FastCGIProxyHandler Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.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.transport.HttpClientTransportOverFCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Transport;
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.URIUtil;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Specific implementation of {@link ProxyHandler.Reverse} for FastCGI.
* 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.
*
* @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 fcgiEnvNames;
private Path unixDomainPath;
/**
* Creates a new instance that rewrites the {@code HttpURI}
* with the given pattern and replacement strings, using
* {@link String#replaceAll(String, String)}.
*
* @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);
}
/**
* Creates a new instance with the given {@code HttpURI} rewriter
* function.
* The {@code HttpURI} rewriter function should return the URI
* of the FastCGI server.
* 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.
*
* @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 httpURIRewriter, String scriptRoot)
{
super(httpURIRewriter);
this.scriptRoot = Objects.requireNonNull(scriptRoot);
}
/**
* Get the root directory path of the FastCGI files.
* @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;
}
/**
* Sets a regular expression with at least 1 and at most 2 groups
* that specify respectively:
*
* - the FastCGI {@code SCRIPT_NAME} parameter
* - the FastCGI {@code PATH_INFO} parameter
*
*
* @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;
}
/**
* Sets the client-to-proxy request attribute name to use to
* retrieve the original request path.
* For example, the request URI may be rewritten by a previous
* handler that might save the original request path in a request
* attribute.
*
* @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;
}
/**
* Sets the client-to-proxy request attribute name to use to
* retrieve the original request query.
* For example, the request URI may be rewritten by a previous
* handler that might save the original request query in a request
* attribute.
*
* @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;
}
/**
* Sets whether to forward the {@code HTTPS} FastCGI parameter
* in the FastCGI request to the FastCGI server.
*
* @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 getFastCGIEnvNames()
{
return fcgiEnvNames;
}
/**
* 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.
*
* @param fcgiEnvNames the names of the environment variables
* forwarded in the FastCGI request
* @see System#getenv(String)
*/
public void setFastCGIEnvNames(Set 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;
}
/**
* Sets the Unix-Domain path the FastCGI server listens to.
* 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.
*
* @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 = 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.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);
// If the request URI been rewritten, use the original request URI.
String originalPath = null;
String originalQuery = null;
String originalPathAttribute = getOriginalPathAttribute();
if (originalPathAttribute != null)
originalPath = (String)clientToProxyRequest.getAttribute(originalPathAttribute);
if (originalPath != null)
{
String originalQueryAttribute = getOriginalQueryAttribute();
if (originalQueryAttribute != null)
{
originalQuery = (String)clientToProxyRequest.getAttribute(originalQueryAttribute);
if (originalQuery != null)
originalPath += "?" + originalQuery;
}
}
if (originalPath != null)
proxyToServerRequest.attribute(REQUEST_URI_ATTRIBUTE, originalPath);
if (originalQuery != null)
proxyToServerRequest.attribute(REQUEST_QUERY_ATTRIBUTE, originalQuery);
// If the Host header is missing, add it.
if (!proxyToServerRequest.getHeaders().contains(HttpHeader.HOST))
{
if (serverPort != URIUtil.getDefaultPortForScheme(scheme))
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 cookies = proxyToServerRequest.getHeaders().getValuesList(HttpHeader.COOKIE);
if (cookies.size() > 1)
{
String allCookies = String.join("; ", cookies);
proxyToServerRequest.headers(headers -> headers.put(HttpHeader.COOKIE, allCookies));
}
Path unixDomain = getUnixDomainPath();
if (unixDomain != null)
proxyToServerRequest.transport(new Transport.TCPUnix(unixDomain));
super.sendProxyToServerRequest(clientToProxyRequest, proxyToServerRequest, proxyToClientResponse, proxyToClientCallback);
}
protected void customizeFastCGIHeaders(org.eclipse.jetty.client.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.Request proxyToServerRequest, HttpFields.Mutable fastCGIHeaders)
{
super.customize(proxyToServerRequest, fastCGIHeaders);
customizeFastCGIHeaders(proxyToServerRequest, fastCGIHeaders);
if (LOG.isDebugEnabled())
{
TreeMap 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)));
}
}
}
}