Skip to content

Instantly share code, notes, and snippets.

@devld
Created April 14, 2023 09:33
Show Gist options
  • Save devld/ab7c0feb89b4292a138947911bd5e50b to your computer and use it in GitHub Desktop.
Save devld/ab7c0feb89b4292a138947911bd5e50b to your computer and use it in GitHub Desktop.
HTTP serve file with range and cache headers
package me.devld;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
public final class HttpServeFile {
public static class Request {
public final String method;
public final Headers headers;
public final File file;
public final String mimeType;
public Request(String method, Headers headers, File file, String mimeType) {
this.method = method.toUpperCase();
this.headers = headers;
this.file = file;
this.mimeType = mimeType;
}
public static Request from(HttpServletRequest request, File file, String mimeType) {
Headers headers = new Headers();
@SuppressWarnings("unchecked")
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
headers.set(key, request.getHeader(key));
}
return new Request(request.getMethod(), headers, file, mimeType);
}
}
public static class ServletResponse implements Response {
private final HttpServletResponse resp;
public ServletResponse(HttpServletResponse resp) {
this.resp = resp;
}
@Override
public void writeHeader(int status, Headers headers) {
for (String key : headers.keys()) {
resp.setHeader(key, headers.get(key));
}
resp.setStatus(status);
}
@Override
public OutputStream getOutput() throws IOException {
return resp.getOutputStream();
}
}
public interface Response {
void writeHeader(int status, Headers headers) throws IOException;
OutputStream getOutput() throws IOException;
}
private final int input;
private final String mimeSeparation;
public HttpServeFile() {
this(2048, "CATALINA_MIME_BOUNDARY");
}
public HttpServeFile(int input, String mimeSeparation) {
this.input = input;
this.mimeSeparation = mimeSeparation;
}
public void serve(Request request, Response response) throws IOException {
boolean serveContent = !"HEAD".equals(request.method);
File resource = request.file;
if (!resource.exists()) {
new ShortResponse(404).write(response);
return;
}
if (!resource.canRead()) {
new ShortResponse(403).write(response);
return;
}
// Check if the conditions specified in the optional If headers are
// satisfied.
if (resource.isFile()) {
// Checking If headers
ShortResponse r = checkIfHeaders(request, resource);
if (r != null) {
r.write(response);
return;
}
}
String contentType = request.mimeType;
if (contentType == null) {
contentType = "application/octet-stream";
}
// These need to reflect the original resource, not the potentially
// precompressed version of the resource so get them now if they are going to
// be needed later
String eTag = null;
String lastModifiedHttp = null;
if (resource.isFile()) {
eTag = generateETag(resource);
lastModifiedHttp = DateUtils.formatDate(new Date(resource.lastModified()));
}
if (resource.isDirectory()) {
new ShortResponse(405).write(response);
return;
}
Ranges ranges;
long contentLength;
int status = 200;
Headers respHeaders = new Headers();
respHeaders.set("Accept-Ranges", "bytes");
// Parse range specifier
try {
ranges = parseRange(request, resource);
} catch (ParseRangeException e) {
e.shortResponse.write(response);
return;
}
// ETag header
respHeaders.set("ETag", eTag);
// Last-Modified header
respHeaders.set("Last-Modified", lastModifiedHttp);
// Get content length
contentLength = resource.length();
// Special case for zero length files, which would cause a
// (silent) ISE when setting the output buffer size
if (contentLength == 0L) {
serveContent = false;
}
OutputStream ostream = null;
if (serveContent) ostream = response.getOutput();
if (ranges == Ranges.FULL) {
// Set the appropriate output headers
respHeaders.set("Content-Type", contentType);
if (resource.isFile() && contentLength >= 0 &&
(!serveContent || ostream != null)) {
respHeaders.set("Content-Length", String.valueOf(contentLength));
}
if (serveContent) {
InputStream renderResult = new FileInputStream(resource);
copy(renderResult, ostream);
}
} else {
if ((ranges == null) || (ranges.getEntries().isEmpty())) {
response.writeHeader(200, respHeaders);
return;
}
status = 206;
if (ranges.getEntries().size() == 1) {
Ranges.Entry range = ranges.getEntries().get(0);
long start = rangeGetStart(range, contentLength);
long end = rangeGetEnd(range, contentLength);
respHeaders.set("Content-Range",
"bytes " + start + "-" + end + "/" + contentLength);
long length = end - start + 1;
respHeaders.set("Content-Length", String.valueOf(length));
respHeaders.set("Content-Type", contentType);
if (serveContent) {
if (ostream != null) {
copy(resource, contentLength, ostream, range);
} else {
// we should not get here
throw new IllegalStateException();
}
}
} else {
respHeaders.set("Content-Type", "multipart / byteranges; boundary = " + mimeSeparation);
if (serveContent) {
if (ostream != null) {
copy(resource, contentLength, ostream, ranges, contentType);
} else {
// we should not get here
throw new IllegalStateException();
}
}
}
}
response.writeHeader(status, respHeaders);
}
private void copy(InputStream is, OutputStream ostream) throws IOException {
IOException exception;
InputStream istream = new BufferedInputStream(is, input);
// Copy the input stream to the output stream
exception = copyRange(istream, ostream);
// Clean up the input stream
istream.close();
// Rethrow any exception that has occurred
if (exception != null) {
throw exception;
}
}
private void copy(File resource, long length, OutputStream ostream, Ranges.Entry range)
throws IOException {
IOException exception;
InputStream resourceInputStream = new FileInputStream(resource);
InputStream istream =
new BufferedInputStream(resourceInputStream, input);
exception = copyRange(istream, ostream, rangeGetStart(range, length), rangeGetEnd(range, length));
// Clean up the input stream
istream.close();
// Rethrow any exception that has occurred
if (exception != null) {
throw exception;
}
}
private void copy(File resource, long length, OutputStream ostream, Ranges ranges, String contentType)
throws IOException {
IOException exception = null;
for (Ranges.Entry range : ranges.getEntries()) {
if (exception != null) {
break;
}
InputStream resourceInputStream = new FileInputStream(resource);
try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) {
// Writing MIME header.
ostream.write("\r\n".getBytes(StandardCharsets.UTF_8));
ostream.write(("--" + mimeSeparation + "\r\n").getBytes(StandardCharsets.UTF_8));
if (contentType != null) {
ostream.write(("Content-Type: " + contentType + "\r\n").getBytes(StandardCharsets.UTF_8));
}
long start = rangeGetStart(range, length);
long end = rangeGetEnd(range, length);
ostream.write(("Content-Range: bytes " + start
+ "-" + end + "/"
+ (end - start) + "\r\n").getBytes(StandardCharsets.UTF_8));
ostream.write("\r\n".getBytes(StandardCharsets.UTF_8));
// Printing content
exception = copyRange(istream, ostream, start, end);
}
}
ostream.write("\r\n".getBytes(StandardCharsets.UTF_8));
ostream.write(("--" + mimeSeparation + "--").getBytes(StandardCharsets.UTF_8));
// Rethrow any exception that has occurred
if (exception != null) {
throw exception;
}
}
private IOException copyRange(InputStream istream, OutputStream ostream) {
// Copy the input stream to the output stream
IOException exception = null;
byte[] buffer = new byte[input];
int len;
while (true) {
try {
len = istream.read(buffer);
if (len == -1) {
break;
}
ostream.write(buffer, 0, len);
} catch (IOException e) {
exception = e;
// len = -1;
break;
}
}
return exception;
}
private IOException copyRange(InputStream istream, OutputStream ostream, long start, long end) {
long skipped;
try {
skipped = istream.skip(start);
} catch (IOException e) {
return e;
}
if (skipped < start) {
return new IOException("skip failed: " + skipped + ", " + start);
}
IOException exception = null;
long bytesToRead = end - start + 1;
byte[] buffer = new byte[input];
int len = buffer.length;
while ((bytesToRead > 0) && (len >= buffer.length)) {
try {
len = istream.read(buffer);
if (bytesToRead >= len) {
ostream.write(buffer, 0, len);
bytesToRead -= len;
} else {
ostream.write(buffer, 0, (int) bytesToRead);
bytesToRead = 0;
}
} catch (IOException e) {
exception = e;
len = -1;
}
if (len < buffer.length) {
break;
}
}
return exception;
}
private Ranges parseRange(Request request, File resource) throws IOException, ParseRangeException {
// Range headers are only valid on GET requests. That implies they are
// also valid on HEAD requests. This method is only called by doGet()
// and doHead() so no need to check the request method.
// Checking If-Range
String headerValue = request.headers.get("If-Range");
if (headerValue != null) {
long headerValueTime = (-1L);
try {
headerValueTime = request.headers.getDate("If-Range");
} catch (IllegalArgumentException e) {
// Ignore
}
String eTag = generateETag(resource);
long lastModified = resource.lastModified();
if (headerValueTime == (-1L)) {
// If the ETag the client gave does not match the entity
// etag, then the entire entity is returned.
if (!eTag.equals(headerValue.trim())) {
return Ranges.FULL;
}
} else {
// If the timestamp of the entity the client got differs from
// the last modification date of the entity, the entire entity
// is returned.
if (Math.abs(lastModified - headerValueTime) > 1000) {
return Ranges.FULL;
}
}
}
long fileLength = resource.length();
if (fileLength == 0) {
// Range header makes no sense for a zero length resource. Tomcat
// therefore opts to ignore it.
return Ranges.FULL;
}
// Retrieving the range header (if any is specified
String rangeHeader = request.headers.get("Range");
if (rangeHeader == null) {
// No Range header is the same as ignoring any Range header
return Ranges.FULL;
}
Ranges ranges = Ranges.parse(new StringReader(rangeHeader));
if (ranges == null) {
// The Range header is present but not formatted correctly.
// Could argue for a 400 response but 416 is more specific.
// There is also the option to ignore the (invalid) Range header.
// RFC7233#4.4 notes that many servers do ignore the Range header in
// these circumstances but Tomcat has always returned a 416.
throw new ParseRangeException(
new ShortResponse(416, Collections.singletonMap("Content-Range", "bytes */" + fileLength))
);
}
// bytes is the only range unit supported (and I don't see the point
// of adding new ones).
if (!ranges.getUnits().equals("bytes")) {
// RFC7233#3.1 Servers must ignore range units they don't understand
return Ranges.FULL;
}
for (Ranges.Entry range : ranges.getEntries()) {
if (!validateRange(range, fileLength)) {
throw new ParseRangeException(
new ShortResponse(416, Collections.singletonMap("Content-Range", "bytes */" + fileLength))
);
}
}
return ranges;
}
private static boolean validateRange(Ranges.Entry range, long length) {
long start = rangeGetStart(range, length);
long end = rangeGetEnd(range, length);
return (start >= 0) && (end >= 0) && (start <= end);
}
private static long rangeGetStart(Ranges.Entry range, long length) {
long start = range.getStart();
if (start == -1) {
long end = range.getEnd();
// If there is no start, then the start is based on the end
if (end >= length) {
return 0;
} else {
return length - end;
}
} else {
return start;
}
}
private static long rangeGetEnd(Ranges.Entry range, long length) {
long end = range.getEnd();
if (range.getStart() == -1 || end == -1 || end >= length) {
return length - 1;
} else {
return end;
}
}
private ShortResponse checkIfHeaders(Request request, File resource) throws IOException {
ShortResponse r = checkIfMatch(request, resource);
if (r == null) r = checkIfModifiedSince(request, resource);
if (r == null) r = checkIfNoneMatch(request, resource);
if (r == null) r = checkIfUnmodifiedSince(request, resource);
return r;
}
private ShortResponse checkIfMatch(Request request, File resource) throws IOException {
String headerValue = request.headers.get("If-Match");
if (headerValue != null) {
boolean conditionSatisfied;
if (!headerValue.equals("*")) {
String resourceETag = generateETag(resource);
if (resourceETag == null) {
conditionSatisfied = false;
} else {
// RFC 7232 requires strong comparison for If-Match headers
Boolean matched = compareEntityTag(new StringReader(headerValue), false, resourceETag);
if (matched == null) {
return new ShortResponse(400);
}
conditionSatisfied = matched;
}
} else {
conditionSatisfied = true;
}
if (!conditionSatisfied) {
return new ShortResponse(412);
}
}
return null;
}
protected ShortResponse checkIfModifiedSince(Request request, File resource) {
try {
long headerValue = request.headers.getDate("If-Modified-Since");
long lastModified = resource.lastModified();
if (headerValue != -1) {
// If an If-None-Match header has been specified, if modified since
// is ignored.
if ((request.headers.get("If-None-Match") == null)
&& (lastModified < headerValue + 1000)) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
return new ShortResponse(304, Collections.singletonMap("ETag", generateETag(resource)));
}
}
} catch (IllegalArgumentException illegalArgument) {
return null;
}
return null;
}
protected ShortResponse checkIfNoneMatch(Request request, File resource) throws IOException {
String headerValue = request.headers.get("If-None-Match");
if (headerValue != null) {
boolean conditionSatisfied;
String resourceETag = generateETag(resource);
if (!headerValue.equals("*")) {
if (resourceETag == null) {
conditionSatisfied = false;
} else {
// RFC 7232 requires weak comparison for If-None-Match headers
Boolean matched = compareEntityTag(new StringReader(headerValue), true, resourceETag);
if (matched == null) {
return new ShortResponse(400);
}
conditionSatisfied = matched;
}
} else {
conditionSatisfied = true;
}
if (conditionSatisfied) {
// For GET and HEAD, we should respond with
// 304 Not Modified.
// For every other method, 412 Precondition Failed is sent
// back.
ShortResponse shortResponse;
if ("GET".equals(request.method) || "HEAD".equals(request.method)) {
shortResponse = new ShortResponse(304, Collections.singletonMap("ETag", resourceETag));
} else {
shortResponse = new ShortResponse(412);
}
return shortResponse;
}
}
return null;
}
protected ShortResponse checkIfUnmodifiedSince(Request request, File resource) {
try {
long lastModified = resource.lastModified();
long headerValue = request.headers.getDate("If-Unmodified-Since");
if (headerValue != -1) {
if (lastModified >= (headerValue + 1000)) {
return new ShortResponse(412);
}
}
} catch (IllegalArgumentException illegalArgument) {
return null;
}
return null;
}
private String generateETag(File resource) {
long mod = resource.lastModified();
long len = resource.length();
return String.format("\"%x-%x\"", mod, len);
}
public static Boolean compareEntityTag(StringReader input, boolean compareWeak, String resourceETag)
throws IOException {
// The resourceETag may be weak so to do weak comparison remove /W
// before comparison
String comparisonETag;
if (compareWeak && resourceETag.startsWith("W/")) {
comparisonETag = resourceETag.substring(2);
} else {
comparisonETag = resourceETag;
}
Boolean result = Boolean.FALSE;
while (true) {
boolean strong = false;
HttpParser.skipLws(input);
switch (HttpParser.skipConstant(input, "W/")) {
case EOF:
// Empty values are invalid
return null;
case NOT_FOUND:
strong = true;
break;
case FOUND:
strong = false;
break;
}
// Note: RFC 2616 allowed quoted string
// RFC 7232 does not allow " in the entity-tag
String value = HttpParser.readQuotedString(input);
if (value == null) {
// Not a quoted string so the header is invalid
return null;
}
if (strong || compareWeak) {
if (comparisonETag.equals(value)) {
result = Boolean.TRUE;
}
}
HttpParser.skipLws(input);
switch (HttpParser.skipConstant(input, ",")) {
case EOF:
return result;
case NOT_FOUND:
// Not EOF and not "," so must be invalid
return null;
case FOUND:
// Parse next entry
break;
}
}
}
private static class ShortResponse {
public final int status;
public final Headers headers;
ShortResponse(int status, Map<String, String> headers) {
this.status = status;
this.headers = new Headers(headers);
}
ShortResponse(int status) {
this(status, Collections.emptyMap());
}
void write(Response response) throws IOException {
response.writeHeader(status, headers);
}
}
static class ParseRangeException extends Exception {
private final ShortResponse shortResponse;
ParseRangeException(ShortResponse shortResponse) {
this.shortResponse = shortResponse;
}
}
public static class Headers {
private final Map<String, String> map = new HashMap<>();
public Headers() {
}
public Headers(Map<String, String> headers) {
super();
for (String key : headers.keySet()) {
this.set(key, headers.get(key));
}
}
public String get(String key) {
return this.map.get(key.toLowerCase());
}
public long getDate(String key) {
String val = get(key);
try {
return DateUtils.parseDate(val).getTime();
} catch (IllegalArgumentException e) {
return -1;
}
}
public void set(String key, String value) {
this.map.put(key.toLowerCase(), value);
}
public Set<String> keys() {
return this.map.keySet();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (String key : this.map.keySet()) {
sb.append(key).append(": ").append(this.map.get(key)).append("\n");
}
return sb.toString().trim();
}
}
}
class DateUtils {
/**
* Date format pattern used to parse HTTP date headers in RFC 1123 format.
*/
public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
/**
* Date format pattern used to parse HTTP date headers in RFC 1036 format.
*/
public static final String PATTERN_RFC1036 = "EEEE, dd-MMM-yy HH:mm:ss zzz";
/**
* Date format pattern used to parse HTTP date headers in ANSI C
* <code>asctime()</code> format.
*/
public static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
private static final String[] DEFAULT_PATTERNS = new String[]{
PATTERN_RFC1036,
PATTERN_RFC1123,
PATTERN_ASCTIME
};
private static final Date DEFAULT_TWO_DIGIT_YEAR_START;
static {
Calendar calendar = Calendar.getInstance();
calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
calendar.set(Calendar.MILLISECOND, 0);
DEFAULT_TWO_DIGIT_YEAR_START = calendar.getTime();
}
public static Date parseDate(String dateValue) {
return parseDate(dateValue, null, null);
}
public static Date parseDate(String dateValue, String[] dateFormats, Date startDate) {
if (dateValue == null) {
throw new IllegalArgumentException("dateValue is null");
}
if (dateFormats == null) {
dateFormats = DEFAULT_PATTERNS;
}
if (startDate == null) {
startDate = DEFAULT_TWO_DIGIT_YEAR_START;
}
// trim single quotes around date if present
// see issue #5279
if (dateValue.length() > 1
&& dateValue.startsWith("'")
&& dateValue.endsWith("'")
) {
dateValue = dateValue.substring(1, dateValue.length() - 1);
}
for (String dateFormat : dateFormats) {
SimpleDateFormat dateParser = DateFormatHolder.formatFor(dateFormat);
dateParser.set2DigitYearStart(startDate);
try {
return dateParser.parse(dateValue);
} catch (ParseException pe) {
// ignore this exception, we will try the next format
}
}
// we were unable to parse the date
throw new IllegalArgumentException("Unable to parse the date " + dateValue);
}
public static String formatDate(Date date) {
return formatDate(date, PATTERN_RFC1123);
}
public static String formatDate(Date date, String pattern) {
if (date == null) throw new IllegalArgumentException("date is null");
if (pattern == null) throw new IllegalArgumentException("pattern is null");
SimpleDateFormat formatter = DateFormatHolder.formatFor(pattern);
return formatter.format(date);
}
private final static class DateFormatHolder {
private static final ThreadLocal<SoftReference<Map<String, SimpleDateFormat>>>
THREADLOCAL_FORMATS = ThreadLocal.withInitial(() -> new SoftReference<>(
new HashMap<>()));
public static SimpleDateFormat formatFor(String pattern) {
SoftReference<Map<String, SimpleDateFormat>> ref = THREADLOCAL_FORMATS.get();
Map<String, SimpleDateFormat> formats = ref.get();
if (formats == null) {
formats = new HashMap<>();
THREADLOCAL_FORMATS.set(new SoftReference<>(formats));
}
SimpleDateFormat format = formats.get(pattern);
if (format == null) {
format = new SimpleDateFormat(pattern, Locale.US);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
formats.put(pattern, format);
}
return format;
}
}
}
class HttpParser {
public enum SkipResult {FOUND, NOT_FOUND, EOF}
private static final int ARRAY_SIZE = 128;
private static final boolean[] IS_CONTROL = new boolean[ARRAY_SIZE];
private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE];
private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];
private static final boolean[] IS_NUMERIC = new boolean[ARRAY_SIZE];
static {
for (int i = 0; i < ARRAY_SIZE; i++) {
// Control> 0-31, 127
if (i < 32 || i == 127) {
IS_CONTROL[i] = true;
}
// Separator
if (i == '(' || i == ')' || i == '<' || i == '>' || i == '@' ||
i == ',' || i == ';' || i == ':' || i == '\\' || i == '\"' ||
i == '/' || i == '[' || i == ']' || i == '?' || i == '=' ||
i == '{' || i == '}' || i == ' ' || i == '\t') {
IS_SEPARATOR[i] = true;
}
// Token: Anything 0-127 that is not a control and not a separator
if (!IS_CONTROL[i] && !IS_SEPARATOR[i]) {
IS_TOKEN[i] = true;
}
if (i >= '0' && i <= '9') {
IS_NUMERIC[i] = true;
}
}
}
static void skipLws(Reader input) throws IOException {
input.mark(1);
int c = input.read();
while (c == 32 || c == 9 || c == 10 || c == 13) {
input.mark(1);
c = input.read();
}
input.reset();
}
static SkipResult skipConstant(Reader input, String constant) throws IOException {
int len = constant.length();
skipLws(input);
input.mark(len);
int c = input.read();
for (int i = 0; i < len; i++) {
if (i == 0 && c == -1) {
return SkipResult.EOF;
}
if (c != constant.charAt(i)) {
input.reset();
return SkipResult.NOT_FOUND;
}
if (i != (len - 1)) {
c = input.read();
}
}
return SkipResult.FOUND;
}
static String readQuotedString(Reader input) throws IOException {
skipLws(input);
int c = input.read();
if (c != '"') {
return null;
}
StringBuilder result = new StringBuilder();
result.append('\"');
c = input.read();
while (c != '"') {
if (c == -1) {
return null;
} else if (c == '\\') {
c = input.read();
result.append('\\');
result.append((char) c);
} else {
result.append((char) c);
}
c = input.read();
}
result.append('\"');
return result.toString();
}
static boolean isToken(int c) {
// Fast for correct values, slower for incorrect ones
try {
return IS_TOKEN[c];
} catch (ArrayIndexOutOfBoundsException ex) {
return false;
}
}
static boolean isNumeric(int c) {
// Fast for valid numeric characters, slower for some incorrect
// ones
try {
return IS_NUMERIC[c];
} catch (ArrayIndexOutOfBoundsException ex) {
return false;
}
}
static String readToken(Reader input) throws IOException {
StringBuilder result = new StringBuilder();
skipLws(input);
input.mark(1);
int c = input.read();
while (c != -1 && isToken(c)) {
result.append((char) c);
input.mark(1);
c = input.read();
}
// Use mark(1)/reset() rather than skip(-1) since skip() is a NOP
// once the end of the String has been reached.
input.reset();
if (c != -1 && result.length() == 0) {
return null;
} else {
return result.toString();
}
}
static String readDigits(Reader input) throws IOException {
StringBuilder result = new StringBuilder();
skipLws(input);
input.mark(1);
int c = input.read();
while (c != -1 && isNumeric(c)) {
result.append((char) c);
input.mark(1);
c = input.read();
}
// Use mark(1)/reset() rather than skip(-1) since skip() is a NOP
// once the end of the String has been reached.
input.reset();
return result.toString();
}
public static long readLong(Reader input) throws IOException {
String digits = readDigits(input);
if (digits.length() == 0) {
return -1;
}
return Long.parseLong(digits);
}
}
class Ranges {
public static final Ranges FULL = new Ranges(null, new ArrayList<>());
private final String units;
private final List<Entry> entries;
public Ranges(String units, List<Entry> entries) {
// Units are lower case (RFC 9110, section 14.1)
if (units == null) {
this.units = null;
} else {
this.units = units.toLowerCase(Locale.ENGLISH);
}
this.entries = Collections.unmodifiableList(entries);
}
public List<Entry> getEntries() {
return entries;
}
public String getUnits() {
return units;
}
public static class Entry {
private final long start;
private final long end;
public Entry(long start, long end) {
this.start = start;
this.end = end;
}
public long getStart() {
return start;
}
public long getEnd() {
return end;
}
}
/**
* Parses a Range header from an HTTP header.
*
* @param input a reader over the header text
* @return a set of ranges parsed from the input, or null if not valid
* @throws IOException if there was a problem reading the input
*/
public static Ranges parse(StringReader input) throws IOException {
// Units (required)
String units = HttpParser.readToken(input);
if (units == null || units.length() == 0) {
return null;
}
// Must be followed by '='
if (HttpParser.skipConstant(input, "=") != HttpParser.SkipResult.FOUND) {
return null;
}
// Range entries
List<Entry> entries = new ArrayList<>();
HttpParser.SkipResult skipResult;
do {
long start = HttpParser.readLong(input);
// Must be followed by '-'
if (HttpParser.skipConstant(input, "-") != HttpParser.SkipResult.FOUND) {
return null;
}
long end = HttpParser.readLong(input);
if (start == -1 && end == -1) {
// Invalid range
return null;
}
entries.add(new Entry(start, end));
skipResult = HttpParser.skipConstant(input, ",");
if (skipResult == HttpParser.SkipResult.NOT_FOUND) {
// Invalid range
return null;
}
} while (skipResult == HttpParser.SkipResult.FOUND);
return new Ranges(units, entries);
}
}
package me.devld;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
public class Usage extends HttpServlet {
private final HttpServeFile serveFile;
public Usage() {
this.serveFile = new HttpServeFile();
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
File file = new File("root", req.getRequestURI());
serveFile.serve(
HttpServeFile.Request.from(req, file, "application/octet-stream"),
new HttpServeFile.ServletResponse(resp)
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment