From ec28d222c412e733abf9b1131210cee183d64bc8 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 27 Oct 2022 11:17:53 -0500 Subject: [PATCH 1/7] Example of using TryPathsHandler with PHP + This utilizes the matching logic of the PathMappingsHandler --- .../jetty/server/handler/TryPathsHandler.java | 16 ++- .../server/handler/TryPathsHandlerTest.java | 131 +++++++++++++++--- 2 files changed, 126 insertions(+), 21 deletions(-) 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..26f29aa7dd19 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 @@ -87,10 +87,22 @@ private static class TryPathsRequest extends Request.Wrapper { private final HttpURI _uri; - public TryPathsRequest(Request wrapped, String pathInContext) + public TryPathsRequest(Request wrapped, String interpolated) { super(wrapped); - _uri = Request.newHttpURIFrom(wrapped, URIUtil.canonicalPath(pathInContext)); + int queryIdx = interpolated.indexOf('?'); + if (queryIdx >= 0) + { + String path = interpolated.substring(0, queryIdx); + _uri = HttpURI.build(wrapped.getHttpURI()) + .path(URIUtil.addPaths(Request.getContextPath(wrapped), path)) + .query(interpolated.substring(queryIdx+1)) + .asImmutable(); + } + else + { + _uri = Request.newHttpURIFrom(wrapped, URIUtil.canonicalPath(interpolated)); + } } @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..f868440b18ac 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 @@ -16,22 +16,24 @@ import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; 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.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.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,12 +43,14 @@ 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 Server server; private SslContextFactory.Server sslContextFactory; private ServerConnector connector; @@ -68,19 +72,15 @@ private void start(List paths, Handler handler) throws Exception contextPath = "/ctx"; ContextHandler context = new ContextHandler(contextPath); - rootPath = Files.createDirectories(MavenTestingUtils.getTargetTestingPath(getClass().getSimpleName())); - FS.cleanDirectory(rootPath); + 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.start(); } @@ -94,16 +94,10 @@ public void dispose() @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(request), equalTo("/forward%3Fp=/last")); - response.setStatus(HttpStatus.NO_CONTENT_204); - callback.succeeded(); - } - }); + ResourceHandler resourceHandler = new ResourceHandler(); + resourceHandler.setHandler(new NoContentHandler()); + + start(List.of("/maintenance.txt", "$path", "/forward?p=$path"), resourceHandler); try (SocketChannel channel = SocketChannel.open()) { @@ -143,6 +137,66 @@ public void process(Request request, Response response, Callback callback) } } + @Test + public void testTryPathsPhpPathMappingsHandler() throws Exception + { + ResourceHandler resourceHandler = new ResourceHandler(); + + PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); + pathMappingsHandler.addMapping(new ServletPathSpec("/"), resourceHandler); + pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new ExamplePhpHandler()); + pathMappingsHandler.addMapping(new ServletPathSpec("/forward"), new NoContentHandler()); + + start(List.of("/maintenance.txt", "$path", "/forward?p=$path"), pathMappingsHandler); + + try (SocketChannel channel = SocketChannel.open()) + { + channel.connect(new InetSocketAddress("localhost", connector.getLocalPort())); + + // Request something that doesn't exist + 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 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); + channel.write(request.generate()); + response = HttpTester.parseResponse(channel); + assertNotNull(response); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertEquals("hello", response.getContent()); + + // Request a php resource + Files.writeString(rootPath.resolve("index.php"), "raw-php-contents", StandardOpenOption.CREATE); + request = HttpTester.newRequest(); + request.setURI(contextPath + "/index.php"); + channel.write(request.generate()); + response = HttpTester.parseResponse(channel); + assertNotNull(response); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContent(), startsWith("Example 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"); + 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 { @@ -175,4 +229,43 @@ public void process(Request request, Response response, Callback callback) assertEquals(HttpStatus.OK_200, response.getStatus()); } } + + public static class ExamplePhpHandler extends Handler.Abstract + { + @Override + public Request.Processor handle(Request request) throws Exception + { + return new Handler.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 = "Example PHP: pathInContext=%s, query=%s".formatted(Request.getPathInContext(request), request.getHttpURI().getQuery()); + + response.write(true, BufferUtil.toBuffer(message, StandardCharsets.UTF_8), callback); + } + }; + } + } + + public static class NoContentHandler extends Handler.Abstract + { + @Override + public Request.Processor handle(Request request) throws Exception + { + 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(); + } + }; + } + } } From 63d0f5977f52effa13789e827921b7315d51b421 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 27 Oct 2022 11:19:21 -0500 Subject: [PATCH 2/7] Removing fallback method (not used) --- .../org/eclipse/jetty/server/handler/TryPathsHandler.java | 7 ------- 1 file changed, 7 deletions(-) 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 26f29aa7dd19..079e8dcb6323 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 @@ -70,13 +70,6 @@ public Request.Processor handle(Request request) throws Exception 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(request); From aff4c96d7cacf092bc1d0848bfd6d5bd6cccaf6d Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 27 Oct 2022 11:44:37 -0500 Subject: [PATCH 3/7] Fixing checkstyle --- .../java/org/eclipse/jetty/server/handler/TryPathsHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 079e8dcb6323..94208a772879 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 @@ -89,7 +89,7 @@ public TryPathsRequest(Request wrapped, String interpolated) String path = interpolated.substring(0, queryIdx); _uri = HttpURI.build(wrapped.getHttpURI()) .path(URIUtil.addPaths(Request.getContextPath(wrapped), path)) - .query(interpolated.substring(queryIdx+1)) + .query(interpolated.substring(queryIdx + 1)) .asImmutable(); } else From 2c8be7ab26add02cd7b0fe97c7ba8aac3c993692 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 27 Oct 2022 11:47:27 -0500 Subject: [PATCH 4/7] Easier to follow TryPathsRequest --- .../jetty/server/handler/TryPathsHandler.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 94208a772879..f327746078d7 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 @@ -83,19 +83,21 @@ private static class TryPathsRequest extends Request.Wrapper public TryPathsRequest(Request wrapped, String interpolated) { super(wrapped); + + HttpURI.Mutable rewrittenUri = HttpURI.build(wrapped.getHttpURI()); + int queryIdx = interpolated.indexOf('?'); if (queryIdx >= 0) { String path = interpolated.substring(0, queryIdx); - _uri = HttpURI.build(wrapped.getHttpURI()) - .path(URIUtil.addPaths(Request.getContextPath(wrapped), path)) - .query(interpolated.substring(queryIdx + 1)) - .asImmutable(); + rewrittenUri.path(URIUtil.addPaths(Request.getContextPath(wrapped), path)); + rewrittenUri.query(interpolated.substring(queryIdx + 1)); } else { - _uri = Request.newHttpURIFrom(wrapped, URIUtil.canonicalPath(interpolated)); + rewrittenUri.path(URIUtil.addPaths(Request.getContextPath(wrapped), interpolated)); } + _uri = rewrittenUri.asImmutable(); } @Override From b67db99a2180dac3ecc724eed1d7b2b1d199cf7b Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 27 Oct 2022 15:40:38 -0500 Subject: [PATCH 5/7] Request.Processor for TryPathsHandler --- .../jetty/server/handler/TryPathsHandler.java | 20 ++--- .../server/handler/TryPathsHandlerTest.java | 81 ++++++++++++------- 2 files changed, 57 insertions(+), 44 deletions(-) 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 f327746078d7..d3b58c10a868 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 @@ -20,7 +20,6 @@ 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.

@@ -54,20 +53,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)); + return null; } private String interpolate(Request request, String value) 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 f868440b18ac..bf03c5643759 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 @@ -51,12 +51,12 @@ 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 { @@ -70,8 +70,7 @@ 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); + ContextHandler context = new ContextHandler(CONTEXT_PATH); rootPath = workDir.getEmptyPathDir(); context.setBaseResourceAsPath(rootPath); server.setHandler(context); @@ -82,6 +81,7 @@ private void start(List paths, Handler handler) throws Exception tryPaths.setPaths(paths); tryPaths.setHandler(handler); + server.setDumpAfterStart(true); server.start(); } @@ -95,7 +95,27 @@ public void dispose() public void testTryPaths() throws Exception { ResourceHandler resourceHandler = new ResourceHandler(); - resourceHandler.setHandler(new NoContentHandler()); + resourceHandler.setDirAllowed(false); + resourceHandler.setHandler(new Handler.Abstract() + { + @Override + public Request.Processor handle(Request request) + { + 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); @@ -105,7 +125,7 @@ public void testTryPaths() throws Exception // 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); @@ -116,7 +136,7 @@ public void testTryPaths() throws Exception 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); @@ -128,7 +148,7 @@ public void testTryPaths() throws Exception 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); @@ -141,11 +161,28 @@ public void testTryPaths() throws Exception public void testTryPathsPhpPathMappingsHandler() throws Exception { ResourceHandler resourceHandler = new ResourceHandler(); + resourceHandler.setDirAllowed(false); PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); pathMappingsHandler.addMapping(new ServletPathSpec("/"), resourceHandler); pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new ExamplePhpHandler()); - pathMappingsHandler.addMapping(new ServletPathSpec("/forward"), new NoContentHandler()); + 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); @@ -155,7 +192,7 @@ public void testTryPathsPhpPathMappingsHandler() throws Exception // Request something that doesn't exist 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); @@ -166,7 +203,7 @@ public void testTryPathsPhpPathMappingsHandler() throws Exception 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); @@ -176,7 +213,7 @@ public void testTryPathsPhpPathMappingsHandler() throws Exception // Request a php resource Files.writeString(rootPath.resolve("index.php"), "raw-php-contents", StandardOpenOption.CREATE); request = HttpTester.newRequest(); - request.setURI(contextPath + "/index.php"); + request.setURI(CONTEXT_PATH + "/index.php"); channel.write(request.generate()); response = HttpTester.parseResponse(channel); assertNotNull(response); @@ -188,7 +225,7 @@ public void testTryPathsPhpPathMappingsHandler() throws Exception 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); @@ -219,7 +256,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(); @@ -250,22 +287,4 @@ public void process(Request request, Response response, Callback callback) }; } } - - public static class NoContentHandler extends Handler.Abstract - { - @Override - public Request.Processor handle(Request request) throws Exception - { - 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(); - } - }; - } - } } From bc2cf6e68de68e9d552bc4971f20a9bfe23488cb Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 28 Oct 2022 16:17:46 +0200 Subject: [PATCH 6/7] Updated TryPathsHandler. Improved javadocs. Introduced original[Path|Query]Attribute. Signed-off-by: Simone Bordet --- .../jetty/server/handler/TryPathsHandler.java | 132 ++++++++++++++---- .../server/handler/TryPathsHandlerTest.java | 54 ++++--- 2 files changed, 133 insertions(+), 53 deletions(-) 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 d3b58c10a868..e7f007aa724d 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,35 +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; /** *

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; @@ -70,28 +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 interpolated) + public TryPathsRequest(Request wrapped, String newPathQuery) { super(wrapped); - HttpURI.Mutable rewrittenUri = HttpURI.build(wrapped.getHttpURI()); + 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()); - int queryIdx = interpolated.indexOf('?'); + String originalContextPath = Request.getContextPath(wrapped); + HttpURI.Mutable rewrittenURI = HttpURI.build(originalURI); + int queryIdx = newPathQuery.indexOf('?'); if (queryIdx >= 0) { - String path = interpolated.substring(0, queryIdx); - rewrittenUri.path(URIUtil.addPaths(Request.getContextPath(wrapped), path)); - rewrittenUri.query(interpolated.substring(queryIdx + 1)); + String path = newPathQuery.substring(0, queryIdx); + rewrittenURI.path(URIUtil.addPaths(originalContextPath, path)); + rewrittenURI.query(newPathQuery.substring(queryIdx + 1)); } else { - rewrittenUri.path(URIUtil.addPaths(Request.getContextPath(wrapped), interpolated)); + rewrittenURI.path(URIUtil.addPaths(originalContextPath, newPathQuery)); } - _uri = rewrittenUri.asImmutable(); + _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 bf03c5643759..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 @@ -16,7 +16,6 @@ import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -28,6 +27,7 @@ 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; @@ -131,7 +131,7 @@ public void process(Request request, Response response, Callback callback) 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. @@ -146,7 +146,7 @@ public void process(Request request, Response response, Callback callback) // 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. + // 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()); @@ -158,14 +158,31 @@ public void process(Request request, Response response, Callback callback) } @Test - public void testTryPathsPhpPathMappingsHandler() throws Exception + 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 ExamplePhpHandler()); + 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 @@ -190,7 +207,7 @@ public void process(Request request, Response response, Callback callback) { channel.connect(new InetSocketAddress("localhost", connector.getLocalPort())); - // Request something that doesn't exist + // Make a first request without existing file paths. HttpTester.Request request = HttpTester.newRequest(); request.setURI(CONTEXT_PATH + "/last"); channel.write(request.generate()); @@ -210,7 +227,7 @@ public void process(Request request, Response response, Callback callback) assertEquals(HttpStatus.OK_200, response.getStatus()); assertEquals("hello", response.getContent()); - // Request a php resource + // 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"); @@ -218,7 +235,7 @@ public void process(Request request, Response response, Callback callback) response = HttpTester.parseResponse(channel); assertNotNull(response); assertEquals(HttpStatus.OK_200, response.getStatus()); - assertThat(response.getContent(), startsWith("Example PHP: pathInContext=/index.php")); + assertThat(response.getContent(), startsWith("PHP: pathInContext=/index.php")); // Create the "maintenance" file, it should be served first. path = "maintenance.txt"; @@ -266,25 +283,4 @@ public void process(Request request, Response response, Callback callback) assertEquals(HttpStatus.OK_200, response.getStatus()); } } - - public static class ExamplePhpHandler extends Handler.Abstract - { - @Override - public Request.Processor handle(Request request) throws Exception - { - return new Handler.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 = "Example PHP: pathInContext=%s, query=%s".formatted(Request.getPathInContext(request), request.getHttpURI().getQuery()); - - response.write(true, BufferUtil.toBuffer(message, StandardCharsets.UTF_8), callback); - } - }; - } - } } From 7f1798c823f739937a5a93ba4c436a43dd813020 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 28 Oct 2022 16:48:04 +0200 Subject: [PATCH 7/7] Fixed javadoc errors. Signed-off-by: Simone Bordet --- .../org/eclipse/jetty/server/handler/TryPathsHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e7f007aa724d..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 @@ -23,7 +23,7 @@ /** *

Inspired by nginx's {@code try_files} functionality.

* - *

This handler can be configured with a list of rewrite URI paths. + *

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).

* @@ -115,7 +115,7 @@ public List getPaths() /** *

Sets a list of rewrite URI paths.

- * The special token {@code $path} represents the current request + *

The special token {@code $path} represents the current request * {@code pathInContext} (the portion after the context path).

* * @param paths the rewrite URI paths