io.undertow.server.handlers.resource.ResourceHandler Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.undertow.server.handlers.resource;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import io.undertow.UndertowLogger;
import io.undertow.predicate.Predicate;
import io.undertow.predicate.Predicates;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.ResponseCodeHandler;
import io.undertow.server.handlers.builder.HandlerBuilder;
import io.undertow.util.ByteRange;
import io.undertow.util.CanonicalPathUtils;
import io.undertow.util.DateUtils;
import io.undertow.util.ETag;
import io.undertow.util.ETagUtils;
import io.undertow.httpcore.HttpHeaderNames;
import io.undertow.httpcore.HttpMethodNames;
import io.undertow.util.MimeMappings;
import io.undertow.util.RedirectBuilder;
import io.undertow.httpcore.StatusCodes;
/**
* @author Stuart Douglas
*/
public class ResourceHandler implements HttpHandler {
/**
* Set of methods prescribed by HTTP 1.1. If request method is not one of those, handler will
* return NOT_IMPLEMENTED.
*/
private static final Set KNOWN_METHODS = new HashSet<>();
static {
KNOWN_METHODS.add(HttpMethodNames.OPTIONS);
KNOWN_METHODS.add(HttpMethodNames.GET);
KNOWN_METHODS.add(HttpMethodNames.HEAD);
KNOWN_METHODS.add(HttpMethodNames.POST);
KNOWN_METHODS.add(HttpMethodNames.PUT);
KNOWN_METHODS.add(HttpMethodNames.DELETE);
KNOWN_METHODS.add(HttpMethodNames.TRACE);
KNOWN_METHODS.add(HttpMethodNames.CONNECT);
}
private final List welcomeFiles = new CopyOnWriteArrayList<>(new String[]{"index.html", "index.htm", "default.html", "default.htm"});
/**
* If directory listing is enabled.
*/
private volatile boolean directoryListingEnabled = false;
/**
* If the canonical version of paths should be passed into the resource manager.
*/
private volatile boolean canonicalizePaths = true;
/**
* The mime mappings that are used to determine the content type.
*/
private volatile MimeMappings mimeMappings = MimeMappings.DEFAULT;
private volatile Predicate allowed = Predicates.truePredicate();
private volatile ResourceSupplier resourceSupplier;
private volatile ResourceManager resourceManager;
/**
* Handler that is called if no resource is found
*/
private final HttpHandler next;
public ResourceHandler(ResourceManager resourceSupplier) {
this(resourceSupplier, ResponseCodeHandler.HANDLE_404);
}
public ResourceHandler(ResourceManager resourceManager, HttpHandler next) {
this.resourceSupplier = new DefaultResourceSupplier(resourceManager);
this.resourceManager = resourceManager;
this.next = next;
}
public ResourceHandler(ResourceSupplier resourceSupplier) {
this(resourceSupplier, ResponseCodeHandler.HANDLE_404);
}
public ResourceHandler(ResourceSupplier resourceManager, HttpHandler next) {
this.resourceSupplier = resourceManager;
this.next = next;
}
/**
* You should use {@link ResourceHandler(ResourceManager)} instead.
*/
@Deprecated
public ResourceHandler() {
this.next = ResponseCodeHandler.HANDLE_404;
}
@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
if (exchange.getRequestMethod().equals(HttpMethodNames.GET) ||
exchange.getRequestMethod().equals(HttpMethodNames.POST)) {
serveResource(exchange, true);
} else if (exchange.getRequestMethod().equals(HttpMethodNames.HEAD)) {
serveResource(exchange, false);
} else {
if (KNOWN_METHODS.contains(exchange.getRequestMethod())) {
exchange.setStatusCode(StatusCodes.METHOD_NOT_ALLOWED);
exchange.addResponseHeader(HttpHeaderNames.ALLOW,
String.join(", ", HttpMethodNames.GET, HttpMethodNames.HEAD, HttpMethodNames.POST));
} else {
exchange.setStatusCode(StatusCodes.NOT_IMPLEMENTED);
}
exchange.endExchange();
}
}
private void serveResource(final HttpServerExchange exchange, final boolean sendContent) throws Exception {
if (DirectoryUtils.sendRequestedBlobs(exchange)) {
return;
}
if (!allowed.resolve(exchange)) {
exchange.setStatusCode(StatusCodes.FORBIDDEN);
exchange.endExchange();
return;
}
//we now dispatch to a worker thread
//as resource manager methods are potentially blocking
HttpHandler dispatchTask = new HttpHandler() {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
Resource resource = null;
try {
if (File.separatorChar == '/' || !exchange.getRelativePath().contains(File.separator)) {
//we don't process resources that contain the sperator character if this is not /
//this prevents attacks where people use windows path seperators in file URLS's
resource = resourceSupplier.getResource(exchange, canonicalize(exchange.getRelativePath()));
}
} catch (IOException e) {
clearCacheHeaders(exchange);
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.endExchange();
return;
}
if (resource == null) {
clearCacheHeaders(exchange);
//usually a 404 handler
next.handleRequest(exchange);
return;
}
if (resource.isDirectory()) {
Resource indexResource;
try {
indexResource = getIndexFiles(exchange, resourceSupplier, resource.getPath(), welcomeFiles);
} catch (IOException e) {
UndertowLogger.REQUEST_IO_LOGGER.ioException(e);
exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
exchange.endExchange();
return;
}
if (indexResource == null) {
if (directoryListingEnabled) {
DirectoryUtils.renderDirectoryListing(exchange, resource);
return;
} else {
exchange.setStatusCode(StatusCodes.FORBIDDEN);
exchange.endExchange();
return;
}
} else if (!exchange.getRequestPath().endsWith("/")) {
exchange.setStatusCode(StatusCodes.FOUND);
exchange.setResponseHeader(HttpHeaderNames.LOCATION, RedirectBuilder.redirect(exchange, exchange.getRelativePath() + "/", true));
exchange.endExchange();
return;
}
resource = indexResource;
} else if (exchange.getRelativePath().endsWith("/")) {
//UNDERTOW-432
exchange.setStatusCode(StatusCodes.NOT_FOUND);
exchange.endExchange();
return;
}
final ETag etag = resource.getETag();
final Date lastModified = resource.getLastModified();
if (!ETagUtils.handleIfMatch(exchange, etag, false) ||
!DateUtils.handleIfUnmodifiedSince(exchange, lastModified)) {
exchange.setStatusCode(StatusCodes.PRECONDITION_FAILED);
exchange.endExchange();
return;
}
if (!ETagUtils.handleIfNoneMatch(exchange, etag, true) ||
!DateUtils.handleIfModifiedSince(exchange, lastModified)) {
exchange.setStatusCode(StatusCodes.NOT_MODIFIED);
exchange.endExchange();
return;
}
Long contentLength = resource.getContentLength();
if (contentLength != null && !exchange.containsResponseHeader(HttpHeaderNames.TRANSFER_ENCODING)) {
exchange.setResponseContentLength(contentLength);
}
ByteRange.RangeResponseResult rangeResponse = null;
long start = -1, end = -1;
if (resource instanceof RangeAwareResource && ((RangeAwareResource) resource).isRangeSupported() && contentLength != null) {
exchange.setResponseHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes");
//TODO: figure out what to do with the content encoded resource manager
ByteRange range = ByteRange.parse(exchange.getRequestHeader(HttpHeaderNames.RANGE));
if (range != null && range.getRanges() == 1 && resource.getContentLength() != null) {
rangeResponse = range.getResponseResult(resource.getContentLength(), exchange.getRequestHeader(HttpHeaderNames.IF_RANGE), resource.getLastModified(), resource.getETag() == null ? null : resource.getETag().getTag());
if (rangeResponse != null) {
start = rangeResponse.getStart();
end = rangeResponse.getEnd();
exchange.setStatusCode(rangeResponse.getStatusCode());
exchange.setResponseHeader(HttpHeaderNames.CONTENT_RANGE, rangeResponse.getContentRange());
long length = rangeResponse.getContentLength();
exchange.setResponseContentLength(length);
if (rangeResponse.getStatusCode() == StatusCodes.REQUEST_RANGE_NOT_SATISFIABLE) {
return;
}
}
}
}
//we are going to proceed. Set the appropriate headers
if (!exchange.containsResponseHeader(HttpHeaderNames.CONTENT_TYPE)) {
final String contentType = resource.getContentType(mimeMappings);
if (contentType != null) {
exchange.setResponseHeader(HttpHeaderNames.CONTENT_TYPE, contentType);
} else {
exchange.setResponseHeader(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
}
}
if (lastModified != null) {
exchange.setResponseHeader(HttpHeaderNames.LAST_MODIFIED, resource.getLastModifiedString());
}
if (etag != null) {
exchange.setResponseHeader(HttpHeaderNames.ETAG, etag.toString());
}
if (!sendContent) {
exchange.endExchange();
} else if (rangeResponse != null) {
((RangeAwareResource) resource).serveRangeAsync(exchange.getOutputChannel(), exchange, start, end);
} else {
resource.serveAsync(exchange.getOutputChannel(), exchange);
}
}
};
if (exchange.isInIoThread()) {
exchange.dispatch(dispatchTask);
} else {
dispatchTask.handleRequest(exchange);
}
}
private void clearCacheHeaders(HttpServerExchange exchange) {
exchange.removeResponseHeader(HttpHeaderNames.CACHE_CONTROL);
exchange.removeResponseHeader(HttpHeaderNames.EXPIRES);
}
private Resource getIndexFiles(HttpServerExchange exchange, ResourceSupplier resourceManager, final String base, List possible) throws IOException {
String realBase;
if (base.endsWith("/")) {
realBase = base;
} else {
realBase = base + "/";
}
for (String possibility : possible) {
Resource index = resourceManager.getResource(exchange, canonicalize(realBase + possibility));
if (index != null) {
return index;
}
}
return null;
}
private String canonicalize(String s) {
if (canonicalizePaths) {
return CanonicalPathUtils.canonicalize(s);
}
return s;
}
public boolean isDirectoryListingEnabled() {
return directoryListingEnabled;
}
public ResourceHandler setDirectoryListingEnabled(final boolean directoryListingEnabled) {
this.directoryListingEnabled = directoryListingEnabled;
return this;
}
public ResourceHandler addWelcomeFiles(String... files) {
this.welcomeFiles.addAll(Arrays.asList(files));
return this;
}
public ResourceHandler setWelcomeFiles(String... files) {
this.welcomeFiles.clear();
this.welcomeFiles.addAll(Arrays.asList(files));
return this;
}
public MimeMappings getMimeMappings() {
return mimeMappings;
}
public ResourceHandler setMimeMappings(final MimeMappings mimeMappings) {
this.mimeMappings = mimeMappings;
return this;
}
public Predicate getAllowed() {
return allowed;
}
public ResourceHandler setAllowed(final Predicate allowed) {
this.allowed = allowed;
return this;
}
public ResourceSupplier getResourceSupplier() {
return resourceSupplier;
}
public ResourceHandler setResourceSupplier(final ResourceSupplier resourceSupplier) {
this.resourceSupplier = resourceSupplier;
this.resourceManager = null;
return this;
}
public ResourceManager getResourceManager() {
return resourceManager;
}
public ResourceHandler setResourceManager(final ResourceManager resourceManager) {
this.resourceManager = resourceManager;
this.resourceSupplier = new DefaultResourceSupplier(resourceManager);
return this;
}
public boolean isCanonicalizePaths() {
return canonicalizePaths;
}
/**
* If this handler should use canonicalized paths.
*
* WARNING: If this is not true and {@link io.undertow.server.handlers.CanonicalPathHandler} is not installed in
* the handler chain then is may be possible to perform a directory traversal attack. If you set this to false make
* sure you have some kind of check in place to control the path.
*
* @param canonicalizePaths If paths should be canonicalized
*/
public void setCanonicalizePaths(boolean canonicalizePaths) {
this.canonicalizePaths = canonicalizePaths;
}
public static class Builder implements HandlerBuilder {
@Override
public String name() {
return "resource";
}
@Override
public Map> parameters() {
Map> params = new HashMap<>();
params.put("location", String.class);
params.put("allow-listing", boolean.class);
return params;
}
@Override
public Set requiredParameters() {
return Collections.singleton("location");
}
@Override
public String defaultParameter() {
return "location";
}
@Override
public HandlerWrapper build(Map config) {
return new Wrapper((String) config.get("location"), (Boolean) config.get("allow-listing"));
}
}
private static class Wrapper implements HandlerWrapper {
private final String location;
private final boolean allowDirectoryListing;
private Wrapper(String location, boolean allowDirectoryListing) {
this.location = location;
this.allowDirectoryListing = allowDirectoryListing;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
ResourceManager rm = new PathResourceManager(Paths.get(location), 1024);
ResourceHandler resourceHandler = new ResourceHandler(rm);
resourceHandler.setDirectoryListingEnabled(allowDirectoryListing);
return resourceHandler;
}
}
}