Fix/jetty 12 restore ee n fcgi (#9796)

* Restored ee9 FastCGIProxyServlet and TryFilesFilter.
* Restored ee10 FastCGIProxyServlet and TryFilesFilter.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2023-06-05 16:50:11 +02:00 committed by GitHub
parent 1c01a49149
commit 22641f1267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1434 additions and 1 deletions

View File

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

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-ee10-fcgi-proxy</artifactId>
<name>EE10 :: FCGI Proxy</name>
<properties>
<bundle-symbolic-name>${project.groupId}.fcgi.proxy</bundle-symbolic-name>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-proxy</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -0,0 +1,23 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
module org.eclipse.jetty.ee10.fcgi.proxy
{
requires transitive org.eclipse.jetty.ee10.proxy;
requires transitive org.eclipse.jetty.fcgi.client;
requires transitive org.eclipse.jetty.server;
requires static jakarta.servlet;
exports org.eclipse.jetty.ee10.fcgi.proxy;
}

View File

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

View File

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

View File

@ -0,0 +1,107 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.fcgi.proxy;
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TryFilesFilterTest
{
private Server server;
private ServerConnector connector;
private ServerConnector sslConnector;
private HttpClient client;
private String forwardPath;
public void prepare(HttpServlet servlet) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
serverSslContextFactory.setKeyStorePassword("storepwd");
sslConnector = new ServerConnector(server, serverSslContextFactory);
server.addConnector(sslConnector);
ServletContextHandler context = new ServletContextHandler(server, "/");
FilterHolder filterHolder = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
forwardPath = "/index.php";
filterHolder.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path " + forwardPath + "?p=$path");
context.addServlet(new ServletHolder(servlet), "/*");
ClientConnector clientConnector = new ClientConnector();
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(true);
// clientSslContextFactory.setEndpointIdentificationAlgorithm(null);
// clientSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
// clientSslContextFactory.setKeyStorePassword("storepwd");
clientConnector.setSslContextFactory(clientSslContextFactory);
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector));
server.addBean(client);
server.start();
}
@AfterEach
public void dispose() throws Exception
{
server.stop();
}
@Test
public void testHTTPSRequestIsForwarded() throws Exception
{
final String path = "/one/";
prepare(new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
{
assertTrue("https".equalsIgnoreCase(req.getScheme()));
assertTrue(req.isSecure());
assertEquals(forwardPath, req.getRequestURI());
assertTrue(req.getQueryString().endsWith(path));
}
});
ContentResponse response = client.newRequest("localhost", sslConnector.getLocalPort())
.scheme("https")
.path(path)
.send();
assertEquals(200, response.getStatus());
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-ee9-fcgi-proxy</artifactId>
<name>EE9 :: FCGI Proxy</name>
<properties>
<bundle-symbolic-name>${project.groupId}.fcgi.proxy</bundle-symbolic-name>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-proxy</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>jetty-fcgi-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-servlet</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -0,0 +1,23 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
module org.eclipse.jetty.ee9.fcgi.proxy
{
requires transitive org.eclipse.jetty.ee9.proxy;
requires transitive org.eclipse.jetty.fcgi.client;
requires transitive org.eclipse.jetty.server;
requires static jetty.servlet.api;
exports org.eclipse.jetty.ee9.fcgi.proxy;
}

View File

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

View File

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

View File

@ -0,0 +1,107 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee9.fcgi.proxy;
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.ee9.servlet.FilterHolder;
import org.eclipse.jetty.ee9.servlet.ServletContextHandler;
import org.eclipse.jetty.ee9.servlet.ServletHolder;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TryFilesFilterTest
{
private Server server;
private ServerConnector connector;
private ServerConnector sslConnector;
private HttpClient client;
private String forwardPath;
public void prepare(HttpServlet servlet) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server();
serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
serverSslContextFactory.setKeyStorePassword("storepwd");
sslConnector = new ServerConnector(server, serverSslContextFactory);
server.addConnector(sslConnector);
ServletContextHandler context = new ServletContextHandler(server, "/");
FilterHolder filterHolder = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
forwardPath = "/index.php";
filterHolder.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path " + forwardPath + "?p=$path");
context.addServlet(new ServletHolder(servlet), "/*");
ClientConnector clientConnector = new ClientConnector();
SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(true);
// clientSslContextFactory.setEndpointIdentificationAlgorithm(null);
// clientSslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
// clientSslContextFactory.setKeyStorePassword("storepwd");
clientConnector.setSslContextFactory(clientSslContextFactory);
client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector));
server.addBean(client);
server.start();
}
@AfterEach
public void dispose() throws Exception
{
server.stop();
}
@Test
public void testHTTPSRequestIsForwarded() throws Exception
{
final String path = "/one/";
prepare(new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
{
assertTrue("https".equalsIgnoreCase(req.getScheme()));
assertTrue(req.isSecure());
assertEquals(forwardPath, req.getRequestURI());
assertTrue(req.getQueryString().endsWith(path));
}
});
ContentResponse response = client.newRequest("localhost", sslConnector.getLocalPort())
.scheme("https")
.path(path)
.send();
assertEquals(200, response.getStatus());
}
}

View File

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

View File

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

View File

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

View File

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