Skip to content

Commit e9e1b5e

Browse files
committed
update http.client
1 parent 6c4bdd8 commit e9e1b5e

File tree

1 file changed

+80
-91
lines changed

1 file changed

+80
-91
lines changed

python-stdlib/http/client.py

Lines changed: 80 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# SPDX-FileCopyrightText: 2023 Python Software Foundation
22
# SPDX-License-Identifier: Python-2.0
33

4+
# https://github.com/python/cpython/blob/31c9f3ced293492b38e784c17c4befe425da5dab/Lib/http/client.py
5+
46
r"""HTTP/1.1 client library
57
68
<intro stuff goes here>
@@ -154,6 +156,13 @@ def _encode(data, name='data'):
154156
"""Call data.encode("latin-1") but show a better error message."""
155157
return data.encode()
156158

159+
def _strip_ipv6_iface(enc_name: bytes) -> bytes:
160+
"""Remove interface scope from IPv6 address."""
161+
enc_name, percent, _ = enc_name.partition(b"%")
162+
if percent:
163+
assert enc_name.startswith(b'['), enc_name
164+
enc_name += b']'
165+
return enc_name
157166

158167
def _read_headers(fp):
159168
"""Reads potential header lines into a list from a file pointer.
@@ -210,7 +219,7 @@ def __init__(self, sock, debuglevel=0, method=None, url=None):
210219
# happen if a self.fp.read() is done (without a size) whether
211220
# self.fp is buffered or not. So, no self.fp.read() by
212221
# clients unless they know what they are doing.
213-
self.fp = sock
222+
self.fp = sock.makefile("rb")
214223
self.debuglevel = debuglevel
215224
self._method = method
216225

@@ -373,8 +382,7 @@ def _close_conn(self):
373382

374383
def close(self):
375384
try:
376-
# super().close() # set "closed" flag
377-
pass
385+
super().close() # set "closed" flag
378386
finally:
379387
if self.fp:
380388
self._close_conn()
@@ -618,17 +626,10 @@ def read1(self, n=-1):
618626
self._close_conn()
619627
elif self.length is not None:
620628
self.length -= len(result)
629+
if not self.length:
630+
self._close_conn()
621631
return result
622632

623-
def peek(self, n=-1):
624-
# Having this enables IOBase.readline() to read more than one
625-
# byte at a time
626-
if self.fp is None or self._method == "HEAD":
627-
return b""
628-
if self.chunked:
629-
return self._peek_chunked(n)
630-
return self.fp.peek(n)
631-
632633
def readline(self, limit=-1):
633634
if self.fp is None or self._method == "HEAD":
634635
return b""
@@ -642,6 +643,8 @@ def readline(self, limit=-1):
642643
self._close_conn()
643644
elif self.length is not None:
644645
self.length -= len(result)
646+
if not self.length:
647+
self._close_conn()
645648
return result
646649

647650
def _read1_chunked(self, n):
@@ -658,19 +661,6 @@ def _read1_chunked(self, n):
658661
raise IncompleteRead(b"")
659662
return read
660663

661-
def _peek_chunked(self, n):
662-
# Strictly speaking, _get_chunk_left() may cause more than one read,
663-
# but that is ok, since that is to satisfy the chunked protocol.
664-
try:
665-
chunk_left = self._get_chunk_left()
666-
except IncompleteRead:
667-
return b'' # peek doesn't worry about protocol
668-
if chunk_left is None:
669-
return b'' # eof
670-
# peek is allowed to return more than requested. Just request the
671-
# entire chunk, and truncate what we get.
672-
return self.fp.peek(chunk_left)[:chunk_left]
673-
674664
def fileno(self):
675665
return self.fp.fileno()
676666

@@ -810,6 +800,7 @@ def __init__(self, host, port=None, timeout=-1,
810800
self._tunnel_host = None
811801
self._tunnel_port = None
812802
self._tunnel_headers = {}
803+
self._raw_proxy_headers = None
813804

814805
(self.host, self.port) = self._get_hostport(host, port)
815806

@@ -822,27 +813,39 @@ def __init__(self, host, port=None, timeout=-1,
822813
def set_tunnel(self, host, port=None, headers=None):
823814
"""Set up host and port for HTTP CONNECT tunnelling.
824815
825-
In a connection that uses HTTP CONNECT tunneling, the host passed to the
826-
constructor is used as a proxy server that relays all communication to
827-
the endpoint passed to `set_tunnel`. This done by sending an HTTP
816+
In a connection that uses HTTP CONNECT tunnelling, the host passed to
817+
the constructor is used as a proxy server that relays all communication
818+
to the endpoint passed to `set_tunnel`. This done by sending an HTTP
828819
CONNECT request to the proxy server when the connection is established.
829820
830821
This method must be called before the HTTP connection has been
831822
established.
832823
833824
The headers argument should be a mapping of extra HTTP headers to send
834825
with the CONNECT request.
826+
827+
As HTTP/1.1 is used for HTTP CONNECT tunnelling request, as per the RFC
828+
(https://tools.ietf.org/html/rfc7231#section-4.3.6), a HTTP Host:
829+
header must be provided, matching the authority-form of the request
830+
target provided as the destination for the CONNECT request. If a
831+
HTTP Host: header is not provided via the headers argument, one
832+
is generated and transmitted automatically.
835833
"""
836834

837835
if self.sock:
838836
raise RuntimeError("Can't set up tunnel for established connection")
839837

840838
self._tunnel_host, self._tunnel_port = self._get_hostport(host, port)
841839
if headers:
842-
self._tunnel_headers = headers
840+
self._tunnel_headers = headers.copy()
843841
else:
844842
self._tunnel_headers.clear()
845843

844+
if not any(header.lower() == "host" for header in self._tunnel_headers):
845+
encoded_host = self._tunnel_host.encode("idna").decode("ascii")
846+
self._tunnel_headers["Host"] = "%s:%d" % (
847+
encoded_host, self._tunnel_port)
848+
846849
def _get_hostport(self, host, port):
847850
if port is None:
848851
i = host.rfind(':')
@@ -858,17 +861,24 @@ def _get_hostport(self, host, port):
858861
host = host[:i]
859862
else:
860863
port = self.default_port
861-
if host and host[0] == '[' and host[-1] == ']':
862-
host = host[1:-1]
864+
if host and host[0] == '[' and host[-1] == ']':
865+
host = host[1:-1]
863866

864867
return (host, port)
865868

866869
def set_debuglevel(self, level):
867870
self.debuglevel = level
868871

872+
def _wrap_ipv6(self, ip):
873+
if b':' in ip and ip[0] != b'['[0]:
874+
return b"[" + ip + b"]"
875+
return ip
876+
869877
def _tunnel(self):
870-
connect = b"CONNECT %s:%d HTTP/1.0\r\n" % (
871-
self._tunnel_host.encode("ascii"), self._tunnel_port)
878+
connect = b"CONNECT %s:%d %s\r\n" % (
879+
self._wrap_ipv6(self._tunnel_host.encode("idna")),
880+
self._tunnel_port,
881+
self._http_vsn_str.encode("ascii"))
872882
headers = [connect]
873883
for header, value in self._tunnel_headers.items():
874884
headers.append(f"{header}: {value}\r\n".encode("latin-1"))
@@ -883,24 +893,33 @@ def _tunnel(self):
883893
try:
884894
(version, code, message) = response._read_status()
885895

896+
self._raw_proxy_headers = _read_headers(response.fp)
897+
898+
if self.debuglevel > 0:
899+
for header in self._raw_proxy_headers:
900+
print('header:', header.decode())
901+
886902
if code != http.HTTPStatus.OK:
887903
self.close()
888904
raise OSError(f"Tunnel connection failed: {code} {message.strip()}")
889-
while True:
890-
line = response.fp.readline(_MAXLINE + 1)
891-
if len(line) > _MAXLINE:
892-
raise LineTooLong("header line")
893-
if not line:
894-
# for sites which EOF without sending a trailer
895-
break
896-
if line in (b'\r\n', b'\n', b''):
897-
break
898905

899-
if self.debuglevel > 0:
900-
print('header:', line.decode())
901906
finally:
902907
response.close()
903908

909+
def get_proxy_response_headers(self):
910+
"""
911+
Returns a dictionary with the headers of the response
912+
received from the proxy server to the CONNECT request
913+
sent to set the tunnel.
914+
915+
If the CONNECT request was not sent, the method returns None.
916+
"""
917+
return (
918+
_parse_header_lines(self._raw_proxy_headers)
919+
if self._raw_proxy_headers is not None
920+
else None
921+
)
922+
904923
def connect(self):
905924
"""Connect to the host and port specified in __init__."""
906925
self.sock = self._create_connection(
@@ -931,7 +950,7 @@ def close(self):
931950
response.close()
932951

933952
def send(self, data):
934-
"""Send `data' to the server.
953+
"""Send 'data' to the server.
935954
``data`` can be a string object, a bytes object, an array object, a
936955
file-like object that supports a .read() method, or an iterable object.
937956
"""
@@ -956,14 +975,14 @@ def send(self, data):
956975
break
957976
if encode:
958977
datablock = datablock.encode("iso-8859-1")
959-
self.sock.write(datablock)
978+
self.sock.sendall(datablock)
960979
return
961980
try:
962-
self.sock.write(data)
981+
self.sock.sendall(data)
963982
except TypeError:
964983
try:
965984
for d in data:
966-
self.sock.write(d)
985+
self.sock.sendall(d)
967986
except TypeError:
968987
raise TypeError("data should be a bytes-like object "
969988
"or an iterable, got %r" % type(data))
@@ -1047,10 +1066,10 @@ def putrequest(self, method, url, skip_host=False,
10471066
skip_accept_encoding=False):
10481067
"""Send a request to the server.
10491068
1050-
`method' specifies an HTTP request method, e.g. 'GET'.
1051-
`url' specifies the object being requested, e.g. '/index.html'.
1052-
`skip_host' if True does not add automatically a 'Host:' header
1053-
`skip_accept_encoding' if True does not add automatically an
1069+
'method' specifies an HTTP request method, e.g. 'GET'.
1070+
'url' specifies the object being requested, e.g. '/index.html'.
1071+
'skip_host' if True does not add automatically a 'Host:' header
1072+
'skip_accept_encoding' if True does not add automatically an
10541073
'Accept-Encoding:' header
10551074
"""
10561075

@@ -1121,7 +1140,7 @@ def putrequest(self, method, url, skip_host=False,
11211140
netloc_enc = netloc.encode("ascii")
11221141
except UnicodeEncodeError:
11231142
netloc_enc = netloc.encode("idna")
1124-
self.putheader('Host', netloc_enc)
1143+
self.putheader('Host', _strip_ipv6_iface(netloc_enc))
11251144
else:
11261145
if self._tunnel_host:
11271146
host = self._tunnel_host
@@ -1137,9 +1156,9 @@ def putrequest(self, method, url, skip_host=False,
11371156

11381157
# As per RFC 273, IPv6 address should be wrapped with []
11391158
# when used as Host header
1140-
1141-
if host.find(':') >= 0:
1142-
host_enc = b'[' + host_enc + b']'
1159+
host_enc = self._wrap_ipv6(host_enc)
1160+
if ":" in host:
1161+
host_enc = _strip_ipv6_iface(host_enc)
11431162

11441163
if port == self.default_port:
11451164
self.putheader('Host', host_enc)
@@ -1341,8 +1360,7 @@ def getresponse(self):
13411360

13421361
if response.will_close:
13431362
# this effectively passes the connection to the response
1344-
# self.close()
1345-
pass
1363+
self.close()
13461364
else:
13471365
# remember this, so we can tell when it is complete
13481366
self.__response = response
@@ -1364,44 +1382,15 @@ class HTTPSConnection(HTTPConnection):
13641382

13651383
# XXX Should key_file and cert_file be deprecated in favour of context?
13661384

1367-
def __init__(self, host, port=None, key_file=None, cert_file=None,
1368-
timeout=-1,
1369-
source_address=None, *, context=None,
1370-
check_hostname=None, blocksize=8192):
1385+
def __init__(self, host, port=None,
1386+
*, timeout=-1,
1387+
source_address=None, context=None, blocksize=8192):
13711388
super(HTTPSConnection, self).__init__(host, port, timeout,
13721389
source_address,
13731390
blocksize=blocksize)
1374-
if (key_file is not None or cert_file is not None or
1375-
check_hostname is not None):
1376-
import warnings
1377-
warnings.warn("key_file, cert_file and check_hostname are "
1378-
"deprecated, use a custom context instead.",
1379-
DeprecationWarning, 2)
1380-
self.key_file = key_file
1381-
self.cert_file = cert_file
13821391
if context is None:
1383-
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
1384-
# send ALPN extension to indicate HTTP/1.1 protocol
1385-
# if self._http_vsn == 11:
1386-
# context.set_alpn_protocols(['http/1.1'])
1387-
# enable PHA for TLS 1.3 connections if available
1388-
# if context.post_handshake_auth is not None:
1389-
# context.post_handshake_auth = True
1390-
will_verify = context.verify_mode != ssl.CERT_NONE
1391-
if check_hostname is None:
1392-
check_hostname = False
1393-
if check_hostname and not will_verify:
1394-
raise ValueError("check_hostname needs a SSL context with "
1395-
"either CERT_OPTIONAL or CERT_REQUIRED")
1396-
if key_file or cert_file:
1397-
context.load_cert_chain(cert_file, key_file)
1398-
# cert and key file means the user wants to authenticate.
1399-
# enable TLS 1.3 PHA implicitly even for custom contexts.
1400-
if context.post_handshake_auth is not None:
1401-
context.post_handshake_auth = True
1392+
context = _create_https_context(self._http_vsn)
14021393
self._context = context
1403-
# if check_hostname is not None:
1404-
# self._context.check_hostname = check_hostname
14051394

14061395
def connect(self):
14071396
"Connect to a host on a given (SSL) port."

0 commit comments

Comments
 (0)