/*
* 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 java.util.concurrent.
TimeUnit;
import io.undertow.
UndertowLogger;
import io.undertow.io.
IoCallback;
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.server.handlers.cache.
ResponseCache;
import io.undertow.server.handlers.encoding.
ContentEncodedResource;
import io.undertow.server.handlers.encoding.
ContentEncodedResourceManager;
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.util.
Headers;
import io.undertow.util.
HttpString;
import io.undertow.util.
Methods;
import io.undertow.util.
MimeMappings;
import io.undertow.util.
RedirectBuilder;
import io.undertow.util.
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<
HttpString>
KNOWN_METHODS = new
HashSet<>();
static {
KNOWN_METHODS.
add(
Methods.
OPTIONS);
KNOWN_METHODS.
add(
Methods.
GET);
KNOWN_METHODS.
add(
Methods.
HEAD);
KNOWN_METHODS.
add(
Methods.
POST);
KNOWN_METHODS.
add(
Methods.
PUT);
KNOWN_METHODS.
add(
Methods.
DELETE);
KNOWN_METHODS.
add(
Methods.
TRACE);
KNOWN_METHODS.
add(
Methods.
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 cachable =
Predicates.
truePredicate();
private volatile
Predicate allowed =
Predicates.
truePredicate();
private volatile
ResourceSupplier resourceSupplier;
private volatile
ResourceManager resourceManager;
/**
* If this is set this will be the maximum time (in seconds) the client will cache the resource.
* <p/>
* Note: Do not set this for private resources, as it will cause a Cache-Control: public
* to be sent.
* <p/>
* TODO: make this more flexible
* <p/>
* This will only be used if the {@link #cachable} predicate returns true
*/
private volatile
Integer cacheTime;
private volatile
ContentEncodedResourceManager contentEncodedResourceManager;
/**
* 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(
Methods.
GET) ||
exchange.
getRequestMethod().
equals(
Methods.
POST)) {
serveResource(
exchange, true);
} else if (
exchange.
getRequestMethod().
equals(
Methods.
HEAD)) {
serveResource(
exchange, false);
} else {
if (
KNOWN_METHODS.
contains(
exchange.
getRequestMethod())) {
exchange.
setStatusCode(
StatusCodes.
METHOD_NOT_ALLOWED);
exchange.
getResponseHeaders().
add(
Headers.
ALLOW,
String.
join(", ",
Methods.
GET_STRING,
Methods.
HEAD_STRING,
Methods.
POST_STRING));
} 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;
}
ResponseCache cache =
exchange.
getAttachment(
ResponseCache.
ATTACHMENT_KEY);
final boolean
cachable = this.
cachable.
resolve(
exchange);
//we set caching headers before we try and serve from the cache
if (
cachable &&
cacheTime != null) {
exchange.
getResponseHeaders().
put(
Headers.
CACHE_CONTROL, "public, max-age=" +
cacheTime);
long
date =
System.
currentTimeMillis() +
TimeUnit.
SECONDS.
toMillis(
cacheTime);
String dateHeader =
DateUtils.
toDateString(new
Date(
date));
exchange.
getResponseHeaders().
put(
Headers.
EXPIRES,
dateHeader);
}
if (
cache != null &&
cachable) {
if (
cache.
tryServeResponse()) {
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.
getResponseHeaders().
put(
Headers.
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;
}
final
ContentEncodedResourceManager contentEncodedResourceManager =
ResourceHandler.this.
contentEncodedResourceManager;
Long contentLength =
resource.
getContentLength();
if (
contentLength != null && !
exchange.
getResponseHeaders().
contains(
Headers.
TRANSFER_ENCODING)) {
exchange.
setResponseContentLength(
contentLength);
}
ByteRange.
RangeResponseResult rangeResponse = null;
long
start = -1,
end = -1;
if(
resource instanceof
RangeAwareResource && ((
RangeAwareResource)
resource).
isRangeSupported() &&
contentLength != null &&
contentEncodedResourceManager == null) {
exchange.
getResponseHeaders().
put(
Headers.
ACCEPT_RANGES, "bytes");
//TODO: figure out what to do with the content encoded resource manager
ByteRange range =
ByteRange.
parse(
exchange.
getRequestHeaders().
getFirst(
Headers.
RANGE));
if(
range != null &&
range.
getRanges() == 1 &&
resource.
getContentLength() != null) {
rangeResponse =
range.
getResponseResult(
resource.
getContentLength(),
exchange.
getRequestHeaders().
getFirst(
Headers.
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.
getResponseHeaders().
put(
Headers.
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.
getResponseHeaders().
contains(
Headers.
CONTENT_TYPE)) {
final
String contentType =
resource.
getContentType(
mimeMappings);
if (
contentType != null) {
exchange.
getResponseHeaders().
put(
Headers.
CONTENT_TYPE,
contentType);
} else {
exchange.
getResponseHeaders().
put(
Headers.
CONTENT_TYPE, "application/octet-stream");
}
}
if (
lastModified != null) {
exchange.
getResponseHeaders().
put(
Headers.
LAST_MODIFIED,
resource.
getLastModifiedString());
}
if (
etag != null) {
exchange.
getResponseHeaders().
put(
Headers.
ETAG,
etag.
toString());
}
if (
contentEncodedResourceManager != null) {
try {
ContentEncodedResource encoded =
contentEncodedResourceManager.
getResource(
resource,
exchange);
if (
encoded != null) {
exchange.
getResponseHeaders().
put(
Headers.
CONTENT_ENCODING,
encoded.
getContentEncoding());
exchange.
getResponseHeaders().
put(
Headers.
CONTENT_LENGTH,
encoded.
getResource().
getContentLength());
encoded.
getResource().
serve(
exchange.
getResponseSender(),
exchange,
IoCallback.
END_EXCHANGE);
return;
}
} catch (
IOException e) {
//TODO: should this be fatal
UndertowLogger.
REQUEST_IO_LOGGER.
ioException(
e);
exchange.
setStatusCode(
StatusCodes.
INTERNAL_SERVER_ERROR);
exchange.
endExchange();
return;
}
}
if (!
sendContent) {
exchange.
endExchange();
} else if(
rangeResponse != null) {
((
RangeAwareResource)
resource).
serveRange(
exchange.
getResponseSender(),
exchange,
start,
end,
IoCallback.
END_EXCHANGE);
} else {
resource.
serve(
exchange.
getResponseSender(),
exchange,
IoCallback.
END_EXCHANGE);
}
}
};
if(
exchange.
isInIoThread()) {
exchange.
dispatch(
dispatchTask);
} else {
dispatchTask.
handleRequest(
exchange);
}
}
private void
clearCacheHeaders(
HttpServerExchange exchange) {
exchange.
getResponseHeaders().
remove(
Headers.
CACHE_CONTROL);
exchange.
getResponseHeaders().
remove(
Headers.
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 getCachable() {
return
cachable;
}
public
ResourceHandler setCachable(final
Predicate cachable) {
this.
cachable =
cachable;
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
Integer getCacheTime() {
return
cacheTime;
}
public
ResourceHandler setCacheTime(final
Integer cacheTime) {
this.
cacheTime =
cacheTime;
return this;
}
public
ContentEncodedResourceManager getContentEncodedResourceManager() {
return
contentEncodedResourceManager;
}
public
ResourceHandler setContentEncodedResourceManager(
ContentEncodedResourceManager contentEncodedResourceManager) {
this.
contentEncodedResourceManager =
contentEncodedResourceManager;
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<
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;
}
}
}