Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.1.x to ee11
# Conflicts: # documentation/jetty-asciidoctor-extensions/pom.xml # documentation/jetty-documentation/pom.xml # jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java # jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java
This commit is contained in:
parent
ff70c0e267
commit
2e5583d7ff
|
@ -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.ee11.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;
|
||||
|
||||
/**
|
||||
* <p>A Servlet that handles static resources.</p>
|
||||
* <p>The following init parameters are supported:</p>
|
||||
* <dl>
|
||||
* <dt>acceptRanges</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to accept range requests, defaults to {@code true}.
|
||||
* </dd>
|
||||
* <dt>baseResource</dt>
|
||||
* <dd>
|
||||
* 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)}.
|
||||
* </dd>
|
||||
* <dt>cacheControl</dt>
|
||||
* <dd>
|
||||
* The value of the {@code Cache-Control} header.
|
||||
* If omitted, no {@code Cache-Control} header is generated in responses.
|
||||
* By default is omitted.
|
||||
* </dd>
|
||||
* <dt>cacheValidationTime</dt>
|
||||
* <dd>
|
||||
* 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.
|
||||
* </dd>
|
||||
* <dt>dirAllowed</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to serve directory listing if no welcome file is found.
|
||||
* Otherwise responds with {@code 403 Forbidden}.
|
||||
* Defaults to {@code true}.
|
||||
* </dd>
|
||||
* <dt>encodingHeaderCacheSize</dt>
|
||||
* <dd>
|
||||
* Max number of cached {@code Accept-Encoding} entries.
|
||||
* Use {@code -1} for the default value (100), {@code 0} for no cache.
|
||||
* </dd>
|
||||
* <dt>etags</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to generate ETags in responses.
|
||||
* Defaults to {@code false}.
|
||||
* </dd>
|
||||
* <dt>maxCachedFiles</dt>
|
||||
* <dd>
|
||||
* The max number of cached static resources.
|
||||
* Use {@code -1} for the default value (2048) or {@code 0} for no cache.
|
||||
* </dd>
|
||||
* <dt>maxCachedFileSize</dt>
|
||||
* <dd>
|
||||
* 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.
|
||||
* </dd>
|
||||
* <dt>maxCacheSize</dt>
|
||||
* <dd>
|
||||
* 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.
|
||||
* </dd>
|
||||
* <dt>otherGzipFileExtensions</dt>
|
||||
* <dd>
|
||||
* A comma-separated list of extensions of files whose content is implicitly
|
||||
* gzipped.
|
||||
* Defaults to {@code .svgz}.
|
||||
* </dd>
|
||||
* <dt>pathInfoOnly</dt>
|
||||
* <dd>
|
||||
* 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}.
|
||||
* </dd>
|
||||
* <dt>precompressed</dt>
|
||||
* <dd>
|
||||
* 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.
|
||||
* </dd>
|
||||
* <dt>redirectWelcome</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to redirect welcome files, otherwise they are forwarded.
|
||||
* Defaults to {@code false}.
|
||||
* </dd>
|
||||
* <dt>stylesheet</dt>
|
||||
* <dd>
|
||||
* Defaults to the {@code Server}'s default stylesheet, {@code jetty-dir.css}.
|
||||
* The path of a custom stylesheet to style the directory listing HTML.
|
||||
* </dd>
|
||||
* <dt>useFileMappedBuffer</dt>
|
||||
* <dd>
|
||||
* Use {@code true} to use file mapping to serve static resources.
|
||||
* Defaults to {@code false}.
|
||||
* </dd>
|
||||
* <dt>welcomeServlets</dt>
|
||||
* <dd>
|
||||
* 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}.
|
||||
* </dd>
|
||||
* </dl>
|
||||
*/
|
||||
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<CompressedContentFormat> 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<String> 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<CompressedContentFormat> parsePrecompressedFormats(String precompressed, Boolean gzip, List<CompressedContentFormat> dft)
|
||||
{
|
||||
if (precompressed == null && gzip == null)
|
||||
{
|
||||
return dft;
|
||||
}
|
||||
List<CompressedContentFormat> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>The different modes a welcome resource may be served by a Servlet.</p>
|
||||
*/
|
||||
private enum WelcomeServletMode
|
||||
{
|
||||
/**
|
||||
* <p>Welcome targets are not served by Servlets.</p>
|
||||
* <p>The welcome target must exist as a file on the filesystem.</p>
|
||||
*/
|
||||
NONE,
|
||||
/**
|
||||
* <p>Welcome target that exist as files on the filesystem are
|
||||
* served, otherwise a matching Servlet may serve the welcome target.</p>
|
||||
*/
|
||||
MATCH,
|
||||
/**
|
||||
* <p>Welcome target that exist as files on the filesystem are
|
||||
* served, otherwise an exact matching Servlet may serve the welcome target.</p>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
this is alternate index content.
|
|
@ -0,0 +1 @@
|
|||
THIS CANNOT BE SEEN
|
Loading…
Reference in New Issue