1
1
# SPDX-FileCopyrightText: 2023 Python Software Foundation
2
2
# SPDX-License-Identifier: Python-2.0
3
3
4
+ # https://github.com/python/cpython/blob/31c9f3ced293492b38e784c17c4befe425da5dab/Lib/http/client.py
5
+
4
6
r"""HTTP/1.1 client library
5
7
6
8
<intro stuff goes here>
@@ -154,6 +156,13 @@ def _encode(data, name='data'):
154
156
"""Call data.encode("latin-1") but show a better error message."""
155
157
return data .encode ()
156
158
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
157
166
158
167
def _read_headers (fp ):
159
168
"""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):
210
219
# happen if a self.fp.read() is done (without a size) whether
211
220
# self.fp is buffered or not. So, no self.fp.read() by
212
221
# clients unless they know what they are doing.
213
- self .fp = sock
222
+ self .fp = sock . makefile ( "rb" )
214
223
self .debuglevel = debuglevel
215
224
self ._method = method
216
225
@@ -373,8 +382,7 @@ def _close_conn(self):
373
382
374
383
def close (self ):
375
384
try :
376
- # super().close() # set "closed" flag
377
- pass
385
+ super ().close () # set "closed" flag
378
386
finally :
379
387
if self .fp :
380
388
self ._close_conn ()
@@ -618,17 +626,10 @@ def read1(self, n=-1):
618
626
self ._close_conn ()
619
627
elif self .length is not None :
620
628
self .length -= len (result )
629
+ if not self .length :
630
+ self ._close_conn ()
621
631
return result
622
632
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
-
632
633
def readline (self , limit = - 1 ):
633
634
if self .fp is None or self ._method == "HEAD" :
634
635
return b""
@@ -642,6 +643,8 @@ def readline(self, limit=-1):
642
643
self ._close_conn ()
643
644
elif self .length is not None :
644
645
self .length -= len (result )
646
+ if not self .length :
647
+ self ._close_conn ()
645
648
return result
646
649
647
650
def _read1_chunked (self , n ):
@@ -658,19 +661,6 @@ def _read1_chunked(self, n):
658
661
raise IncompleteRead (b"" )
659
662
return read
660
663
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
-
674
664
def fileno (self ):
675
665
return self .fp .fileno ()
676
666
@@ -810,6 +800,7 @@ def __init__(self, host, port=None, timeout=-1,
810
800
self ._tunnel_host = None
811
801
self ._tunnel_port = None
812
802
self ._tunnel_headers = {}
803
+ self ._raw_proxy_headers = None
813
804
814
805
(self .host , self .port ) = self ._get_hostport (host , port )
815
806
@@ -822,27 +813,39 @@ def __init__(self, host, port=None, timeout=-1,
822
813
def set_tunnel (self , host , port = None , headers = None ):
823
814
"""Set up host and port for HTTP CONNECT tunnelling.
824
815
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
828
819
CONNECT request to the proxy server when the connection is established.
829
820
830
821
This method must be called before the HTTP connection has been
831
822
established.
832
823
833
824
The headers argument should be a mapping of extra HTTP headers to send
834
825
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.
835
833
"""
836
834
837
835
if self .sock :
838
836
raise RuntimeError ("Can't set up tunnel for established connection" )
839
837
840
838
self ._tunnel_host , self ._tunnel_port = self ._get_hostport (host , port )
841
839
if headers :
842
- self ._tunnel_headers = headers
840
+ self ._tunnel_headers = headers . copy ()
843
841
else :
844
842
self ._tunnel_headers .clear ()
845
843
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
+
846
849
def _get_hostport (self , host , port ):
847
850
if port is None :
848
851
i = host .rfind (':' )
@@ -858,17 +861,24 @@ def _get_hostport(self, host, port):
858
861
host = host [:i ]
859
862
else :
860
863
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 ]
863
866
864
867
return (host , port )
865
868
866
869
def set_debuglevel (self , level ):
867
870
self .debuglevel = level
868
871
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
+
869
877
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" ))
872
882
headers = [connect ]
873
883
for header , value in self ._tunnel_headers .items ():
874
884
headers .append (f"{ header } : { value } \r \n " .encode ("latin-1" ))
@@ -883,24 +893,33 @@ def _tunnel(self):
883
893
try :
884
894
(version , code , message ) = response ._read_status ()
885
895
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
+
886
902
if code != http .HTTPStatus .OK :
887
903
self .close ()
888
904
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
898
905
899
- if self .debuglevel > 0 :
900
- print ('header:' , line .decode ())
901
906
finally :
902
907
response .close ()
903
908
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
+
904
923
def connect (self ):
905
924
"""Connect to the host and port specified in __init__."""
906
925
self .sock = self ._create_connection (
@@ -931,7 +950,7 @@ def close(self):
931
950
response .close ()
932
951
933
952
def send (self , data ):
934
- """Send ` data' to the server.
953
+ """Send ' data' to the server.
935
954
``data`` can be a string object, a bytes object, an array object, a
936
955
file-like object that supports a .read() method, or an iterable object.
937
956
"""
@@ -956,14 +975,14 @@ def send(self, data):
956
975
break
957
976
if encode :
958
977
datablock = datablock .encode ("iso-8859-1" )
959
- self .sock .write (datablock )
978
+ self .sock .sendall (datablock )
960
979
return
961
980
try :
962
- self .sock .write (data )
981
+ self .sock .sendall (data )
963
982
except TypeError :
964
983
try :
965
984
for d in data :
966
- self .sock .write (d )
985
+ self .sock .sendall (d )
967
986
except TypeError :
968
987
raise TypeError ("data should be a bytes-like object "
969
988
"or an iterable, got %r" % type (data ))
@@ -1047,10 +1066,10 @@ def putrequest(self, method, url, skip_host=False,
1047
1066
skip_accept_encoding = False ):
1048
1067
"""Send a request to the server.
1049
1068
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
1054
1073
'Accept-Encoding:' header
1055
1074
"""
1056
1075
@@ -1121,7 +1140,7 @@ def putrequest(self, method, url, skip_host=False,
1121
1140
netloc_enc = netloc .encode ("ascii" )
1122
1141
except UnicodeEncodeError :
1123
1142
netloc_enc = netloc .encode ("idna" )
1124
- self .putheader ('Host' , netloc_enc )
1143
+ self .putheader ('Host' , _strip_ipv6_iface ( netloc_enc ) )
1125
1144
else :
1126
1145
if self ._tunnel_host :
1127
1146
host = self ._tunnel_host
@@ -1137,9 +1156,9 @@ def putrequest(self, method, url, skip_host=False,
1137
1156
1138
1157
# As per RFC 273, IPv6 address should be wrapped with []
1139
1158
# 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 )
1143
1162
1144
1163
if port == self .default_port :
1145
1164
self .putheader ('Host' , host_enc )
@@ -1341,8 +1360,7 @@ def getresponse(self):
1341
1360
1342
1361
if response .will_close :
1343
1362
# this effectively passes the connection to the response
1344
- # self.close()
1345
- pass
1363
+ self .close ()
1346
1364
else :
1347
1365
# remember this, so we can tell when it is complete
1348
1366
self .__response = response
@@ -1364,44 +1382,15 @@ class HTTPSConnection(HTTPConnection):
1364
1382
1365
1383
# XXX Should key_file and cert_file be deprecated in favour of context?
1366
1384
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 ):
1371
1388
super (HTTPSConnection , self ).__init__ (host , port , timeout ,
1372
1389
source_address ,
1373
1390
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
1382
1391
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 )
1402
1393
self ._context = context
1403
- # if check_hostname is not None:
1404
- # self._context.check_hostname = check_hostname
1405
1394
1406
1395
def connect (self ):
1407
1396
"Connect to a host on a given (SSL) port."
0 commit comments