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 67bb0e76604f..815c2437b92c 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; @@ -165,4 +166,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..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 @@ -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,49 @@ 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; + 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 matchedPath.getPathMatch(); + } + + @Override + public String getPathInContext(String 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..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 @@ -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,58 @@ public void testSeveralMappingAndNoWrapper(String requestPath, int expectedStatu assertEquals(expectedResponseBody, response.getContent()); } + public static Stream pathInContextInput() + { + return Stream.of( + 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", "/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) + ); + } + + @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()); + 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); + 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 +362,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 +372,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(); + } + } }