Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9da047b

Browse files
committed
Issue #7776: Fix ``Host:'' header and reconnection when using http.client.HTTPConnection.set_tunnel().
Patch by Nikolaus Rath.
1 parent b814057 commit 9da047b

3 files changed

Lines changed: 100 additions & 26 deletions

File tree

Lib/http/client.py

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -747,22 +747,38 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
747747
self._tunnel_port = None
748748
self._tunnel_headers = {}
749749

750-
self._set_hostport(host, port)
750+
(self.host, self.port) = self._get_hostport(host, port)
751+
752+
# This is stored as an instance variable to allow unit
753+
# tests to replace it with a suitable mockup
754+
self._create_connection = socket.create_connection
751755

752756
def set_tunnel(self, host, port=None, headers=None):
753-
""" Sets up the host and the port for the HTTP CONNECT Tunnelling.
757+
"""Set up host and port for HTTP CONNECT tunnelling.
758+
759+
In a connection that uses HTTP CONNECT tunneling, the host passed to the
760+
constructor is used as a proxy server that relays all communication to
761+
the endpoint passed to `set_tunnel`. This done by sending an HTTP
762+
CONNECT request to the proxy server when the connection is established.
754763
755-
The headers argument should be a mapping of extra HTTP headers
756-
to send with the CONNECT request.
764+
This method must be called before the HTML connection has been
765+
established.
766+
767+
The headers argument should be a mapping of extra HTTP headers to send
768+
with the CONNECT request.
757769
"""
770+
771+
if self.sock:
772+
raise RuntimeError("Can't set up tunnel for established connection")
773+
758774
self._tunnel_host = host
759775
self._tunnel_port = port
760776
if headers:
761777
self._tunnel_headers = headers
762778
else:
763779
self._tunnel_headers.clear()
764780

765-
def _set_hostport(self, host, port):
781+
def _get_hostport(self, host, port):
766782
if port is None:
767783
i = host.rfind(':')
768784
j = host.rfind(']') # ipv6 addresses have [...]
@@ -779,15 +795,16 @@ def _set_hostport(self, host, port):
779795
port = self.default_port
780796
if host and host[0] == '[' and host[-1] == ']':
781797
host = host[1:-1]
782-
self.host = host
783-
self.port = port
798+
799+
return (host, port)
784800

785801
def set_debuglevel(self, level):
786802
self.debuglevel = level
787803

788804
def _tunnel(self):
789-
self._set_hostport(self._tunnel_host, self._tunnel_port)
790-
connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (self.host, self.port)
805+
(host, port) = self._get_hostport(self._tunnel_host,
806+
self._tunnel_port)
807+
connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port)
791808
connect_bytes = connect_str.encode("ascii")
792809
self.send(connect_bytes)
793810
for header, value in self._tunnel_headers.items():
@@ -815,8 +832,9 @@ def _tunnel(self):
815832

816833
def connect(self):
817834
"""Connect to the host and port specified in __init__."""
818-
self.sock = socket.create_connection((self.host,self.port),
819-
self.timeout, self.source_address)
835+
self.sock = self._create_connection((self.host,self.port),
836+
self.timeout, self.source_address)
837+
820838
if self._tunnel_host:
821839
self._tunnel()
822840

@@ -985,22 +1003,29 @@ def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0):
9851003
netloc_enc = netloc.encode("idna")
9861004
self.putheader('Host', netloc_enc)
9871005
else:
1006+
if self._tunnel_host:
1007+
host = self._tunnel_host
1008+
port = self._tunnel_port
1009+
else:
1010+
host = self.host
1011+
port = self.port
1012+
9881013
try:
989-
host_enc = self.host.encode("ascii")
1014+
host_enc = host.encode("ascii")
9901015
except UnicodeEncodeError:
991-
host_enc = self.host.encode("idna")
1016+
host_enc = host.encode("idna")
9921017

9931018
# As per RFC 273, IPv6 address should be wrapped with []
9941019
# when used as Host header
9951020

996-
if self.host.find(':') >= 0:
1021+
if host.find(':') >= 0:
9971022
host_enc = b'[' + host_enc + b']'
9981023

999-
if self.port == self.default_port:
1024+
if port == self.default_port:
10001025
self.putheader('Host', host_enc)
10011026
else:
10021027
host_enc = host_enc.decode("ascii")
1003-
self.putheader('Host', "%s:%s" % (host_enc, self.port))
1028+
self.putheader('Host', "%s:%s" % (host_enc, port))
10041029

10051030
# note: we are assuming that clients will not attempt to set these
10061031
# headers since *this* library must deal with the
@@ -1193,19 +1218,19 @@ def __init__(self, host, port=None, key_file=None, cert_file=None,
11931218
def connect(self):
11941219
"Connect to a host on a given (SSL) port."
11951220

1196-
sock = socket.create_connection((self.host, self.port),
1197-
self.timeout, self.source_address)
1221+
super().connect()
11981222

11991223
if self._tunnel_host:
1200-
self.sock = sock
1201-
self._tunnel()
1224+
server_hostname = self._tunnel_host
1225+
else:
1226+
server_hostname = self.host
1227+
sni_hostname = server_hostname if ssl.HAS_SNI else None
12021228

1203-
server_hostname = self.host if ssl.HAS_SNI else None
1204-
self.sock = self._context.wrap_socket(sock,
1205-
server_hostname=server_hostname)
1229+
self.sock = self._context.wrap_socket(self.sock,
1230+
server_hostname=sni_hostname)
12061231
if not self._context.check_hostname and self._check_hostname:
12071232
try:
1208-
ssl.match_hostname(self.sock.getpeercert(), self.host)
1233+
ssl.match_hostname(self.sock.getpeercert(), server_hostname)
12091234
except Exception:
12101235
self.sock.shutdown(socket.SHUT_RDWR)
12111236
self.sock.close()

Lib/test/test_httplib.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
HOST = support.HOST
2222

2323
class FakeSocket:
24-
def __init__(self, text, fileclass=io.BytesIO):
24+
def __init__(self, text, fileclass=io.BytesIO, host=None, port=None):
2525
if isinstance(text, str):
2626
text = text.encode("ascii")
2727
self.text = text
2828
self.fileclass = fileclass
2929
self.data = b''
3030
self.sendall_calls = 0
31+
self.host = host
32+
self.port = port
3133

3234
def sendall(self, data):
3335
self.sendall_calls += 1
@@ -38,6 +40,9 @@ def makefile(self, mode, bufsize=None):
3840
raise client.UnimplementedFileMode()
3941
return self.fileclass(self.text)
4042

43+
def close(self):
44+
pass
45+
4146
class EPipeSocket(FakeSocket):
4247

4348
def __init__(self, text, pipe_trigger):
@@ -970,10 +975,51 @@ def test_getting_header_defaultint(self):
970975
header = self.resp.getheader('No-Such-Header',default=42)
971976
self.assertEqual(header, 42)
972977

978+
class TunnelTests(TestCase):
979+
980+
def test_connect(self):
981+
response_text = (
982+
'HTTP/1.0 200 OK\r\n\r\n' # Reply to CONNECT
983+
'HTTP/1.1 200 OK\r\n' # Reply to HEAD
984+
'Content-Length: 42\r\n\r\n'
985+
)
986+
987+
def create_connection(address, timeout=None, source_address=None):
988+
return FakeSocket(response_text, host=address[0],
989+
port=address[1])
990+
991+
conn = client.HTTPConnection('proxy.com')
992+
conn._create_connection = create_connection
993+
994+
# Once connected, we shouldn't be able to tunnel anymore
995+
conn.connect()
996+
self.assertRaises(RuntimeError, conn.set_tunnel,
997+
'destination.com')
998+
999+
# But if we close the connection, we're good
1000+
conn.close()
1001+
conn.set_tunnel('destination.com')
1002+
conn.request('HEAD', '/', '')
1003+
1004+
self.assertEqual(conn.sock.host, 'proxy.com')
1005+
self.assertEqual(conn.sock.port, 80)
1006+
self.assertTrue(b'CONNECT destination.com' in conn.sock.data)
1007+
self.assertTrue(b'Host: destination.com' in conn.sock.data)
1008+
1009+
# This test should be removed when CONNECT gets the HTTP/1.1 blessing
1010+
self.assertTrue(b'Host: proxy.com' not in conn.sock.data)
1011+
1012+
conn.close()
1013+
conn.request('PUT', '/', '')
1014+
self.assertEqual(conn.sock.host, 'proxy.com')
1015+
self.assertEqual(conn.sock.port, 80)
1016+
self.assertTrue(b'CONNECT destination.com' in conn.sock.data)
1017+
self.assertTrue(b'Host: destination.com' in conn.sock.data)
1018+
9731019
def test_main(verbose=None):
9741020
support.run_unittest(HeaderTests, OfflineTest, BasicTest, TimeoutTest,
9751021
HTTPSTest, RequestBodyTest, SourceAddressTest,
976-
HTTPResponseTest)
1022+
HTTPResponseTest, TunnelTests)
9771023

9781024
if __name__ == '__main__':
9791025
test_main()

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ Core and Builtins
3333
Library
3434
-------
3535

36+
- Issue #7776: Fix ``Host:'' header and reconnection when using
37+
http.client.HTTPConnection.set_tunnel(). Patch by Nikolaus Rath.
38+
3639
- Issue #20968: unittest.mock.MagicMock now supports division.
3740
Patch by Johannes Baiter.
3841

0 commit comments

Comments
 (0)