diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java index 31355a0c8d6..1c1320ea9ec 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CachingContentFactory.java @@ -177,6 +177,7 @@ public class CachingContentFactory implements HttpContent.ContentFactory else removeFromCache(cachingHttpContent); } + HttpContent httpContent = _authority.getContent(path, maxBuffer); // Do not cache directories or files that are too big if (httpContent != null && !httpContent.getResource().isDirectory() && httpContent.getContentLengthValue() <= _maxCachedFileSize) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java index 05d2726b9d9..467bdf1ae85 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java @@ -19,7 +19,6 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.InvalidPathException; -import java.nio.file.Path; import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -43,7 +42,6 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.URIUtil; -import org.eclipse.jetty.util.resource.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,7 +57,6 @@ public class ResourceService private static final int NO_CONTENT_LENGTH = -1; private static final int USE_KNOWN_CONTENT_LENGTH = -2; - private boolean _pathInfoOnly = false; private CompressedContentFormat[] _precompressedFormats = new CompressedContentFormat[0]; private WelcomeFactory _welcomeFactory; private boolean _redirectWelcome = false; @@ -392,7 +389,7 @@ public class ResourceService } } - // look for a welcome file + // process optional Welcome behaviors if (welcome(request, response, callback)) return; @@ -400,41 +397,81 @@ public class ResourceService sendDirectory(request, response, content, callback, pathInContext); } + public enum WelcomeActionType + { + REDIRECT, + SERVE + } + + /** + * Behavior for a potential welcome action + * as determined by {@link ResourceService#processWelcome(Request, Response)} + * + *
+ * For {@link WelcomeActionType#REDIRECT} this is the resulting `Location` response header. + * For {@link WelcomeActionType#SERVE} this is the resulting path to for welcome serve, note that + * this is just a path, and can point to a real file, or a dynamic handler for + * welcome processing (such as Jetty core Handler, or EE Servlet), it's up + * to the implementation of {@link ResourceService#welcome(Request, Response, Callback)} + * to handle the various action types. + *
+ * @param type the type of action + * @param target The target URI path of the action. + */ + public record WelcomeAction(WelcomeActionType type, String target) {} + protected boolean welcome(Request request, Response response, Callback callback) throws IOException { - String pathInContext = request.getPathInContext(); - String welcome = _welcomeFactory == null ? null : _welcomeFactory.getWelcomeFile(pathInContext); - if (welcome != null) + WelcomeAction welcomeAction = processWelcome(request, response); + if (welcomeAction == null) + return false; + + switch (welcomeAction.type) { - String contextPath = request.getContext().getContextPath(); - - if (_pathInfoOnly) - welcome = URIUtil.addPaths(contextPath, welcome); - - if (LOG.isDebugEnabled()) - LOG.debug("welcome={}", welcome); - - if (_redirectWelcome) + case REDIRECT -> { - // Redirect to the index response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, 0); - // TODO need URI util that handles param and query without reconstructing entire URI with scheme and authority - HttpURI.Mutable uri = HttpURI.build(request.getHttpURI()); - String parameter = uri.getParam(); - uri.path(URIUtil.addPaths(request.getContext().getContextPath(), welcome)); - uri.param(parameter); - Response.sendRedirect(request, response, callback, uri.getPathQuery()); + Response.sendRedirect(request, response, callback, welcomeAction.target); + return true; + } + case SERVE -> + { + // TODO output buffer size???? + HttpContent c = _contentFactory.getContent(welcomeAction.target, 16 * 1024); + sendData(request, response, callback, c, null); return true; } - - // Serve welcome file - HttpContent c = _contentFactory.getContent(welcome, 16 * 1024); // TODO output buffer size???? - sendData(request, response, callback, c, null); - return true; } return false; } + protected WelcomeAction processWelcome(Request request, Response response) throws IOException + { + String welcomeTarget = _welcomeFactory.getWelcomeTarget(request); + if (welcomeTarget == null) + return null; + + String contextPath = request.getContext().getContextPath(); + + if (LOG.isDebugEnabled()) + LOG.debug("welcome={}", welcomeTarget); + + if (_redirectWelcome) + { + // Redirect to the index + response.getHeaders().putLongField(HttpHeader.CONTENT_LENGTH, 0); + // TODO need URI util that handles param and query without reconstructing entire URI with scheme and authority + HttpURI.Mutable uri = HttpURI.build(request.getHttpURI()); + String parameter = uri.getParam(); + uri.path(URIUtil.addPaths(request.getContext().getContextPath(), welcomeTarget)); + uri.param(parameter); + return new WelcomeAction(WelcomeActionType.REDIRECT, uri.getPathQuery()); + } + + // Serve welcome file + return new WelcomeAction(WelcomeActionType.SERVE, welcomeTarget); + } + private void sendDirectory(Request request, Response response, HttpContent httpContent, Callback callback, String pathInContext) throws IOException { if (!_dirAllowed) @@ -678,14 +715,6 @@ public class ResourceService return _precompressedFormats; } - /** - * @return true, only the path info will be applied to the resourceBase - */ - public boolean isPathInfoOnly() - { - return _pathInfoOnly; - } - /** * @return If true, welcome files are redirected rather than forwarded to. */ @@ -761,14 +790,6 @@ public class ResourceService return _encodingCacheSize; } - /** - * @param pathInfoOnly true, only the path info will be applied to the resourceBase - */ - public void setPathInfoOnly(boolean pathInfoOnly) - { - _pathInfoOnly = pathInfoOnly; - } - /** * @param redirectWelcome If true, welcome files are redirected rather than forwarded to. * redirection is always used if the ResourceHandler is not scoped by @@ -786,14 +807,14 @@ public class ResourceService public interface WelcomeFactory { - /** - * Finds a matching welcome file for the supplied {@link Resource}. - * TODO would be better to pass back a URI or a Resource - * @param pathInContext the path of the request - * @return The path of the matching welcome file in context or null. + * Finds a matching welcome target URI path for the request. + * + * @param request the request to use to determine the matching welcome target from. + * @return The URI path of the matching welcome target in context or null + * (null means no welcome target was found) */ - String getWelcomeFile(String pathInContext) throws IOException; + String getWelcomeTarget(Request request) throws IOException; } private static class ContentWriterIteratingCallback extends IteratingCallback diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java index 0b5e778be19..c34bd00e2cb 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java @@ -81,15 +81,15 @@ public class ResourceHandler extends Handler.Wrapper { HttpContent.ContentFactory contentFactory = new CachingContentFactory(new ResourceContentFactory(_resourceBase, _mimeTypes, _resourceService.getPrecompressedFormats())); _resourceService.setContentFactory(contentFactory); - _resourceService.setWelcomeFactory(pathInContext -> + _resourceService.setWelcomeFactory(request -> { if (_welcomes == null) return null; for (String welcome : _welcomes) { - String welcomeInContext = URIUtil.addPaths(pathInContext, welcome); - Resource welcomePath = _resourceBase.resolve(pathInContext).resolve(welcome); + String welcomeInContext = URIUtil.addPaths(request.getPathInContext(), welcome); + Resource welcomePath = _resourceBase.resolve(request.getPathInContext()).resolve(welcome); if (welcomePath != null && welcomePath.exists()) return welcomeInContext; } @@ -204,14 +204,6 @@ public class ResourceHandler extends Handler.Wrapper return _resourceService.getPrecompressedFormats(); } - /** - * @return true, only the path info will be applied to the resourceBase - */ - public boolean isPathInfoOnly() - { - return _resourceService.isPathInfoOnly(); - } - /** * @return If true, welcome files are redirected rather than forwarded to. */ @@ -296,14 +288,6 @@ public class ResourceHandler extends Handler.Wrapper setupContentFactory(); } - /** - * @param pathInfoOnly true, only the path info will be applied to the resourceBase - */ - public void setPathInfoOnly(boolean pathInfoOnly) - { - _resourceService.setPathInfoOnly(pathInfoOnly); - } - /** * @param redirectWelcome If true, welcome files are redirected rather than forwarded to. * redirection is always used if the ResourceHandler is not scoped by diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java index f5f7b560fa6..4bb0f910855 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/DefaultServlet.java @@ -75,10 +75,13 @@ public class DefaultServlet extends HttpServlet private boolean _welcomeExactServlets = false; private Resource.Mount _resourceBaseMount; + private Resource _baseResource; private Resource.Mount _stylesheetMount; private Resource _stylesheet; private boolean _useFileMappedBuffer = false; + private boolean _isPathInfoOnly = false; + @Override public void init() throws ServletException { @@ -86,7 +89,7 @@ public class DefaultServlet extends HttpServlet _resourceService = new ServletResourceService(servletContextHandler); _resourceService.setWelcomeFactory(_resourceService); - Resource baseResource = servletContextHandler.getResourceBase(); + _baseResource = servletContextHandler.getResourceBase(); String rb = getInitParameter("resourceBase"); if (rb != null) { @@ -94,7 +97,7 @@ public class DefaultServlet extends HttpServlet { URI resourceUri = Resource.toURI(rb); _resourceBaseMount = Resource.mountIfNeeded(resourceUri); - baseResource = Resource.newResource(resourceUri); + _baseResource = Resource.newResource(resourceUri); } catch (Exception e) { @@ -109,7 +112,7 @@ public class DefaultServlet extends HttpServlet CompressedContentFormat[] precompressedFormats = new CompressedContentFormat[0]; _useFileMappedBuffer = getInitBoolean("useFileMappedBuffer", _useFileMappedBuffer); - ResourceContentFactory resourceContentFactory = new ResourceContentFactory(baseResource, mimeTypes, precompressedFormats); + ResourceContentFactory resourceContentFactory = new ResourceContentFactory(_baseResource, mimeTypes, precompressedFormats); CachingContentFactory cached = new CachingContentFactory(resourceContentFactory, _useFileMappedBuffer); int maxCacheSize = getInitInt("maxCacheSize", -2); @@ -145,9 +148,10 @@ public class DefaultServlet extends HttpServlet _resourceService.setDirAllowed(getInitBoolean("dirAllowed", _resourceService.isDirAllowed())); _resourceService.setRedirectWelcome(getInitBoolean("redirectWelcome", _resourceService.isRedirectWelcome())); _resourceService.setPrecompressedFormats(parsePrecompressedFormats(getInitParameter("precompressed"), getInitBoolean("gzip"), _resourceService.getPrecompressedFormats())); - _resourceService.setPathInfoOnly(getInitBoolean("pathInfoOnly", _resourceService.isPathInfoOnly())); _resourceService.setEtags(getInitBoolean("etags", _resourceService.isEtags())); + _isPathInfoOnly = getInitBoolean("pathInfoOnly", _isPathInfoOnly); + if ("exact".equals(getInitParameter("welcomeServlets"))) { _welcomeExactServlets = true; @@ -216,7 +220,7 @@ public class DefaultServlet extends HttpServlet // TODO: remove? _servletHandler = _contextHandler.getChildHandlerByClass(ServletHandler.class); if (LOG.isDebugEnabled()) - LOG.debug("base resource = {}", baseResource); + LOG.debug("base resource = {}", _baseResource); } @Override @@ -302,10 +306,19 @@ public class DefaultServlet extends HttpServlet servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName()); } + protected boolean isPathInfoOnly() + { + return _isPathInfoOnly; + } + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String pathInContext = _resourceService.isPathInfoOnly() ? req.getPathInfo() : URIUtil.addPaths(req.getServletPath(), req.getPathInfo()); + // TODO: need special handling here for included if request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) is not null? + // Handling things like RequestDispatcher.INCLUDE_PATH_INFO? + // or is this handled elsewhere now? + + String pathInContext = isPathInfoOnly() ? req.getPathInfo() : URIUtil.addPaths(req.getServletPath(), req.getPathInfo()); HttpContent content = _resourceService.getContent(pathInContext, resp.getBufferSize()); if (content == null) { @@ -776,20 +789,23 @@ public class DefaultServlet extends HttpServlet } @Override - public String getWelcomeFile(String pathInContext) throws IOException + public String getWelcomeTarget(Request coreRequest) throws IOException { String[] welcomes = _servletContextHandler.getWelcomeFiles(); if (welcomes == null) return null; - // TODO this feels inefficient - Resource base = _servletContextHandler.getResourceBase().resolve(pathInContext); + HttpServletRequest request = getServletRequest(coreRequest); + + String requestTarget = isPathInfoOnly() ? request.getPathInfo() : coreRequest.getPathInContext(); + + Resource base = _baseResource.resolve(requestTarget); String welcomeServlet = null; for (String welcome : welcomes) { Resource welcomePath = base.resolve(welcome); - String welcomeInContext = URIUtil.addPaths(pathInContext, welcome); + String welcomeInContext = URIUtil.addPaths(coreRequest.getPathInContext(), welcome); if (welcomePath != null && welcomePath.exists()) return welcomeInContext; @@ -806,81 +822,92 @@ public class DefaultServlet extends HttpServlet } @Override - protected boolean welcome(Request rq, Response rs, Callback callback) throws IOException + protected boolean welcome(Request coreRequest, Response coreResponse, Callback callback) throws IOException { - // TODO The contract of this method is very confused: it has a callback a return and throws? - - // TODO, this unwrapping is fragile - HttpServletRequest request = ((ServletCoreRequest)rq)._request; - HttpServletResponse response = ((ServletCoreResponse)rs)._response; - String pathInContext = rq.getPathInContext(); - WelcomeFactory welcomeFactory = getWelcomeFactory(); - String welcome = welcomeFactory == null ? null : welcomeFactory.getWelcomeFile(pathInContext); + HttpServletRequest request = getServletRequest(coreRequest); + HttpServletResponse response = getServletResponse(coreResponse); boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; - if (welcome == null) + WelcomeAction welcomeAction = super.processWelcome(coreRequest, coreResponse); + if (welcomeAction == null) return false; - String servletPath = included ? (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) - : request.getServletPath(); - - if (isPathInfoOnly()) - welcome = URIUtil.addPaths(servletPath, welcome); + String welcome = welcomeAction.target(); if (LOG.isDebugEnabled()) LOG.debug("welcome={}", welcome); ServletContext context = request.getServletContext(); - - if (isRedirectWelcome() || context == null) + switch (welcomeAction.type()) { - // Redirect to the index - response.setContentLength(0); - // TODO need URI util that handles param and query without reconstructing entire URI with scheme and authority - HttpURI.Mutable uri = HttpURI.build(rq.getHttpURI()); - String parameter = uri.getParam(); - uri.path(URIUtil.addPaths(rq.getContext().getContextPath(), welcome)); - uri.param(parameter); - response.sendRedirect(response.encodeRedirectURL(uri.getPathQuery())); - callback.succeeded(); - return true; - } - - RequestDispatcher dispatcher = context.getRequestDispatcher(URIUtil.encodePath(welcome)); - if (dispatcher != null) - { - // Forward to the index - try + case REDIRECT -> { - if (included) + if (isRedirectWelcome() || context == null) { - dispatcher.include(request, response); + String servletPath = included ? (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) + : request.getServletPath(); + + if (isPathInfoOnly()) + welcome = URIUtil.addPaths(servletPath, welcome); + + response.setContentLength(0); + response.sendRedirect(welcome); + callback.succeeded(); + return true; } - else - { - request.setAttribute("org.eclipse.jetty.server.welcome", welcome); - dispatcher.forward(request, response); - } - callback.succeeded(); return true; } - catch (ServletException e) + case SERVE -> { - callback.failed(e); + RequestDispatcher dispatcher = context.getRequestDispatcher(welcome); + if (dispatcher != null) + { + // Forward to the index + try + { + if (included) + { + dispatcher.include(request, response); + } + else + { + request.setAttribute("org.eclipse.jetty.server.welcome", welcomeAction.target()); + dispatcher.forward(request, response); + } + callback.succeeded(); + return true; + } + catch (ServletException e) + { + callback.failed(e); + return true; + } + } return true; } } - return false; } @Override protected boolean passConditionalHeaders(Request request, Response response, HttpContent content, Callback callback) throws IOException { - boolean included = ((ServletCoreRequest)request)._request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; + boolean included = getServletRequest(request).getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; if (included) return true; return super.passConditionalHeaders(request, response, content, callback); } + + private HttpServletRequest getServletRequest(Request request) + { + // TODO, this unwrapping is fragile + return ((ServletCoreRequest)request)._request; + } + + private HttpServletResponse getServletResponse(Response response) + { + // TODO, this unwrapping is fragile + return ((ServletCoreResponse)response)._response; + } } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java index bec1423de6f..7d0ed465b9a 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/DefaultServletTest.java @@ -15,7 +15,6 @@ package org.eclipse.jetty.ee10.servlet; import java.io.File; import java.io.IOException; -import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; @@ -153,7 +152,7 @@ public class DefaultServletTest response = HttpTester.parseResponse(rawResponse); assertThat(response.toString(), response.getStatus(), is(HttpStatus.NOT_FOUND_404)); - createFile(file, "How now brown cow"); + Files.writeString(file, "How now brown cow", UTF_8); rawResponse = connector.getResponse(""" GET /context/file.txt HTTP/1.1\r @@ -418,7 +417,6 @@ public class DefaultServletTest } @Test - @Disabled public void testListingProperUrlEncoding() throws Exception { ServletHolder defholder = context.addServlet(DefaultServlet.class, "/*"); @@ -710,7 +708,7 @@ public class DefaultServletTest /* create some content in the docroot */ Path index = docRoot.resolve("index.html"); - createFile(index, "