-
Notifications
You must be signed in to change notification settings - Fork 88
Add partial support for the 'Range' header when serving static files #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -563,6 +563,70 @@ boost::shared_ptr<HttpResponse> RWebApplication::staticFileResponse( | |
| } | ||
| } | ||
|
|
||
| // Check the HTTP Range header, if it has been specified. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this stuff is sufficiently complex that it makes sense to pull it out into a separate function.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit awkward to do so, since there are currently three variables set in this section.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be simpler to review and maintain the code if the range header parsing was a separate function. Right now the string parsing is mixed in with the logic for setting range and generating error responses; if the string parsing were done in a separate step, that would make the code more manageable. Even if the code is correct now, I would feel uneasy about touching it in the future. Something like this would be easier to review and maintain: std::pair<int64_t, int64_t> parseRangeHeader(std::string rangeHeader) {
if (rangeHeader.find("bytes=") != 0) {
return std::make_pair<int64_t, int64_t>(-1, -1);
}
rangeHeader = rangeHeader.substr(6);
int sep_offset = rangeHeader.find("-");
if (sep_offset == std::string::npos) {
return std::make_pair<int64_t, int64_t>(-1, -1);
}
std::string start_str = rangeHeader.substr(0, sep_offset+1);
std::string end_str = rangeHeader.substr(sep_offset+1);
int64_t start;
int64_t end;
// TODO: Do number parsing here; return (-1, -1) if either start or end are not
// well-formed.
return std::make_pair<int64_t, int64_t>(start, end);
}The number parsing could be done with |
||
| // | ||
| // We only support a limited form of the HTTP Range header (something like | ||
| // 'bytes=(\\d+)-(\\d+)?'), so some client requests can't be fulfilled and we | ||
| // need to fall back on a 200 OK instead. | ||
| bool hasRange = false; | ||
| uint64_t start = 0; | ||
| uint64_t end = pDataSource->fileSize() - 1; | ||
| if (pRequest->hasHeader("Range")) { | ||
atheriel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| std::string rangeHeader = pRequest->getHeader("Range"); | ||
| size_t cursor = rangeHeader.find("bytes="); | ||
| size_t sep = rangeHeader.find("-", cursor + 6); | ||
| if (cursor != std::string::npos && sep != std::string::npos && sep != cursor + 6) { | ||
| // At this point we can be sure we have something like 'bytes=([^-]+)-.*' | ||
| // and we can try parsing the range itself. | ||
| char *ptr = ((char *) rangeHeader.c_str()) + cursor + 6; // Skip over 'bytes='. | ||
| char *endptr = ptr; | ||
| // strtoull() will return 0 when it can't find a number or ULLONG_MAX on | ||
| // overflow. Since we can't have a file larger than ULLONG_MAX anyway, | ||
| // this is OK because we'll just return HTTP 416 below. | ||
| start = strtoull(ptr, &endptr, 10); | ||
| if (start == 0 && (endptr == ptr || *endptr != '-')) { | ||
| // Either there was no number at all or there was garbage *after* the | ||
| // number but before the '-' separator. This is invalid range syntax | ||
| // from the client. | ||
| return error_response(pRequest, 400); | ||
| } | ||
| if (start >= pDataSource->fileSize()) { | ||
| boost::shared_ptr<HttpResponse> pResponse = error_response(pRequest, 416); | ||
| pResponse->headers().push_back( | ||
| std::make_pair("Content-Range", "bytes */" + toString(pDataSource->fileSize())) | ||
| ); | ||
| return pResponse; | ||
| } | ||
| ptr = endptr + 1; // Skip the '-'. | ||
| if (*ptr != '\0') { | ||
| end = strtoull(ptr, &endptr, 10); | ||
| if (*endptr != '\0' && *endptr != ',') { | ||
| // We hit a non-digit, non-multirange character at some point. | ||
| return error_response(pRequest, 400); | ||
| } | ||
| if (end < start) { | ||
| return error_response(pRequest, 400); | ||
| } | ||
| ptr = endptr; | ||
| } | ||
| // A comma indicates we're parsing a multipart range, which we don't | ||
| // support. So we need to fallback on 200 behaviour instead when we detect | ||
| // one. | ||
| if (*ptr != ',') { | ||
| if (end > pDataSource->fileSize() - 1) { | ||
| // The client might not know the size, so the range end is supposed to | ||
| // be redefined to be the last byte in this case instead of issuing an | ||
| // error. This also catches overflow in strtoull() above, which would | ||
| // be OK. | ||
| // | ||
| // See: https://tools.ietf.org/html/rfc7233#section-2.1 | ||
| end = pDataSource->fileSize() - 1; | ||
| } | ||
| hasRange = pDataSource->setRange(start, end); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ================================== | ||
| // Create the HTTP response | ||
| // ================================== | ||
|
|
@@ -585,6 +649,10 @@ boost::shared_ptr<HttpResponse> RWebApplication::staticFileResponse( | |
| status_code = 304; | ||
| } | ||
|
|
||
| if (hasRange) { | ||
| status_code = 206; | ||
| } | ||
|
|
||
| boost::shared_ptr<HttpResponse> pResponse = boost::shared_ptr<HttpResponse>( | ||
| new HttpResponse(pRequest, status_code, getStatusDescription(status_code), pDataSource2), | ||
| auto_deleter_background<HttpResponse> | ||
|
|
@@ -622,6 +690,14 @@ boost::shared_ptr<HttpResponse> RWebApplication::staticFileResponse( | |
| respHeaders.push_back(std::make_pair("Content-Length", toString(pDataSource->size()))); | ||
| respHeaders.push_back(std::make_pair("Content-Type", content_type)); | ||
| respHeaders.push_back(std::make_pair("Last-Modified", http_date_string(pDataSource->getMtime()))); | ||
| respHeaders.push_back(std::make_pair("Accept-Ranges", "bytes")); | ||
| } | ||
|
|
||
| if (status_code == 206) { | ||
| respHeaders.push_back(std::make_pair( | ||
| "Content-Range", | ||
| "bytes " + toString(start) + "-" + toString(end) + "/" + toString(pDataSource2->fileSize()) | ||
| )); | ||
| } | ||
|
|
||
| return pResponse; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.