Skip to content

Commit 5808e05

Browse files
committed
Add basic HTTP parsing tools.
They provide adequate parsing of HTTP headers with the standard library.
1 parent 4d9214b commit 5808e05

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed

websockets/http.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
HTTP parsing utilities, adequate for the WebSocket handshake.
3+
"""
4+
5+
import email.parser
6+
import io
7+
8+
import tulip
9+
10+
11+
MAX_HEADERS = 256
12+
MAX_LINE = 4096
13+
14+
15+
@tulip.coroutine
16+
def read_request(stream):
17+
"""
18+
Read an HTTP/1.1 request that doesn't have a body from `stream`.
19+
20+
Return `(uri, headers)` where `uri` is a `str` and `headers` an
21+
`email.message.Message`.
22+
23+
`uri` is transmitted as-is; it isn't URL-decoded.
24+
"""
25+
request_line, headers = yield from read_message(stream)
26+
method, uri, version = request_line[:-2].decode().split(None, 2)
27+
if method != 'GET':
28+
raise ValueError("Unsupported method")
29+
if version != 'HTTP/1.1':
30+
raise ValueError("Unsupported HTTP version")
31+
return uri, headers
32+
33+
34+
@tulip.coroutine
35+
def read_response(stream):
36+
"""
37+
Read an HTTP/1.1 response that doesn't have a body from `stream`.
38+
39+
Return `(status, headers)` where `status` is an `int` and `headers` an
40+
`email.message.Message`.
41+
"""
42+
status_line, headers = yield from read_message(stream)
43+
version, status, reason = status_line[:-2].decode().split(None, 2)
44+
if version != 'HTTP/1.1':
45+
raise ValueError("Unsupported HTTP version")
46+
return int(status), headers
47+
48+
49+
@tulip.coroutine
50+
def read_message(stream):
51+
"""
52+
Read an HTTP message that doesn't have a body from `stream`.
53+
54+
Return `(start line, headers)` where `start_line` is `bytes` and `headers`
55+
an `email.message.Message`.
56+
"""
57+
start_line = yield from read_line(stream)
58+
header_lines = io.BytesIO()
59+
for num in range(MAX_HEADERS):
60+
header_line = yield from read_line(stream)
61+
header_lines.write(header_line)
62+
if header_line == b'\r\n':
63+
break
64+
else:
65+
raise ValueError("Too many headers")
66+
header_lines.seek(0)
67+
headers = email.parser.BytesHeaderParser().parse(header_lines)
68+
return start_line, headers
69+
70+
71+
@tulip.coroutine
72+
def read_line(stream):
73+
"""
74+
Read a single line from `stream`.
75+
"""
76+
line = yield from stream.readline()
77+
if len(line) > MAX_LINE:
78+
raise ValueError("Line too long")
79+
if not line.endswith(b'\r\n'):
80+
raise ValueError("Line without CRLF")
81+
return line

websockets/test_http.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import unittest
2+
3+
import tulip
4+
import tulip.test_utils
5+
6+
from .http import *
7+
8+
9+
class HTTPTests(tulip.test_utils.LogTrackingTestCase, unittest.TestCase):
10+
11+
def setUp(self):
12+
super().setUp()
13+
self.loop = tulip.new_event_loop()
14+
tulip.set_event_loop(self.loop)
15+
self.stream = tulip.StreamReader()
16+
17+
def tearDown(self):
18+
self.loop.close()
19+
super().tearDown()
20+
21+
def test_read_request(self):
22+
# Example from the protocol overview in RFC 6455
23+
self.stream.feed_data(
24+
b'GET /chat HTTP/1.1\r\n'
25+
b'Host: server.example.com\r\n'
26+
b'Upgrade: websocket\r\n'
27+
b'Connection: Upgrade\r\n'
28+
b'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n'
29+
b'Origin: http://example.com\r\n'
30+
b'Sec-WebSocket-Protocol: chat, superchat\r\n'
31+
b'Sec-WebSocket-Version: 13\r\n'
32+
b'\r\n'
33+
)
34+
uri, hdrs = self.loop.run_until_complete(read_request(self.stream))
35+
self.assertEqual(uri, '/chat')
36+
self.assertEqual(hdrs['Upgrade'], 'websocket')
37+
38+
def test_read_response(self):
39+
# Example from the protocol overview in RFC 6455
40+
self.stream.feed_data(
41+
b'HTTP/1.1 101 Switching Protocols\r\n'
42+
b'Upgrade: websocket\r\n'
43+
b'Connection: Upgrade\r\n'
44+
b'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n'
45+
b'Sec-WebSocket-Protocol: chat\r\n'
46+
b'\r\n'
47+
)
48+
status, hdrs = self.loop.run_until_complete(read_response(self.stream))
49+
self.assertEqual(status, 101)
50+
self.assertEqual(hdrs['Upgrade'], 'websocket')
51+
52+
def test_method(self):
53+
self.suppress_log_errors()
54+
self.stream.feed_data(b'OPTIONS * HTTP/1.1\r\n\r\n')
55+
with self.assertRaises(ValueError):
56+
self.loop.run_until_complete(read_request(self.stream))
57+
58+
def test_version(self):
59+
self.suppress_log_errors()
60+
self.stream.feed_data(b'GET /chat HTTP/1.0\r\n\r\n')
61+
with self.assertRaises(ValueError):
62+
self.loop.run_until_complete(read_request(self.stream))
63+
self.stream.feed_data(b'HTTP/1.0 400 Bad Request\r\n\r\n')
64+
with self.assertRaises(ValueError):
65+
self.loop.run_until_complete(read_response(self.stream))
66+
67+
def test_headers_limit(self):
68+
self.suppress_log_errors()
69+
self.stream.feed_data(b'foo: bar\r\n' * 500 + b'\r\n')
70+
with self.assertRaises(ValueError):
71+
self.loop.run_until_complete(read_message(self.stream))
72+
73+
def test_line_limit(self):
74+
self.suppress_log_errors()
75+
self.stream.feed_data(b'a' * 5000 + b'\r\n\r\n')
76+
with self.assertRaises(ValueError):
77+
self.loop.run_until_complete(read_message(self.stream))
78+
79+
def test_line_ending(self):
80+
self.suppress_log_errors()
81+
self.stream.feed_data(b'GET / HTTP/1.1\n\n')
82+
with self.assertRaises(ValueError):
83+
self.loop.run_until_complete(read_message(self.stream))

0 commit comments

Comments
 (0)