Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit b6d0ec1

Browse files
committed
Basic pure-Python parser.
1 parent 5814fee commit b6d0ec1

File tree

2 files changed

+127
-16
lines changed

2 files changed

+127
-16
lines changed

hyper/http11/parser.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010
from collections import namedtuple
1111

1212

13-
Request = namedtuple(
14-
'Request', ['method', 'path', 'minor_version', 'headers', 'consumed']
15-
)
1613
Response = namedtuple(
1714
'Response', ['status', 'msg', 'minor_version', 'headers', 'consumed']
1815
)
@@ -27,25 +24,14 @@ class ParseError(Exception):
2724

2825
class Parser(object):
2926
"""
30-
A single HTTP parser object. This object can parse HTTP requests and
31-
responses using picohttpparser.
27+
A single HTTP parser object.
3228
This object is not thread-safe, and it does maintain state that is shared
3329
across parsing requests. For this reason, make sure that access to this
3430
object is synchronized if you use it across multiple threads.
3531
"""
3632
def __init__(self):
3733
pass
3834

39-
def parse_request(self, buffer):
40-
"""
41-
Parses a single HTTP request from a buffer.
42-
:param buffer: A ``memoryview`` object wrapping a buffer containing a
43-
HTTP request.
44-
:returns: A :class:`Request <hyper.http11.parser.Request>` object, or
45-
``None`` if there is not enough data in the buffer.
46-
"""
47-
pass
48-
4935
def parse_response(self, buffer):
5036
"""
5137
Parses a single HTTP response from a buffer.
@@ -54,4 +40,43 @@ def parse_response(self, buffer):
5440
:returns: A :class:`Response <hyper.http11.parser.Response>` object, or
5541
``None`` if there is not enough data in the buffer.
5642
"""
57-
pass
43+
# Begin by copying the data out of the buffer. This is necessary
44+
# because as much as possible we want to use the built-in bytestring
45+
# methods, rather than looping over the data in Python.
46+
temp_buffer = buffer.tobytes()
47+
48+
index = temp_buffer.find(b'\n')
49+
if index == -1:
50+
return None
51+
52+
version, status, reason = temp_buffer[0:index].split(None, 2)
53+
if not version.startswith(b'HTTP/1.'):
54+
raise ParseError("Not HTTP/1.X!")
55+
56+
minor_version = int(version[7:])
57+
status = int(status)
58+
reason = memoryview(reason.strip())
59+
60+
# Chomp the newline.
61+
index += 1
62+
63+
# Now, parse the headers out.
64+
end_index = index
65+
headers = []
66+
67+
while True:
68+
end_index = temp_buffer.find(b'\n', index)
69+
if end_index == -1:
70+
return None
71+
elif (end_index - index) <= 1:
72+
# Chomp the newline
73+
end_index += 1
74+
break
75+
76+
name, value = temp_buffer[index:end_index].split(b':', 1)
77+
value = value.strip()
78+
headers.append((memoryview(name), memoryview(value)))
79+
index = end_index + 1
80+
81+
resp = Response(status, reason, minor_version, headers, end_index)
82+
return resp

test/test_parser.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
test_http11_parser.py
4+
~~~~~~~~~~~~~~~~~~~~~
5+
6+
Unit tests for hyper's HTTP/1.1 parser.
7+
"""
8+
import hyper
9+
import pytest
10+
11+
from hyper.http11.parser import Parser, ParseError
12+
13+
14+
class TestHTTP11Parser(object):
15+
def test_basic_http11_parsing(self):
16+
data = (
17+
b"HTTP/1.1 200 OK\r\n"
18+
b"Server: h2o\r\n"
19+
b"content-length: 2\r\n"
20+
b"Vary: accept-encoding\r\n"
21+
b"\r\n"
22+
b"hi"
23+
)
24+
m = memoryview(data)
25+
26+
c = Parser()
27+
r = c.parse_response(m)
28+
29+
assert r
30+
assert r.status == 200
31+
assert r.msg.tobytes() == b'OK'
32+
assert r.minor_version == 1
33+
34+
expected_headers = [
35+
(b'Server', b'h2o'),
36+
(b'content-length', b'2'),
37+
(b'Vary', b'accept-encoding'),
38+
]
39+
40+
assert len(expected_headers) == len(r.headers)
41+
42+
for (n1, v1), (n2, v2) in zip(r.headers, expected_headers):
43+
assert n1.tobytes() == n2
44+
assert v1.tobytes() == v2
45+
46+
assert r.consumed == len(data) - 2
47+
48+
def test_short_response_one(self):
49+
data = (
50+
b"HTTP/1.1 200 OK\r\n"
51+
b"Server: h2o\r\n"
52+
b"content"
53+
)
54+
m = memoryview(data)
55+
56+
c = Parser()
57+
r = c.parse_response(m)
58+
59+
assert r is None
60+
61+
def test_short_response_two(self):
62+
data = (
63+
b"HTTP/1.1 "
64+
)
65+
m = memoryview(data)
66+
67+
c = Parser()
68+
r = c.parse_response(m)
69+
70+
assert r is None
71+
72+
def test_invalid_version(self):
73+
data = (
74+
b"SQP/1 200 OK\r\n"
75+
b"Server: h2o\r\n"
76+
b"content-length: 2\r\n"
77+
b"Vary: accept-encoding\r\n"
78+
b"\r\n"
79+
b"hi"
80+
)
81+
m = memoryview(data)
82+
83+
c = Parser()
84+
85+
with pytest.raises(ParseError):
86+
c.parse_response(m)

0 commit comments

Comments
 (0)