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

Commit e7852f3

Browse files
committed
Initial HTTP/1.1 work
1 parent 8cab7eb commit e7852f3

File tree

4 files changed

+267
-0
lines changed

4 files changed

+267
-0
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ omit =
44
hyper/httplib_compat.py
55
hyper/ssl_compat.py
66
hyper/http20/hpack_compat.py
7+
hyper/http11/*

hyper/http11/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
hyper/http11
4+
~~~~~~~~~~~~
5+
6+
The HTTP/1.1 submodule that powers hyper.
7+
"""

hyper/http11/connection.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
hyper/http11/connection
4+
~~~~~~~~~~~~~~~~~~~~~~~
5+
6+
Objects that build hyper's connection-level HTTP/1.1 abstraction.
7+
"""
8+
import io
9+
import logging
10+
import socket
11+
12+
from .response import HTTP11Response
13+
from ..http20.bufsocket import BufferedSocket
14+
15+
log = logging.getLogger(__name__)
16+
17+
18+
class HTTP11Connection(object):
19+
"""
20+
An object representing a single HTTP/1.1 connection to a server.
21+
22+
:param host: The host to connect to. This may be an IP address or a
23+
hostname, and optionally may include a port: for example,
24+
``'twitter.com'``, ``'twitter.com:443'`` or ``'127.0.0.1'``.
25+
:param port: (optional) The port to connect to. If not provided and one also
26+
isn't provided in the ``host`` parameter, defaults to 443.
27+
"""
28+
def __init__(self, host, port):
29+
if port is None:
30+
try:
31+
self.host, self.port = host.split(':')
32+
self.port = int(self.port)
33+
except ValueError:
34+
self.host, self.port = host, 443
35+
else:
36+
self.host, self.port = host, port
37+
38+
self._sock = None
39+
40+
#: The size of the in-memory buffer used to store data from the
41+
#: network. This is used as a performance optimisation. Increase buffer
42+
#: size to improve performance: decrease it to conserve memory.
43+
#: Defaults to 64kB.
44+
self.network_buffer_size = 65536
45+
46+
def connect(self):
47+
"""
48+
Connect to the server specified when the object was created. This is a
49+
no-op if we're already connected.
50+
51+
:returns: Nothing.
52+
"""
53+
if self._sock is None:
54+
sock = socket.create_connection((self.host, self.port), 5)
55+
self._sock = BufferedSocket(sock, self.network_buffer_size)
56+
57+
return
58+
59+
def request(self, method, url, body=None, headers={}):
60+
"""
61+
This will send a request to the server using the HTTP request method
62+
``method`` and the selector ``url``. If the ``body`` argument is
63+
present, it should be string or bytes object of data to send after the
64+
headers are finished. Strings are encoded as UTF-8. To use other
65+
encodings, pass a bytes object. The Content-Length header is set to the
66+
length of the body field.
67+
68+
:param method: The request method, e.g. ``'GET'``.
69+
:param url: The URL to contact, e.g. ``'/path/segment'``.
70+
:param body: (optional) The request body to send. Must be a bytestring
71+
or a file-like object.
72+
:param headers: (optional) The headers to send on the request.
73+
:returns: Nothing.
74+
"""
75+
if self._sock is None:
76+
self.connect()
77+
78+
# In this initial implementation, let's just write straight to the
79+
# socket. We'll fix this up as we go.
80+
# TODO: Fix fix fix.
81+
self._sock.send(b' '.join([method, url, b'HTTP/1.1\r\n']))
82+
83+
for name, value in headers.items():
84+
self._sock.send(name)
85+
self._sock.send(b': ')
86+
self._sock.send(value)
87+
self._sock.send(b'\r\n')
88+
89+
self._sock.send(b'\r\n')
90+
91+
if body:
92+
# TODO: Come back here to support non-string bodies.
93+
self._sock.send(body)
94+
95+
return
96+
97+
def get_response(self):
98+
"""
99+
Returns a response object.
100+
101+
This is an early beta, so the response object is pretty stupid. That's
102+
ok, we'll fix it later.
103+
"""
104+
headers = {}
105+
106+
# First read the header line and drop it on the floor.
107+
self._sock.readline()
108+
109+
while True:
110+
line = self._sock.readline().tobytes()
111+
if len(line) <= 2:
112+
break
113+
114+
name, val = line.split(b':', 1)
115+
val = val.lstrip().rstrip(b'\r\n')
116+
headers[name] = val
117+
118+
return HTTP11Response(headers, self._sock)

hyper/http11/response.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
hyper/http20/response
4+
~~~~~~~~~~~~~~~~~~~~~
5+
6+
Contains the HTTP/2 equivalent of the HTTPResponse object defined in
7+
httplib/http.client.
8+
"""
9+
import logging
10+
11+
log = logging.getLogger(__name__)
12+
13+
14+
class DeflateDecoder(object):
15+
"""
16+
This is a decoding object that wraps ``zlib`` and is used for decoding
17+
deflated content.
18+
19+
This rationale for the existence of this object is pretty unpleasant.
20+
The HTTP RFC specifies that 'deflate' is a valid content encoding. However,
21+
the spec _meant_ the zlib encoding form. Unfortunately, people who didn't
22+
read the RFC very carefully actually implemented a different form of
23+
'deflate'. Insanely, ``zlib`` handles them using two wbits values. This is
24+
such a mess it's hard to adequately articulate.
25+
26+
This class was lovingly borrowed from the excellent urllib3 library under
27+
license: see NOTICES. If you ever see @shazow, you should probably buy him
28+
a drink or something.
29+
"""
30+
def __init__(self):
31+
self._first_try = True
32+
self._data = b''
33+
self._obj = zlib.decompressobj(zlib.MAX_WBITS)
34+
35+
def __getattr__(self, name):
36+
return getattr(self._obj, name)
37+
38+
def decompress(self, data):
39+
if not self._first_try:
40+
return self._obj.decompress(data)
41+
42+
self._data += data
43+
try:
44+
return self._obj.decompress(data)
45+
except zlib.error:
46+
self._first_try = False
47+
self._obj = zlib.decompressobj(-zlib.MAX_WBITS)
48+
try:
49+
return self.decompress(self._data)
50+
finally:
51+
self._data = None
52+
53+
54+
class HTTP11Response(object):
55+
"""
56+
An ``HTTP11Response`` wraps the HTTP/1.1 response from the server. It
57+
provides access to the response headers and the entity body. The response
58+
is an iterable object and can be used in a with statement.
59+
"""
60+
def __init__(self, headers, sock):
61+
#: The reason phrase returned by the server.
62+
self.reason = ''
63+
64+
#: The status code returned by the server.
65+
self.status = 0
66+
67+
# The response headers. These are determined upon creation, assigned
68+
# once, and never assigned again.
69+
self._headers = headers
70+
71+
# The response trailers. These are always intially ``None``.
72+
self._trailers = None
73+
74+
# The socket this response is being sent over.
75+
self._sock = sock
76+
77+
# We always read in one-data-frame increments from the stream, so we
78+
# may need to buffer some for incomplete reads.
79+
self._data_buffer = b''
80+
81+
def read(self, amt=None, decode_content=True):
82+
"""
83+
Reads the response body, or up to the next ``amt`` bytes.
84+
85+
:param amt: (optional) The amount of data to read. If not provided, all
86+
the data will be read from the response.
87+
:param decode_content: (optional) If ``True``, will transparently
88+
decode the response data.
89+
:returns: The read data. Note that if ``decode_content`` is set to
90+
``True``, the actual amount of data returned may be different to
91+
the amount requested.
92+
"""
93+
# For now, just read what we're asked, unless we're not asked:
94+
# then, read content-length. This obviously doesn't work longer term,
95+
# we need to do some content-length processing there.
96+
if amt is None:
97+
amt = self.headers.get(b'content-length', 0)
98+
99+
# Return early if we've lost our connection.
100+
if self._sock is None:
101+
return b''
102+
103+
data = self._sock.read(amt)
104+
105+
# We may need to decode the body.
106+
if decode_content and self._decompressobj and data:
107+
data = self._decompressobj.decompress(data)
108+
109+
# If we're at the end of the request, we have some cleaning up to do.
110+
# Close the stream, and if necessary flush the buffer.
111+
if decode_content and self._decompressobj:
112+
data += self._decompressobj.flush()
113+
114+
# We're at the end. Close the connection.
115+
if not data:
116+
self.close()
117+
118+
return data
119+
120+
def fileno(self):
121+
"""
122+
Return the ``fileno`` of the underlying socket. This function is
123+
currently not implemented.
124+
"""
125+
raise NotImplementedError("Not currently implemented.")
126+
127+
def close(self):
128+
"""
129+
Close the response. In effect this closes the backing HTTP/2 stream.
130+
131+
:returns: Nothing.
132+
"""
133+
self._sock = None
134+
135+
# The following methods implement the context manager protocol.
136+
def __enter__(self):
137+
return self
138+
139+
def __exit__(self, *args):
140+
self.close()
141+
return False # Never swallow exceptions.

0 commit comments

Comments
 (0)