From e73f34e335aeb9a76a308d5643b16b8384d883ad Mon Sep 17 00:00:00 2001 From: David Shirley Date: Fri, 2 Jan 2015 13:22:42 -0800 Subject: [PATCH 1/6] Added a /range-request endpoint This endpoint conforms to RFC7233. http://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7233.html It is functionally very similar to /stream-bytes, but it also allows specifying a "Range" header, which allows the client to ask for a specific portion of the resource. I didn't add this functionality to /stream-bytes so as not to break compatibility with any clients that expect range requests to fail on that endpoint. Perhaps I'm just being overly cautious. --- README.rst | 2 + httpbin/core.py | 75 +++++++++++++++++++++++++++- httpbin/helpers.py | 40 +++++++++++++++ httpbin/templates/httpbin.1.html | 1 + test_httpbin.py | 84 ++++++++++++++++++++++++++++++++ 5 files changed, 200 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f0daa61..cb12fc2 100644 --- a/README.rst +++ b/README.rst @@ -40,6 +40,7 @@ Endpoint Description `/stream/:n`_ Streams *n* – 100 lines. `/delay/:n`_ Delays responding for *n* – 10 seconds. `/drip`_ Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code. +`/range-request/:n`_ Streams *n* bytes, and allows specifying a *Range* header to select a subset of the data. Accepts a *chunk\_size* and request *duration* parameter. `/html`_ Renders an HTML Page. `/robots.txt`_ Returns some robots.txt rules. `/deny`_ Denied by robots.txt file. @@ -74,6 +75,7 @@ Endpoint Description .. _/stream/:n: http://httpbin.org/stream/20 .. _/delay/:n: http://httpbin.org/delay/3 .. _/drip: http://httpbin.org/drip?numbytes=5&duration=5&code=200 +.. _/range-request/:n: http://httpbin.org/range-request/1024 .. _/html: http://httpbin.org/html .. _/robots.txt: http://httpbin.org/robots.txt .. _/deny: http://httpbin.org/deny diff --git a/httpbin/core.py b/httpbin/core.py index af22285..846d0e2 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -21,7 +21,7 @@ from werkzeug.wrappers import BaseResponse from six.moves import range as xrange from . import filters -from .helpers import get_headers, status_code, get_dict, check_basic_auth, check_digest_auth, secure_cookie, H, ROBOT_TXT, ANGRY_ASCII +from .helpers import get_headers, status_code, get_dict, parse_request_range, check_basic_auth, check_digest_auth, secure_cookie, H, ROBOT_TXT, ANGRY_ASCII from .utils import weighted_choice from .structures import CaseInsensitiveDict @@ -537,10 +537,81 @@ def stream_random_bytes(n): yield(bytes(chunks)) headers = {'Transfer-Encoding': 'chunked', - 'Content-Type': 'application/octet-stream'} + 'Content-Type': 'application/octet-stream' } return Response(generate_bytes(), headers=headers) +@app.route('/range-request/') +def range_request(numbytes): + """Streams n random bytes generated with given seed, at given chunk size per packet.""" + numbytes = min(numbytes, 100 * 1024) # set 100KB limit + + params = CaseInsensitiveDict(request.args.items()) + if 'chunk_size' in params: + chunk_size = max(1, int(params['chunk_size'])) + else: + chunk_size = 10 * 1024 + + duration = float(params.get('duration', 0)) + pause = 0 + if numbytes: + pause = duration / numbytes + + request_headers = get_headers() + request_range = parse_request_range(request_headers['range'], numbytes) + + first_byte_pos = request_range[0] + last_byte_pos = request_range[1] + if first_byte_pos is None and last_byte_pos is None: + first_byte_pos = 0 + last_byte_pos = numbytes - 1 + elif first_byte_pos is None: + first_byte_pos = max(0, numbytes - last_byte_pos) + last_byte_pos = numbytes - 1 + elif last_byte_pos is None: + last_byte_pos = numbytes - 1 + + if first_byte_pos > last_byte_pos or first_byte_pos not in xrange(0, numbytes) or last_byte_pos not in xrange(0, numbytes): + response = Response(headers={ + 'ETag' : 'range-request%d' % numbytes, + 'Accept-Ranges' : 'bytes', + 'Content-Range' : 'bytes */%d' % numbytes + }) + response.status_code = 416 + return response + + def generate_bytes(): + chunks = bytearray() + + for i in xrange(first_byte_pos, last_byte_pos + 1): + time.sleep(pause) + + # We don't want the resource to change across requests, so we need + # to use a predictable data generation function + chunks.append(ord('a') + (i % 26)) + if len(chunks) == chunk_size: + yield(bytes(chunks)) + chunks = bytearray() + + if chunks: + yield(bytes(chunks)) + + content_range = 'bytes %d-%d/%d' % (first_byte_pos, last_byte_pos, numbytes) + response_headers = { + 'Transfer-Encoding': 'chunked', + 'Content-Type': 'application/octet-stream', + 'ETag' : 'range-request%d' % numbytes, + 'Accept-Ranges' : 'bytes', + 'Content-Range' : content_range } + + response = Response(generate_bytes(), headers=response_headers) + + if (first_byte_pos == 0) and (last_byte_pos == (numbytes - 1)): + response.status_code = 200 + else: + response.status_code = 206 + + return response @app.route('/links//') def link_page(n, offset): diff --git a/httpbin/helpers.py b/httpbin/helpers.py index e0dcad8..15805fb 100644 --- a/httpbin/helpers.py +++ b/httpbin/helpers.py @@ -333,3 +333,43 @@ def check_digest_auth(user, passwd): def secure_cookie(): """Return true if cookie should have secure attribute""" return request.environ['wsgi.url_scheme'] == 'https' + +def parse_request_range(range_header_text, upper_bound): + """ Return a tuple describing the byte range requested in a GET request + If the range is open ended on the left or right side, then a value of None + will be set. + RFC7233: http://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7233.html#header.range + Examples: + Range : bytes=1024- + Range : bytes=10-20 + Range : bytes=-999 + """ + + left = None + right = None + + if not range_header_text: + return (left, right) + + range_header_text = range_header_text.strip() + if not range_header_text.startswith('bytes'): + return (left, right) + + components = range_header_text.split("=") + if len(components) != 2: + return (left, right) + + components = components[1].split("-") + + try: + right = int(components[1]) + except: + pass + + try: + left = int(components[0]) + except: + pass + + return (left, right) + diff --git a/httpbin/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html index ee96383..32eb6ea 100644 --- a/httpbin/templates/httpbin.1.html +++ b/httpbin/templates/httpbin.1.html @@ -34,6 +34,7 @@
  • /stream/:n Streams n–100 lines.
  • /delay/:n Delays responding for n–10 seconds.
  • /drip?numbytes=n&duration=s&delay=s&code=code Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code.
  • +
  • /range-request/1024?duration=s&chunk_size=code Streams n bytes, and allows specifying a Range header to select a subset of the data. Accepts a chunk_size and request duration parameter.
  • /html Renders an HTML Page.
  • /robots.txt Returns some robots.txt rules.
  • /deny Denied by robots.txt file.
  • diff --git a/test_httpbin.py b/test_httpbin.py index 23e4268..ca12b23 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -335,5 +335,89 @@ class HttpbinTestCase(unittest.TestCase): response.headers.get('Location'), 'http://localhost/get' ) + def test_request_range(self): + response1 = self.app.get('/range-request/1234') + self.assertEqual(response1.status_code, 200) + self.assertEqual(response1.headers.get('ETag'), 'range-request1234') + self.assertEqual(response1.headers.get('Content-range'), 'bytes 0-1233/1234') + self.assertEqual(response1.headers.get('Accept-ranges'), 'bytes') + self.assertEqual(len(response1.get_data()), 1234) + + response2 = self.app.get('/range-request/1234') + self.assertEqual(response2.status_code, 200) + self.assertEqual(response2.headers.get('ETag'), 'range-request1234') + self.assertEqual(response1.get_data(), response2.get_data()) + + def test_request_range_with_parameters(self): + response = self.app.get( + '/range-request/100?duration=1.5&chunk_size=5', + headers={ 'Range': 'bytes=10-24' } + ) + + self.assertEqual(response.status_code, 206) + self.assertEqual(response.headers.get('ETag'), 'range-request100') + self.assertEqual(response.headers.get('Content-range'), 'bytes 10-24/100') + self.assertEqual(response.headers.get('Accept-ranges'), 'bytes') + self.assertEqual(response.get_data(), 'klmnopqrstuvwxy') + + def test_request_range_first_15_bytes(self): + response = self.app.get( + '/range-request/1000', + headers={ 'Range': 'bytes=0-15' } + ) + + self.assertEqual(response.status_code, 206) + self.assertEqual(response.headers.get('ETag'), 'range-request1000') + self.assertEqual(response.get_data(), 'abcdefghijklmnop') + self.assertEqual(response.headers.get('Content-range'), 'bytes 0-15/1000') + + def test_request_range_open_ended_last_6_bytes(self): + response = self.app.get( + '/range-request/26', + headers={ 'Range': 'bytes=20-' } + ) + + self.assertEqual(response.status_code, 206) + self.assertEqual(response.headers.get('ETag'), 'range-request26') + self.assertEqual(response.get_data(), 'uvwxyz') + self.assertEqual(response.headers.get('Content-range'), 'bytes 20-25/26') + + def test_request_range_suffix(self): + response = self.app.get( + '/range-request/26', + headers={ 'Range': 'bytes=-5' } + ) + + self.assertEqual(response.status_code, 206) + self.assertEqual(response.headers.get('ETag'), 'range-request26') + self.assertEqual(response.get_data(), 'vwxyz') + self.assertEqual(response.headers.get('Content-range'), 'bytes 21-25/26') + + def test_request_out_of_bounds(self): + response = self.app.get( + '/range-request/26', + headers={ 'Range': 'bytes=10-5', + } + ) + + self.assertEqual(response.status_code, 416) + self.assertEqual(response.headers.get('ETag'), 'range-request26') + self.assertEqual(len(response.get_data()), 0) + self.assertEqual(response.headers.get('Content-range'), 'bytes */26') + + response = self.app.get( + '/range-request/26', + headers={ 'Range': 'bytes=32-40', + } + ) + + self.assertEqual(response.status_code, 416) + response = self.app.get( + '/range-request/26', + headers={ 'Range': 'bytes=0-40', + } + ) + self.assertEqual(response.status_code, 416) + if __name__ == '__main__': unittest.main() From d7180a9df85c54c87c31942f414cf5887763e8a5 Mon Sep 17 00:00:00 2001 From: David Shirley Date: Tue, 6 Jan 2015 14:02:03 -0800 Subject: [PATCH 2/6] Tweaked the behavior of the 'duration' parameter of '/range-request' This endpoint will now respond immediately with the first chunk of data. It will then sleep right after sending each chunk. --- httpbin/core.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/httpbin/core.py b/httpbin/core.py index 846d0e2..e631b85 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -544,7 +544,15 @@ def stream_random_bytes(n): @app.route('/range-request/') def range_request(numbytes): """Streams n random bytes generated with given seed, at given chunk size per packet.""" - numbytes = min(numbytes, 100 * 1024) # set 100KB limit + + if numbytes <= 0 or numbytes > (100 * 1024): + response = Response(headers={ + 'ETag' : 'range-request%d' % numbytes, + 'Accept-Ranges' : 'bytes' + }) + response.status_code = 404 + response.data = 'number of bytes must be in the range (0, 10240]' + return response params = CaseInsensitiveDict(request.args.items()) if 'chunk_size' in params: @@ -553,9 +561,7 @@ def range_request(numbytes): chunk_size = 10 * 1024 duration = float(params.get('duration', 0)) - pause = 0 - if numbytes: - pause = duration / numbytes + pause_per_byte = duration / numbytes request_headers = get_headers() request_range = parse_request_range(request_headers['range'], numbytes) @@ -563,12 +569,15 @@ def range_request(numbytes): first_byte_pos = request_range[0] last_byte_pos = request_range[1] if first_byte_pos is None and last_byte_pos is None: + # Request full range first_byte_pos = 0 last_byte_pos = numbytes - 1 elif first_byte_pos is None: + # Request the last X bytes first_byte_pos = max(0, numbytes - last_byte_pos) last_byte_pos = numbytes - 1 elif last_byte_pos is None: + # Request the last X bytes last_byte_pos = numbytes - 1 if first_byte_pos > last_byte_pos or first_byte_pos not in xrange(0, numbytes) or last_byte_pos not in xrange(0, numbytes): @@ -584,16 +593,17 @@ def range_request(numbytes): chunks = bytearray() for i in xrange(first_byte_pos, last_byte_pos + 1): - time.sleep(pause) # We don't want the resource to change across requests, so we need # to use a predictable data generation function chunks.append(ord('a') + (i % 26)) if len(chunks) == chunk_size: yield(bytes(chunks)) + time.sleep(pause_per_byte * chunk_size) chunks = bytearray() if chunks: + time.sleep(pause_per_byte * len(chunks)) yield(bytes(chunks)) content_range = 'bytes %d-%d/%d' % (first_byte_pos, last_byte_pos, numbytes) From 548e4df1f2dcac2ea65797f3d60475a09a65b2b3 Mon Sep 17 00:00:00 2001 From: David Shirley Date: Fri, 16 Jan 2015 11:28:47 -0800 Subject: [PATCH 3/6] Remove trailing space from dictionary --- httpbin/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpbin/core.py b/httpbin/core.py index e631b85..79086b9 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -537,7 +537,7 @@ def stream_random_bytes(n): yield(bytes(chunks)) headers = {'Transfer-Encoding': 'chunked', - 'Content-Type': 'application/octet-stream' } + 'Content-Type': 'application/octet-stream'} return Response(generate_bytes(), headers=headers) From 7e18da5ae9bace95115978d269cda8bc0b883911 Mon Sep 17 00:00:00 2001 From: David Shirley Date: Mon, 26 Jan 2015 13:51:47 -0800 Subject: [PATCH 4/6] Renamed endpoint /range-request -> /range --- README.rst | 4 ++-- httpbin/core.py | 8 ++++---- httpbin/templates/httpbin.1.html | 2 +- test_httpbin.py | 32 ++++++++++++++++---------------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index cb12fc2..a805283 100644 --- a/README.rst +++ b/README.rst @@ -40,7 +40,7 @@ Endpoint Description `/stream/:n`_ Streams *n* – 100 lines. `/delay/:n`_ Delays responding for *n* – 10 seconds. `/drip`_ Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code. -`/range-request/:n`_ Streams *n* bytes, and allows specifying a *Range* header to select a subset of the data. Accepts a *chunk\_size* and request *duration* parameter. +`/range/:n`_ Streams *n* bytes, and allows specifying a *Range* header to select a subset of the data. Accepts a *chunk\_size* and request *duration* parameter. `/html`_ Renders an HTML Page. `/robots.txt`_ Returns some robots.txt rules. `/deny`_ Denied by robots.txt file. @@ -75,7 +75,7 @@ Endpoint Description .. _/stream/:n: http://httpbin.org/stream/20 .. _/delay/:n: http://httpbin.org/delay/3 .. _/drip: http://httpbin.org/drip?numbytes=5&duration=5&code=200 -.. _/range-request/:n: http://httpbin.org/range-request/1024 +.. _/range/:n: http://httpbin.org/range/1024 .. _/html: http://httpbin.org/html .. _/robots.txt: http://httpbin.org/robots.txt .. _/deny: http://httpbin.org/deny diff --git a/httpbin/core.py b/httpbin/core.py index 79086b9..a37a38d 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -541,13 +541,13 @@ def stream_random_bytes(n): return Response(generate_bytes(), headers=headers) -@app.route('/range-request/') +@app.route('/range/') def range_request(numbytes): """Streams n random bytes generated with given seed, at given chunk size per packet.""" if numbytes <= 0 or numbytes > (100 * 1024): response = Response(headers={ - 'ETag' : 'range-request%d' % numbytes, + 'ETag' : 'range%d' % numbytes, 'Accept-Ranges' : 'bytes' }) response.status_code = 404 @@ -582,7 +582,7 @@ def range_request(numbytes): if first_byte_pos > last_byte_pos or first_byte_pos not in xrange(0, numbytes) or last_byte_pos not in xrange(0, numbytes): response = Response(headers={ - 'ETag' : 'range-request%d' % numbytes, + 'ETag' : 'range%d' % numbytes, 'Accept-Ranges' : 'bytes', 'Content-Range' : 'bytes */%d' % numbytes }) @@ -610,7 +610,7 @@ def range_request(numbytes): response_headers = { 'Transfer-Encoding': 'chunked', 'Content-Type': 'application/octet-stream', - 'ETag' : 'range-request%d' % numbytes, + 'ETag' : 'range%d' % numbytes, 'Accept-Ranges' : 'bytes', 'Content-Range' : content_range } diff --git a/httpbin/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html index 32eb6ea..35895b8 100644 --- a/httpbin/templates/httpbin.1.html +++ b/httpbin/templates/httpbin.1.html @@ -34,7 +34,7 @@
  • /stream/:n Streams n–100 lines.
  • /delay/:n Delays responding for n–10 seconds.
  • /drip?numbytes=n&duration=s&delay=s&code=code Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code.
  • -
  • /range-request/1024?duration=s&chunk_size=code Streams n bytes, and allows specifying a Range header to select a subset of the data. Accepts a chunk_size and request duration parameter.
  • +
  • /range/1024?duration=s&chunk_size=code Streams n bytes, and allows specifying a Range header to select a subset of the data. Accepts a chunk_size and request duration parameter.
  • /html Renders an HTML Page.
  • /robots.txt Returns some robots.txt rules.
  • /deny Denied by robots.txt file.
  • diff --git a/test_httpbin.py b/test_httpbin.py index ca12b23..149707e 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -336,84 +336,84 @@ class HttpbinTestCase(unittest.TestCase): ) def test_request_range(self): - response1 = self.app.get('/range-request/1234') + response1 = self.app.get('/range/1234') self.assertEqual(response1.status_code, 200) - self.assertEqual(response1.headers.get('ETag'), 'range-request1234') + self.assertEqual(response1.headers.get('ETag'), 'range1234') self.assertEqual(response1.headers.get('Content-range'), 'bytes 0-1233/1234') self.assertEqual(response1.headers.get('Accept-ranges'), 'bytes') self.assertEqual(len(response1.get_data()), 1234) - response2 = self.app.get('/range-request/1234') + response2 = self.app.get('/range/1234') self.assertEqual(response2.status_code, 200) - self.assertEqual(response2.headers.get('ETag'), 'range-request1234') + self.assertEqual(response2.headers.get('ETag'), 'range1234') self.assertEqual(response1.get_data(), response2.get_data()) def test_request_range_with_parameters(self): response = self.app.get( - '/range-request/100?duration=1.5&chunk_size=5', + '/range/100?duration=1.5&chunk_size=5', headers={ 'Range': 'bytes=10-24' } ) self.assertEqual(response.status_code, 206) - self.assertEqual(response.headers.get('ETag'), 'range-request100') + self.assertEqual(response.headers.get('ETag'), 'range100') self.assertEqual(response.headers.get('Content-range'), 'bytes 10-24/100') self.assertEqual(response.headers.get('Accept-ranges'), 'bytes') self.assertEqual(response.get_data(), 'klmnopqrstuvwxy') def test_request_range_first_15_bytes(self): response = self.app.get( - '/range-request/1000', + '/range/1000', headers={ 'Range': 'bytes=0-15' } ) self.assertEqual(response.status_code, 206) - self.assertEqual(response.headers.get('ETag'), 'range-request1000') + self.assertEqual(response.headers.get('ETag'), 'range1000') self.assertEqual(response.get_data(), 'abcdefghijklmnop') self.assertEqual(response.headers.get('Content-range'), 'bytes 0-15/1000') def test_request_range_open_ended_last_6_bytes(self): response = self.app.get( - '/range-request/26', + '/range/26', headers={ 'Range': 'bytes=20-' } ) self.assertEqual(response.status_code, 206) - self.assertEqual(response.headers.get('ETag'), 'range-request26') + self.assertEqual(response.headers.get('ETag'), 'range26') self.assertEqual(response.get_data(), 'uvwxyz') self.assertEqual(response.headers.get('Content-range'), 'bytes 20-25/26') def test_request_range_suffix(self): response = self.app.get( - '/range-request/26', + '/range/26', headers={ 'Range': 'bytes=-5' } ) self.assertEqual(response.status_code, 206) - self.assertEqual(response.headers.get('ETag'), 'range-request26') + self.assertEqual(response.headers.get('ETag'), 'range26') self.assertEqual(response.get_data(), 'vwxyz') self.assertEqual(response.headers.get('Content-range'), 'bytes 21-25/26') def test_request_out_of_bounds(self): response = self.app.get( - '/range-request/26', + '/range/26', headers={ 'Range': 'bytes=10-5', } ) self.assertEqual(response.status_code, 416) - self.assertEqual(response.headers.get('ETag'), 'range-request26') + self.assertEqual(response.headers.get('ETag'), 'range26') self.assertEqual(len(response.get_data()), 0) self.assertEqual(response.headers.get('Content-range'), 'bytes */26') response = self.app.get( - '/range-request/26', + '/range/26', headers={ 'Range': 'bytes=32-40', } ) self.assertEqual(response.status_code, 416) response = self.app.get( - '/range-request/26', + '/range/26', headers={ 'Range': 'bytes=0-40', } ) From 0484ea2c9142ddab602eb87b0cc5274ced809743 Mon Sep 17 00:00:00 2001 From: David Shirley Date: Mon, 26 Jan 2015 14:54:36 -0800 Subject: [PATCH 5/6] Tidied the /range controller code Extracted some of the range processing logic into a separate function --- httpbin/core.py | 18 ++---------------- httpbin/helpers.py | 27 ++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/httpbin/core.py b/httpbin/core.py index a37a38d..8c69dcb 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -21,7 +21,7 @@ from werkzeug.wrappers import BaseResponse from six.moves import range as xrange from . import filters -from .helpers import get_headers, status_code, get_dict, parse_request_range, check_basic_auth, check_digest_auth, secure_cookie, H, ROBOT_TXT, ANGRY_ASCII +from .helpers import get_headers, status_code, get_dict, get_request_range, check_basic_auth, check_digest_auth, secure_cookie, H, ROBOT_TXT, ANGRY_ASCII from .utils import weighted_choice from .structures import CaseInsensitiveDict @@ -564,21 +564,7 @@ def range_request(numbytes): pause_per_byte = duration / numbytes request_headers = get_headers() - request_range = parse_request_range(request_headers['range'], numbytes) - - first_byte_pos = request_range[0] - last_byte_pos = request_range[1] - if first_byte_pos is None and last_byte_pos is None: - # Request full range - first_byte_pos = 0 - last_byte_pos = numbytes - 1 - elif first_byte_pos is None: - # Request the last X bytes - first_byte_pos = max(0, numbytes - last_byte_pos) - last_byte_pos = numbytes - 1 - elif last_byte_pos is None: - # Request the last X bytes - last_byte_pos = numbytes - 1 + first_byte_pos, last_byte_pos = get_request_range(request_headers, numbytes) if first_byte_pos > last_byte_pos or first_byte_pos not in xrange(0, numbytes) or last_byte_pos not in xrange(0, numbytes): response = Response(headers={ diff --git a/httpbin/helpers.py b/httpbin/helpers.py index 15805fb..7213cf8 100644 --- a/httpbin/helpers.py +++ b/httpbin/helpers.py @@ -334,7 +334,7 @@ def secure_cookie(): """Return true if cookie should have secure attribute""" return request.environ['wsgi.url_scheme'] == 'https' -def parse_request_range(range_header_text, upper_bound): +def __parse_request_range(range_header_text): """ Return a tuple describing the byte range requested in a GET request If the range is open ended on the left or right side, then a value of None will be set. @@ -349,15 +349,15 @@ def parse_request_range(range_header_text, upper_bound): right = None if not range_header_text: - return (left, right) + return left, right range_header_text = range_header_text.strip() if not range_header_text.startswith('bytes'): - return (left, right) + return left, right components = range_header_text.split("=") if len(components) != 2: - return (left, right) + return left, right components = components[1].split("-") @@ -371,5 +371,22 @@ def parse_request_range(range_header_text, upper_bound): except: pass - return (left, right) + return left, right + +def get_request_range(request_headers, upper_bound): + first_byte_pos, last_byte_pos = __parse_request_range(request_headers['range']) + + if first_byte_pos is None and last_byte_pos is None: + # Request full range + first_byte_pos = 0 + last_byte_pos = upper_bound - 1 + elif first_byte_pos is None: + # Request the last X bytes + first_byte_pos = max(0, upper_bound - last_byte_pos) + last_byte_pos = upper_bound - 1 + elif last_byte_pos is None: + # Request the last X bytes + last_byte_pos = upper_bound - 1 + + return first_byte_pos, last_byte_pos From 60b0ada6f1541fd82d009bd08b8bbd7e0509bbf3 Mon Sep 17 00:00:00 2001 From: David Shirley Date: Thu, 19 Feb 2015 11:41:43 -0800 Subject: [PATCH 6/6] Fixed unit tests for python3.4 --- test_httpbin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_httpbin.py b/test_httpbin.py index 149707e..95e366d 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -358,7 +358,7 @@ class HttpbinTestCase(unittest.TestCase): self.assertEqual(response.headers.get('ETag'), 'range100') self.assertEqual(response.headers.get('Content-range'), 'bytes 10-24/100') self.assertEqual(response.headers.get('Accept-ranges'), 'bytes') - self.assertEqual(response.get_data(), 'klmnopqrstuvwxy') + self.assertEqual(response.get_data(), 'klmnopqrstuvwxy'.encode('utf8')) def test_request_range_first_15_bytes(self): response = self.app.get( @@ -368,7 +368,7 @@ class HttpbinTestCase(unittest.TestCase): self.assertEqual(response.status_code, 206) self.assertEqual(response.headers.get('ETag'), 'range1000') - self.assertEqual(response.get_data(), 'abcdefghijklmnop') + self.assertEqual(response.get_data(), 'abcdefghijklmnop'.encode('utf8')) self.assertEqual(response.headers.get('Content-range'), 'bytes 0-15/1000') def test_request_range_open_ended_last_6_bytes(self): @@ -379,7 +379,7 @@ class HttpbinTestCase(unittest.TestCase): self.assertEqual(response.status_code, 206) self.assertEqual(response.headers.get('ETag'), 'range26') - self.assertEqual(response.get_data(), 'uvwxyz') + self.assertEqual(response.get_data(), 'uvwxyz'.encode('utf8')) self.assertEqual(response.headers.get('Content-range'), 'bytes 20-25/26') def test_request_range_suffix(self): @@ -390,7 +390,7 @@ class HttpbinTestCase(unittest.TestCase): self.assertEqual(response.status_code, 206) self.assertEqual(response.headers.get('ETag'), 'range26') - self.assertEqual(response.get_data(), 'vwxyz') + self.assertEqual(response.get_data(), 'vwxyz'.encode('utf8')) self.assertEqual(response.headers.get('Content-range'), 'bytes 21-25/26') def test_request_out_of_bounds(self):