Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable tunnel for http2 requests #3321

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/urllib3/http2.py
Expand Up @@ -61,6 +61,9 @@ def __init__(

super().__init__(host, port, **kwargs)

if self._tunnel_host is not None:
raise NotImplementedError("Tunneling isn't supported with HTTP/2")

def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]:
config = h2.config.H2Configuration(client_side=True)
return _LockedObject(h2.connection.H2Connection(config=config))
Expand Down Expand Up @@ -118,6 +121,17 @@ def send(self, data: bytes) -> None: # type: ignore[override] # Defensive:
return
raise NotImplementedError("Sending data isn't supported yet")

def set_tunnel(
self,
host: str,
port: int | None = None,
headers: typing.Mapping[str, str] | None = None,
scheme: str = "http",
) -> None:
raise NotImplementedError(
"HTTP/2 does not support setting up a tunnel through a proxy"
)

def getresponse( # type: ignore[override]
self,
) -> HTTP2Response:
Expand Down
47 changes: 29 additions & 18 deletions test/with_dummyserver/test_connectionpool.py
Expand Up @@ -437,33 +437,44 @@ def test_socket_timeout_updated_on_reuse_parameter(
[mock.call(x) for x in expect_settimeout_calls]
)

def test_tunnel(self) -> None:
def test_tunnel(self, http_version: str) -> None:
# note the actual httplib.py has no tests for this functionality
timeout = Timeout(total=None)
with HTTPConnectionPool(self.host, self.port, timeout=timeout) as pool:
conn = pool._get_conn()
try:
conn.set_tunnel(self.host, self.port)
with mock.patch.object(
conn, "_tunnel", create=True, return_value=None
) as conn_tunnel:
pool._make_request(conn, "GET", "/")
conn_tunnel.assert_called_once_with()
finally:
conn.close()
if http_version == "h2":
with pytest.raises(NotImplementedError) as e:
conn.set_tunnel(self.host, self.port)
assert (
str(e.value)
== "HTTP/2 does not support setting up a tunnel through a proxy"
)
else:
try:
conn.set_tunnel(self.host, self.port)
with mock.patch.object(
conn, "_tunnel", create=True, return_value=None
) as conn_tunnel:
pool._make_request(conn, "GET", "/")
conn_tunnel.assert_called_once_with()
finally:
conn.close()

# test that it's not called when tunnel is not set
timeout = Timeout(total=None)
with HTTPConnectionPool(self.host, self.port, timeout=timeout) as pool:
conn = pool._get_conn()
try:
with mock.patch.object(
conn, "_tunnel", create=True, return_value=None
) as conn_tunnel:
pool._make_request(conn, "GET", "/")
assert not conn_tunnel.called
finally:
conn.close()
if http_version == "h2":
pass
else:
try:
with mock.patch.object(
conn, "_tunnel", create=True, return_value=None
) as conn_tunnel:
pool._make_request(conn, "GET", "/")
assert not conn_tunnel.called
finally:
conn.close()

def test_redirect_relative_url_no_deprecation(self) -> None:
with HTTPConnectionPool(self.host, self.port) as pool:
Expand Down
24 changes: 16 additions & 8 deletions test/with_dummyserver/test_https.py
Expand Up @@ -603,7 +603,7 @@ def test_https_timeout(self) -> None:
with pytest.warns(InsecureRequestWarning):
https_pool.request("GET", "/")

def test_tunnel(self) -> None:
def test_tunnel(self, http_version: str) -> None:
"""test the _tunnel behavior"""
timeout = Timeout(total=None)
with HTTPSConnectionPool(
Expand All @@ -614,13 +614,21 @@ def test_tunnel(self) -> None:
ssl_minimum_version=self.tls_version(),
) as https_pool:
with contextlib.closing(https_pool._new_conn()) as conn:
conn.set_tunnel(self.host, self.port)
with mock.patch.object(
conn, "_tunnel", create=True, return_value=None
) as conn_tunnel:
with pytest.warns(InsecureRequestWarning):
https_pool._make_request(conn, "GET", "/")
conn_tunnel.assert_called_once_with()
if http_version == "h2":
with pytest.raises(NotImplementedError) as e:
conn.set_tunnel(self.host, self.port)
assert (
str(e.value)
== "HTTP/2 does not support setting up a tunnel through a proxy"
)
else:
conn.set_tunnel(self.host, self.port)
with mock.patch.object(
conn, "_tunnel", create=True, return_value=None
) as conn_tunnel:
with pytest.warns(InsecureRequestWarning):
https_pool._make_request(conn, "GET", "/")
conn_tunnel.assert_called_once_with()

@requires_network()
def test_enhanced_timeout(self) -> None:
Expand Down
28 changes: 28 additions & 0 deletions test/with_dummyserver/test_socketlevel.py
Expand Up @@ -1133,6 +1133,34 @@ def echo_socket_handler(listener: socket.socket) -> None:
retries=False,
)

def test_alpn_protocol_in_first_request_packet(self) -> None:
self.buf = b""

def echo_socket_handler(listener: socket.socket) -> None:
sock = listener.accept()[0]
self.buf = b""
while not self.buf.endswith(b"\r\n\r\n"):
self.buf += sock.recv(65536)

sock.send(
(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: %d\r\n"
"\r\n"
"%s" % (len(self.buf), self.buf.decode("utf-8"))
).encode("utf-8")
)
sock.close()

self._start_server(echo_socket_handler)
base_url = f"http://{self.host}:{self.port}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is using HTTP, tunneling and ALPN are TLS features so requires HTTPS. It's expected for the handshake to fail, we want to see what ALPN values are being offered and not complete the handshake.

with proxy_from_url(base_url) as proxy:
r = proxy.request("GET", "http://google.com/")
assert r.status == 200
assert b"HTTP/1.1" in self.buf
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ALPN the value is b"http/1.1".

assert b"h2" not in self.buf

def test_connect_reconn(self) -> None:
def proxy_ssl_one(listener: socket.socket) -> None:
sock = listener.accept()[0]
Expand Down