diff options
-rw-r--r-- | src/http-header-glue.c | 50 | ||||
-rwxr-xr-x | tests/cachable.t | 6 | ||||
-rw-r--r-- | tests/docroot/123/100.txt | 10 | ||||
-rw-r--r-- | tests/docroot/123/Makefile.am | 2 | ||||
-rwxr-xr-x | tests/prepare.sh | 2 | ||||
-rwxr-xr-x | tests/request.t | 76 |
6 files changed, 111 insertions, 35 deletions
diff --git a/src/http-header-glue.c b/src/http-header-glue.c index 19039043..eef96321 100644 --- a/src/http-header-glue.c +++ b/src/http-header-glue.c @@ -185,10 +185,7 @@ int http_response_handle_cachable(request_st * const r, const buffer * const mti if ((vb = http_header_request_get(r, HTTP_HEADER_IF_NONE_MATCH, CONST_STR_LEN("If-None-Match")))) { /*(weak etag comparison must not be used for ranged requests)*/ - int range_request = - (light_btst(r->rqst_htags, HTTP_HEADER_RANGE) - && r->conf.range_requests - && (200 == r->http_status || 0 == r->http_status)); + int range_request = (0 != light_btst(r->rqst_htags, HTTP_HEADER_RANGE)); if (etag_is_equal(&r->physical.etag, vb->ptr, !range_request)) { if (http_method_get_or_head(r->http_method)) { r->http_status = 304; @@ -328,6 +325,46 @@ 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, const buffer * const path, const int fd, const stat_cache_entry * const sce, const char * const range) { int n = 0; int error; @@ -465,6 +502,8 @@ static int http_response_parse_range(request_st * const r, const buffer * const /* 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]; @@ -601,6 +640,9 @@ 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"), diff --git a/tests/cachable.t b/tests/cachable.t index 35b9ea90..fbc49e47 100755 --- a/tests/cachable.t +++ b/tests/cachable.t @@ -163,12 +163,14 @@ EOF } $t->{REQUEST} = ( <<EOF -GET / HTTP/1.0 +GET / HTTP/1.1 +Host: www.example.org If-None-Match: W/$etag +Connection: close Range: bytes=0-0 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206, 'HTTP-Content' => '<' } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'HTTP-Content' => '<' } ]; ok($tf->handle_http($t) == 0, 'A weak etag does not match for ranged requests'); $t->{REQUEST} = ( <<EOF diff --git a/tests/docroot/123/100.txt b/tests/docroot/123/100.txt new file mode 100644 index 00000000..c4782fd3 --- /dev/null +++ b/tests/docroot/123/100.txt @@ -0,0 +1,10 @@ +123456789 +123456789 +123456789 +123456789 +123456789 +123456789 +123456789 +123456789 +123456789 +abcdefghi diff --git a/tests/docroot/123/Makefile.am b/tests/docroot/123/Makefile.am index e998042e..f77f883a 100644 --- a/tests/docroot/123/Makefile.am +++ b/tests/docroot/123/Makefile.am @@ -1 +1 @@ -EXTRA_DIST=12345.html 12345.txt dummyfile.bla phpinfo.php +EXTRA_DIST=100.txt 12345.html 12345.txt dummyfile.bla phpinfo.php diff --git a/tests/prepare.sh b/tests/prepare.sh index 98595230..14d94eaa 100755 --- a/tests/prepare.sh +++ b/tests/prepare.sh @@ -54,7 +54,7 @@ touch "${tmpdir}/servers/www.example.org/pages/image.jpg" \ "${tmpdir}/servers/www.example.org/pages/Foo.txt" \ "${tmpdir}/servers/www.example.org/pages/a" \ "${tmpdir}/servers/www.example.org/pages/index.html~" -echo "12345" > "${tmpdir}/servers/www.example.org/pages/range.pdf" +echo "12345" > "${tmpdir}/servers/123.example.org/pages/range.pdf" printf "%-40s" "preparing infrastructure" diff --git a/tests/request.t b/tests/request.t index 75a96350..ba001ad8 100755 --- a/tests/request.t +++ b/tests/request.t @@ -8,7 +8,7 @@ BEGIN { use strict; use IO::Socket; -use Test::More tests => 51; +use Test::More tests => 52; use LightyTest; my $tf = LightyTest->new(); @@ -253,80 +253,97 @@ ok($tf->handle_http($t) == 0, 'POST via Transfer-Encoding: chunked; chunked head ## ranges $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=0-3 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206, 'HTTP-Content' => '1234' } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'HTTP-Content' => '1234' } ]; ok($tf->handle_http($t) == 0, 'GET, Range 0-3'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=-3 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206, 'HTTP-Content' => '45'."\n" } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'HTTP-Content' => '45'."\n" } ]; ok($tf->handle_http($t) == 0, 'GET, Range -3'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=3- EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206, 'HTTP-Content' => '45'."\n" } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'HTTP-Content' => '45'."\n" } ]; ok($tf->handle_http($t) == 0, 'GET, Range 3-'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=0-1,3-4 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206, 'HTTP-Content' => <<EOF +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'HTTP-Content' => '12345' } ]; +ok($tf->handle_http($t) == 0, 'GET, Range 0-1,3-4 (ranges merged)'); + +$t->{REQUEST} = ( <<EOF +GET /100.txt HTTP/1.1 +Host: 123.example.org +Connection: close +Range: bytes=0-1,97-98 +EOF + ); +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206, 'HTTP-Content' => <<EOF \r --fkj49sn38dcn3\r -Content-Range: bytes 0-1/6\r +Content-Range: bytes 0-1/100\r Content-Type: text/plain\r \r 12\r --fkj49sn38dcn3\r -Content-Range: bytes 3-4/6\r +Content-Range: bytes 97-98/100\r Content-Type: text/plain\r \r -45\r +hi\r --fkj49sn38dcn3--\r EOF } ]; -ok($tf->handle_http($t) == 0, 'GET, Range 0-1,3-4'); +ok($tf->handle_http($t) == 0, 'GET, Range 0-1,97-98 (ranges not merged)'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=0-- EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; ok($tf->handle_http($t) == 0, 'GET, Range 0--'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=-2-3 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; ok($tf->handle_http($t) == 0, 'GET, Range -2-3'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=-0 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 416, 'HTTP-Content' => <<EOF +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 416, 'HTTP-Content' => <<EOF <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> @@ -343,12 +360,13 @@ EOF ok($tf->handle_http($t) == 0, 'GET, Range -0'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=25- EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 416, 'HTTP-Content' => <<EOF +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 416, 'HTTP-Content' => <<EOF <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> @@ -384,31 +402,35 @@ ok($tf->handle_http($t) == 0, 'larger headers'); $t->{REQUEST} = ( <<EOF -GET /range.pdf HTTP/1.0 +GET /range.pdf HTTP/1.1 +Host: 123.example.org Range: bytes=0- +Connection: close EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200 } ]; ok($tf->handle_http($t) == 0, 'GET, Range with range-requests-disabled'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: 0 Range: bytes=0-3 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200, 'HTTP-Content' => "12345\n" } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 200, 'HTTP-Content' => "12345\n" } ]; ok($tf->handle_http($t) == 0, 'GET, Range invalid range-unit (first)'); $t->{REQUEST} = ( <<EOF -GET /12345.txt HTTP/1.0 +GET /12345.txt HTTP/1.1 Host: 123.example.org +Connection: close Range: bytes=0-3 Range: 0 EOF ); -$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206 } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.1', 'HTTP-Status' => 206 } ]; ok($tf->handle_http($t) == 0, 'GET, Range ignore invalid range (second)'); $t->{REQUEST} = ( <<EOF |