From 6966d734904422acd6d1627e5ab1ffb15eaa49a3 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 7 Mar 2024 09:37:59 -0600 Subject: [PATCH 1/2] Issue #11494 - PathMappingsHandler exposes PathSpec and Context based on PathSpec. --- .../org/eclipse/jetty/server/Context.java | 107 ++++++++++++++++++ .../server/handler/PathMappingsHandler.java | 41 ++++++- .../handler/PathMappingsHandlerTest.java | 97 +++++++++++++++- 3 files changed, 241 insertions(+), 4 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Context.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Context.java index 5eadbefcbe77..0ea012ed235f 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Context.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Context.java @@ -15,6 +15,7 @@ import java.io.File; import java.util.List; +import java.util.Set; import java.util.concurrent.Executor; import org.eclipse.jetty.http.MimeTypes; @@ -142,4 +143,110 @@ static String getPathInContext(String encodedContextPath, String encodedPath) return null; return encodedPath.substring(encodedContextPath.length()); } + + public static class Wrapper implements Context + { + private final Context _wrapped; + + public Wrapper(Context context) + { + _wrapped = context; + } + + @Override + public T decorate(T o) + { + return _wrapped.decorate(o); + } + + @Override + public void destroy(Object o) + { + _wrapped.destroy(o); + } + + @Override + public String getContextPath() + { + return _wrapped.getContextPath(); + } + + @Override + public ClassLoader getClassLoader() + { + return _wrapped.getClassLoader(); + } + + @Override + public Resource getBaseResource() + { + return _wrapped.getBaseResource(); + } + + @Override + public Request.Handler getErrorHandler() + { + return _wrapped.getErrorHandler(); + } + + @Override + public List getVirtualHosts() + { + return _wrapped.getVirtualHosts(); + } + + @Override + public MimeTypes getMimeTypes() + { + return _wrapped.getMimeTypes(); + } + + @Override + public void execute(Runnable task) + { + _wrapped.execute(task); + } + + @Override + public Object removeAttribute(String name) + { + return _wrapped.removeAttribute(name); + } + + @Override + public Object setAttribute(String name, Object attribute) + { + return _wrapped.setAttribute(name, attribute); + } + + @Override + public Object getAttribute(String name) + { + return _wrapped.getAttribute(name); + } + + @Override + public Set getAttributeNameSet() + { + return _wrapped.getAttributeNameSet(); + } + + @Override + public void run(Runnable task) + { + _wrapped.run(task); + } + + @Override + public void run(Runnable task, Request request) + { + _wrapped.run(task, request); + } + + @Override + public File getTempDirectory() + { + return _wrapped.getTempDirectory(); + } + } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java index 9e9ac957a51e..20e9bf487228 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java @@ -18,9 +18,11 @@ import java.util.Objects; import org.eclipse.jetty.http.pathmap.MappedResource; +import org.eclipse.jetty.http.pathmap.MatchedPath; import org.eclipse.jetty.http.pathmap.MatchedResource; import org.eclipse.jetty.http.pathmap.PathMappings; import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; @@ -111,11 +113,48 @@ public boolean handle(Request request, Response response, Callback callback) thr return false; } Handler handler = matchedResource.getResource(); + PathSpec pathSpec = matchedResource.getPathSpec(); if (LOG.isDebugEnabled()) LOG.debug("Matched {} to {} -> {}", pathInContext, matchedResource.getPathSpec(), handler); - boolean handled = handler.handle(request, response, callback); + + PathSpecRequest pathSpecRequest = new PathSpecRequest(request, pathSpec); + boolean handled = handler.handle(pathSpecRequest, response, callback); if (LOG.isDebugEnabled()) LOG.debug("Handled {} {} by {}", handled, pathInContext, handler); return handled; } + + private static class PathSpecRequest extends Request.Wrapper + { + private final PathSpec pathSpec; + private final Context context; + + public PathSpecRequest(Request request, PathSpec pathSpec) + { + super(request); + this.pathSpec = pathSpec; + setAttribute(PathSpec.class.getName(), this.pathSpec); + this.context = new Context.Wrapper(request.getContext()) + { + @Override + public String getContextPath() + { + return pathSpec.getPrefix(); + } + + @Override + public String getPathInContext(String canonicallyEncodedPath) + { + MatchedPath matchedPath = pathSpec.matched(canonicallyEncodedPath); + return matchedPath.getPathInfo(); + } + }; + } + + @Override + public Context getContext() + { + return context; + } + } } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java index 9ae6f2d5de72..8082abe5d070 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java @@ -13,7 +13,9 @@ package org.eclipse.jetty.server.handler; -import java.nio.charset.StandardCharsets; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -22,13 +24,15 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.http.pathmap.RegexPathSpec; import org.eclipse.jetty.http.pathmap.ServletPathSpec; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.component.LifeCycle; import org.junit.jupiter.api.AfterEach; @@ -178,6 +182,56 @@ public void testSeveralMappingAndNoWrapper(String requestPath, int expectedStatu assertEquals(expectedResponseBody, response.getContent()); } + public static Stream pathInContextInput() + { + return Stream.of( + Arguments.of("/", "null", "null", ServletPathSpec.class.getSimpleName(), "/"), + Arguments.of("/foo/test", "/foo", "/test", ServletPathSpec.class.getSimpleName(), "/foo/*"), + Arguments.of("/index.html", "/index.html", "null", ServletPathSpec.class.getSimpleName(), "/index.html"), + Arguments.of("/does-not-exist", "null", "null", ServletPathSpec.class.getSimpleName(), "/"), + Arguments.of("/deep/path/foo.php", "null", "null", ServletPathSpec.class.getSimpleName(), "*.php"), + Arguments.of("/re/1234/baz", "null", "null", ServletPathSpec.class.getSimpleName(), "/"), + Arguments.of("/re/ABC/baz", "null", "null", RegexPathSpec.class.getSimpleName(), "/re/[A-Z]*/.*"), + Arguments.of("/zed/test.txt", "/zed", "/test.txt", null, null) + ); + } + + @ParameterizedTest + @MethodSource("pathInContextInput") + public void testPathContextResolution(String requestPath, String expectedContextPath, String expectedPathInContext, + String expectedPathSpecImpl, String expectedPathSpecDeclaration) throws Exception + { + ContextHandler contextHandler = new ContextHandler(); + contextHandler.setContextPath("/"); + + PathMappingsHandler pathMappingsHandler = new PathMappingsHandler(); + pathMappingsHandler.addMapping(new ServletPathSpec("/"), new ContextDumpHandler()); + pathMappingsHandler.addMapping(new ServletPathSpec("/index.html"), new ContextDumpHandler()); + pathMappingsHandler.addMapping(new ServletPathSpec("/foo/*"), new ContextDumpHandler()); + pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new ContextDumpHandler()); + pathMappingsHandler.addMapping(new RegexPathSpec("/re/[A-Z]*/.*"), new ContextDumpHandler()); + ContextHandler zedContext = new ContextHandler("/zed"); + zedContext.setHandler(new ContextDumpHandler()); + pathMappingsHandler.addMapping(new ServletPathSpec("/zed/*"), zedContext); + contextHandler.setHandler(pathMappingsHandler); + + startServer(contextHandler); + + HttpTester.Response response = executeRequest(""" + GET %s HTTP/1.1\r + Host: local\r + Connection: close\r + + """.formatted(requestPath)); + assertEquals(200, response.getStatus()); + assertThat(response.getContent(), containsString("contextPath=[" + expectedContextPath + "]")); + assertThat(response.getContent(), containsString("pathInContext=[" + expectedPathInContext + "]")); + if (expectedPathSpecImpl != null) + assertThat(response.getContent(), containsString("pathSpec=[" + expectedPathSpecImpl + "]")); + if (expectedPathSpecDeclaration != null) + assertThat(response.getContent(), containsString("pathSpec.declaration=[" + expectedPathSpecDeclaration + "]")); + } + @Test public void testDump() throws Exception { @@ -306,7 +360,7 @@ public boolean handle(Request request, Response response, Callback callback) assertTrue(isStarted()); response.setStatus(HttpStatus.OK_200); response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8"); - response.write(true, BufferUtil.toBuffer(message, StandardCharsets.UTF_8), callback); + Content.Sink.write(response, true, message, callback); return true; } @@ -316,4 +370,41 @@ public String toString() return String.format("%s[msg=\"%s\"]", SimpleHandler.class.getSimpleName(), message); } } + + private static class ContextDumpHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + String message = null; + PathSpec pathSpec = (PathSpec)request.getAttribute(PathSpec.class.getName()); + try (StringWriter stringWriter = new StringWriter(); + PrintWriter out = new PrintWriter(stringWriter)) + { + out.printf("contextPath=[%s]\n", Request.getContextPath(request)); + out.printf("pathInContext=[%s]\n", Request.getPathInContext(request)); + if (pathSpec != null) + { + out.printf("pathSpec=[%s]\n", pathSpec.getClass().getSimpleName()); + out.printf("pathSpec.declaration=[%s]\n", pathSpec.getDeclaration()); + } + message = stringWriter.toString(); + } + catch (IOException e) + { + callback.failed(e); + return true; + } + response.setStatus(HttpStatus.OK_200); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8"); + Content.Sink.write(response, true, message, callback); + return true; + } + + @Override + public String toString() + { + return ContextDumpHandler.class.getSimpleName(); + } + } } From 946070f84fe3b4697b71b8bda1f69424ef1593d5 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 11 Mar 2024 12:28:45 -0500 Subject: [PATCH 2/2] Adding RegexPathSpec example on PathMappingsHandler --- .../jetty/server/handler/PathMappingsHandler.java | 5 +++-- .../server/handler/PathMappingsHandlerTest.java | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java index 20e9bf487228..7313a18f0c29 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/PathMappingsHandler.java @@ -128,24 +128,25 @@ private static class PathSpecRequest extends Request.Wrapper { private final PathSpec pathSpec; private final Context context; + private final MatchedPath matchedPath; public PathSpecRequest(Request request, PathSpec pathSpec) { super(request); this.pathSpec = pathSpec; + matchedPath = pathSpec.matched(request.getHttpURI().getCanonicalPath()); setAttribute(PathSpec.class.getName(), this.pathSpec); this.context = new Context.Wrapper(request.getContext()) { @Override public String getContextPath() { - return pathSpec.getPrefix(); + return matchedPath.getPathMatch(); } @Override public String getPathInContext(String canonicallyEncodedPath) { - MatchedPath matchedPath = pathSpec.matched(canonicallyEncodedPath); return matchedPath.getPathInfo(); } }; diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java index 8082abe5d070..1bfffe168a2d 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/PathMappingsHandlerTest.java @@ -185,13 +185,14 @@ public void testSeveralMappingAndNoWrapper(String requestPath, int expectedStatu public static Stream pathInContextInput() { return Stream.of( - Arguments.of("/", "null", "null", ServletPathSpec.class.getSimpleName(), "/"), + Arguments.of("/", "/", "null", ServletPathSpec.class.getSimpleName(), "/"), Arguments.of("/foo/test", "/foo", "/test", ServletPathSpec.class.getSimpleName(), "/foo/*"), Arguments.of("/index.html", "/index.html", "null", ServletPathSpec.class.getSimpleName(), "/index.html"), - Arguments.of("/does-not-exist", "null", "null", ServletPathSpec.class.getSimpleName(), "/"), - Arguments.of("/deep/path/foo.php", "null", "null", ServletPathSpec.class.getSimpleName(), "*.php"), - Arguments.of("/re/1234/baz", "null", "null", ServletPathSpec.class.getSimpleName(), "/"), - Arguments.of("/re/ABC/baz", "null", "null", RegexPathSpec.class.getSimpleName(), "/re/[A-Z]*/.*"), + Arguments.of("/does-not-exist", "/does-not-exist", "null", ServletPathSpec.class.getSimpleName(), "/"), + Arguments.of("/deep/path/foo.php", "/deep/path/foo.php", "null", ServletPathSpec.class.getSimpleName(), "*.php"), + Arguments.of("/re/1234/baz", "/re/1234/baz", "null", ServletPathSpec.class.getSimpleName(), "/"), + Arguments.of("/re/ABC/baz", "/re/ABC/baz", "null", RegexPathSpec.class.getSimpleName(), "/re/[A-Z]*/.*"), + Arguments.of("/rest/api/users/ver-1/groupfoo/baruser", "api/users", "groupfoo/baruser", RegexPathSpec.class.getSimpleName(), "^/rest/(?.*)/ver-[0-9]+/(?.*)$"), Arguments.of("/zed/test.txt", "/zed", "/test.txt", null, null) ); } @@ -210,6 +211,7 @@ public void testPathContextResolution(String requestPath, String expectedContext pathMappingsHandler.addMapping(new ServletPathSpec("/foo/*"), new ContextDumpHandler()); pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new ContextDumpHandler()); pathMappingsHandler.addMapping(new RegexPathSpec("/re/[A-Z]*/.*"), new ContextDumpHandler()); + pathMappingsHandler.addMapping(new RegexPathSpec("^/rest/(?.*)/ver-[0-9]+/(?.*)$"), new ContextDumpHandler()); ContextHandler zedContext = new ContextHandler("/zed"); zedContext.setHandler(new ContextDumpHandler()); pathMappingsHandler.addMapping(new ServletPathSpec("/zed/*"), zedContext);