Skip to content

Commit fb0d3fa

Browse files
committed
bpo-42643: Add support for HTTP range requests
1 parent 11276cd commit fb0d3fa

2 files changed

Lines changed: 41 additions & 8 deletions

File tree

Lib/http/server.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,15 @@
8080
# (Actually, the latter is only true if you know the server configuration
8181
# at the time the request was made!)
8282

83-
__version__ = "0.6"
83+
__version__ = "0.7"
8484

8585
__all__ = [
8686
"HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler",
8787
"SimpleHTTPRequestHandler", "CGIHTTPRequestHandler",
8888
]
8989

90+
import re
91+
9092
import copy
9193
import datetime
9294
import email.utils
@@ -129,6 +131,10 @@
129131

130132
DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8"
131133

134+
135+
HTTP_BYTES_RANGE_HEADER = re.compile(r"bytes=(?P<first>\d+)-(?P<last>\d+)$")
136+
137+
132138
class HTTPServer(socketserver.TCPServer):
133139

134140
allow_reuse_address = 1 # Seems to make sense in testing environment
@@ -657,7 +663,12 @@ def do_GET(self):
657663
f = self.send_head()
658664
if f:
659665
try:
660-
self.copyfile(f, self.wfile)
666+
if "Range" in self.headers:
667+
res = HTTP_BYTES_RANGE_HEADER.match(string=self.headers.get("Range"))
668+
if res is not None:
669+
self.copyfile(f, self.wfile, int(res.group("first")), int(res.group("last")))
670+
else:
671+
self.copyfile(f, self.wfile)
661672
finally:
662673
f.close()
663674

@@ -742,10 +753,22 @@ def send_head(self):
742753
self.end_headers()
743754
f.close()
744755
return None
745-
746-
self.send_response(HTTPStatus.OK)
756+
if "Range" in self.headers:
757+
res = HTTP_BYTES_RANGE_HEADER.match(string=self.headers["Range"])
758+
if res is None:
759+
self.send_error(code=HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
760+
message="Range header is not a valid single part ranges")
761+
self.end_headers()
762+
f.close()
763+
return None
764+
self.send_response(HTTPStatus.PARTIAL_CONTENT)
765+
self.send_header("Content-Range", f"{self.headers['Range']}/{fs[6]}")
766+
self.send_header("Content-Length", int(res.group("last"))-int(res.group("first")))
767+
else:
768+
self.send_response(HTTPStatus.OK)
769+
self.send_header("Accept-Ranges", "bytes")
770+
self.send_header("Content-Length", str(fs[6]))
747771
self.send_header("Content-type", ctype)
748-
self.send_header("Content-Length", str(fs[6]))
749772
self.send_header("Last-Modified",
750773
self.date_time_string(fs.st_mtime))
751774
self.end_headers()
@@ -842,8 +865,10 @@ def translate_path(self, path):
842865
path += '/'
843866
return path
844867

845-
def copyfile(self, source, outputfile):
846-
"""Copy all data between two file objects.
868+
def copyfile(self, source, outputfile, start_byte=None, end_byte=None):
869+
"""Copy all data between two file objects if start_byte and end_byte are None.
870+
871+
Otherwise, copy (end_byte-start_byte) bytes data between two file objects.
847872
848873
The SOURCE argument is a file object open for reading
849874
(or anything with a read() method) and the DESTINATION
@@ -856,7 +881,11 @@ def copyfile(self, source, outputfile):
856881
to copy binary data as well.
857882
858883
"""
859-
shutil.copyfileobj(source, outputfile)
884+
if start_byte is not None and end_byte is not None:
885+
source.seek(start_byte)
886+
outputfile.write(source.read(end_byte))
887+
else:
888+
shutil.copyfileobj(source, outputfile)
860889

861890
def guess_type(self, path):
862891
"""Guess the type of a file.

Lib/test/test_httpservers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,10 @@ def test_html_escape_filename(self):
557557
html_text = '>%s<' % html.escape(filename, quote=False)
558558
self.assertIn(html_text.encode(enc), body)
559559

560+
def test_range_requests(self):
561+
response = self.request(self.base_url + '/test', headers={"Range": "bytes=0-10"})
562+
self.check_status_and_reason(response=response, status=HTTPStatus.PARTIAL_CONTENT, data=b'We are the')
563+
560564

561565
cgi_file1 = """\
562566
#!%s

0 commit comments

Comments
 (0)