diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java
index a33b08e63de2..476758311991 100644
--- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java
+++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/TryPathsHandler.java
@@ -16,36 +16,110 @@
import java.util.List;
import org.eclipse.jetty.http.HttpURI;
-import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.URIUtil;
-import org.eclipse.jetty.util.resource.Resource;
/**
*
Inspired by nginx's {@code try_files} functionality.
- * 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).
+ *
+ * This handler can be configured with a list of rewrite URI paths.
+ * The special token {@code $path} represents the current request
+ * {@code pathInContext} (the portion after the context path).
+ *
* Typical example of how this handler can be configured is the following:
* {@code
- * TryPathsHandler tryPaths = new TryPathsHandler();
- * tryPaths.setPaths("/maintenance.html", "$path", "/index.php?p=$path");
+ * TryPathsHandler tryPathsHandler = new TryPathsHandler();
+ * tryPathsHandler.setPaths("/maintenance.html", "$path", "/index.php?p=$path");
+ *
+ * PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
+ * tryPathsHandler.setHandler(pathMappingsHandler);
+ *
+ * pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new PHPHandler());
+ * pathMappingsHandler.addMapping(new ServletPathSpec("/"), new ResourceHandler());
* }
- * 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.
- * 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.
- * The file paths are resolved against {@link Context#getBaseResource()}
- * to make sure that only files visible to the application are served.
+ *
+ * For a request such as {@code /context/path/to/resource.ext}:
+ *
+ * - This handler rewrites the request {@code pathInContext} to
+ * {@code /maintenance.html} and forwards the request to the next handler,
+ * where it matches the {@code /} mapping, hitting the {@code ResourceHandler}
+ * that serves the file if it exists.
+ * - Otherwise, this handler rewrites the request {@code pathInContext} to
+ * {@code /path/to/resource.ext} and forwards the request to the next handler,
+ * where it matches the {@code /} mapping, hitting the {@code ResourceHandler}
+ * that serves the file if it exists.
+ * - Otherwise, this handler rewrites the request {@code pathInContext} to
+ * {@code /index.php?p=/path/to/resource.ext} and forwards the request to
+ * the next handler, where it matches the {@code *.php} mapping, hitting
+ * the {@code PHPHandler}.
+ *
+ *
+ * The original path and query may be stored as request attributes,
+ * under the names specified by {@link #setOriginalPathAttribute(String)}
+ * and {@link #setOriginalQueryAttribute(String)}.
*/
public class TryPathsHandler extends Handler.Wrapper
{
+ private String originalPathAttribute;
+ private String originalQueryAttribute;
private List paths;
+ /**
+ * @return the attribute name of the original request path
+ */
+ public String getOriginalPathAttribute()
+ {
+ return originalPathAttribute;
+ }
+
+ /**
+ * Sets the request attribute name to use to
+ * retrieve the original request path.
+ *
+ * @param originalPathAttribute the attribute name of the original
+ * request path
+ */
+ public void setOriginalPathAttribute(String originalPathAttribute)
+ {
+ this.originalPathAttribute = originalPathAttribute;
+ }
+
+ /**
+ * @return the attribute name of the original request query
+ */
+ public String getOriginalQueryAttribute()
+ {
+ return originalQueryAttribute;
+ }
+
+ /**
+ * Sets the request attribute name to use to
+ * retrieve the original request query.
+ *
+ * @param originalQueryAttribute the attribute name of the original
+ * request query
+ */
+ public void setOriginalQueryAttribute(String originalQueryAttribute)
+ {
+ this.originalQueryAttribute = originalQueryAttribute;
+ }
+
+ /**
+ * @return the rewrite URI paths
+ */
+ public List getPaths()
+ {
+ return paths;
+ }
+
+ /**
+ * Sets a list of rewrite URI paths.
+ * The special token {@code $path} represents the current request
+ * {@code pathInContext} (the portion after the context path).
+ *
+ * @param paths the rewrite URI paths
+ */
public void setPaths(List paths)
{
this.paths = paths;
@@ -54,27 +128,15 @@ public void setPaths(List 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)
{
- for (String path : paths)
- {
- interpolated = interpolate(request, path);
- Resource resource = rootResource.resolve(interpolated);
- if (resource != null && resource.exists())
- break;
- }
+ String interpolated = interpolate(request, path);
+ Request.WrapperProcessor result = new Request.WrapperProcessor(new TryPathsRequest(request, interpolated));
+ Request.Processor childProcessor = super.handle(result);
+ if (childProcessor != null)
+ return result.wrapProcessor(childProcessor);
}
- 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));
+ return null;
}
private String interpolate(Request request, String value)
@@ -83,14 +145,37 @@ private String interpolate(Request request, String value)
return value.replace("$path", path);
}
- private static class TryPathsRequest extends Request.Wrapper
+ private class TryPathsRequest extends Request.Wrapper
{
private final HttpURI _uri;
- public TryPathsRequest(Request wrapped, String pathInContext)
+ public TryPathsRequest(Request wrapped, String newPathQuery)
{
super(wrapped);
- _uri = Request.newHttpURIFrom(wrapped, URIUtil.canonicalPath(pathInContext));
+
+ HttpURI originalURI = wrapped.getHttpURI();
+
+ String originalPathAttribute = getOriginalPathAttribute();
+ if (originalPathAttribute != null)
+ setAttribute(originalPathAttribute, Request.getPathInContext(wrapped));
+ String originalQueryAttribute = getOriginalQueryAttribute();
+ if (originalQueryAttribute != null)
+ setAttribute(originalQueryAttribute, originalURI.getQuery());
+
+ String originalContextPath = Request.getContextPath(wrapped);
+ HttpURI.Mutable rewrittenURI = HttpURI.build(originalURI);
+ int queryIdx = newPathQuery.indexOf('?');
+ if (queryIdx >= 0)
+ {
+ String path = newPathQuery.substring(0, queryIdx);
+ rewrittenURI.path(URIUtil.addPaths(originalContextPath, path));
+ rewrittenURI.query(newPathQuery.substring(queryIdx + 1));
+ }
+ else
+ {
+ rewrittenURI.path(URIUtil.addPaths(originalContextPath, newPathQuery));
+ }
+ _uri = rewrittenURI.asImmutable();
}
@Override
diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/TryPathsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/TryPathsHandlerTest.java
index ea1d98f160cd..5453689d85f0 100644
--- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/TryPathsHandlerTest.java
+++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/TryPathsHandlerTest.java
@@ -22,16 +22,18 @@
import java.util.List;
import javax.net.ssl.SSLSocket;
+import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.pathmap.ServletPathSpec;
+import org.eclipse.jetty.io.Content;
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.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
@@ -41,18 +43,20 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
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
{
+ public WorkDir workDir;
+ private static final String CONTEXT_PATH = "/ctx";
private Server server;
private SslContextFactory.Server sslContextFactory;
private ServerConnector connector;
private ServerConnector sslConnector;
private Path rootPath;
- private String contextPath;
private void start(List paths, Handler handler) throws Exception
{
@@ -66,22 +70,18 @@ private void start(List paths, Handler handler) throws Exception
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);
+ ContextHandler context = new ContextHandler(CONTEXT_PATH);
+ rootPath = workDir.getEmptyPathDir();
context.setBaseResourceAsPath(rootPath);
server.setHandler(context);
TryPathsHandler tryPaths = new TryPathsHandler();
context.setHandler(tryPaths);
- tryPaths.setPaths(paths);
-
- ResourceHandler resourceHandler = new ResourceHandler();
- tryPaths.setHandler(resourceHandler);
- resourceHandler.setHandler(handler);
+ tryPaths.setPaths(paths);
+ tryPaths.setHandler(handler);
+ server.setDumpAfterStart(true);
server.start();
}
@@ -94,47 +94,155 @@ public void dispose()
@Test
public void testTryPaths() throws Exception
{
- start(List.of("/maintenance.txt", "$path", "/forward?p=$path"), new Handler.Processor()
+ ResourceHandler resourceHandler = new ResourceHandler();
+ resourceHandler.setDirAllowed(false);
+ resourceHandler.setHandler(new Handler.Abstract()
{
@Override
- public void process(Request request, Response response, Callback callback)
+ public Request.Processor handle(Request request)
{
- assertThat(Request.getPathInContext(request), equalTo("/forward%3Fp=/last"));
- response.setStatus(HttpStatus.NO_CONTENT_204);
- callback.succeeded();
+ if (!Request.getPathInContext(request).startsWith("/forward"))
+ return null;
+
+ return new Handler.Processor()
+ {
+ public void process(Request request, Response response, Callback callback)
+ {
+ assertThat(Request.getPathInContext(request), equalTo("/forward"));
+ assertThat(request.getHttpURI().getQuery(), equalTo("p=/last"));
+ response.setStatus(HttpStatus.NO_CONTENT_204);
+ callback.succeeded();
+ }
+ };
+ }
+ });
+
+ start(List.of("/maintenance.txt", "$path", "/forward?p=$path"), resourceHandler);
+
+ 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(CONTEXT_PATH + "/last");
+ channel.write(request.generate());
+ HttpTester.Response response = HttpTester.parseResponse(channel);
+ assertNotNull(response);
+ assertEquals(HttpStatus.NO_CONTENT_204, response.getStatus());
+
+ // Create the specific static 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(CONTEXT_PATH + "/" + 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 third request with any path, we should get the maintenance file.
+ request = HttpTester.newRequest();
+ request.setURI(CONTEXT_PATH + "/whatever");
+ channel.write(request.generate());
+ response = HttpTester.parseResponse(channel);
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertEquals("maintenance", response.getContent());
+ }
+ }
+
+ @Test
+ public void testTryPathsWithPathMappings() throws Exception
+ {
+ ResourceHandler resourceHandler = new ResourceHandler();
+ resourceHandler.setDirAllowed(false);
+
+ PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
+ pathMappingsHandler.addMapping(new ServletPathSpec("/"), resourceHandler);
+ pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new Handler.Abstract()
+ {
+ @Override
+ public Request.Processor handle(Request request)
+ {
+ return new Processor()
+ {
+ @Override
+ public void process(Request request, Response response, Callback callback)
+ {
+ response.setStatus(HttpStatus.OK_200);
+ response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8");
+ String message = "PHP: pathInContext=%s, query=%s".formatted(Request.getPathInContext(request), request.getHttpURI().getQuery());
+ Content.Sink.write(response, true, message, callback);
+ }
+ };
+ }
+ });
+ pathMappingsHandler.addMapping(new ServletPathSpec("/forward"), new Handler.Abstract()
+ {
+ @Override
+ public Request.Processor handle(Request request)
+ {
+ return new Handler.Processor()
+ {
+ public void process(Request request, Response response, Callback callback)
+ {
+ assertThat(Request.getPathInContext(request), equalTo("/forward"));
+ assertThat(request.getHttpURI().getQuery(), equalTo("p=/last"));
+ response.setStatus(HttpStatus.NO_CONTENT_204);
+ callback.succeeded();
+ }
+ };
}
});
+ start(List.of("/maintenance.txt", "$path", "/forward?p=$path"), pathMappingsHandler);
+
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");
+ request.setURI(CONTEXT_PATH + "/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.
+ // Create the specific static 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);
+ request.setURI(CONTEXT_PATH + "/" + path);
channel.write(request.generate());
response = HttpTester.parseResponse(channel);
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertEquals("hello", response.getContent());
+ // Request an existing PHP file.
+ Files.writeString(rootPath.resolve("index.php"), "raw-php-contents", StandardOpenOption.CREATE);
+ request = HttpTester.newRequest();
+ request.setURI(CONTEXT_PATH + "/index.php");
+ channel.write(request.generate());
+ response = HttpTester.parseResponse(channel);
+ assertNotNull(response);
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertThat(response.getContent(), startsWith("PHP: pathInContext=/index.php"));
+
// 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");
+ request.setURI(CONTEXT_PATH + "/whatever");
channel.write(request.generate());
response = HttpTester.parseResponse(channel);
assertNotNull(response);
@@ -165,7 +273,7 @@ public void process(Request request, Response response, Callback callback)
sslSocket.connect(new InetSocketAddress("localhost", sslConnector.getLocalPort()));
HttpTester.Request request = HttpTester.newRequest();
- request.setURI(contextPath + path);
+ request.setURI(CONTEXT_PATH + path);
OutputStream output = sslSocket.getOutputStream();
output.write(BufferUtil.toArray(request.generate()));
output.flush();