ret = new ArrayList<>();
- if (precompressed != null && precompressed.indexOf('=') > 0)
- {
- for (String pair : precompressed.split(","))
- {
- String[] setting = pair.split("=");
- String encoding = setting[0].trim();
- String extension = setting[1].trim();
- ret.add(new CompressedContentFormat(encoding, extension));
- if (gzip == Boolean.TRUE && !ret.contains(CompressedContentFormat.GZIP))
- ret.add(CompressedContentFormat.GZIP);
- }
- }
- else if (precompressed != null)
- {
- if (Boolean.parseBoolean(precompressed))
- {
- ret.add(CompressedContentFormat.BR);
- ret.add(CompressedContentFormat.GZIP);
- }
- }
- else if (gzip == Boolean.TRUE)
- {
- // gzip handling is for backwards compatibility with older Jetty
- ret.add(CompressedContentFormat.GZIP);
- }
- return ret;
- }
+ private final AtomicBoolean warned = new AtomicBoolean(false);
/**
*
@@ -405,519 +67,55 @@ public class DefaultServlet extends HttpServlet
return value;
}
- private Boolean getInitBoolean(String name)
- {
- String value = getInitParameter(name);
- if (value == null || value.length() == 0)
- return null;
- return (value.startsWith("t") ||
- value.startsWith("T") ||
- value.startsWith("y") ||
- value.startsWith("Y") ||
- value.startsWith("1"));
- }
-
- private boolean getInitBoolean(String name, boolean dft)
- {
- return Optional.ofNullable(getInitBoolean(name)).orElse(dft);
- }
-
- private int getInitInt(String name, int dft)
- {
- String value = getInitParameter(name);
- if (value != null && value.length() > 0)
- return Integer.parseInt(value);
- return dft;
- }
-
- protected ServletContextHandler initContextHandler(ServletContext servletContext)
- {
- if (servletContext instanceof ServletContextHandler.ServletContextApi api)
- return api.getContext().getServletContextHandler();
-
- Context context = ContextHandler.getCurrentContext();
- if (context instanceof ContextHandler.ScopedContext scopedContext)
- return scopedContext.getContextHandler();
-
- throw new IllegalArgumentException("The servletContext " + servletContext + " " +
- servletContext.getClass().getName() + " is not " + ContextHandler.ScopedContext.class.getName());
- }
-
@Override
- protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
+ public void init() throws ServletException
{
- String includedServletPath = (String)httpServletRequest.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
- String encodedPathInContext = getEncodedPathInContext(httpServletRequest, includedServletPath);
- boolean included = includedServletPath != null;
-
- if (LOG.isDebugEnabled())
- LOG.debug("doGet(hsReq={}, hsResp={}) pathInContext={}, included={}", httpServletRequest, httpServletResponse, encodedPathInContext, included);
-
- try
- {
- HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(httpServletRequest));
- if (LOG.isDebugEnabled())
- LOG.debug("content = {}", content);
-
- if (content == null || Resources.missing(content.getResource()))
- {
- if (included)
- {
- /* https://github.com/jakartaee/servlet/blob/6.0.0-RELEASE/spec/src/main/asciidoc/servlet-spec-body.adoc#93-the-include-method
- * 9.3 - If the default servlet is the target of a RequestDispatch.include() and the requested
- * resource does not exist, then the default servlet MUST throw FileNotFoundException.
- * If the exception isn’t caught and handled, and the response
- * hasn’t been committed, the status code MUST be set to 500.
- */
- throw new FileNotFoundException(encodedPathInContext);
- }
-
- // no content
- httpServletResponse.sendError(404);
- }
- else
- {
- // lookup the core request and response as wrapped by the ServletContextHandler
- ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(httpServletRequest);
- ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse();
- ServletChannel servletChannel = servletContextRequest.getServletChannel();
-
- // If the servlet request has not been wrapped,
- // we can use the core request directly,
- // otherwise wrap the servlet request as a core request
- Request coreRequest = httpServletRequest instanceof ServletApiRequest
- ? servletChannel.getRequest()
- : ServletCoreRequest.wrap(httpServletRequest);
-
- // If the servlet response has been wrapped and has been written to,
- // then the servlet response must be wrapped as a core response
- // otherwise we can use the core response directly.
- boolean useServletResponse = !(httpServletResponse instanceof ServletApiResponse) || servletContextResponse.isWritingOrStreaming();
- Response coreResponse = useServletResponse
- ? new ServletCoreResponse(coreRequest, httpServletResponse, included)
- : servletChannel.getResponse();
-
- // If the core response is already committed then do nothing more
- if (coreResponse.isCommitted())
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Response already committed for {}", coreRequest.getHttpURI());
- return;
- }
-
- // Get the content length before we may wrap the content
- long contentLength = content.getContentLengthValue();
-
- // Servlet Filters could be interacting with the Response already.
- if (useServletResponse)
- content = new UnknownLengthHttpContent(content);
-
- // The character encoding may be forced
- String characterEncoding = servletContextResponse.getRawCharacterEncoding();
- if (characterEncoding != null)
- content = new ForcedCharacterEncodingHttpContent(content, characterEncoding);
-
- // If async is supported and the unwrapped content is larger than an output buffer
- if (httpServletRequest.isAsyncSupported() &&
- (contentLength < 0 || contentLength > coreRequest.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()))
- {
- // send the content asynchronously
- AsyncContext asyncContext = httpServletRequest.startAsync();
- Callback callback = new AsyncContextCallback(asyncContext, httpServletResponse);
- _resourceService.doGet(coreRequest, coreResponse, callback, content);
- }
- else
- {
- // send the content blocking
- try (Blocker.Callback callback = Blocker.callback())
- {
- _resourceService.doGet(coreRequest, coreResponse, callback, content);
- callback.block();
- }
- catch (Exception e)
- {
- throw new ServletException(e);
- }
- }
- }
- }
- catch (InvalidPathException e)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e);
- if (included)
- throw new FileNotFoundException(encodedPathInContext);
- httpServletResponse.setStatus(404);
- }
+ if ("true".equalsIgnoreCase(getInitParameter("pathInfoOnly")))
+ LOG.warn("DefaultServlet pathInfoOnly is set to true. Use ResourceServlet instead.");
+ super.init();
}
+ /**
+ * Get the path in the context, of the resource to serve for a request.
+ * The default implementation considers the {@link jakarta.servlet.http.HttpServletMapping} of the request and
+ * any {@link Dispatcher#INCLUDE_SERVLET_PATH} attributes that may be set.
+ * @param request The request
+ * @param included {@code true} if the request is for an included resource
+ * @return The encoded URI path of the resource to server, relative to the resource base.
+ */
+ @Override
+ protected String getEncodedPathInContext(HttpServletRequest request, boolean included)
+ {
+ String deprecatedPath = getEncodedPathInContext(request, (String)(included ? request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH) : null));
+ if (deprecatedPath != null)
+ return deprecatedPath;
+
+ if (request.getHttpServletMapping().getMappingMatch() != MappingMatch.DEFAULT)
+ {
+ if (warned.compareAndSet(false, true))
+ LOG.warn("Incorrect mapping for DefaultServlet at %s. Use ResourceServlet".formatted(request.getHttpServletMapping().getPattern()));
+ return super.getEncodedPathInContext(request, included);
+ }
+
+ if (included)
+ {
+ if (request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH) instanceof String servletPath)
+ return URIUtil.encodePath(servletPath);
+
+ // must be an include of a named dispatcher. Just use the whole URI
+ return URIUtil.encodePath(request.getServletPath());
+ }
+
+ if (request instanceof ServletApiRequest apiRequest)
+ // Strip the context path from the canonically encoded path, so no need to re-encode (and mess up %2F etc.)
+ return Context.getPathInContext(request.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
+
+ return URIUtil.encodePath(request.getServletPath());
+ }
+
+ @Deprecated(forRemoval = true)
protected String getEncodedPathInContext(HttpServletRequest req, String includedServletPath)
{
- if (includedServletPath != null)
- return encodePath(getIncludedPathInContext(req, includedServletPath, !isDefaultMapping(req)));
- else if (!isDefaultMapping(req))
- {
- //a match via an extension mapping will more than likely
- //have no path info
- String path = req.getPathInfo();
- if (StringUtil.isEmpty(path) &&
- MappingMatch.EXTENSION.equals(req.getHttpServletMapping().getMappingMatch()))
- path = req.getServletPath();
-
- return encodePath(path);
- }
- else if (req instanceof ServletApiRequest apiRequest)
- return Context.getPathInContext(req.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
- else
- return Context.getPathInContext(req.getContextPath(), URIUtil.canonicalPath(req.getRequestURI()));
- }
-
- @Override
- protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
- {
- if (LOG.isDebugEnabled())
- LOG.debug("doHead(req={}, resp={}) (calling doGet())", req, resp);
- doGet(req, resp);
- }
-
- @Override
- protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
- {
- // Always return 405: Method Not Allowed for DefaultServlet
- resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
- }
-
- @Override
- protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
- {
- // override to eliminate TRACE that the default HttpServlet impl adds
- resp.setHeader("Allow", "GET, HEAD, OPTIONS");
- }
-
- private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory
- {
- private final ServletContextHandler _servletContextHandler;
-
- private ServletResourceService(ServletContextHandler servletContextHandler)
- {
- _servletContextHandler = servletContextHandler;
- }
-
- @Override
- public String getWelcomeTarget(HttpContent content, Request coreRequest)
- {
- String[] welcomes = _servletContextHandler.getWelcomeFiles();
- if (welcomes == null)
- return null;
- String pathInContext = Request.getPathInContext(coreRequest);
- String welcomeTarget = null;
- Resource base = content.getResource();
- if (Resources.isReadableDirectory(base))
- {
- for (String welcome : welcomes)
- {
- String welcomeInContext = URIUtil.addPaths(pathInContext, welcome);
-
- // If the welcome resource is a file, it has
- // precedence over resources served by Servlets.
- Resource welcomePath = content.getResource().resolve(welcome);
- if (Resources.isReadableFile(welcomePath))
- return welcomeInContext;
-
- // Check whether a Servlet may serve the welcome resource.
- if (_welcomeServletMode != WelcomeServletMode.NONE && welcomeTarget == null)
- {
- if (!isDefaultMapping(getServletRequest(coreRequest)) && !isIncluded(getServletRequest(coreRequest)))
- welcomeTarget = URIUtil.addPaths(getServletRequest(coreRequest).getPathInfo(), welcome);
-
- ServletHandler.MappedServlet entry = _servletContextHandler.getServletHandler().getMappedServlet(welcomeInContext);
- // Is there a different Servlet that may serve the welcome resource?
- if (entry != null && entry.getServletHolder().getServletInstance() != DefaultServlet.this)
- {
- if (_welcomeServletMode == WelcomeServletMode.MATCH || entry.getPathSpec().getDeclaration().equals(welcomeInContext))
- {
- welcomeTarget = welcomeInContext;
- // Do not break the loop, because we want to try other welcome resources
- // that may be files and take precedence over Servlet welcome resources.
- }
- }
- }
- }
- }
- return welcomeTarget;
- }
-
- @Override
- protected void serveWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
- {
- HttpServletRequest servletRequest = getServletRequest(request);
- HttpServletResponse servletResponse = getServletResponse(response);
-
- boolean included = isIncluded(servletRequest);
-
- RequestDispatcher dispatcher = servletRequest.getServletContext().getRequestDispatcher(welcomeTarget);
- if (dispatcher == null)
- {
- // We know that the welcome target exists and can be served.
- Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500);
- return;
- }
-
- try
- {
- if (included)
- {
- dispatcher.include(servletRequest, servletResponse);
- }
- else
- {
- servletRequest.setAttribute("org.eclipse.jetty.server.welcome", welcomeTarget);
- dispatcher.forward(servletRequest, servletResponse);
- }
- callback.succeeded();
- }
- catch (ServletException e)
- {
- callback.failed(e);
- }
- }
-
- @Override
- protected void rehandleWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
- {
- serveWelcome(request, response, callback, welcomeTarget);
- }
-
- @Override
- protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={})", coreRequest, coreResponse, callback, statusCode);
- writeHttpError(coreRequest, coreResponse, callback, statusCode, null, null);
- }
-
- @Override
- protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, Throwable cause)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, cause={})", coreRequest, coreResponse, callback, cause, cause);
-
- int statusCode = HttpStatus.INTERNAL_SERVER_ERROR_500;
- String reason = null;
- if (cause instanceof HttpException httpException)
- {
- statusCode = httpException.getCode();
- reason = httpException.getReason();
- }
- writeHttpError(coreRequest, coreResponse, callback, statusCode, reason, cause);
- }
-
- @Override
- protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode, String reason, Throwable cause)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={}, reason={}, cause={})", coreRequest, coreResponse, callback, statusCode, reason, cause, cause);
- HttpServletRequest request = getServletRequest(coreRequest);
- HttpServletResponse response = getServletResponse(coreResponse);
- try
- {
- if (isIncluded(request))
- return;
- if (cause != null)
- request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, cause);
- response.sendError(statusCode, reason);
- }
- catch (IOException e)
- {
- // TODO: Need a better exception?
- throw new RuntimeException(e);
- }
- finally
- {
- callback.succeeded();
- }
- }
-
- @Override
- protected boolean passConditionalHeaders(Request request, Response response, HttpContent content, Callback callback) throws IOException
- {
- boolean included = isIncluded(getServletRequest(request));
- if (included)
- return false;
- return super.passConditionalHeaders(request, response, content, callback);
- }
-
- private HttpServletRequest getServletRequest(Request request)
- {
- ServletCoreRequest servletCoreRequest = Request.as(request, ServletCoreRequest.class);
- if (servletCoreRequest != null)
- return servletCoreRequest.getServletRequest();
-
- ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
- if (servletContextRequest != null)
- return servletContextRequest.getServletApiRequest();
-
- throw new IllegalStateException("instanceof " + request.getClass());
- }
-
- private HttpServletResponse getServletResponse(Response response)
- {
- ServletCoreResponse servletCoreResponse = Response.as(response, ServletCoreResponse.class);
- if (servletCoreResponse != null)
- return servletCoreResponse.getServletResponse();
-
- ServletContextResponse servletContextResponse = Response.as(response, ServletContextResponse.class);
- if (servletContextResponse != null)
- return servletContextResponse.getServletApiResponse();
-
- throw new IllegalStateException("instanceof " + response.getClass());
- }
- }
-
- static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly)
- {
- String servletPath = isPathInfoOnly ? "/" : includedServletPath;
- String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
- return URIUtil.addPaths(servletPath, pathInfo);
- }
-
- private static boolean isIncluded(HttpServletRequest request)
- {
- return request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
- }
-
- protected boolean isDefaultMapping(HttpServletRequest req)
- {
- if (req.getHttpServletMapping().getMappingMatch() == MappingMatch.DEFAULT)
- return true;
- return (req.getDispatcherType() != DispatcherType.REQUEST) && "default".equals(getServletConfig().getServletName());
- }
-
- /**
- * Wrap an existing HttpContent with one that takes has an unknown/unspecified length.
- */
- private static class UnknownLengthHttpContent extends HttpContent.Wrapper
- {
- public UnknownLengthHttpContent(HttpContent content)
- {
- super(content);
- }
-
- @Override
- public HttpField getContentLength()
- {
- return null;
- }
-
- @Override
- public long getContentLengthValue()
- {
- return -1;
- }
- }
-
- private static class ForcedCharacterEncodingHttpContent extends HttpContent.Wrapper
- {
- private final String characterEncoding;
- private final String contentType;
-
- public ForcedCharacterEncodingHttpContent(HttpContent content, String characterEncoding)
- {
- super(Objects.requireNonNull(content));
- this.characterEncoding = characterEncoding;
- if (content.getContentTypeValue() == null || content.getResource().isDirectory())
- {
- this.contentType = null;
- }
- else
- {
- String mimeType = content.getContentTypeValue();
- int idx = mimeType.indexOf(";charset");
- if (idx >= 0)
- mimeType = mimeType.substring(0, idx);
- this.contentType = mimeType + ";charset=" + characterEncoding;
- }
- }
-
- @Override
- public HttpField getContentType()
- {
- return new HttpField(HttpHeader.CONTENT_TYPE, this.contentType);
- }
-
- @Override
- public String getContentTypeValue()
- {
- return this.contentType;
- }
-
- @Override
- public String getCharacterEncoding()
- {
- return this.characterEncoding;
- }
- }
-
- /**
- *
The different modes a welcome resource may be served by a Servlet.
- */
- private enum WelcomeServletMode
- {
- /**
- * Welcome targets are not served by Servlets.
- * The welcome target must exist as a file on the filesystem.
- */
- NONE,
- /**
- * Welcome target that exist as files on the filesystem are
- * served, otherwise a matching Servlet may serve the welcome target.
- */
- MATCH,
- /**
- * Welcome target that exist as files on the filesystem are
- * served, otherwise an exact matching Servlet may serve the welcome target.
- */
- EXACT
- }
-
- private static class AsyncContextCallback implements Callback
- {
- private final AsyncContext _asyncContext;
- private final HttpServletResponse _response;
-
- private AsyncContextCallback(AsyncContext asyncContext, HttpServletResponse response)
- {
- _asyncContext = asyncContext;
- _response = response;
- }
-
- @Override
- public void succeeded()
- {
- _asyncContext.complete();
- }
-
- @Override
- public void failed(Throwable x)
- {
- try
- {
- if (LOG.isDebugEnabled())
- LOG.debug("AsyncContextCallback failed {}", _asyncContext, x);
- // It is known that this callback is only failed if the response is already committed,
- // thus we can only abort the response here.
- _response.sendError(-1);
- }
- catch (IOException e)
- {
- ExceptionUtil.addSuppressedIfNotAssociated(x, e);
- }
- finally
- {
- _asyncContext.complete();
- }
- if (LOG.isDebugEnabled())
- LOG.debug("Async get failed", x);
- }
+ return null;
}
}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
index 70f610fc991..3ecf4a3d6b4 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/Dispatcher.java
@@ -258,6 +258,12 @@ public class Dispatcher implements RequestDispatcher
return null;
return vals.toArray(new String[0]);
}
+
+ @Override
+ public String toString()
+ {
+ return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), getRequest());
+ }
}
private class ForwardRequest extends ParameterRequestWrapper
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResourceServlet.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResourceServlet.java
new file mode 100644
index 00000000000..130e637b13b
--- /dev/null
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ResourceServlet.java
@@ -0,0 +1,931 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee10.servlet;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.InvalidPathException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.StringTokenizer;
+
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.UnavailableException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletMapping;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.HttpException;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http.content.FileMappingHttpContentFactory;
+import org.eclipse.jetty.http.content.HttpContent;
+import org.eclipse.jetty.http.content.PreCompressedHttpContentFactory;
+import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
+import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory;
+import org.eclipse.jetty.http.content.VirtualHttpContentFactory;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.server.Context;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.ResourceService;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.Blocker;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.ExceptionUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.Resources;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Servlet that handles static resources.
+ * The following init parameters are supported:
+ *
+ * - acceptRanges
+ * -
+ * Use {@code true} to accept range requests, defaults to {@code true}.
+ *
+ * - baseResource
+ * -
+ * The root directory to look for static resources. Defaults to the context's baseResource. Relative URI
+ * are {@link Resource#resolve(String) resolved} against the context's {@link ServletContextHandler#getBaseResource()}
+ * base resource, all other values are resolved using {@link ServletContextHandler#newResource(String)}.
+ *
+ * - cacheControl
+ * -
+ * The value of the {@code Cache-Control} header.
+ * If omitted, no {@code Cache-Control} header is generated in responses.
+ * By default is omitted.
+ *
+ * - cacheValidationTime
+ * -
+ * How long in milliseconds a resource is cached.
+ * If omitted, defaults to {@code 1000} ms.
+ * Use {@code -1} to cache forever or {@code 0} to not cache.
+ *
+ * - dirAllowed
+ * -
+ * Use {@code true} to serve directory listing if no welcome file is found.
+ * Otherwise responds with {@code 403 Forbidden}.
+ * Defaults to {@code true}.
+ *
+ * - encodingHeaderCacheSize
+ * -
+ * Max number of cached {@code Accept-Encoding} entries.
+ * Use {@code -1} for the default value (100), {@code 0} for no cache.
+ *
+ * - etags
+ * -
+ * Use {@code true} to generate ETags in responses.
+ * Defaults to {@code false}.
+ *
+ * - maxCachedFiles
+ * -
+ * The max number of cached static resources.
+ * Use {@code -1} for the default value (2048) or {@code 0} for no cache.
+ *
+ * - maxCachedFileSize
+ * -
+ * The max size in bytes of a single cached static resource.
+ * Use {@code -1} for the default value (128 MiB) or {@code 0} for no cache.
+ *
+ * - maxCacheSize
+ * -
+ * The max size in bytes of the cache for static resources.
+ * Use {@code -1} for the default value (256 MiB) or {@code 0} for no cache.
+ *
+ * - otherGzipFileExtensions
+ * -
+ * A comma-separated list of extensions of files whose content is implicitly
+ * gzipped.
+ * Defaults to {@code .svgz}.
+ *
+ * - pathInfoOnly
+ * -
+ * Use {@code true} to use only the pathInfo portion of a PATH (aka prefix) match
+ * as obtained from {@link HttpServletRequest#getPathInfo()}.
+ * Defaults to {@code true}.
+ *
+ * - precompressed
+ * -
+ * Omitted by default, so that no pre-compressed content will be served.
+ * If set to {@code true}, the default set of pre-compressed formats will be used.
+ * Otherwise can be set to a comma-separated list of {@code encoding=extension} pairs,
+ * such as: {@code br=.br,gzip=.gz,bzip2=.bz}, where {@code encoding} is used as the
+ * value for the {@code Content-Encoding} header.
+ *
+ * - redirectWelcome
+ * -
+ * Use {@code true} to redirect welcome files, otherwise they are forwarded.
+ * Defaults to {@code false}.
+ *
+ * - stylesheet
+ * -
+ * Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}.
+ * The path of a custom stylesheet to style the directory listing HTML.
+ *
+ * - useFileMappedBuffer
+ * -
+ * Use {@code true} to use file mapping to serve static resources.
+ * Defaults to {@code false}.
+ *
+ * - welcomeServlets
+ * -
+ * Use {@code false} to only serve welcome resources from the file system.
+ * Use {@code true} to dispatch welcome resources to a matching Servlet
+ * (for example mapped to {@code *.welcome}), when the welcome resources
+ * does not exist on file system.
+ * Use {@code exact} to dispatch welcome resource to a Servlet whose mapping
+ * is exactly the same as the welcome resource (for example {@code /index.welcome}),
+ * when the welcome resources does not exist on file system.
+ * Defaults to {@code false}.
+ *
+ *
+ */
+public class ResourceServlet extends HttpServlet
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ResourceServlet.class);
+
+ private ServletResourceService _resourceService;
+ private WelcomeServletMode _welcomeServletMode;
+ private boolean _pathInfoOnly;
+
+ public ResourceService getResourceService()
+ {
+ return _resourceService;
+ }
+
+ @Override
+ public void init() throws ServletException
+ {
+ ServletContextHandler contextHandler = initContextHandler(getServletContext());
+ _resourceService = new ServletResourceService(contextHandler);
+ _resourceService.setWelcomeFactory(_resourceService);
+ Resource baseResource = contextHandler.getBaseResource();
+
+ String rb = getInitParameter("baseResource", "resourceBase");
+ if (rb != null)
+ {
+ try
+ {
+ baseResource = URIUtil.isRelative(rb) ? baseResource.resolve(rb) : contextHandler.newResource(rb);
+ if (baseResource.isAlias())
+ baseResource = contextHandler.newResource(baseResource.getRealURI());
+ }
+ catch (Exception e)
+ {
+ LOG.warn("Unable to create baseResource from {}", rb, e);
+ throw new UnavailableException(e.toString());
+ }
+ }
+ if (baseResource != null && !(baseResource.isDirectory() && baseResource.isReadable()))
+ LOG.warn("baseResource {} is not a readable directory", baseResource);
+
+ List precompressedFormats = parsePrecompressedFormats(getInitParameter("precompressed"),
+ getInitBoolean("gzip"), _resourceService.getPrecompressedFormats());
+
+ // Try to get factory from ServletContext attribute.
+ HttpContent.Factory contentFactory = (HttpContent.Factory)getServletContext().getAttribute(HttpContent.Factory.class.getName());
+ if (contentFactory == null)
+ {
+ MimeTypes mimeTypes = contextHandler.getMimeTypes();
+ contentFactory = new ResourceHttpContentFactory(baseResource, mimeTypes);
+
+ // Use the servers default stylesheet unless there is one explicitly set by an init param.
+ Resource styleSheet = contextHandler.getServer().getDefaultStyleSheet();
+ String stylesheetParam = getInitParameter("stylesheet");
+ if (stylesheetParam != null)
+ {
+ try
+ {
+ HttpContent styleSheetContent = contentFactory.getContent(stylesheetParam);
+ Resource s = styleSheetContent == null ? null : styleSheetContent.getResource();
+ if (Resources.isReadableFile(s))
+ styleSheet = s;
+ else
+ LOG.warn("Stylesheet {} does not exist", stylesheetParam);
+ }
+ catch (Exception e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.warn("Unable to use stylesheet: {}", stylesheetParam, e);
+ else
+ LOG.warn("Unable to use stylesheet: {} - {}", stylesheetParam, e.toString());
+ }
+ }
+
+ if (getInitBoolean("useFileMappedBuffer", false))
+ contentFactory = new FileMappingHttpContentFactory(contentFactory);
+
+ contentFactory = new VirtualHttpContentFactory(contentFactory, styleSheet, "text/css");
+ contentFactory = new PreCompressedHttpContentFactory(contentFactory, precompressedFormats);
+
+ int maxCacheSize = getInitInt("maxCacheSize", -2);
+ int maxCachedFileSize = getInitInt("maxCachedFileSize", -2);
+ int maxCachedFiles = getInitInt("maxCachedFiles", -2);
+ long cacheValidationTime = getInitParameter("cacheValidationTime") != null ? Long.parseLong(getInitParameter("cacheValidationTime")) : -2;
+ if (maxCachedFiles != -2 || maxCacheSize != -2 || maxCachedFileSize != -2 || cacheValidationTime != -2)
+ {
+ ByteBufferPool bufferPool = getByteBufferPool(contextHandler);
+ ValidatingCachingHttpContentFactory cached = new ValidatingCachingHttpContentFactory(contentFactory,
+ (cacheValidationTime > -2) ? cacheValidationTime : Duration.ofSeconds(1).toMillis(), bufferPool);
+ contentFactory = cached;
+ if (maxCacheSize >= 0)
+ cached.setMaxCacheSize(maxCacheSize);
+ if (maxCachedFileSize >= 0)
+ cached.setMaxCachedFileSize(maxCachedFileSize);
+ if (maxCachedFiles >= 0)
+ cached.setMaxCachedFiles(maxCachedFiles);
+ }
+ }
+ _resourceService.setHttpContentFactory(contentFactory);
+
+ if (contextHandler.getWelcomeFiles() == null)
+ contextHandler.setWelcomeFiles(new String[]{"index.html", "index.jsp"});
+
+ _resourceService.setAcceptRanges(getInitBoolean("acceptRanges", _resourceService.isAcceptRanges()));
+ _resourceService.setDirAllowed(getInitBoolean("dirAllowed", _resourceService.isDirAllowed()));
+ boolean redirectWelcome = getInitBoolean("redirectWelcome", false);
+ _resourceService.setWelcomeMode(redirectWelcome ? ResourceService.WelcomeMode.REDIRECT : ResourceService.WelcomeMode.SERVE);
+ _resourceService.setPrecompressedFormats(precompressedFormats);
+ _resourceService.setEtags(getInitBoolean("etags", _resourceService.isEtags()));
+
+ _welcomeServletMode = WelcomeServletMode.NONE;
+ String welcomeServlets = getInitParameter("welcomeServlets");
+ if (welcomeServlets != null)
+ {
+ welcomeServlets = welcomeServlets.toLowerCase(Locale.ENGLISH);
+ _welcomeServletMode = switch (welcomeServlets)
+ {
+ case "true" -> WelcomeServletMode.MATCH;
+ case "exact" -> WelcomeServletMode.EXACT;
+ default -> WelcomeServletMode.NONE;
+ };
+ }
+
+ int encodingHeaderCacheSize = getInitInt("encodingHeaderCacheSize", -1);
+ if (encodingHeaderCacheSize >= 0)
+ _resourceService.setEncodingCacheSize(encodingHeaderCacheSize);
+
+ String cc = getInitParameter("cacheControl");
+ if (cc != null)
+ _resourceService.setCacheControl(cc);
+
+ List gzipEquivalentFileExtensions = new ArrayList<>();
+ String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
+ if (otherGzipExtensions != null)
+ {
+ //comma separated list
+ StringTokenizer tok = new StringTokenizer(otherGzipExtensions, ",", false);
+ while (tok.hasMoreTokens())
+ {
+ String s = tok.nextToken().trim();
+ gzipEquivalentFileExtensions.add((s.charAt(0) == '.' ? s : "." + s));
+ }
+ }
+ else
+ {
+ // .svgz files are gzipped svg files and must be served with Content-Encoding:gzip
+ gzipEquivalentFileExtensions.add(".svgz");
+ }
+ _resourceService.setGzipEquivalentFileExtensions(gzipEquivalentFileExtensions);
+
+ _pathInfoOnly = getInitBoolean("pathInfoOnly", true);
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug(" .baseResource = {}", baseResource);
+ LOG.debug(" .resourceService = {}", _resourceService);
+ LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode);
+ }
+ }
+
+ private static ByteBufferPool getByteBufferPool(ContextHandler contextHandler)
+ {
+ if (contextHandler == null)
+ return ByteBufferPool.NON_POOLING;
+ Server server = contextHandler.getServer();
+ if (server == null)
+ return ByteBufferPool.NON_POOLING;
+ return server.getByteBufferPool();
+ }
+
+ private String getInitParameter(String name, String... deprecated)
+ {
+ String value = getInitParameter(name);
+ if (value != null)
+ return value;
+
+ for (String d : deprecated)
+ {
+ value = getInitParameter(d);
+ if (value != null)
+ {
+ LOG.warn("Deprecated {} used instead of {}", d, name);
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ private List parsePrecompressedFormats(String precompressed, Boolean gzip, List dft)
+ {
+ if (precompressed == null && gzip == null)
+ {
+ return dft;
+ }
+ List ret = new ArrayList<>();
+ if (precompressed != null && precompressed.indexOf('=') > 0)
+ {
+ for (String pair : precompressed.split(","))
+ {
+ String[] setting = pair.split("=");
+ String encoding = setting[0].trim();
+ String extension = setting[1].trim();
+ ret.add(new CompressedContentFormat(encoding, extension));
+ if (gzip == Boolean.TRUE && !ret.contains(CompressedContentFormat.GZIP))
+ ret.add(CompressedContentFormat.GZIP);
+ }
+ }
+ else if (precompressed != null)
+ {
+ if (Boolean.parseBoolean(precompressed))
+ {
+ ret.add(CompressedContentFormat.BR);
+ ret.add(CompressedContentFormat.GZIP);
+ }
+ }
+ else if (gzip == Boolean.TRUE)
+ {
+ // gzip handling is for backwards compatibility with older Jetty
+ ret.add(CompressedContentFormat.GZIP);
+ }
+ return ret;
+ }
+
+ private Boolean getInitBoolean(String name)
+ {
+ String value = getInitParameter(name);
+ if (value == null || value.isEmpty())
+ return null;
+ return (value.startsWith("t") ||
+ value.startsWith("T") ||
+ value.startsWith("y") ||
+ value.startsWith("Y") ||
+ value.startsWith("1"));
+ }
+
+ private boolean getInitBoolean(String name, boolean dft)
+ {
+ return Optional.ofNullable(getInitBoolean(name)).orElse(dft);
+ }
+
+ private int getInitInt(String name, int dft)
+ {
+ String value = getInitParameter(name);
+ if (value != null && !value.isEmpty())
+ return Integer.parseInt(value);
+ return dft;
+ }
+
+ protected ServletContextHandler initContextHandler(ServletContext servletContext)
+ {
+ if (servletContext instanceof ServletContextHandler.ServletContextApi api)
+ return api.getContext().getServletContextHandler();
+
+ Context context = ContextHandler.getCurrentContext();
+ if (context instanceof ContextHandler.ScopedContext scopedContext)
+ return scopedContext.getContextHandler();
+
+ throw new IllegalArgumentException("The servletContext " + servletContext + " " +
+ servletContext.getClass().getName() + " is not " + ContextHandler.ScopedContext.class.getName());
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
+ {
+ boolean included = httpServletRequest.getDispatcherType() == DispatcherType.INCLUDE;
+ String encodedPathInContext = getEncodedPathInContext(httpServletRequest, included);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("doGet(hsReq={}, hsResp={}) pathInContext={}, included={}", httpServletRequest, httpServletResponse, encodedPathInContext, included);
+
+ try
+ {
+ HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(httpServletRequest));
+ if (LOG.isDebugEnabled())
+ LOG.debug("content = {}", content);
+
+ if (content == null || Resources.missing(content.getResource()))
+ {
+ doNotFound(httpServletRequest, httpServletResponse, encodedPathInContext);
+ }
+ else
+ {
+ // lookup the core request and response as wrapped by the ServletContextHandler
+ ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(httpServletRequest);
+ ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse();
+ ServletChannel servletChannel = servletContextRequest.getServletChannel();
+
+ // If the servlet request has not been wrapped,
+ // we can use the core request directly,
+ // otherwise wrap the servlet request as a core request
+ Request coreRequest = httpServletRequest instanceof ServletApiRequest
+ ? servletChannel.getRequest()
+ : ServletCoreRequest.wrap(httpServletRequest);
+
+ // If the servlet response has been wrapped and has been written to,
+ // then the servlet response must be wrapped as a core response
+ // otherwise we can use the core response directly.
+ boolean useServletResponse = !(httpServletResponse instanceof ServletApiResponse) || servletContextResponse.isWritingOrStreaming();
+ Response coreResponse = useServletResponse
+ ? new ServletCoreResponse(coreRequest, httpServletResponse, included)
+ : servletChannel.getResponse();
+
+ // If the core response is already committed then do nothing more
+ if (coreResponse.isCommitted())
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Response already committed for {}", coreRequest.getHttpURI());
+ return;
+ }
+
+ // Get the content length before we may wrap the content
+ long contentLength = content.getContentLengthValue();
+
+ // Servlet Filters could be interacting with the Response already.
+ if (useServletResponse)
+ content = new UnknownLengthHttpContent(content);
+
+ // The character encoding may be forced
+ String characterEncoding = servletContextResponse.getRawCharacterEncoding();
+ if (characterEncoding != null)
+ content = new ForcedCharacterEncodingHttpContent(content, characterEncoding);
+
+ // If async is supported and the unwrapped content is larger than an output buffer
+ if (httpServletRequest.isAsyncSupported() &&
+ (contentLength < 0 || contentLength > coreRequest.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()))
+ {
+ // send the content asynchronously
+ AsyncContext asyncContext = httpServletRequest.startAsync();
+ Callback callback = new AsyncContextCallback(asyncContext, httpServletResponse);
+ _resourceService.doGet(coreRequest, coreResponse, callback, content);
+ }
+ else
+ {
+ // send the content blocking
+ try (Blocker.Callback callback = Blocker.callback())
+ {
+ _resourceService.doGet(coreRequest, coreResponse, callback, content);
+ callback.block();
+ }
+ catch (Exception e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ }
+ }
+ catch (InvalidPathException e)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e);
+ if (included)
+ throw new FileNotFoundException(encodedPathInContext);
+ httpServletResponse.setStatus(404);
+ }
+ }
+
+ protected String getEncodedPathInContext(HttpServletRequest request, boolean included)
+ {
+ HttpServletMapping mapping = request.getHttpServletMapping();
+ if (included)
+ {
+ if (request.getAttribute(Dispatcher.INCLUDE_MAPPING) instanceof HttpServletMapping httpServletMapping)
+ {
+ mapping = httpServletMapping;
+ }
+ else
+ {
+ // must be an include of a named dispatcher. Just use the whole URI
+ return URIUtil.encodePath(URIUtil.addPaths(request.getServletPath(), request.getPathInfo()));
+ }
+ }
+
+ return switch (mapping.getMappingMatch())
+ {
+ case CONTEXT_ROOT -> "/";
+ case DEFAULT, EXTENSION, EXACT ->
+ {
+ if (included)
+ yield URIUtil.encodePath((String)request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH));
+ else if (request instanceof ServletApiRequest apiRequest)
+ // Strip the context path from the canonically encoded path, so no need to re-encode (and mess up %2F etc.)
+ yield Context.getPathInContext(request.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
+ else
+ yield URIUtil.encodePath(request.getServletPath());
+ }
+ case PATH ->
+ {
+ if (_pathInfoOnly)
+ {
+ if (included)
+ yield URIUtil.encodePath((String)request.getAttribute(Dispatcher.INCLUDE_PATH_INFO));
+ else
+ yield URIUtil.encodePath(request.getPathInfo());
+ }
+ else
+ {
+ if (included)
+ yield URIUtil.encodePath(URIUtil.addPaths((String)request.getAttribute(Dispatcher.INCLUDE_SERVLET_PATH), (String)request.getAttribute(Dispatcher.INCLUDE_PATH_INFO)));
+ else if (request instanceof ServletApiRequest apiRequest)
+ // Strip the context path from the canonically encoded path, so no need to re-encode (and mess up %2F etc.)
+ yield Context.getPathInContext(request.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
+ else
+ yield URIUtil.encodePath(URIUtil.addPaths(request.getServletPath(), request.getPathInfo()));
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("doHead(req={}, resp={}) (calling doGet())", req, resp);
+ doGet(req, resp);
+ }
+
+ @Override
+ protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ {
+ // Always return 405: Method Not Allowed for DefaultServlet
+ resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ }
+
+ @Override
+ protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ {
+ // override to eliminate TRACE that the default HttpServlet impl adds
+ resp.setHeader("Allow", "GET, HEAD, OPTIONS");
+ }
+
+ protected void doNotFound(HttpServletRequest request, HttpServletResponse response, String encodedPathInContext) throws IOException
+ {
+ if (request.getDispatcherType() == DispatcherType.INCLUDE)
+ {
+ /* https://github.com/jakartaee/servlet/blob/6.0.0-RELEASE/spec/src/main/asciidoc/servlet-spec-body.adoc#93-the-include-method
+ * 9.3 - If the default servlet is the target of a RequestDispatch.include() and the requested
+ * resource does not exist, then the default servlet MUST throw FileNotFoundException.
+ * If the exception isn’t caught and handled, and the response
+ * hasn’t been committed, the status code MUST be set to 500.
+ */
+ throw new FileNotFoundException(encodedPathInContext);
+ }
+
+ // no content
+ response.sendError(404);
+ }
+
+ private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory
+ {
+ private final ServletContextHandler _servletContextHandler;
+
+ private ServletResourceService(ServletContextHandler servletContextHandler)
+ {
+ _servletContextHandler = servletContextHandler;
+ }
+
+ @Override
+ public String getWelcomeTarget(HttpContent content, Request coreRequest)
+ {
+ String[] welcomes = _servletContextHandler.getWelcomeFiles();
+ if (welcomes == null)
+ return null;
+ String pathInContext = Request.getPathInContext(coreRequest);
+ String welcomeTarget = null;
+ Resource base = content.getResource();
+ if (Resources.isReadableDirectory(base))
+ {
+ for (String welcome : welcomes)
+ {
+ String welcomeInContext = URIUtil.addPaths(pathInContext, welcome);
+
+ // If the welcome resource is a file, it has
+ // precedence over resources served by Servlets.
+ Resource welcomePath = content.getResource().resolve(welcome);
+ if (Resources.isReadableFile(welcomePath))
+ return welcomeInContext;
+
+ // Check whether a Servlet may serve the welcome resource.
+ if (_welcomeServletMode != WelcomeServletMode.NONE && welcomeTarget == null)
+ {
+ ServletHandler.MappedServlet entry = _servletContextHandler.getServletHandler().getMappedServlet(welcomeInContext);
+ // Is there a different Servlet that may serve the welcome resource?
+ if (entry != null && entry.getServletHolder().getServletInstance() != ResourceServlet.this)
+ {
+ if (_welcomeServletMode == WelcomeServletMode.MATCH || entry.getPathSpec().getDeclaration().equals(welcomeInContext))
+ {
+ welcomeTarget = welcomeInContext;
+ // Do not break the loop, because we want to try other welcome resources
+ // that may be files and take precedence over Servlet welcome resources.
+ }
+ }
+ }
+ }
+ }
+ return welcomeTarget;
+ }
+
+ @Override
+ protected void serveWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
+ {
+ HttpServletRequest servletRequest = getServletRequest(request);
+ HttpServletResponse servletResponse = getServletResponse(response);
+
+ boolean included = isIncluded(servletRequest);
+
+ RequestDispatcher dispatcher = servletRequest.getServletContext().getRequestDispatcher(welcomeTarget);
+ if (dispatcher == null)
+ {
+ // We know that the welcome target exists and can be served.
+ Response.writeError(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500);
+ return;
+ }
+
+ try
+ {
+ if (included)
+ {
+ dispatcher.include(servletRequest, servletResponse);
+ }
+ else
+ {
+ servletRequest.setAttribute("org.eclipse.jetty.server.welcome", welcomeTarget);
+ dispatcher.forward(servletRequest, servletResponse);
+ }
+ callback.succeeded();
+ }
+ catch (ServletException e)
+ {
+ callback.failed(e);
+ }
+ }
+
+ @Override
+ protected void rehandleWelcome(Request request, Response response, Callback callback, String welcomeTarget) throws IOException
+ {
+ serveWelcome(request, response, callback, welcomeTarget);
+ }
+
+ @Override
+ protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={})", coreRequest, coreResponse, callback, statusCode);
+ writeHttpError(coreRequest, coreResponse, callback, statusCode, null, null);
+ }
+
+ @Override
+ protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, Throwable cause)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, cause={})", coreRequest, coreResponse, callback, cause, cause);
+
+ int statusCode = HttpStatus.INTERNAL_SERVER_ERROR_500;
+ String reason = null;
+ if (cause instanceof HttpException httpException)
+ {
+ statusCode = httpException.getCode();
+ reason = httpException.getReason();
+ }
+ writeHttpError(coreRequest, coreResponse, callback, statusCode, reason, cause);
+ }
+
+ @Override
+ protected void writeHttpError(Request coreRequest, Response coreResponse, Callback callback, int statusCode, String reason, Throwable cause)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("writeHttpError(coreRequest={}, coreResponse={}, callback={}, statusCode={}, reason={}, cause={})", coreRequest, coreResponse, callback, statusCode, reason, cause, cause);
+ HttpServletRequest request = getServletRequest(coreRequest);
+ HttpServletResponse response = getServletResponse(coreResponse);
+ try
+ {
+ if (isIncluded(request))
+ return;
+ if (cause != null)
+ request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, cause);
+ response.sendError(statusCode, reason);
+ }
+ catch (IOException e)
+ {
+ // TODO: Need a better exception?
+ throw new RuntimeException(e);
+ }
+ finally
+ {
+ callback.succeeded();
+ }
+ }
+
+ @Override
+ protected boolean passConditionalHeaders(Request request, Response response, HttpContent content, Callback callback) throws IOException
+ {
+ boolean included = isIncluded(getServletRequest(request));
+ if (included)
+ return false;
+ return super.passConditionalHeaders(request, response, content, callback);
+ }
+
+ private HttpServletRequest getServletRequest(Request request)
+ {
+ ServletCoreRequest servletCoreRequest = Request.as(request, ServletCoreRequest.class);
+ if (servletCoreRequest != null)
+ return servletCoreRequest.getServletRequest();
+
+ ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
+ if (servletContextRequest != null)
+ return servletContextRequest.getServletApiRequest();
+
+ throw new IllegalStateException("instanceof " + request.getClass());
+ }
+
+ private HttpServletResponse getServletResponse(Response response)
+ {
+ ServletCoreResponse servletCoreResponse = Response.as(response, ServletCoreResponse.class);
+ if (servletCoreResponse != null)
+ return servletCoreResponse.getServletResponse();
+
+ ServletContextResponse servletContextResponse = Response.as(response, ServletContextResponse.class);
+ if (servletContextResponse != null)
+ return servletContextResponse.getServletApiResponse();
+
+ throw new IllegalStateException("instanceof " + response.getClass());
+ }
+ }
+
+ static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath)
+ {
+ String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
+ return URIUtil.addPaths(includedServletPath, pathInfo);
+ }
+
+ private static boolean isIncluded(HttpServletRequest request)
+ {
+ return request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
+ }
+
+ /**
+ * Wrap an existing HttpContent with one that takes has an unknown/unspecified length.
+ */
+ private static class UnknownLengthHttpContent extends HttpContent.Wrapper
+ {
+ public UnknownLengthHttpContent(HttpContent content)
+ {
+ super(content);
+ }
+
+ @Override
+ public HttpField getContentLength()
+ {
+ return null;
+ }
+
+ @Override
+ public long getContentLengthValue()
+ {
+ return -1;
+ }
+ }
+
+ private static class ForcedCharacterEncodingHttpContent extends HttpContent.Wrapper
+ {
+ private final String characterEncoding;
+ private final String contentType;
+
+ public ForcedCharacterEncodingHttpContent(HttpContent content, String characterEncoding)
+ {
+ super(Objects.requireNonNull(content));
+ this.characterEncoding = characterEncoding;
+ if (content.getContentTypeValue() == null || content.getResource().isDirectory())
+ {
+ this.contentType = null;
+ }
+ else
+ {
+ String mimeType = content.getContentTypeValue();
+ int idx = mimeType.indexOf(";charset");
+ if (idx >= 0)
+ mimeType = mimeType.substring(0, idx);
+ this.contentType = mimeType + ";charset=" + characterEncoding;
+ }
+ }
+
+ @Override
+ public HttpField getContentType()
+ {
+ return new HttpField(HttpHeader.CONTENT_TYPE, this.contentType);
+ }
+
+ @Override
+ public String getContentTypeValue()
+ {
+ return this.contentType;
+ }
+
+ @Override
+ public String getCharacterEncoding()
+ {
+ return this.characterEncoding;
+ }
+ }
+
+ /**
+ * The different modes a welcome resource may be served by a Servlet.
+ */
+ private enum WelcomeServletMode
+ {
+ /**
+ * Welcome targets are not served by Servlets.
+ * The welcome target must exist as a file on the filesystem.
+ */
+ NONE,
+ /**
+ * Welcome target that exist as files on the filesystem are
+ * served, otherwise a matching Servlet may serve the welcome target.
+ */
+ MATCH,
+ /**
+ * Welcome target that exist as files on the filesystem are
+ * served, otherwise an exact matching Servlet may serve the welcome target.
+ */
+ EXACT
+ }
+
+ private static class AsyncContextCallback implements Callback
+ {
+ private final AsyncContext _asyncContext;
+ private final HttpServletResponse _response;
+
+ private AsyncContextCallback(AsyncContext asyncContext, HttpServletResponse response)
+ {
+ _asyncContext = asyncContext;
+ _response = response;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ _asyncContext.complete();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ try
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("AsyncContextCallback failed {}", _asyncContext, x);
+ // It is known that this callback is only failed if the response is already committed,
+ // thus we can only abort the response here.
+ _response.sendError(-1);
+ }
+ catch (IOException e)
+ {
+ ExceptionUtil.addSuppressedIfNotAssociated(x, e);
+ }
+ finally
+ {
+ _asyncContext.complete();
+ }
+ if (LOG.isDebugEnabled())
+ LOG.debug("Async get failed", x);
+ }
+ }
+}
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
index 0a99cb9f30b..4d7083accfc 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java
@@ -1433,6 +1433,12 @@ public class ServletApiRequest implements HttpServletRequest
return trailersMap;
}
+ @Override
+ public String toString()
+ {
+ return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), _servletContextRequest);
+ }
+
static class AmbiguousURI extends ServletApiRequest
{
private final String msg;
diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreRequest.java
index 37d8b71efd7..fe0b6ffad1c 100644
--- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreRequest.java
+++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletCoreRequest.java
@@ -100,7 +100,7 @@ public class ServletCoreRequest implements Request
.authority(request.getServerName(), request.getServerPort());
if (included)
- builder.path(addEncodedPaths(request.getContextPath(), encodePath(DefaultServlet.getIncludedPathInContext(request, includedServletPath, false))));
+ builder.path(addEncodedPaths(request.getContextPath(), encodePath(ResourceServlet.getIncludedPathInContext(request, includedServletPath))));
else if (request.getDispatcherType() != DispatcherType.REQUEST)
builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))));
else
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 c8b3e4c889c..d53f04b9df9 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
@@ -38,6 +38,7 @@ import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
+import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
@@ -52,7 +53,6 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.content.ResourceHttpContent;
-import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.IOResources;
import org.eclipse.jetty.logging.StacklessLogging;
@@ -1383,7 +1383,7 @@ public class DefaultServletTest
FS.ensureDirExists(altRoot);
ServletHolder defholder = context.addServlet(DefaultServlet.class, "/alt/*");
- defholder.setInitParameter("resourceBase", altRoot.toUri().toASCIIString());
+ defholder.setInitParameter("baseResource", altRoot.toUri().toASCIIString());
defholder.setInitParameter("dirAllowed", "false");
defholder.setInitParameter("redirectWelcome", "false");
defholder.setInitParameter("welcomeServlets", "true");
@@ -1394,7 +1394,8 @@ public class DefaultServletTest
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
String includeTarget = req.getParameter("includeTarget");
- req.getRequestDispatcher(includeTarget).include(req, resp);
+ RequestDispatcher requestDispatcher = req.getRequestDispatcher(includeTarget);
+ requestDispatcher.include(req, resp);
}
});
context.addServlet(gwholder, "/gateway");
@@ -2001,10 +2002,9 @@ public class DefaultServletTest
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
assertThat(response.getContent(), containsString("Hello World
"));
- ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
-
/*
TODO: fix after HttpContent changes.
+ ResourceHttpContentFactory factory = (ResourceHttpContentFactory)context.getServletContext().getAttribute("resourceCache");
HttpContent content = factory.getContent("/index.html", 200);
ByteBuffer buffer = content.getDirectBuffer();
assertThat("Buffer is direct", buffer.isDirect(), is(true));
@@ -3425,22 +3425,17 @@ public class DefaultServletTest
response.setCharacterEncoding("utf-8");
chain.doFilter(request, response);
}
-
- @Override
- public void destroy()
- {
- }
}
@Test
- public void testPathInfoOnly() throws Exception
+ public void testNotPathInfoOnly() throws Exception
{
ServletContextHandler context = new ServletContextHandler("/c1", ServletContextHandler.NO_SESSIONS);
context.setWelcomeFiles(new String[]{"index.y", "index.x"});
ServletHolder indexServlet = new ServletHolder("index-servlet", new HttpServlet()
{
@Override
- protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
@@ -3456,27 +3451,27 @@ public class DefaultServletTest
context.getServletHandler().addServlet(indexServlet);
context.getServletHandler().addServletMapping(indexMapping);
- Path pathTest = MavenTestingUtils.getTestResourcePath("pathTest");
+ Path docroot = MavenTestingUtils.getTestResourcePath("docroot");
- Path defaultDir = pathTest.resolve("default");
ServletHolder slashHolder = new ServletHolder("default", new DefaultServlet());
slashHolder.setInitParameter("redirectWelcome", "false");
slashHolder.setInitParameter("welcomeServlets", "true");
- slashHolder.setInitParameter("baseResource", defaultDir.toAbsolutePath().toString());
+ slashHolder.setInitParameter("baseResource", docroot.toAbsolutePath().toString());
context.addServlet(slashHolder, "/");
- Path rDir = pathTest.resolve("rdir");
- ServletHolder rHolder = new ServletHolder("rdefault", new DefaultServlet());
+ Path altroot = MavenTestingUtils.getTestResourcePath("altroot");
+ ServletHolder rHolder = new ServletHolder("alt", new DefaultServlet());
rHolder.setInitParameter("redirectWelcome", "false");
rHolder.setInitParameter("welcomeServlets", "true");
- rHolder.setInitParameter("baseResource", rDir.toAbsolutePath().toString());
- context.addServlet(rHolder, "/r/*");
+ rHolder.setInitParameter("pathInfoOnly", "false");
+ rHolder.setInitParameter("baseResource", altroot.toAbsolutePath().toString());
+ context.addServlet(rHolder, "/all/*");
server.stop();
server.setHandler(context);
server.start();
String rawRequest = """
- GET /c1/r/ HTTP/1.1\r
+ GET /c1/all/index.html HTTP/1.1\r
Host: localhost\r
Connection: close\r
\r
@@ -3484,7 +3479,7 @@ public class DefaultServletTest
String rawResponse = connector.getResponse(rawRequest);
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
- assertThat(response.getContent(), containsString("testPathInfoOnly-OK"));
+ assertThat(response.getContent(), containsString("this is alternate index content"));
}
@Test
@@ -3652,11 +3647,6 @@ public class DefaultServletTest
response.setCharacterEncoding("utf-8");
chain.doFilter(request, response);
}
-
- @Override
- public void destroy()
- {
- }
}
private boolean deleteFile(Path file) throws IOException
diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResourceServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResourceServletTest.java
new file mode 100644
index 00000000000..2bd18bced7d
--- /dev/null
+++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/ResourceServletTest.java
@@ -0,0 +1,3930 @@
+//
+// ========================================================================
+// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+// which is available at https://www.apache.org/licenses/LICENSE-2.0.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+package org.eclipse.jetty.ee10.servlet;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.http.CompressedContentFormat;
+import org.eclipse.jetty.http.DateGenerator;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.http.UriCompliance;
+import org.eclipse.jetty.http.content.ResourceHttpContent;
+import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.IOResources;
+import org.eclipse.jetty.logging.StacklessLogging;
+import org.eclipse.jetty.server.AllowedResourceAliasChecker;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.ResourceService;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.MavenPaths;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
+import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
+import org.eclipse.jetty.util.resource.URLResourceFactory;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jetty.http.tools.matchers.HttpFieldsMatchers.containsHeader;
+import static org.eclipse.jetty.http.tools.matchers.HttpFieldsMatchers.containsHeaderValue;
+import static org.eclipse.jetty.http.tools.matchers.HttpFieldsMatchers.headerValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+@ExtendWith(WorkDirExtension.class)
+public class ResourceServletTest
+{
+ public WorkDir workDir;
+
+ public Path docRoot;
+
+ // The name of the odd-jar used for testing "jar:file://" based resource access.
+ private static final String ODD_JAR = "jar-resource-odd.jar";
+
+ private Server server;
+ private LocalConnector connector;
+ private ServletContextHandler context;
+
+ @BeforeEach
+ public void init() throws Exception
+ {
+ docRoot = workDir.getEmptyPathDir().resolve("docroot");
+ FS.ensureDirExists(docRoot);
+
+ server = new Server();
+
+ connector = new LocalConnector(server);
+ connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false);
+ Path extraJarResources = MavenPaths.findTestResourceFile(ODD_JAR);
+ URL[] urls = new URL[]{extraJarResources.toUri().toURL()};
+
+ ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
+ URLClassLoader extraClassLoader = new URLClassLoader(urls, parentClassLoader);
+
+ context = new ServletContextHandler();
+ context.setBaseResourceAsPath(docRoot);
+ context.setContextPath("/context");
+ context.setWelcomeFiles(new String[]{"index.html", "index.jsp", "index.htm"});
+ context.setClassLoader(extraClassLoader);
+
+ server.setHandler(context);
+ server.addConnector(connector);
+
+ server.start();
+ }
+
+ @AfterEach
+ public void destroy() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ public static Stream getTests()
+ {
+ return Stream.of(
+ Arguments.of(null, "file.txt", "/", null, "file.txt"),
+ Arguments.of(null, "dir/file.txt", "/", null, "dir/file.txt"),
+
+ Arguments.of(null, "file.txt", "/*", null, "file.txt"),
+ Arguments.of(null, "dir/file.txt", "/*", null, "dir/file.txt"),
+
+ Arguments.of("dir", "dir/file.txt", "/dir/*", null, "dir/file.txt"),
+ Arguments.of("dir", "dir/file.txt", "/dir/*", true, "dir/file.txt"),
+ Arguments.of(null, "dir/file.txt", "/dir/*", false, "dir/file.txt"),
+ Arguments.of(".", "dir/file.txt", "/dir/*", false, "dir/file.txt"),
+
+ Arguments.of(null, "file.txt", "/file.txt", null, "file.txt"),
+ Arguments.of(null, "dir/file.txt", "/dir/file.txt", null, "dir/file.txt"),
+
+ Arguments.of(null, "file.txt", "*.txt", null, "file.txt"),
+ Arguments.of(null, "dir/file.txt", "*.txt", null, "dir/file.txt")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("getTests")
+ public void testGet(String baseResource, String resourceInContext, String mapping, Boolean pathInfoOnly, String pathInContext) throws Exception
+ {
+ Path file = docRoot.resolve(resourceInContext);
+ Path directory = file.getParent();
+ if (!directory.equals(docRoot) && !Files.exists(directory))
+ Files.createDirectories(directory);
+
+ ServletHolder servletHolder = context.addServlet(ResourceServlet.class, mapping);
+ if (StringUtil.isNotBlank(baseResource))
+ servletHolder.getInitParameters().put("baseResource", baseResource);
+ if (pathInfoOnly != null)
+ servletHolder.getInitParameters().put("pathInfoOnly", String.valueOf(pathInfoOnly));
+
+ String rawResponse;
+ HttpTester.Response response;
+
+ rawResponse = connector.getResponse("""
+ GET /context/%s HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """.formatted(pathInContext));
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.NOT_FOUND_404));
+
+ Files.writeString(file, "How now brown cow", UTF_8);
+
+ rawResponse = connector.getResponse("""
+ GET /context/%s HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """.formatted(pathInContext));
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(response.toString(), response.getContent(), is("How now brown cow"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("getTests")
+ public void testHead(String baseResource, String resourceInContext, String mapping, Boolean pathInfoOnly, String pathInContext) throws Exception
+ {
+ Path file = docRoot.resolve(resourceInContext);
+ Path directory = file.getParent();
+ if (!directory.equals(docRoot) && !Files.exists(directory))
+ Files.createDirectories(directory);
+
+ ServletHolder servletHolder = context.addServlet(ResourceServlet.class, mapping);
+ if (StringUtil.isNotBlank(baseResource))
+ servletHolder.getInitParameters().put("baseResource", baseResource);
+ if (pathInfoOnly != null)
+ servletHolder.getInitParameters().put("pathInfoOnly", String.valueOf(pathInfoOnly));
+
+ String rawResponse;
+ String headResponse;
+ HttpTester.Response response;
+
+ rawResponse = connector.getResponse("""
+ GET /context/%s HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """.formatted(pathInContext));
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.NOT_FOUND_404));
+
+ headResponse = connector.getResponse("""
+ HEAD /context/%s HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """.formatted(pathInContext));
+
+ assertThat(rawResponse.replaceFirst("Date: .* GMT", "Date: yyyy-mm-dd"), startsWith(headResponse.replaceFirst("Date: .* GMT", "Date: yyyy-mm-dd")));
+
+
+ Files.writeString(file, "How now brown cow", UTF_8);
+
+ rawResponse = connector.getResponse("""
+ GET /context/%s HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """.formatted(pathInContext));
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(response.toString(), response.getContent(), is("How now brown cow"));
+
+ headResponse = connector.getResponse("""
+ HEAD /context/%s HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """.formatted(pathInContext));
+ assertThat(rawResponse.replaceFirst("Date: .* GMT", "Date: yyyy-mm-dd"), startsWith(headResponse.replaceFirst("Date: .* GMT", "Date: yyyy-mm-dd")));
+ }
+
+ @Test
+ public void testPost() throws Exception
+ {
+ Path file = docRoot.resolve("file.txt");
+
+ context.addServlet(ResourceServlet.class, "/");
+
+ String rawResponse;
+ HttpTester.Response response;
+
+ rawResponse = connector.getResponse("""
+ POST /context/file.txt HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ Content-Length: 5\r
+ \r
+ abcde
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.METHOD_NOT_ALLOWED_405));
+
+ Files.writeString(file, "How now brown cow", UTF_8);
+
+ rawResponse = connector.getResponse("""
+ POST /context/file.txt HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ Content-Length: 5\r
+ \r
+ abcde
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.METHOD_NOT_ALLOWED_405));
+ }
+
+ @Test
+ public void testTrace() throws Exception
+ {
+ context.addServlet(ResourceServlet.class, "/");
+
+ String rawResponse;
+ HttpTester.Response response;
+
+ rawResponse = connector.getResponse("""
+ TRACE /context/file.txt HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.METHOD_NOT_ALLOWED_405));
+ }
+
+ @Test
+ public void testOptions() throws Exception
+ {
+ context.addServlet(ResourceServlet.class, "/");
+
+ String rawResponse;
+ HttpTester.Response response;
+
+ rawResponse = connector.getResponse("""
+ OPTIONS /context/ HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(response.get(HttpHeader.ALLOW), is("GET, HEAD, OPTIONS"));
+ }
+
+ @Test
+ public void testGetBinaryWithUtfResponseEncoding() throws Exception
+ {
+ Path path = docRoot.resolve("keystore.p12");
+ byte[] originalBytes;
+
+ try (InputStream is = getClass().getResourceAsStream("/keystore.p12");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ OutputStream fos = Files.newOutputStream(path))
+ {
+ IO.copy(is, baos);
+ originalBytes = baos.toByteArray();
+ fos.write(originalBytes);
+ }
+
+ context.setDefaultResponseCharacterEncoding("utf-8");
+ context.addServlet(ResourceServlet.class, "/");
+
+ String rawResponse;
+ HttpTester.Response response;
+
+ rawResponse = connector.getResponse("""
+ GET /context/keystore.p12 HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
+ byte[] readContentBytes = response.getContentBytes();
+
+ assertThat(Arrays.equals(readContentBytes, originalBytes), is(true));
+ }
+
+ @Test
+ public void testGetPercent2F() throws Exception
+ {
+ connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setUriCompliance(UriCompliance.UNSAFE);
+
+ Path file = docRoot.resolve("file.txt");
+ Files.writeString(file, "How now brown cow", UTF_8);
+
+ context.addServlet(ResourceServlet.class, "/");
+
+ String rawResponse;
+ HttpTester.Response response;
+
+ // Access normally, in root of context
+
+ rawResponse = connector.getResponse("""
+ GET /context/file.txt HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
+ assertThat(response.toString(), response.getContent(), is("How now brown cow"));
+
+ // Attempt access using "%2F" instead of "/", should be a 404 (mainly because context isn't found)
+
+ rawResponse = connector.getResponse("""
+ GET /context%2Ffile.txt HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.NOT_FOUND_404));
+
+ Path dir = docRoot.resolve("dirFoo");
+ Files.createDirectory(dir);
+ Path other = dir.resolve("other.txt");
+ Files.writeString(other, "In a while", UTF_8);
+
+ // Attempt access of content in sub-dir of context, using "%2F" instead of "/", should be a 404
+ // as neither getServletPath and getPathInfo are used and thus they don't throw.
+ rawResponse = connector.getResponse("""
+ GET /context/dirFoo%2Fother.txt HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """);
+ response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.toString(), response.getStatus(), is(HttpStatus.NOT_FOUND_404));
+ }
+
+ @Test
+ public void testListingWithSession() throws Exception
+ {
+ ServletHolder holder = context.addServlet(ResourceServlet.class, "/");
+ holder.setInitParameter("dirAllowed", "true");
+ holder.setInitParameter("redirectWelcome", "false");
+ holder.setInitParameter("gzip", "false");
+
+ /* create some content in the docroot */
+ FS.ensureDirExists(docRoot.resolve("one"));
+ FS.ensureDirExists(docRoot.resolve("two"));
+ FS.ensureDirExists(docRoot.resolve("three"));
+
+ String rawResponse = connector.getResponse("""
+ GET /context/;JSESSIONID=1234567890 HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+ assertThat(response.getStatus(), is(200));
+
+ String body = response.getContent();
+
+ assertThat(body, containsString("/one/;JSESSIONID=1234567890"));
+ assertThat(body, containsString("/two/;JSESSIONID=1234567890"));
+ assertThat(body, containsString("/three/;JSESSIONID=1234567890"));
+
+ assertThat(body, not(containsString(" HTTP/1.1\r
+ Host: local\r
+ Connection: close\r
+ \r
+ """;
+ String rawResponse = connector.getResponse(req1);
+ HttpTester.Response response = HttpTester.parseResponse(rawResponse);
+
+ String body = response.getContent();
+ assertThat(body, not(containsString("