|
|
|
@ -46,13 +46,13 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
|
|
|
|
|
protected abstract Resource createResource(HttpRouterContext httpRouterContext) throws IOException;
|
|
|
|
|
|
|
|
|
|
protected abstract boolean isETagResponseEnabled();
|
|
|
|
|
protected abstract boolean isETagEnabled();
|
|
|
|
|
|
|
|
|
|
protected abstract boolean isCacheResponseEnabled();
|
|
|
|
|
protected abstract boolean isRangeEnabled();
|
|
|
|
|
|
|
|
|
|
protected abstract boolean isRangeResponseEnabled();
|
|
|
|
|
protected abstract int getCacheMaxAgeSeconds();
|
|
|
|
|
|
|
|
|
|
protected abstract int getMaxAgeSeconds();
|
|
|
|
|
protected abstract String getCacheControl();
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void handle(HttpRouterContext context) throws IOException {
|
|
|
|
@ -75,7 +75,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
.build()
|
|
|
|
|
.toString();
|
|
|
|
|
logger.log(Level.FINEST, "client must add a /, external redirect to = " + loc);
|
|
|
|
|
context.header(HttpHeaderNames.LOCATION, loc)
|
|
|
|
|
context.setHeader(HttpHeaderNames.LOCATION, loc)
|
|
|
|
|
.status(HttpResponseStatus.TEMPORARY_REDIRECT);
|
|
|
|
|
} else if (resource.isExistsIndexFile()) {
|
|
|
|
|
// internal redirect to default index file in this directory
|
|
|
|
@ -102,7 +102,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
}
|
|
|
|
|
HttpHeaders headers = context.getRequestBuilder().getHeaders();
|
|
|
|
|
String contentType = resource.getMimeType();
|
|
|
|
|
context.header(CONTENT_TYPE, contentType);
|
|
|
|
|
context.setHeader(CONTENT_TYPE, contentType);
|
|
|
|
|
// heuristic for inline disposition
|
|
|
|
|
String disposition;
|
|
|
|
|
if (!contentType.startsWith("text") && !contentType.startsWith("image") && !contentType.startsWith("font")) {
|
|
|
|
@ -114,22 +114,26 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
if (resource.getBaseName() != null && resource.getSuffix() != null) {
|
|
|
|
|
String contentDisposition = disposition + ";filename=\"" + resource.getBaseName() + '.' + resource.getSuffix() + '"';
|
|
|
|
|
logger.log(Level.FINEST, () -> "content type = " + contentType + " content disposition = " + contentDisposition);
|
|
|
|
|
context.header(HttpHeaderNames.CONTENT_DISPOSITION, contentDisposition);
|
|
|
|
|
context.setHeader(HttpHeaderNames.CONTENT_DISPOSITION, contentDisposition);
|
|
|
|
|
}
|
|
|
|
|
long expirationMillis = System.currentTimeMillis() + 1000L * getMaxAgeSeconds();
|
|
|
|
|
String expires = DateTimeUtil.formatRfc1123(expirationMillis);
|
|
|
|
|
if (isCacheResponseEnabled()) {
|
|
|
|
|
String cacheControl = "public, max-age=" + getMaxAgeSeconds();
|
|
|
|
|
logger.log(Level.FINEST, () -> "cache response, expires = " + expires + " cache control = " + cacheControl);
|
|
|
|
|
context.header(HttpHeaderNames.EXPIRES, expires)
|
|
|
|
|
.header(HttpHeaderNames.CACHE_CONTROL, cacheControl);
|
|
|
|
|
if (getCacheMaxAgeSeconds() > 0) {
|
|
|
|
|
long expirationMillis = System.currentTimeMillis() + 1000L * getCacheMaxAgeSeconds();
|
|
|
|
|
String expires = DateTimeUtil.formatRfc1123(expirationMillis);
|
|
|
|
|
String cacheControl = "public, max-age=" + getCacheMaxAgeSeconds();
|
|
|
|
|
logger.log(Level.FINEST, () -> "expires = " + expires + " cache control = " + cacheControl);
|
|
|
|
|
context.setHeader(HttpHeaderNames.EXPIRES, expires)
|
|
|
|
|
.setHeader(HttpHeaderNames.CACHE_CONTROL, cacheControl);
|
|
|
|
|
} else {
|
|
|
|
|
logger.log(Level.FINEST, () -> "uncached response");
|
|
|
|
|
context.header(HttpHeaderNames.EXPIRES, "0")
|
|
|
|
|
.header(HttpHeaderNames.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
|
|
|
|
|
if (getCacheControl() == null) {
|
|
|
|
|
context.setHeader(HttpHeaderNames.EXPIRES, "0")
|
|
|
|
|
.setHeader(HttpHeaderNames.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
|
|
|
|
|
} else {
|
|
|
|
|
context.setHeader(HttpHeaderNames.EXPIRES, "0")
|
|
|
|
|
.setHeader(HttpHeaderNames.CACHE_CONTROL, getCacheControl());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
boolean sent = false;
|
|
|
|
|
if (isETagResponseEnabled()) {
|
|
|
|
|
if (isETagEnabled()) {
|
|
|
|
|
Instant lastModifiedInstant = resource.getLastModified();
|
|
|
|
|
String eTag = Long.toHexString(resource.getResourcePath().hashCode() + lastModifiedInstant.toEpochMilli() + resource.getLength());
|
|
|
|
|
logger.log(Level.FINEST, () -> "eTag = " + eTag);
|
|
|
|
@ -149,7 +153,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH);
|
|
|
|
|
if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
|
|
|
|
|
logger.log(Level.FINEST, () -> "not modified, eTag = " + eTag);
|
|
|
|
|
context.header(HttpHeaderNames.ETAG, eTag)
|
|
|
|
|
context.setHeader(HttpHeaderNames.ETAG, eTag)
|
|
|
|
|
.status(HttpResponseStatus.NOT_MODIFIED);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@ -157,15 +161,15 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
if (ifModifiedSinceInstant != null &&
|
|
|
|
|
ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
|
|
|
|
|
logger.log(Level.FINEST, () -> "not modified (after if-modified-since), eTag = " + eTag);
|
|
|
|
|
context.header(HttpHeaderNames.ETAG, eTag)
|
|
|
|
|
context.setHeader(HttpHeaderNames.ETAG, eTag)
|
|
|
|
|
.status(HttpResponseStatus.NOT_MODIFIED);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
String lastModified = DateTimeUtil.formatRfc1123(lastModifiedInstant);
|
|
|
|
|
logger.log(Level.FINEST, () -> "sending resource, lastModified = " + lastModified);
|
|
|
|
|
context.header(HttpHeaderNames.ETAG, eTag)
|
|
|
|
|
.header(HttpHeaderNames.LAST_MODIFIED, lastModified);
|
|
|
|
|
if (isRangeResponseEnabled()) {
|
|
|
|
|
context.setHeader(HttpHeaderNames.ETAG, eTag)
|
|
|
|
|
.setHeader(HttpHeaderNames.LAST_MODIFIED, lastModified);
|
|
|
|
|
if (isRangeEnabled()) {
|
|
|
|
|
performRangeResponse(context, resource, contentType, eTag, headers);
|
|
|
|
|
sent = true;
|
|
|
|
|
} else {
|
|
|
|
@ -176,7 +180,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
long length = resource.getLength();
|
|
|
|
|
if (length > 0L) {
|
|
|
|
|
String string = Long.toString(resource.getLength());
|
|
|
|
|
context.header(HttpHeaderNames.CONTENT_LENGTH, string);
|
|
|
|
|
context.setHeader(HttpHeaderNames.CONTENT_LENGTH, string);
|
|
|
|
|
logger.log(Level.FINEST, "length is known = " + resource.getLength());
|
|
|
|
|
send(resource, HttpResponseStatus.OK, contentType, context, 0L, resource.getLength());
|
|
|
|
|
} else {
|
|
|
|
@ -194,13 +198,13 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
HttpHeaders headers) throws IOException {
|
|
|
|
|
long length = resource.getLength();
|
|
|
|
|
logger.log(Level.FINEST, "performing range response on resource = " + resource);
|
|
|
|
|
context.header(HttpHeaderNames.ACCEPT_RANGES, "bytes");
|
|
|
|
|
context.setHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes");
|
|
|
|
|
Range full = new Range(0, length - 1, length);
|
|
|
|
|
List<Range> ranges = new ArrayList<>();
|
|
|
|
|
String range = headers.get(HttpHeaderNames.RANGE);
|
|
|
|
|
if (range != null) {
|
|
|
|
|
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
|
|
|
|
|
context.header(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length)
|
|
|
|
|
context.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length)
|
|
|
|
|
.status(HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@ -226,7 +230,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
end = length - 1;
|
|
|
|
|
}
|
|
|
|
|
if (start > end) {
|
|
|
|
|
context.header(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length)
|
|
|
|
|
context.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length)
|
|
|
|
|
.status(HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@ -235,16 +239,16 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (ranges.isEmpty() || ranges.getFirst() == full) {
|
|
|
|
|
context.header(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total)
|
|
|
|
|
.header(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length));
|
|
|
|
|
context.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total)
|
|
|
|
|
.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length));
|
|
|
|
|
send(resource, HttpResponseStatus.OK, contentType, context, full.start, full.length);
|
|
|
|
|
} else if (ranges.size() == 1) {
|
|
|
|
|
Range r = ranges.getFirst();
|
|
|
|
|
context.header(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total)
|
|
|
|
|
.header(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length));
|
|
|
|
|
context.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total)
|
|
|
|
|
.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length));
|
|
|
|
|
send(resource, HttpResponseStatus.PARTIAL_CONTENT, contentType, context, r.start, r.length);
|
|
|
|
|
} else {
|
|
|
|
|
context.header(CONTENT_TYPE, "multipart/byteranges; boundary=MULTIPART_BOUNDARY");
|
|
|
|
|
context.setHeader(CONTENT_TYPE, "multipart/byteranges; boundary=MULTIPART_BOUNDARY");
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
for (Range r : ranges) {
|
|
|
|
|
try {
|
|
|
|
@ -262,7 +266,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
context.status(HttpResponseStatus.OK)
|
|
|
|
|
.header(CONTENT_TYPE, contentType)
|
|
|
|
|
.setHeader(CONTENT_TYPE, contentType)
|
|
|
|
|
.body(CharBuffer.wrap(sb), StandardCharsets.ISO_8859_1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -296,7 +300,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
} else if (context.getRequestBuilder().getMethod() == HttpMethod.HEAD) {
|
|
|
|
|
logger.log(Level.FINEST, "HEAD request, do not send body");
|
|
|
|
|
context.status(HttpResponseStatus.OK)
|
|
|
|
|
.header(CONTENT_TYPE, contentType);
|
|
|
|
|
.setHeader(CONTENT_TYPE, contentType);
|
|
|
|
|
} else {
|
|
|
|
|
if ("file".equals(url.getScheme())) {
|
|
|
|
|
Path path = resource.getPath();
|
|
|
|
@ -338,7 +342,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
try (ReadableByteChannel channel = fileChannel) {
|
|
|
|
|
DataBuffer dataBuffer = DataBufferUtil.readBuffer(context.getDataBufferFactory(), channel, size);
|
|
|
|
|
context.status(httpResponseStatus)
|
|
|
|
|
.header(CONTENT_TYPE, contentType)
|
|
|
|
|
.setHeader(CONTENT_TYPE, contentType)
|
|
|
|
|
.body(dataBuffer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -358,7 +362,7 @@ public abstract class AbstractResourceHandler implements HttpHandler {
|
|
|
|
|
try (ReadableByteChannel channel = Channels.newChannel(inputStream)) {
|
|
|
|
|
DataBuffer dataBuffer = DataBufferUtil.readBuffer(context.getDataBufferFactory(), channel, size);
|
|
|
|
|
context.status(httpResponseStatus)
|
|
|
|
|
.header(CONTENT_TYPE, contentType)
|
|
|
|
|
.setHeader(CONTENT_TYPE, contentType)
|
|
|
|
|
.body(dataBuffer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|