omlish 0.0.0.dev368__py3-none-any.whl → 0.0.0.dev369__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omlish/__about__.py +2 -2
- omlish/http/coro/client/__init__.py +0 -0
- omlish/http/coro/client/client.py +795 -0
- omlish/http/coro/client/errors.py +57 -0
- omlish/http/coro/client/headers.py +99 -0
- omlish/http/coro/client/io.py +50 -0
- omlish/http/coro/client/response.py +445 -0
- omlish/http/coro/client/status.py +86 -0
- omlish/http/coro/client/validation.py +109 -0
- omlish/http/coro/server/__init__.py +0 -0
- omlish/http/coro/{fdio.py → server/fdio.py} +6 -6
- omlish/http/coro/{server.py → server/server.py} +13 -13
- omlish/http/coro/{simple.py → server/simple.py} +16 -16
- omlish/http/coro/{sockets.py → server/sockets.py} +4 -4
- {omlish-0.0.0.dev368.dist-info → omlish-0.0.0.dev369.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev368.dist-info → omlish-0.0.0.dev369.dist-info}/RECORD +20 -11
- {omlish-0.0.0.dev368.dist-info → omlish-0.0.0.dev369.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev368.dist-info → omlish-0.0.0.dev369.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev368.dist-info → omlish-0.0.0.dev369.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev368.dist-info → omlish-0.0.0.dev369.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,795 @@
|
|
1
|
+
# @omlish-lite
|
2
|
+
# ruff: noqa: UP006 UP007 UP043 UP045
|
3
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
4
|
+
# --------------------------------------------
|
5
|
+
#
|
6
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
7
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
8
|
+
# documentation.
|
9
|
+
#
|
10
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
11
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
12
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
13
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
14
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
|
15
|
+
# alone or in any derivative version prepared by Licensee.
|
16
|
+
#
|
17
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
18
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
19
|
+
# any such work a brief summary of the changes made to Python.
|
20
|
+
#
|
21
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
22
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
23
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
24
|
+
# RIGHTS.
|
25
|
+
#
|
26
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
27
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
28
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
29
|
+
#
|
30
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
31
|
+
#
|
32
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
33
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
34
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
35
|
+
#
|
36
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
37
|
+
# License Agreement.
|
38
|
+
"""
|
39
|
+
https://github.com/python/cpython/blob/9b335cc8104dd83a5a1343dc649d1f3606682098/Lib/http/client.py
|
40
|
+
"""
|
41
|
+
import collections.abc
|
42
|
+
import email.parser
|
43
|
+
import enum
|
44
|
+
import http
|
45
|
+
import io
|
46
|
+
import typing as ta
|
47
|
+
import urllib.parse
|
48
|
+
|
49
|
+
from ....lite.check import check
|
50
|
+
from .errors import CoroHttpClientErrors
|
51
|
+
from .headers import CoroHttpClientHeaders
|
52
|
+
from .io import CoroHttpClientIo
|
53
|
+
from .response import CoroHttpClientResponse
|
54
|
+
from .status import CoroHttpClientStatusLine
|
55
|
+
from .validation import CoroHttpClientValidation
|
56
|
+
|
57
|
+
|
58
|
+
##
|
59
|
+
|
60
|
+
|
61
|
+
class CoroHttpClientConnection(
|
62
|
+
CoroHttpClientErrors,
|
63
|
+
CoroHttpClientIo,
|
64
|
+
):
|
65
|
+
"""
|
66
|
+
HTTPConnection goes through a number of "states", which define when a client may legally make another request or
|
67
|
+
fetch the response for a particular request. This diagram details these state transitions:
|
68
|
+
|
69
|
+
(null)
|
70
|
+
|
|
71
|
+
| HTTPConnection()
|
72
|
+
v
|
73
|
+
Idle
|
74
|
+
|
|
75
|
+
| put_request()
|
76
|
+
v
|
77
|
+
Request-started
|
78
|
+
|
|
79
|
+
| ( put_header() )* end_headers()
|
80
|
+
v
|
81
|
+
Request-sent
|
82
|
+
|______________________________
|
83
|
+
| | get_response() raises
|
84
|
+
| response = get_response() | ConnectionError
|
85
|
+
v v
|
86
|
+
Unread-response Idle
|
87
|
+
[Response-headers-read]
|
88
|
+
|_____________________
|
89
|
+
| |
|
90
|
+
| response.read() | put_request()
|
91
|
+
v v
|
92
|
+
Idle Req-started-unread-response
|
93
|
+
______/|
|
94
|
+
/ |
|
95
|
+
response.read() | | ( put_header() )* end_headers()
|
96
|
+
v v
|
97
|
+
Request-started Req-sent-unread-response
|
98
|
+
|
|
99
|
+
| response.read()
|
100
|
+
v
|
101
|
+
Request-sent
|
102
|
+
|
103
|
+
This diagram presents the following rules:
|
104
|
+
-- a second request may not be started until {response-headers-read}
|
105
|
+
-- a response [object] cannot be retrieved until {request-sent}
|
106
|
+
-- there is no differentiation between an unread response body and a
|
107
|
+
partially read response body
|
108
|
+
|
109
|
+
Logical State _state _response
|
110
|
+
------------- ------- ----------
|
111
|
+
Idle IDLE None
|
112
|
+
Request-started REQ_STARTED None
|
113
|
+
Request-sent REQ_SENT None
|
114
|
+
Unread-response IDLE <response_class>
|
115
|
+
Req-started-unread-response REQ_STARTED <response_class>
|
116
|
+
Req-sent-unread-response REQ_SENT <response_class>
|
117
|
+
"""
|
118
|
+
|
119
|
+
_http_version = 11
|
120
|
+
_http_version_str = 'HTTP/1.1'
|
121
|
+
|
122
|
+
http_port: ta.ClassVar[int] = 80
|
123
|
+
https_port: ta.ClassVar[int] = 443
|
124
|
+
|
125
|
+
default_port = http_port
|
126
|
+
|
127
|
+
class _NOT_SET: # noqa
|
128
|
+
def __new__(cls, *args, **kwargs): # noqa
|
129
|
+
raise NotImplementedError
|
130
|
+
|
131
|
+
class _State(enum.Enum):
|
132
|
+
IDLE = 'Idle'
|
133
|
+
REQ_STARTED = 'Request-started'
|
134
|
+
REQ_SENT = 'Request-sent'
|
135
|
+
|
136
|
+
def __init__(
|
137
|
+
self,
|
138
|
+
host: str,
|
139
|
+
port: ta.Optional[int] = None,
|
140
|
+
*,
|
141
|
+
timeout: ta.Union[float, ta.Type[_NOT_SET], None] = _NOT_SET,
|
142
|
+
source_address: ta.Optional[str] = None,
|
143
|
+
block_size: int = 8192,
|
144
|
+
auto_open: bool = True,
|
145
|
+
) -> None:
|
146
|
+
super().__init__()
|
147
|
+
|
148
|
+
self._timeout = timeout
|
149
|
+
self._source_address = source_address
|
150
|
+
self._block_size = block_size
|
151
|
+
self._auto_open = auto_open
|
152
|
+
|
153
|
+
self._connected = False
|
154
|
+
self._buffer: ta.List[bytes] = []
|
155
|
+
self._response: ta.Optional[CoroHttpClientResponse] = None
|
156
|
+
self._state = self._State.IDLE
|
157
|
+
self._method: ta.Optional[str] = None
|
158
|
+
|
159
|
+
self._tunnel_host: ta.Optional[str] = None
|
160
|
+
self._tunnel_port: ta.Optional[int] = None
|
161
|
+
self._tunnel_headers: ta.Dict[str, str] = {}
|
162
|
+
self._raw_proxy_headers: ta.Optional[ta.Sequence[bytes]] = None
|
163
|
+
|
164
|
+
(self._host, self._port) = self._get_hostport(host, port)
|
165
|
+
|
166
|
+
CoroHttpClientValidation.validate_host(self._host)
|
167
|
+
|
168
|
+
#
|
169
|
+
|
170
|
+
def _get_hostport(self, host: str, port: ta.Optional[int]) -> ta.Tuple[str, int]:
|
171
|
+
if port is None:
|
172
|
+
i = host.rfind(':')
|
173
|
+
j = host.rfind(']') # ipv6 addresses have [...]
|
174
|
+
if i > j:
|
175
|
+
try:
|
176
|
+
port = int(host[i + 1:])
|
177
|
+
except ValueError:
|
178
|
+
if host[i + 1:] == '': # http://foo.com:/ == http://foo.com/
|
179
|
+
port = self.default_port
|
180
|
+
else:
|
181
|
+
raise self.InvalidUrlError(f"non-numeric port: '{host[i + 1:]}'") from None
|
182
|
+
host = host[:i]
|
183
|
+
else:
|
184
|
+
port = self.default_port
|
185
|
+
|
186
|
+
if host and host[0] == '[' and host[-1] == ']':
|
187
|
+
host = host[1:-1]
|
188
|
+
|
189
|
+
return (host, port)
|
190
|
+
|
191
|
+
def _wrap_ipv6(self, ip: bytes) -> bytes:
|
192
|
+
if b':' in ip and ip[0] != b'['[0]:
|
193
|
+
return b'[' + ip + b']'
|
194
|
+
return ip
|
195
|
+
|
196
|
+
#
|
197
|
+
|
198
|
+
def set_tunnel(
|
199
|
+
self,
|
200
|
+
host: str,
|
201
|
+
port: ta.Optional[int] = None,
|
202
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None,
|
203
|
+
) -> None:
|
204
|
+
"""
|
205
|
+
Set up host and port for HTTP CONNECT tunnelling.
|
206
|
+
|
207
|
+
In a connection that uses HTTP CONNECT tunnelling, the host passed to the constructor is used as a proxy server
|
208
|
+
that relays all communication to the endpoint passed to `set_tunnel`. This done by sending an HTTP CONNECT
|
209
|
+
request to the proxy server when the connection is established.
|
210
|
+
|
211
|
+
This method must be called before the HTTP connection has been established.
|
212
|
+
|
213
|
+
The headers argument should be a mapping of extra HTTP headers to send with the CONNECT request.
|
214
|
+
|
215
|
+
As HTTP/1.1 is used for HTTP CONNECT tunnelling request, as per the RFC
|
216
|
+
(https://tools.ietf.org/html/rfc7231#section-4.3.6), a HTTP Host: header must be provided, matching the
|
217
|
+
authority-form of the request target provided as the destination for the CONNECT request. If a HTTP Host: header
|
218
|
+
is not provided via the headers argument, one is generated and transmitted automatically.
|
219
|
+
"""
|
220
|
+
|
221
|
+
if self._connected:
|
222
|
+
raise RuntimeError("Can't set up tunnel for established connection")
|
223
|
+
|
224
|
+
self._tunnel_host, self._tunnel_port = self._get_hostport(host, port)
|
225
|
+
|
226
|
+
if headers:
|
227
|
+
self._tunnel_headers = dict(headers)
|
228
|
+
else:
|
229
|
+
self._tunnel_headers.clear()
|
230
|
+
|
231
|
+
if not any(header.lower() == 'host' for header in self._tunnel_headers):
|
232
|
+
encoded_host = self._tunnel_host.encode('idna').decode('ascii')
|
233
|
+
self._tunnel_headers['Host'] = f'{encoded_host}:{self._tunnel_port:d}'
|
234
|
+
|
235
|
+
def _tunnel(self) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
|
236
|
+
connect = b'CONNECT %s:%d %s\r\n' % (
|
237
|
+
self._wrap_ipv6(check.not_none(self._tunnel_host).encode('idna')),
|
238
|
+
check.not_none(self._tunnel_port),
|
239
|
+
self._http_version_str.encode('ascii'),
|
240
|
+
)
|
241
|
+
|
242
|
+
headers = [connect]
|
243
|
+
for header, value in self._tunnel_headers.items():
|
244
|
+
headers.append(f'{header}: {value}\r\n'.encode('latin-1'))
|
245
|
+
headers.append(b'\r\n')
|
246
|
+
|
247
|
+
# Making a single send() call instead of one per line encourages the host OS to use a more optimal packet size
|
248
|
+
# instead of potentially emitting a series of small packets.
|
249
|
+
yield from self.send(b''.join(headers))
|
250
|
+
del headers
|
251
|
+
|
252
|
+
try:
|
253
|
+
(version, code, message) = (yield from CoroHttpClientStatusLine.read())
|
254
|
+
except self.BadStatusLineError: # noqa
|
255
|
+
# self._close_conn()
|
256
|
+
raise
|
257
|
+
|
258
|
+
self._raw_proxy_headers = yield from CoroHttpClientHeaders.read_headers()
|
259
|
+
|
260
|
+
if code != http.HTTPStatus.OK:
|
261
|
+
yield from self.close()
|
262
|
+
raise OSError(f'Tunnel connection failed: {code} {message.strip()}')
|
263
|
+
|
264
|
+
def get_proxy_response_headers(self) -> ta.Optional[email.message.Message]:
|
265
|
+
"""
|
266
|
+
Returns a dictionary with the headers of the response received from the proxy server to the CONNECT request sent
|
267
|
+
to set the tunnel.
|
268
|
+
|
269
|
+
If the CONNECT request was not sent, the method returns None.
|
270
|
+
"""
|
271
|
+
|
272
|
+
return (
|
273
|
+
CoroHttpClientHeaders.parse_header_lines(self._raw_proxy_headers)
|
274
|
+
if self._raw_proxy_headers is not None
|
275
|
+
else None
|
276
|
+
)
|
277
|
+
|
278
|
+
#
|
279
|
+
|
280
|
+
def connect(self) -> ta.Generator[CoroHttpClientIo.Io, None, None]:
|
281
|
+
"""Connect to the host and port specified in __init__."""
|
282
|
+
|
283
|
+
if self._connected:
|
284
|
+
return
|
285
|
+
|
286
|
+
check.none((yield self.ConnectIo(
|
287
|
+
((self._host, self._port),),
|
288
|
+
dict(
|
289
|
+
source_address=self._source_address,
|
290
|
+
**(dict(timeout=self._timeout) if self._timeout is not self._NOT_SET else {}),
|
291
|
+
),
|
292
|
+
)))
|
293
|
+
|
294
|
+
self._connected = True
|
295
|
+
|
296
|
+
if self._tunnel_host:
|
297
|
+
yield from self._tunnel()
|
298
|
+
|
299
|
+
#
|
300
|
+
|
301
|
+
def close(self) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
|
302
|
+
"""Close the connection to the HTTP server."""
|
303
|
+
|
304
|
+
self._state = self._State.IDLE
|
305
|
+
|
306
|
+
try:
|
307
|
+
if self._connected:
|
308
|
+
yield self.CloseIo() # Close it manually... there may be other refs
|
309
|
+
self._connected = False
|
310
|
+
|
311
|
+
finally:
|
312
|
+
response = self._response
|
313
|
+
if response:
|
314
|
+
self._response = None
|
315
|
+
response.close()
|
316
|
+
|
317
|
+
#
|
318
|
+
|
319
|
+
@staticmethod
|
320
|
+
def _is_text_io(stream: ta.Any) -> bool:
|
321
|
+
"""Test whether a file-like object is a text or a binary stream."""
|
322
|
+
|
323
|
+
return isinstance(stream, io.TextIOBase)
|
324
|
+
|
325
|
+
def send(self, data: ta.Any) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
|
326
|
+
"""
|
327
|
+
Send `data' to the server. ``data`` can be a string object, a bytes object, an array object, a file-like object
|
328
|
+
that supports a .read() method, or an iterable object.
|
329
|
+
"""
|
330
|
+
|
331
|
+
if not self._connected:
|
332
|
+
if self._auto_open:
|
333
|
+
yield from self.connect()
|
334
|
+
else:
|
335
|
+
raise self.NotConnectedError
|
336
|
+
|
337
|
+
check.state(self._connected)
|
338
|
+
|
339
|
+
if hasattr(data, 'read'):
|
340
|
+
encode = self._is_text_io(data)
|
341
|
+
while data_block := data.read(self._block_size):
|
342
|
+
if encode:
|
343
|
+
data_block = data_block.encode('iso-8859-1')
|
344
|
+
check.none((yield self.WriteIo(data_block)))
|
345
|
+
return
|
346
|
+
|
347
|
+
if isinstance(data, (bytes, bytearray)):
|
348
|
+
check.none((yield self.WriteIo(data)))
|
349
|
+
|
350
|
+
elif isinstance(data, collections.abc.Iterable):
|
351
|
+
for d in data:
|
352
|
+
check.none((yield self.WriteIo(d)))
|
353
|
+
|
354
|
+
else:
|
355
|
+
raise TypeError(f'data should be a bytes-like object or an iterable, got {type(data)!r}') from None
|
356
|
+
|
357
|
+
def _output(self, s: bytes) -> None:
|
358
|
+
"""
|
359
|
+
Add a line of output to the current request buffer.
|
360
|
+
|
361
|
+
Assumes that the line does *not* end with \\r\\n.
|
362
|
+
"""
|
363
|
+
|
364
|
+
self._buffer.append(s)
|
365
|
+
|
366
|
+
def _read_readable(self, readable: ta.Union[ta.IO, ta.TextIO]) -> ta.Iterator[bytes]:
|
367
|
+
while data := readable.read(self._block_size):
|
368
|
+
if isinstance(data, str):
|
369
|
+
yield data.encode('iso-8859-1')
|
370
|
+
else:
|
371
|
+
yield data
|
372
|
+
|
373
|
+
def _send_output(
|
374
|
+
self,
|
375
|
+
message_body: ta.Optional[ta.Any] = None,
|
376
|
+
encode_chunked: bool = False,
|
377
|
+
) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
|
378
|
+
"""
|
379
|
+
Send the currently buffered request and clear the buffer.
|
380
|
+
|
381
|
+
Appends an extra \\r\\n to the buffer. A message_body may be specified, to be appended to the request.
|
382
|
+
"""
|
383
|
+
|
384
|
+
self._buffer.extend((b'', b''))
|
385
|
+
msg = b'\r\n'.join(self._buffer)
|
386
|
+
del self._buffer[:]
|
387
|
+
yield from self.send(msg)
|
388
|
+
|
389
|
+
chunks: ta.Iterable[bytes]
|
390
|
+
if message_body is not None:
|
391
|
+
# Create a consistent interface to message_body
|
392
|
+
if hasattr(message_body, 'read'):
|
393
|
+
# Let file-like take precedence over byte-like. This is needed to allow the current position of mmap'ed
|
394
|
+
# files to be taken into account.
|
395
|
+
chunks = self._read_readable(message_body)
|
396
|
+
|
397
|
+
else:
|
398
|
+
try:
|
399
|
+
# This is solely to check to see if message_body implements the buffer API. it /would/ be easier to
|
400
|
+
# capture if PyObject_CheckBuffer was exposed to Python.
|
401
|
+
memoryview(message_body)
|
402
|
+
|
403
|
+
except TypeError:
|
404
|
+
try:
|
405
|
+
chunks = iter(message_body)
|
406
|
+
except TypeError as e:
|
407
|
+
raise TypeError(
|
408
|
+
f'message_body should be a bytes-like object or an iterable, got {type(message_body)!r}',
|
409
|
+
) from e
|
410
|
+
|
411
|
+
else:
|
412
|
+
# The object implements the buffer interface and can be passed directly into socket methods
|
413
|
+
chunks = (message_body,)
|
414
|
+
|
415
|
+
for chunk in chunks:
|
416
|
+
if not chunk:
|
417
|
+
continue
|
418
|
+
|
419
|
+
if encode_chunked and self._http_version == 11:
|
420
|
+
# Chunked encoding
|
421
|
+
chunk = f'{len(chunk):X}\r\n'.encode('ascii') + chunk + b'\r\n'
|
422
|
+
yield from self.send(chunk)
|
423
|
+
|
424
|
+
if encode_chunked and self._http_version == 11:
|
425
|
+
# End chunked transfer
|
426
|
+
yield from self.send(b'0\r\n\r\n')
|
427
|
+
|
428
|
+
#
|
429
|
+
|
430
|
+
@staticmethod
|
431
|
+
def _strip_ipv6_iface(enc_name: bytes) -> bytes:
|
432
|
+
"""Remove interface scope from IPv6 address."""
|
433
|
+
|
434
|
+
enc_name, percent, _ = enc_name.partition(b'%')
|
435
|
+
if percent:
|
436
|
+
check.state(enc_name.startswith(b'['))
|
437
|
+
enc_name += b']'
|
438
|
+
return enc_name
|
439
|
+
|
440
|
+
def put_request(
|
441
|
+
self,
|
442
|
+
method: str,
|
443
|
+
url: str,
|
444
|
+
*,
|
445
|
+
skip_host: bool = False,
|
446
|
+
skip_accept_encoding: bool = False,
|
447
|
+
) -> None:
|
448
|
+
"""
|
449
|
+
Send a request to the server.
|
450
|
+
|
451
|
+
`method' specifies an HTTP request method, e.g. 'GET'.
|
452
|
+
`url' specifies the object being requested, e.g. '/index.html'.
|
453
|
+
`skip_host' if True does not add automatically a 'Host:' header
|
454
|
+
`skip_accept_encoding' if True does not add automatically an 'Accept-Encoding:' header
|
455
|
+
"""
|
456
|
+
|
457
|
+
# If a prior response has been completed, then forget about it.
|
458
|
+
if self._response and self._response.is_closed():
|
459
|
+
self._response = None
|
460
|
+
|
461
|
+
# In certain cases, we cannot issue another request on this connection.
|
462
|
+
# this occurs when:
|
463
|
+
# 1) we are in the process of sending a request. (_CS_REQ_STARTED)
|
464
|
+
# 2) a response to a previous request has signalled that it is going to close the connection upon completion.
|
465
|
+
# 3) the headers for the previous response have not been read, thus we cannot determine whether point (2) is
|
466
|
+
# true. (_CS_REQ_SENT)
|
467
|
+
#
|
468
|
+
# If there is no prior response, then we can request at will.
|
469
|
+
#
|
470
|
+
# If point (2) is true, then we will have passed the socket to the response (effectively meaning, "there is no
|
471
|
+
# prior response"), and will open a new one when a new request is made.
|
472
|
+
#
|
473
|
+
# Note: if a prior response exists, then we *can* start a new request. We are not allowed to begin fetching the
|
474
|
+
# response to this new request, however, until that prior response is complete.
|
475
|
+
#
|
476
|
+
if self._state == self._State.IDLE:
|
477
|
+
self._state = self._State.REQ_STARTED
|
478
|
+
else:
|
479
|
+
raise self.CannotSendRequestError(self._state)
|
480
|
+
|
481
|
+
CoroHttpClientValidation.validate_method(method)
|
482
|
+
|
483
|
+
# Save the method for use later in the response phase
|
484
|
+
self._method = method
|
485
|
+
|
486
|
+
url = url or '/'
|
487
|
+
CoroHttpClientValidation.validate_path(url)
|
488
|
+
|
489
|
+
request = f'{method} {url} {self._http_version_str}'
|
490
|
+
|
491
|
+
self._output(self._encode_request(request))
|
492
|
+
|
493
|
+
if self._http_version == 11:
|
494
|
+
# Issue some standard headers for better HTTP/1.1 compliance
|
495
|
+
|
496
|
+
if not skip_host:
|
497
|
+
# This header is issued *only* for HTTP/1.1 connections. more specifically, this means it is only issued
|
498
|
+
# when the client uses the new HTTPConnection() class. backwards-compat clients will be using HTTP/1.0
|
499
|
+
# and those clients may be issuing this header themselves. we should NOT issue it twice; some web
|
500
|
+
# servers (such as Apache) barf when they see two Host: headers
|
501
|
+
|
502
|
+
# If we need a non-standard port,include it in the header. If the request is going through a proxy, but
|
503
|
+
# the host of the actual URL, not the host of the proxy.
|
504
|
+
netloc = ''
|
505
|
+
if url.startswith('http'):
|
506
|
+
netloc = urllib.parse.urlsplit(url).netloc
|
507
|
+
|
508
|
+
if netloc:
|
509
|
+
try:
|
510
|
+
netloc_enc = netloc.encode('ascii')
|
511
|
+
except UnicodeEncodeError:
|
512
|
+
netloc_enc = netloc.encode('idna')
|
513
|
+
self.put_header('Host', self._strip_ipv6_iface(netloc_enc))
|
514
|
+
else:
|
515
|
+
if self._tunnel_host:
|
516
|
+
host = self._tunnel_host
|
517
|
+
port = self._tunnel_port
|
518
|
+
else:
|
519
|
+
host = self._host
|
520
|
+
port = self._port
|
521
|
+
|
522
|
+
try:
|
523
|
+
host_enc = host.encode('ascii')
|
524
|
+
except UnicodeEncodeError:
|
525
|
+
host_enc = host.encode('idna')
|
526
|
+
|
527
|
+
# As per RFC 273, IPv6 address should be wrapped with [] when used as Host header
|
528
|
+
host_enc = self._wrap_ipv6(host_enc)
|
529
|
+
if ':' in host:
|
530
|
+
host_enc = self._strip_ipv6_iface(host_enc)
|
531
|
+
|
532
|
+
if port == self.default_port:
|
533
|
+
self.put_header('Host', host_enc)
|
534
|
+
else:
|
535
|
+
self.put_header('Host', f"{host_enc.decode('ascii')}:{port}")
|
536
|
+
|
537
|
+
# NOTE: We are assuming that clients will not attempt to set these headers since *this* library must deal
|
538
|
+
# with the consequences. this also means that when the supporting libraries are updated to recognize other
|
539
|
+
# forms, then this code should be changed (removed or updated).
|
540
|
+
|
541
|
+
# We only want a Content-Encoding of "identity" since we don't support encodings such as x-gzip or
|
542
|
+
# x-deflate.
|
543
|
+
if not skip_accept_encoding:
|
544
|
+
self.put_header('Accept-Encoding', 'identity')
|
545
|
+
|
546
|
+
# We can accept "chunked" Transfer-Encodings, but no others.
|
547
|
+
# NOTE: no TE header implies *only* "chunked"
|
548
|
+
#self.put_header('TE', 'chunked')
|
549
|
+
|
550
|
+
# If TE is supplied in the header, then it must appear in a Connection header.
|
551
|
+
#self.put_header('Connection', 'TE')
|
552
|
+
|
553
|
+
else:
|
554
|
+
# For HTTP/1.0, the server will assume "not chunked"
|
555
|
+
pass
|
556
|
+
|
557
|
+
def _encode_request(self, request: str) -> bytes:
|
558
|
+
# ASCII also helps prevent CVE-2019-9740.
|
559
|
+
return request.encode('ascii')
|
560
|
+
|
561
|
+
#
|
562
|
+
|
563
|
+
def put_header(self, header: ta.Union[str, bytes], *values: ta.Union[bytes, str, int]) -> None:
|
564
|
+
"""
|
565
|
+
Send a request header line to the server.
|
566
|
+
|
567
|
+
For example: h.put_header('Accept', 'text/html')
|
568
|
+
"""
|
569
|
+
|
570
|
+
if self._state != self._State.REQ_STARTED:
|
571
|
+
raise self.CannotSendHeaderError
|
572
|
+
|
573
|
+
if hasattr(header, 'encode'):
|
574
|
+
bh = header.encode('ascii')
|
575
|
+
else:
|
576
|
+
bh = header
|
577
|
+
|
578
|
+
CoroHttpClientValidation.validate_header_name(bh)
|
579
|
+
|
580
|
+
bvs = []
|
581
|
+
for one_value in values:
|
582
|
+
if hasattr(one_value, 'encode'):
|
583
|
+
bv = one_value.encode('latin-1')
|
584
|
+
elif isinstance(one_value, int):
|
585
|
+
bv = str(one_value).encode('ascii')
|
586
|
+
else:
|
587
|
+
bv = one_value
|
588
|
+
|
589
|
+
CoroHttpClientValidation.validate_header_value(bv)
|
590
|
+
bvs.append(bv)
|
591
|
+
|
592
|
+
value = b'\r\n\t'.join(bvs)
|
593
|
+
bh = bh + b': ' + value
|
594
|
+
self._output(bh)
|
595
|
+
|
596
|
+
def end_headers(
|
597
|
+
self,
|
598
|
+
message_body: ta.Optional[ta.Any] = None,
|
599
|
+
*,
|
600
|
+
encode_chunked: bool = False,
|
601
|
+
) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
|
602
|
+
"""
|
603
|
+
Indicate that the last header line has been sent to the server.
|
604
|
+
|
605
|
+
This method sends the request to the server. The optional message_body argument can be used to pass a message
|
606
|
+
body associated with the request.
|
607
|
+
"""
|
608
|
+
|
609
|
+
if self._state == self._State.REQ_STARTED:
|
610
|
+
self._state = self._State.REQ_SENT
|
611
|
+
else:
|
612
|
+
raise self.CannotSendHeaderError
|
613
|
+
|
614
|
+
yield from self._send_output(message_body, encode_chunked=encode_chunked)
|
615
|
+
|
616
|
+
#
|
617
|
+
|
618
|
+
def request(
|
619
|
+
self,
|
620
|
+
method: str,
|
621
|
+
url: str,
|
622
|
+
body: ta.Optional[ta.Any] = None,
|
623
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None,
|
624
|
+
*,
|
625
|
+
encode_chunked: bool = False,
|
626
|
+
) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
|
627
|
+
"""Send a complete request to the server."""
|
628
|
+
|
629
|
+
yield from self._send_request(method, url, body, dict(headers or {}), encode_chunked)
|
630
|
+
|
631
|
+
_METHODS_EXPECTING_BODY: ta.ClassVar[ta.Container[str]] = {'PATCH', 'POST', 'PUT'}
|
632
|
+
|
633
|
+
@classmethod
|
634
|
+
def _get_content_length(
|
635
|
+
cls,
|
636
|
+
body: ta.Optional[ta.Any],
|
637
|
+
method: str,
|
638
|
+
) -> ta.Optional[int]:
|
639
|
+
"""
|
640
|
+
Get the content-length based on the body.
|
641
|
+
|
642
|
+
If the body is None, we set Content-Length: 0 for methods that expect a body (RFC 7230, Section 3.3.2). We also
|
643
|
+
set the Content-Length for any method if the body is a str or bytes-like object and not a file.
|
644
|
+
"""
|
645
|
+
|
646
|
+
if body is None:
|
647
|
+
# Do an explicit check for not None here to distinguish between unset and set but empty
|
648
|
+
if method.upper() in cls._METHODS_EXPECTING_BODY:
|
649
|
+
return 0
|
650
|
+
else:
|
651
|
+
return None
|
652
|
+
|
653
|
+
if hasattr(body, 'read'):
|
654
|
+
# File-like object.
|
655
|
+
return None
|
656
|
+
|
657
|
+
try:
|
658
|
+
# Does it implement the buffer protocol (bytes, bytearray, array)?
|
659
|
+
mv = memoryview(body)
|
660
|
+
return mv.nbytes
|
661
|
+
except TypeError:
|
662
|
+
pass
|
663
|
+
|
664
|
+
if isinstance(body, str):
|
665
|
+
return len(body)
|
666
|
+
|
667
|
+
return None
|
668
|
+
|
669
|
+
@staticmethod
|
670
|
+
def _encode(data: str, name: str = 'data') -> bytes:
|
671
|
+
"""Call data.encode("latin-1") but show a better error message."""
|
672
|
+
|
673
|
+
try:
|
674
|
+
return data.encode('latin-1')
|
675
|
+
|
676
|
+
except UnicodeEncodeError as err:
|
677
|
+
raise UnicodeEncodeError(
|
678
|
+
err.encoding,
|
679
|
+
err.object,
|
680
|
+
err.start,
|
681
|
+
err.end,
|
682
|
+
"%s (%.20r) is not valid Latin-1. Use %s.encode('utf-8') if you want to send it encoded in UTF-8." % ( # noqa
|
683
|
+
name.title(),
|
684
|
+
data[err.start:err.end],
|
685
|
+
name,
|
686
|
+
),
|
687
|
+
) from None
|
688
|
+
|
689
|
+
def _send_request(
|
690
|
+
self,
|
691
|
+
method: str,
|
692
|
+
url: str,
|
693
|
+
body: ta.Optional[ta.Any],
|
694
|
+
headers: ta.Mapping[str, str],
|
695
|
+
encode_chunked: bool,
|
696
|
+
) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
|
697
|
+
# Honor explicitly requested Host: and Accept-Encoding: headers.
|
698
|
+
header_names = frozenset(k.lower() for k in headers)
|
699
|
+
skips = {}
|
700
|
+
if 'host' in header_names:
|
701
|
+
skips['skip_host'] = True
|
702
|
+
if 'accept-encoding' in header_names:
|
703
|
+
skips['skip_accept_encoding'] = True
|
704
|
+
|
705
|
+
self.put_request(method, url, **skips)
|
706
|
+
|
707
|
+
# Chunked encoding will happen if HTTP/1.1 is used and either the caller passes encode_chunked=True or the
|
708
|
+
# following conditions hold:
|
709
|
+
# 1) Content-Length has not been explicitly set
|
710
|
+
# 2) The body is a file or iterable, but not a str or bytes-like
|
711
|
+
# 3) Transfer-Encoding has NOT been explicitly set by the caller
|
712
|
+
|
713
|
+
if 'content-length' not in header_names:
|
714
|
+
# Only chunk body if not explicitly set for backwards compatibility, assuming the client code is already
|
715
|
+
# handling the chunking
|
716
|
+
if 'transfer-encoding' not in header_names:
|
717
|
+
# If Content-Length cannot be automatically determined, fall back to chunked encoding
|
718
|
+
encode_chunked = False
|
719
|
+
content_length = self._get_content_length(body, method)
|
720
|
+
if content_length is None:
|
721
|
+
if body is not None:
|
722
|
+
encode_chunked = True
|
723
|
+
self.put_header('Transfer-Encoding', 'chunked')
|
724
|
+
else:
|
725
|
+
self.put_header('Content-Length', str(content_length))
|
726
|
+
else:
|
727
|
+
encode_chunked = False
|
728
|
+
|
729
|
+
for hdr, value in headers.items():
|
730
|
+
self.put_header(hdr, value)
|
731
|
+
|
732
|
+
if isinstance(body, str):
|
733
|
+
# RFC 2616 Section 3.7.1 says that text default has a default charset of iso-8859-1.
|
734
|
+
body = self._encode(body, 'body')
|
735
|
+
|
736
|
+
yield from self.end_headers(body, encode_chunked=encode_chunked)
|
737
|
+
|
738
|
+
#
|
739
|
+
|
740
|
+
def _new_response(self) -> CoroHttpClientResponse:
|
741
|
+
return CoroHttpClientResponse(check.not_none(self._method))
|
742
|
+
|
743
|
+
def get_response(self) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], CoroHttpClientResponse]:
|
744
|
+
"""
|
745
|
+
Get the response from the server.
|
746
|
+
|
747
|
+
If the HTTPConnection is in the correct state, returns an instance of HttpResponse or of whatever object is
|
748
|
+
returned by the response_class variable.
|
749
|
+
|
750
|
+
If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the
|
751
|
+
HTTP response indicates that the connection should be closed, then it will be closed before the response is
|
752
|
+
returned. When the connection is closed, the underlying socket is closed.
|
753
|
+
"""
|
754
|
+
|
755
|
+
# If a prior response has been completed, then forget about it.
|
756
|
+
if self._response and self._response.is_closed():
|
757
|
+
self._response = None
|
758
|
+
|
759
|
+
# If a prior response exists, then it must be completed (otherwise, we cannot read this response's header to
|
760
|
+
# determine the connection-close behavior).
|
761
|
+
#
|
762
|
+
# NOTE: If a prior response existed, but was connection-close, then the socket and response were made
|
763
|
+
# independent of this HTTPConnection object since a new request requires that we open a whole new connection.
|
764
|
+
#
|
765
|
+
# This means the prior response had one of two states:
|
766
|
+
# 1) will_close: this connection was reset and the prior socket and response operate independently
|
767
|
+
# 2) persistent: the response was retained and we await its is_closed() status to become true.
|
768
|
+
if self._state != self._State.REQ_SENT or self._response:
|
769
|
+
raise self.ResponseNotReadyError(self._state)
|
770
|
+
|
771
|
+
resp = self._new_response()
|
772
|
+
resp_state = resp._state # noqa
|
773
|
+
|
774
|
+
try:
|
775
|
+
try:
|
776
|
+
yield from resp._begin() # noqa
|
777
|
+
except ConnectionError:
|
778
|
+
yield from self.close()
|
779
|
+
raise
|
780
|
+
|
781
|
+
check.state(hasattr(resp_state, 'will_close'))
|
782
|
+
self._state = self._State.IDLE
|
783
|
+
|
784
|
+
if resp_state.will_close:
|
785
|
+
# This effectively passes the connection to the response
|
786
|
+
yield from self.close()
|
787
|
+
else:
|
788
|
+
# Remember this, so we can tell when it is complete
|
789
|
+
self._response = resp
|
790
|
+
|
791
|
+
return resp
|
792
|
+
|
793
|
+
except: # noqa
|
794
|
+
resp.close()
|
795
|
+
raise
|