From 9f5ce0930ba7488c954b0d9cf018ddc4a37d3109 Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Mon, 16 Jul 2018 14:36:00 -0700 Subject: [PATCH 01/12] bpo-22708: Upgrade HTTP CONNECT to protocol HTTP/1.1 (GH-NNNN) Use protocol HTTP/1.1 when sending HTTP CONNECT tunnelling requests; generate Host: headers if one is not already provided (required by HTTP/1.1), convert IDN domains to punycode in HTTP CONNECT requests. --- Doc/library/http.client.rst | 12 ++ Lib/http/client.py | 33 +++- Lib/test/test_httplib.py | 147 ++++++++++++++++-- Misc/ACKS | 1 + .../2018-07-16-14-10-29.bpo-22708.592iRR.rst | 3 + 5 files changed, 173 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-07-16-14-10-29.bpo-22708.592iRR.rst diff --git a/Doc/library/http.client.rst b/Doc/library/http.client.rst index c4b7c79730f49a..51256f96595709 100644 --- a/Doc/library/http.client.rst +++ b/Doc/library/http.client.rst @@ -320,6 +320,13 @@ HTTPConnection Objects The headers argument should be a mapping of extra HTTP headers to send with the CONNECT request. + As HTTP/1.1 is used for HTTP CONNECT tunnelling request, `as per the RFC + `_, a HTTP ``Host:`` + header must be provided, matching the authority-form of the request target + provided as the destination for the CONNECT request. If a HTTP ``Host:`` + header is not provided via the headers argument, one is generated and + transmitted automatically. + For example, to tunnel through a HTTPS proxy server running locally on port 8080, we would pass the address of the proxy to the :class:`HTTPSConnection` constructor, and the address of the host that we eventually want to reach to @@ -332,6 +339,11 @@ HTTPConnection Objects .. versionadded:: 3.2 + .. versionchanged:: 3.7 + HTTP CONNECT tunnelling requests use protocol HTTP/1.1, upgraded from + protocol HTTP/1.0. ``Host:`` HTTP headers are mandatory for HTTP/1.1, so + one will be automatically generated and transmitted if not provided in + the headers argument. .. method:: HTTPConnection.connect() diff --git a/Lib/http/client.py b/Lib/http/client.py index 5aa178d7b127d9..664d778100c6de 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -68,6 +68,7 @@ Req-sent-unread-response _CS_REQ_SENT """ +import copy import email.parser import email.message import http @@ -845,11 +846,11 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, self._create_connection = socket.create_connection def set_tunnel(self, host, port=None, headers=None): - """Set up host and port for HTTP CONNECT tunnelling. + """Set up host and port for HTTP CONNECT tunneling. - In a connection that uses HTTP CONNECT tunneling, the host passed to the - constructor is used as a proxy server that relays all communication to - the endpoint passed to `set_tunnel`. This done by sending an HTTP + In a connection that uses HTTP CONNECT tunnelling, the host passed to + the constructor is used as a proxy server that relays all communication + to the endpoint passed to `set_tunnel`. This done by sending an HTTP CONNECT request to the proxy server when the connection is established. This method must be called before the HTML connection has been @@ -857,6 +858,13 @@ def set_tunnel(self, host, port=None, headers=None): The headers argument should be a mapping of extra HTTP headers to send with the CONNECT request. + + As HTTP/1.1 is used for HTTP CONNECT tunnelling request, as per the RFC + (https://tools.ietf.org/html/rfc7231#section-4.3.6), a HTTP Host: + header must be provided, matching the authority-form of the request + target provided as the destination for the CONNECT request. If a + HTTP Host: header is not provided via the headers argument, one + is generated and transmitted automatically. """ if self.sock: @@ -864,10 +872,19 @@ def set_tunnel(self, host, port=None, headers=None): self._tunnel_host, self._tunnel_port = self._get_hostport(host, port) if headers: - self._tunnel_headers = headers + self._tunnel_headers = copy.copy(headers) else: self._tunnel_headers.clear() + saw_host_header = False + for header in self._tunnel_headers.keys(): + if header.lower() == "host": + saw_host_header = True + if not saw_host_header: + encoded_host = self._tunnel_host.encode("idna").decode("ascii") + self._tunnel_headers["Host"] = "%s:%d" % ( + encoded_host, self._tunnel_port) + def _get_hostport(self, host, port): if port is None: i = host.rfind(':') @@ -892,9 +909,9 @@ def set_debuglevel(self, level): self.debuglevel = level def _tunnel(self): - connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (self._tunnel_host, - self._tunnel_port) - connect_bytes = connect_str.encode("ascii") + connect_bytes = b"CONNECT %s:%d %s\r\n" % ( + self._tunnel_host.encode("idna"), self._tunnel_port, + self._http_vsn_str.encode("ascii")) self.send(connect_bytes) for header, value in self._tunnel_headers.items(): header_str = "%s: %s\r\n" % (header, value) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index f816eac83b682d..0465eb8922dbb9 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1,3 +1,4 @@ +import copy import errno from http import client import io @@ -1864,11 +1865,12 @@ def test_getting_header_defaultint(self): class TunnelTests(TestCase): def setUp(self): response_text = ( - 'HTTP/1.0 200 OK\r\n\r\n' # Reply to CONNECT + 'HTTP/1.1 200 OK\r\n\r\n' # Reply to CONNECT 'HTTP/1.1 200 OK\r\n' # Reply to HEAD 'Content-Length: 42\r\n\r\n' ) self.host = 'proxy.com' + self.port = client.HTTP_PORT self.conn = client.HTTPConnection(self.host) self.conn._create_connection = self._create_connection(response_text) @@ -1880,15 +1882,45 @@ def create_connection(address, timeout=None, source_address=None): return FakeSocket(response_text, host=address[0], port=address[1]) return create_connection - def test_set_tunnel_host_port_headers(self): + def test_set_tunnel_host_port_headers_add_host_missing(self): tunnel_host = 'destination.com' tunnel_port = 8888 tunnel_headers = {'User-Agent': 'Mozilla/5.0 (compatible, MSIE 11)'} + tunnel_headers_after = copy.copy(tunnel_headers) + tunnel_headers_after['Host'] = '%s:%d' % (tunnel_host, tunnel_port) self.conn.set_tunnel(tunnel_host, port=tunnel_port, headers=tunnel_headers) self.conn.request('HEAD', '/', '') self.assertEqual(self.conn.sock.host, self.host) - self.assertEqual(self.conn.sock.port, client.HTTP_PORT) + self.assertEqual(self.conn.sock.port, self.port) + self.assertEqual(self.conn._tunnel_host, tunnel_host) + self.assertEqual(self.conn._tunnel_port, tunnel_port) + self.assertEqual(self.conn._tunnel_headers, tunnel_headers_after) + + def test_set_tunnel_host_port_headers_set_host_identical(self): + tunnel_host = 'destination.com' + tunnel_port = 8888 + tunnel_headers = {'User-Agent': 'Mozilla/5.0 (compatible, MSIE 11)', + 'Host': '%s:%d' % (tunnel_host, tunnel_port)} + self.conn.set_tunnel(tunnel_host, port=tunnel_port, + headers=tunnel_headers) + self.conn.request('HEAD', '/', '') + self.assertEqual(self.conn.sock.host, self.host) + self.assertEqual(self.conn.sock.port, self.port) + self.assertEqual(self.conn._tunnel_host, tunnel_host) + self.assertEqual(self.conn._tunnel_port, tunnel_port) + self.assertEqual(self.conn._tunnel_headers, tunnel_headers) + + def test_set_tunnel_host_port_headers_set_host_different(self): + tunnel_host = 'destination.com' + tunnel_port = 8888 + tunnel_headers = {'User-Agent': 'Mozilla/5.0 (compatible, MSIE 11)', + 'Host': '%s:%d' % ('example.com', 4200)} + self.conn.set_tunnel(tunnel_host, port=tunnel_port, + headers=tunnel_headers) + self.conn.request('HEAD', '/', '') + self.assertEqual(self.conn.sock.host, self.host) + self.assertEqual(self.conn.sock.port, self.port) self.assertEqual(self.conn._tunnel_host, tunnel_host) self.assertEqual(self.conn._tunnel_port, tunnel_port) self.assertEqual(self.conn._tunnel_headers, tunnel_headers) @@ -1900,25 +1932,110 @@ def test_disallow_set_tunnel_after_connect(self): 'destination.com') def test_connect_with_tunnel(self): - self.conn.set_tunnel('destination.com') + d = { + 'host': 'destination.com', b'host': b'destination.com', + b'port': client.HTTP_PORT, + } + self.conn.set_tunnel(d['host']) self.conn.request('HEAD', '/', '') self.assertEqual(self.conn.sock.host, self.host) - self.assertEqual(self.conn.sock.port, client.HTTP_PORT) - self.assertIn(b'CONNECT destination.com', self.conn.sock.data) + self.assertEqual(self.conn.sock.port, self.port) + self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n' + b'Host: %(host)s:%(port)d\r\n\r\n' % d, + self.conn.sock.data) + self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d, + self.conn.sock.data) + + def test_connect_with_tunnel_with_default_port(self): + d = { + 'host': 'destination.com', b'host': b'destination.com', + 'port': client.HTTP_PORT, b'port': client.HTTP_PORT, + } + self.conn.set_tunnel(d['host'], port=d['port']) + self.conn.request('HEAD', '/', '') + self.assertEqual(self.conn.sock.host, self.host) + self.assertEqual(self.conn.sock.port, self.port) + self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n' + b'Host: %(host)s:%(port)d\r\n\r\n' % d, + self.conn.sock.data) + self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d, + self.conn.sock.data) + + def test_connect_with_tunnel_with_nonstandard_port(self): + d = { + 'host': 'destination.com', b'host': b'destination.com', + 'port': 8888, b'port': 8888, + } + self.conn.set_tunnel(d['host'], port=d['port']) + self.conn.request('HEAD', '/', '') + self.assertEqual(self.conn.sock.host, self.host) + self.assertEqual(self.conn.sock.port, self.port) + self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n' + b'Host: %(host)s:%(port)d\r\n\r\n' % d, + self.conn.sock.data) + self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s:%(port)d\r\n' % d, + self.conn.sock.data) + + # This request is not RFC-valid, but it's been possible with the library + # for years, so don't break it unexpectedly... This also tests + # case-insensitivity when injecting Host: headers if they're missing. + def test_connect_with_tunnel_with_different_host_header(self): + d = { + 'host': 'destination.com', b'host': b'destination.com', + 'tunnel_host_header': 'example.com:9876', + b'tunnel_host_header': b'example.com:9876', + b'port': client.HTTP_PORT, + } + self.conn.set_tunnel( + d['host'], headers={'HOST': d['tunnel_host_header']}) + self.conn.request('HEAD', '/', '') + self.assertEqual(self.conn.sock.host, self.host) + self.assertEqual(self.conn.sock.port, self.port) + self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n' + b'HOST: %(tunnel_host_header)s\r\n\r\n' % d, + self.conn.sock.data) + self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d, + self.conn.sock.data) + + def test_connect_with_tunnel_different_host(self): + d = { + 'host': 'destination.com', b'host': b'destination.com', + 'port': client.HTTP_PORT, b'port': client.HTTP_PORT, + } + self.conn.set_tunnel(d['host']) + self.conn.request('HEAD', '/', '') + self.assertEqual(self.conn.sock.host, self.host) + self.assertEqual(self.conn.sock.port, self.port) + self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\nHost: %(host)s:%(port)d\r\n\r\n' % d, self.conn.sock.data) # issue22095 - self.assertNotIn(b'Host: destination.com:None', self.conn.sock.data) - self.assertIn(b'Host: destination.com', self.conn.sock.data) - - # This test should be removed when CONNECT gets the HTTP/1.1 blessing - self.assertNotIn(b'Host: proxy.com', self.conn.sock.data) + self.assertNotIn(b'Host: %(host)s:None' % d, self.conn.sock.data) + self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d, self.conn.sock.data) + + def test_connect_with_tunnel_idna(self): + dest = '\u03b4\u03c0\u03b8.gr' + dest_port = b'%s:%d' % (dest.encode('idna'), client.HTTP_PORT) + expected = b'CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n' % ( + dest_port, dest_port) + self.conn.set_tunnel(dest) + self.conn.request('HEAD', '/', '') + self.assertEqual(self.conn.sock.host, self.host) + self.assertEqual(self.conn.sock.port, client.HTTP_PORT) + self.assertIn(expected, self.conn.sock.data) def test_connect_put_request(self): - self.conn.set_tunnel('destination.com') + d = { + 'host': 'destination.com', b'host': b'destination.com', + 'port': client.HTTP_PORT, b'port': client.HTTP_PORT, + } + self.conn.set_tunnel(d['host']) self.conn.request('PUT', '/', '') self.assertEqual(self.conn.sock.host, self.host) - self.assertEqual(self.conn.sock.port, client.HTTP_PORT) - self.assertIn(b'CONNECT destination.com', self.conn.sock.data) - self.assertIn(b'Host: destination.com', self.conn.sock.data) + self.assertEqual(self.conn.sock.port, self.port) + self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n' + b'Host: %(host)s:%(port)d\r\n\r\n' % d, + self.conn.sock.data) + self.assertIn(b'PUT / HTTP/1.1\r\nHost: %(host)s\r\n' % d, + self.conn.sock.data) def test_tunnel_debuglog(self): expected_header = 'X-Dummy: 1' diff --git a/Misc/ACKS b/Misc/ACKS index 2cf5e10dd141db..cd87c1ebf48109 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -613,6 +613,7 @@ Anders Hammarquist Mark Hammond Harald Hanche-Olsen Manus Hand +Michael Handler Milton L. Hankins Stephen Hansen Barry Hantman diff --git a/Misc/NEWS.d/next/Library/2018-07-16-14-10-29.bpo-22708.592iRR.rst b/Misc/NEWS.d/next/Library/2018-07-16-14-10-29.bpo-22708.592iRR.rst new file mode 100644 index 00000000000000..00bcf38bbcdf51 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-07-16-14-10-29.bpo-22708.592iRR.rst @@ -0,0 +1,3 @@ +http.client CONNECT method tunnel improvements: Use HTTP 1.1 protocol; send +a matching Host: header with CONNECT, if one is not provided; convert IDN +domain names to Punycode. Patch by Michael Handler. From c3927ed9929afd67ec25794c68c076aed908d13b Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Mon, 16 Jul 2018 16:07:41 -0700 Subject: [PATCH 02/12] Refactor tests to pass under -bb (fix ByteWarnings); missed some lines >80. --- Lib/test/test_httplib.py | 41 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 0465eb8922dbb9..2925df3dd8d8ef 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1933,10 +1933,10 @@ def test_disallow_set_tunnel_after_connect(self): def test_connect_with_tunnel(self): d = { - 'host': 'destination.com', b'host': b'destination.com', + b'host': b'destination.com', b'port': client.HTTP_PORT, } - self.conn.set_tunnel(d['host']) + self.conn.set_tunnel(d[b'host'].decode('ascii')) self.conn.request('HEAD', '/', '') self.assertEqual(self.conn.sock.host, self.host) self.assertEqual(self.conn.sock.port, self.port) @@ -1948,10 +1948,10 @@ def test_connect_with_tunnel(self): def test_connect_with_tunnel_with_default_port(self): d = { - 'host': 'destination.com', b'host': b'destination.com', - 'port': client.HTTP_PORT, b'port': client.HTTP_PORT, + b'host': b'destination.com', + b'port': client.HTTP_PORT, } - self.conn.set_tunnel(d['host'], port=d['port']) + self.conn.set_tunnel(d[b'host'].decode('ascii'), port=d[b'port']) self.conn.request('HEAD', '/', '') self.assertEqual(self.conn.sock.host, self.host) self.assertEqual(self.conn.sock.port, self.port) @@ -1963,10 +1963,10 @@ def test_connect_with_tunnel_with_default_port(self): def test_connect_with_tunnel_with_nonstandard_port(self): d = { - 'host': 'destination.com', b'host': b'destination.com', - 'port': 8888, b'port': 8888, + b'host': b'destination.com', + b'port': 8888, } - self.conn.set_tunnel(d['host'], port=d['port']) + self.conn.set_tunnel(d[b'host'].decode('ascii'), port=d[b'port']) self.conn.request('HEAD', '/', '') self.assertEqual(self.conn.sock.host, self.host) self.assertEqual(self.conn.sock.port, self.port) @@ -1981,13 +1981,13 @@ def test_connect_with_tunnel_with_nonstandard_port(self): # case-insensitivity when injecting Host: headers if they're missing. def test_connect_with_tunnel_with_different_host_header(self): d = { - 'host': 'destination.com', b'host': b'destination.com', - 'tunnel_host_header': 'example.com:9876', + b'host': b'destination.com', b'tunnel_host_header': b'example.com:9876', b'port': client.HTTP_PORT, } self.conn.set_tunnel( - d['host'], headers={'HOST': d['tunnel_host_header']}) + d[b'host'].decode('ascii'), + headers={'HOST': d[b'tunnel_host_header'].decode('ascii')}) self.conn.request('HEAD', '/', '') self.assertEqual(self.conn.sock.host, self.host) self.assertEqual(self.conn.sock.port, self.port) @@ -1999,17 +1999,20 @@ def test_connect_with_tunnel_with_different_host_header(self): def test_connect_with_tunnel_different_host(self): d = { - 'host': 'destination.com', b'host': b'destination.com', - 'port': client.HTTP_PORT, b'port': client.HTTP_PORT, + b'host': b'destination.com', + b'port': client.HTTP_PORT, } - self.conn.set_tunnel(d['host']) + self.conn.set_tunnel(d[b'host'].decode('ascii')) self.conn.request('HEAD', '/', '') self.assertEqual(self.conn.sock.host, self.host) self.assertEqual(self.conn.sock.port, self.port) - self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\nHost: %(host)s:%(port)d\r\n\r\n' % d, self.conn.sock.data) + self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n' + b'Host: %(host)s:%(port)d\r\n\r\n' % d, + self.conn.sock.data) # issue22095 self.assertNotIn(b'Host: %(host)s:None' % d, self.conn.sock.data) - self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d, self.conn.sock.data) + self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d, + self.conn.sock.data) def test_connect_with_tunnel_idna(self): dest = '\u03b4\u03c0\u03b8.gr' @@ -2024,10 +2027,10 @@ def test_connect_with_tunnel_idna(self): def test_connect_put_request(self): d = { - 'host': 'destination.com', b'host': b'destination.com', - 'port': client.HTTP_PORT, b'port': client.HTTP_PORT, + b'host': b'destination.com', + b'port': client.HTTP_PORT, } - self.conn.set_tunnel(d['host']) + self.conn.set_tunnel(d[b'host'].decode('ascii')) self.conn.request('PUT', '/', '') self.assertEqual(self.conn.sock.host, self.host) self.assertEqual(self.conn.sock.port, self.port) From 2fe3ade45a12570d0ac5d5a8d3fb9064e7a3dfd4 Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Thu, 19 Jul 2018 17:27:57 -0700 Subject: [PATCH 03/12] Use consistent 'tunnelling' spelling in Lib/http/client.py --- Lib/http/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index 664d778100c6de..4209accacc7016 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -846,7 +846,7 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, self._create_connection = socket.create_connection def set_tunnel(self, host, port=None, headers=None): - """Set up host and port for HTTP CONNECT tunneling. + """Set up host and port for HTTP CONNECT tunnelling. In a connection that uses HTTP CONNECT tunnelling, the host passed to the constructor is used as a proxy server that relays all communication From b2f76d729e14048a10269e53bfa2205f014354b2 Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Thu, 19 Jul 2018 22:31:53 -0700 Subject: [PATCH 04/12] Lib/test/test_httplib: Remove remnant of obsoleted test. --- Lib/test/test_httplib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 2925df3dd8d8ef..30b68232749eec 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -2009,8 +2009,6 @@ def test_connect_with_tunnel_different_host(self): self.assertIn(b'CONNECT %(host)s:%(port)d HTTP/1.1\r\n' b'Host: %(host)s:%(port)d\r\n\r\n' % d, self.conn.sock.data) - # issue22095 - self.assertNotIn(b'Host: %(host)s:None' % d, self.conn.sock.data) self.assertIn(b'HEAD / HTTP/1.1\r\nHost: %(host)s\r\n' % d, self.conn.sock.data) From 9990baa82c6f541a2c5f31216d5ea4db3ea47d30 Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Mon, 23 Jul 2018 22:45:37 -0700 Subject: [PATCH 05/12] Use dict.copy() not copy.copy() --- Lib/http/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index 4209accacc7016..b61c37117b0e0b 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -68,7 +68,6 @@ Req-sent-unread-response _CS_REQ_SENT """ -import copy import email.parser import email.message import http @@ -872,7 +871,7 @@ def set_tunnel(self, host, port=None, headers=None): self._tunnel_host, self._tunnel_port = self._get_hostport(host, port) if headers: - self._tunnel_headers = copy.copy(headers) + self._tunnel_headers = headers.copy() else: self._tunnel_headers.clear() From 367b40758cd139ee68ff9ef4dbfdf6926dca3c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric?= Date: Fri, 7 Oct 2022 11:02:16 -0400 Subject: [PATCH 06/12] fix version changed --- Doc/library/http.client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/http.client.rst b/Doc/library/http.client.rst index 51256f96595709..271d8b8e98f617 100644 --- a/Doc/library/http.client.rst +++ b/Doc/library/http.client.rst @@ -339,7 +339,7 @@ HTTPConnection Objects .. versionadded:: 3.2 - .. versionchanged:: 3.7 + .. versionchanged:: 3.12 HTTP CONNECT tunnelling requests use protocol HTTP/1.1, upgraded from protocol HTTP/1.0. ``Host:`` HTTP headers are mandatory for HTTP/1.1, so one will be automatically generated and transmitted if not provided in From 8b556427c14a48942690fa0f9acf078ad936c414 Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Fri, 7 Oct 2022 20:31:31 -0700 Subject: [PATCH 07/12] Update Lib/http/client.py Co-authored-by: bgehman --- Lib/http/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/http/client.py b/Lib/http/client.py index 8091d25aa0544a..1048c9c558f550 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -905,6 +905,7 @@ def set_tunnel(self, host, port=None, headers=None): for header in self._tunnel_headers.keys(): if header.lower() == "host": saw_host_header = True + break if not saw_host_header: encoded_host = self._tunnel_host.encode("idna").decode("ascii") self._tunnel_headers["Host"] = "%s:%d" % ( From aa1508ee7c91686fffa587c9c3846d3cdef69fc9 Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Mon, 10 Oct 2022 14:34:46 -0700 Subject: [PATCH 08/12] Switch to for/else: syntax, as suggested --- Lib/http/client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index 1048c9c558f550..7dbda7fcd3707e 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -901,12 +901,10 @@ def set_tunnel(self, host, port=None, headers=None): else: self._tunnel_headers.clear() - saw_host_header = False for header in self._tunnel_headers.keys(): if header.lower() == "host": - saw_host_header = True break - if not saw_host_header: + else: encoded_host = self._tunnel_host.encode("idna").decode("ascii") self._tunnel_headers["Host"] = "%s:%d" % ( encoded_host, self._tunnel_port) From ef8b896ff5bf9502da41726afc427832e8b84711 Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Mon, 24 Oct 2022 18:09:06 -0700 Subject: [PATCH 09/12] Don't use for: else: --- Lib/http/client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index 7dbda7fcd3707e..e258b1d1f634ab 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -901,10 +901,7 @@ def set_tunnel(self, host, port=None, headers=None): else: self._tunnel_headers.clear() - for header in self._tunnel_headers.keys(): - if header.lower() == "host": - break - else: + if "host" not in [x.lower() for x in self._tunnel_headers.keys()]: encoded_host = self._tunnel_host.encode("idna").decode("ascii") self._tunnel_headers["Host"] = "%s:%d" % ( encoded_host, self._tunnel_port) From 87e514c390c34da499b89acea08be38fe95fdd1f Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Tue, 25 Oct 2022 17:03:36 -0700 Subject: [PATCH 10/12] Sure, fine, w/e --- Lib/http/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index e258b1d1f634ab..113cd230aa0de7 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -901,7 +901,7 @@ def set_tunnel(self, host, port=None, headers=None): else: self._tunnel_headers.clear() - if "host" not in [x.lower() for x in self._tunnel_headers.keys()]: + if not any(header.lower() == "host" for header in _tunnel_headers): encoded_host = self._tunnel_host.encode("idna").decode("ascii") self._tunnel_headers["Host"] = "%s:%d" % ( encoded_host, self._tunnel_port) From d224052c52819852d1da81b542aa8a3338ee10bf Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Tue, 25 Oct 2022 18:02:16 -0700 Subject: [PATCH 11/12] Oops --- Lib/http/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/http/client.py b/Lib/http/client.py index 113cd230aa0de7..fb15a055d5759a 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -901,7 +901,7 @@ def set_tunnel(self, host, port=None, headers=None): else: self._tunnel_headers.clear() - if not any(header.lower() == "host" for header in _tunnel_headers): + if not any(header.lower() == "host" for header in self._tunnel_headers): encoded_host = self._tunnel_host.encode("idna").decode("ascii") self._tunnel_headers["Host"] = "%s:%d" % ( encoded_host, self._tunnel_port) From 70ba474b00360a5630b5c1f6a912ab4f3f169f6f Mon Sep 17 00:00:00 2001 From: Michael Handler Date: Tue, 25 Oct 2022 18:03:22 -0700 Subject: [PATCH 12/12] 1nm to the left --- Lib/test/test_httplib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 83f28d2a11cfac..cd3add9ec9da65 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1,4 +1,3 @@ -import copy import enum import errno from http import client, HTTPStatus @@ -2230,7 +2229,7 @@ def test_set_tunnel_host_port_headers_add_host_missing(self): tunnel_host = 'destination.com' tunnel_port = 8888 tunnel_headers = {'User-Agent': 'Mozilla/5.0 (compatible, MSIE 11)'} - tunnel_headers_after = copy.copy(tunnel_headers) + tunnel_headers_after = tunnel_headers.copy() tunnel_headers_after['Host'] = '%s:%d' % (tunnel_host, tunnel_port) self.conn.set_tunnel(tunnel_host, port=tunnel_port, headers=tunnel_headers)