diff --git a/src/http-header-glue.c b/src/http-header-glue.c index c2982951..b8c3fb9f 100644 --- a/src/http-header-glue.c +++ b/src/http-header-glue.c @@ -305,267 +305,9 @@ handler_t http_response_reqbody_read_error (request_st * const r, int http_statu } -static int http_response_coalesce_ranges (off_t * const ranges, int n) -{ - /* coalesce/combine overlapping ranges and ranges separated by a - * gap which is smaller than the overhead of sending multiple parts - * (typically around 80 bytes) ([RFC7233] 4.1 206 Partial Content) - * (ranges are known to be positive, so subtract 80 instead of add 80 - * to avoid any chance of integer overflow) - * (max n should be limited in caller since a malicious set of ranges has - * n^2 cost for the simplistic algorithm below) - * (sorting the ranges and then combining would lower the cost, but the - * cost should not be an issue since client should not send many ranges - * and caller should restrict the max number of ranges to limit abuse) - * [RFC7233] 4.1 206 Partial Content recommends: - * When a multipart response payload is generated, the server SHOULD send - * the parts in the same order that the corresponding byte-range-spec - * appeared in the received Range header field, excluding those ranges - * that were deemed unsatisfiable or that were coalesced into other ranges - */ - for (int i = 0; i+2 < n; i += 2) { - const off_t b = ranges[i]; - const off_t e = ranges[i+1]; - for (int j = i+2; j < n; j += 2) { - /* common case: ranges do not overlap */ - if (b <= ranges[j] ? e < ranges[j]-80 : ranges[j+1] < b-80) - continue; - /* else ranges do overlap, so combine into first range */ - ranges[i] = b <= ranges[j] ? b : ranges[j]; - ranges[i+1] = e >= ranges[j+1] ? e : ranges[j+1]; - memmove(ranges+j, ranges+j+2, (n-j-2)*sizeof(off_t)); - /* restart outer loop from beginning */ - n -= 2; - i = -2; - break; - } - } - - return n; -} - - -static int http_response_parse_range(request_st * const r, stat_cache_entry * const sce, const char * const range) { - int n = 0; - int error; - off_t start, end; - const off_t st_size = sce->st.st_size; - const char *s, *minus; - static const char boundary[] = "fkj49sn38dcn3"; - const buffer *content_type = - http_header_response_get(r, HTTP_HEADER_CONTENT_TYPE, - CONST_STR_LEN("Content-Type")); - off_t ranges[16]; - - start = 0; - end = st_size - 1; - - for (s = range, error = 0; - !error && *s && NULL != (minus = strchr(s, '-')); ) { - char *err; - off_t la = 0, le; - *((const char **)&err) = s; /*(quiet clang --analyze)*/ - - if (s != minus) { - la = strtoll(s, &err, 10); - if (err != minus) { - /* should not have multiple range-unit in Range, but - * handle just in case multiple Range headers merged */ - while (*s == ' ' || *s == '\t') ++s; - if (0 != strncmp(s, "bytes=", 6)) return -1; - s += 6; - if (s != minus) { - la = strtoll(s, &err, 10); - if (err != minus) return -1; - } - } - } - - if (s == minus) { - /* - */ - - le = strtoll(s, &err, 10); - - if (le == 0) { - /* RFC 2616 - 14.35.1 */ - - r->http_status = 416; - error = 1; - } else if (*err == '\0') { - /* end */ - s = err; - - end = st_size - 1; - start = st_size + le; - } else if (*err == ',') { - s = err + 1; - - end = st_size - 1; - start = st_size + le; - } else { - error = 1; - } - - } else if (*(minus+1) == '\0' || *(minus+1) == ',') { - /* - */ - - /* ok */ - - if (*(err + 1) == '\0') { - s = err + 1; - - end = st_size - 1; - start = la; - - } else if (*(err + 1) == ',') { - s = err + 2; - - end = st_size - 1; - start = la; - } else { - error = 1; - } - } else { - /* - */ - - le = strtoll(minus+1, &err, 10); - - /* RFC 2616 - 14.35.1 */ - if (la > le) { - error = 1; - } - - if (*err == '\0') { - /* ok, end*/ - s = err; - - end = le; - start = la; - } else if (*err == ',') { - s = err + 1; - - end = le; - start = la; - } else { - /* error */ - - error = 1; - } - } - - if (!error) { - if (start < 0) start = 0; - - /* RFC 2616 - 14.35.1 */ - if (end > st_size - 1) end = st_size - 1; - - if (start > st_size - 1) { - error = 1; - - r->http_status = 416; - } - } - - if (!error) { - if (n < (int)(sizeof(ranges)/sizeof(*ranges))) { - ranges[n] = start; - ranges[n+1] = end; - n += 2; - } - else { /* excessive num ranges in request */ - error = 1; - r->http_status = 416; - } - } - } - - /* something went wrong */ - if (error) return -1; - - if (n > 2) n = http_response_coalesce_ranges(ranges, n); - - for (int i = 0; i < n; i += 2) { - start = ranges[i]; - end = ranges[i+1]; - if (n > 2) { - /* write boundary-header */ - buffer *b = r->tmp_buf; - buffer_copy_string_len(b, CONST_STR_LEN("\r\n--")); - buffer_append_string_len(b, boundary, sizeof(boundary)-1); - - /* write Content-Range */ - buffer_append_string_len(b, CONST_STR_LEN("\r\nContent-Range: bytes ")); - buffer_append_int(b, start); - buffer_append_string_len(b, CONST_STR_LEN("-")); - buffer_append_int(b, end); - buffer_append_string_len(b, CONST_STR_LEN("/")); - buffer_append_int(b, st_size); - - if (content_type) { - buffer_append_string_len(b, CONST_STR_LEN("\r\nContent-Type: ")); - buffer_append_string_buffer(b, content_type); - } - - /* write END-OF-HEADER */ - buffer_append_string_len(b, CONST_STR_LEN("\r\n\r\n")); - http_chunk_append_mem(r, CONST_BUF_LEN(b)); - } - - http_chunk_append_file_ref_range(r, sce, start, end - start + 1); - } - - buffer * const tb = r->tmp_buf; - - if (n > 2) { - /* add boundary end */ - buffer_copy_string_len(tb, "\r\n--", 4); - buffer_append_string_len(tb, boundary, sizeof(boundary)-1); - buffer_append_string_len(tb, "--\r\n", 4); - http_chunk_append_mem(r, CONST_BUF_LEN(tb)); - - /* set header-fields */ - - buffer_copy_string_len(tb, CONST_STR_LEN("multipart/byteranges; boundary=")); - buffer_append_string_len(tb, boundary, sizeof(boundary)-1); - - /* overwrite content-type */ - http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, - CONST_STR_LEN("Content-Type"), - CONST_BUF_LEN(tb)); - } else { - /* add Content-Range-header */ - - buffer_copy_string_len(tb, CONST_STR_LEN("bytes ")); - buffer_append_int(tb, start); - buffer_append_string_len(tb, CONST_STR_LEN("-")); - buffer_append_int(tb, end); - buffer_append_string_len(tb, CONST_STR_LEN("/")); - buffer_append_int(tb, st_size); - - http_header_response_set(r, HTTP_HEADER_CONTENT_RANGE, - CONST_STR_LEN("Content-Range"), - CONST_BUF_LEN(tb)); - } - - /* ok, the file is set-up */ - return 0; -} - - -__attribute_pure__ -static int http_response_match_if_range(request_st * const r, const buffer * const mtime) { - const buffer *vb = http_header_request_get(r, HTTP_HEADER_IF_RANGE, - CONST_STR_LEN("If-Range")); - return NULL == vb - || ((vb->ptr[0] == '"') - ? buffer_is_equal(vb, &r->physical.etag) /*compare ETag ("...") */ - : mtime && buffer_is_equal(vb, mtime)); /*compare Last-Modified*/ -} - void http_response_send_file (request_st * const r, buffer * const path) { stat_cache_entry * const sce = stat_cache_get_entry_open(path, r->conf.follow_symlink); const buffer *mtime = NULL; - const buffer *vb; int allow_caching = (0 == r->http_status || 200 == r->http_status); if (NULL == sce) { @@ -630,15 +372,6 @@ void http_response_send_file (request_st * const r, buffer * const path) { } } - if (!http_method_get_or_head(r->http_method) - || r->http_version < HTTP_VERSION_1_1) - r->conf.range_requests = 0; - if (r->conf.range_requests) { - http_header_response_append(r, HTTP_HEADER_ACCEPT_RANGES, - CONST_STR_LEN("Accept-Ranges"), - CONST_STR_LEN("bytes")); - } - if (allow_caching) { if (!light_btst(r->resp_htags, HTTP_HEADER_ETAG) && 0 != r->conf.etag_flags) { @@ -674,19 +407,6 @@ void http_response_send_file (request_st * const r, buffer * const path) { return; } - if (r->conf.range_requests - && (200 == r->http_status || 0 == r->http_status) - && NULL != (vb = http_header_request_get(r, HTTP_HEADER_RANGE, - CONST_STR_LEN("Range"))) - && !light_btst(r->resp_htags, HTTP_HEADER_CONTENT_ENCODING) - && http_response_match_if_range(r, mtime) /* "If-Range" */ - && !buffer_string_is_empty(vb) && 0 == strncmp(vb->ptr, "bytes=", 6)) { - r->resp_body_finished = 1; - if (0 == http_response_parse_range(r, sce, vb->ptr+6)) - r->http_status = 206; - return; - } - /* if we are still here, prepare body */ /* we add it here for all requests diff --git a/src/response.c b/src/response.c index d243915d..7da19b11 100644 --- a/src/response.c +++ b/src/response.c @@ -11,6 +11,7 @@ #include "chunk.h" #include "http_chunk.h" #include "http_date.h" +#include "http_range.h" #include "plugin.h" @@ -868,6 +869,10 @@ http_response_write_prepare(request_st * const r) } if (r->resp_body_finished) { + /* check for Range request (current impl requires resp_body_finished) */ + if (r->conf.range_requests && http_range_rfc7233(r) >= 400) + http_response_static_errdoc(r); /* 416 Range Not Satisfiable */ + /* set content-length if length is known and not already set */ if (!(r->resp_htags & (light_bshift(HTTP_HEADER_CONTENT_LENGTH) diff --git a/tests/request.t b/tests/request.t index bba4b938..432bb51e 100755 --- a/tests/request.t +++ b/tests/request.t @@ -8,7 +8,7 @@ BEGIN { use strict; use IO::Socket; -use Test::More tests => 53; +use Test::More tests => 54; use LightyTest; my $tf = LightyTest->new(); @@ -300,15 +300,14 @@ Range: bytes=0-1,97-98 EOF ); $t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'HTTP-Content' => <handle_http($t) == 0, 'GET, Range 0-1,97-98 (ranges not merged)'); +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'Content-Range' => 'bytes 0-5/6' } ]; +ok($tf->handle_http($t) == 0, 'GET, Range 0-'); + $t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 416 } ]; ok($tf->handle_http($t) == 0, 'GET, Range 0--'); $t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 416 } ]; ok($tf->handle_http($t) == 0, 'GET, Range -2-3'); $t->{REQUEST} = ( <