This repository was archived by the owner on Jan 13, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 197
Expand file tree
/
Copy pathresponse.py
More file actions
239 lines (189 loc) · 7.89 KB
/
Copy pathresponse.py
File metadata and controls
239 lines (189 loc) · 7.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# -*- coding: utf-8 -*-
"""
hyper/http20/response
~~~~~~~~~~~~~~~~~~~~~
Contains the HTTP/2 equivalent of the HTTPResponse object defined in
httplib/http.client.
"""
import logging
import zlib
import brotli
from ..common.decoder import DeflateDecoder
from ..common.headers import HTTPHeaderMap
from ..common.util import HTTPVersion
log = logging.getLogger(__name__)
def strip_headers(headers):
"""
Strips the headers attached to the instance of any header beginning
with a colon that ``hyper`` doesn't understand. This method logs at
warning level about the deleted headers, for discoverability.
"""
# Convert to list to ensure that we don't mutate the headers while
# we iterate over them.
for name in list(headers.keys()):
if name.startswith(b':'):
del headers[name]
decompressors = {
b'gzip': lambda: zlib.decompressobj(16 + zlib.MAX_WBITS),
b'br': brotli.Decompressor,
b'deflate': DeflateDecoder
}
class HTTP20Response(object):
"""
An ``HTTP20Response`` wraps the HTTP/2 response from the server. It
provides access to the response headers and the entity body. The response
is an iterable object and can be used in a with statement (though due to
the persistent connections used in HTTP/2 this has no effect, and is done
soley for compatibility).
"""
version = HTTPVersion.http20
_decompressobj = None
def __init__(self, headers, stream):
#: The reason phrase returned by the server. This is not used in
#: HTTP/2, and so is always the empty string.
self.reason = ''
status = headers[b':status'][0]
strip_headers(headers)
#: The status code returned by the server.
self.status = int(status)
#: The response headers. These are determined upon creation, assigned
#: once, and never assigned again.
self.headers = headers
# The response trailers. These are always intially ``None``.
self._trailers = None
# The stream this response is being sent over.
self._stream = stream
# We always read in one-data-frame increments from the stream, so we
# may need to buffer some for incomplete reads.
self._data_buffer = b''
# This object is used for decompressing gzipped request bodies. Right
# now we only support gzip because that's all the RFC mandates of us.
# Later we'll add support for more encodings.
# This 16 + MAX_WBITS nonsense is to force gzip. See this
# Stack Overflow answer for more:
# http://stackoverflow.com/a/2695466/1401686
for c in self.headers.get(b'content-encoding', []):
if c in decompressors:
self._decompressobj = decompressors.get(c)()
break
@property
def trailers(self):
"""
Trailers on the HTTP message, if any.
.. warning:: Note that this property requires that the stream is
totally exhausted. This means that, if you have not
completely read from the stream, all stream data will be
read into memory.
"""
if self._trailers is None:
self._trailers = self._stream.gettrailers() or HTTPHeaderMap()
strip_headers(self._trailers)
return self._trailers
def read(self, amt=None, decode_content=True):
"""
Reads the response body, or up to the next ``amt`` bytes.
:param amt: (optional) The amount of data to read. If not provided, all
the data will be read from the response.
:param decode_content: (optional) If ``True``, will transparently
decode the response data.
:returns: The read data. Note that if ``decode_content`` is set to
``True``, the actual amount of data returned may be different to
the amount requested.
"""
if amt is not None and amt <= len(self._data_buffer):
data = self._data_buffer[:amt]
self._data_buffer = self._data_buffer[amt:]
response_complete = False
elif amt is not None:
read_amt = amt - len(self._data_buffer)
self._data_buffer += self._stream._read(read_amt)
data = self._data_buffer[:amt]
self._data_buffer = self._data_buffer[amt:]
response_complete = len(data) < amt
else:
data = b''.join([self._data_buffer, self._stream._read()])
response_complete = True
# We may need to decode the body.
if decode_content and self._decompressobj and data:
data = self._decompressobj.decompress(data)
# If we're at the end of the request, we have some cleaning up to do.
# Close the stream, and if necessary flush the buffer.
if response_complete:
if decode_content and self._decompressobj:
data += self._decompressobj.flush()
if self._stream.response_headers:
self.headers.merge(self._stream.response_headers)
# We're at the end, close the connection.
if response_complete:
self.close()
return data
def read_chunked(self, decode_content=True):
"""
Reads chunked transfer encoded bodies. This method returns a generator:
each iteration of which yields one data frame *unless* the frames
contain compressed data and ``decode_content`` is ``True``, in which
case it yields whatever the decompressor provides for each chunk.
.. warning:: This may yield the empty string, without that being the
end of the body!
"""
while True:
data = self._stream._read_one_frame()
if data is None:
break
if decode_content and self._decompressobj:
data = self._decompressobj.decompress(data)
yield data
if decode_content and self._decompressobj:
yield self._decompressobj.flush()
self.close()
return
def fileno(self):
"""
Return the ``fileno`` of the underlying socket. This function is
currently not implemented.
"""
raise NotImplementedError("Not currently implemented.")
def close(self):
"""
Close the response. In effect this closes the backing HTTP/2 stream.
:returns: Nothing.
"""
self._stream.close()
# The following methods implement the context manager protocol.
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
return False # Never swallow exceptions.
class HTTP20Push(object):
"""
Represents a request-response pair sent by the server through the server
push mechanism.
"""
def __init__(self, request_headers, stream):
#: The scheme of the simulated request
self.scheme = request_headers[b':scheme'][0]
#: The method of the simulated request (must be safe and cacheable,
#: e.g. GET)
self.method = request_headers[b':method'][0]
#: The authority of the simulated request (usually host:port)
self.authority = request_headers[b':authority'][0]
#: The path of the simulated request
self.path = request_headers[b':path'][0]
strip_headers(request_headers)
#: The headers the server attached to the simulated request.
self.request_headers = request_headers
self._stream = stream
def get_response(self):
"""
Get the pushed response provided by the server.
:returns: A :class:`HTTP20Response <hyper.HTTP20Response>` object
representing the pushed response.
"""
return HTTP20Response(self._stream.getheaders(), self._stream)
def cancel(self):
"""
Cancel the pushed response and close the stream.
:returns: Nothing.
"""
self._stream.close(8) # CANCEL