/* * 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<String> 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<String> 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<String> 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. * <p> * 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<String, Class<?>> parameters() { Map<String, Class<?>> params = new HashMap<>(); params.put("location", String.class); params.put("allow-listing", boolean.class); return params; } @Override public Set<String> requiredParameters() { return Collections.singleton("location"); } @Override public String defaultParameter() { return "location"; } @Override public HandlerWrapper build(Map<String, Object> 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; } } }