From: Tom Christie Date: Fri, 20 Dec 2019 10:25:42 +0000 (+0000) Subject: Fix Host header and HSTS when the default port is explicitly included in URL (#649) X-Git-Tag: 0.9.5~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5ee512d803d1d6b49dc171e1114f0075618de78e;p=thirdparty%2Fhttpx.git Fix Host header and HSTS when the default port is explicitly included in URL (#649) --- diff --git a/httpx/client.py b/httpx/client.py index a6c22d8f..1ceea145 100644 --- a/httpx/client.py +++ b/httpx/client.py @@ -338,7 +338,8 @@ class Client: """ url = self.base_url.join(relative_url=url) if url.scheme == "http" and hstspreload.in_hsts_preload(url.host): - url = url.copy_with(scheme="https") + port = None if url.port == 80 else url.port + url = url.copy_with(scheme="https", port=port) return url def merge_cookies( diff --git a/httpx/models.py b/httpx/models.py index ba19f43f..ad3599b7 100644 --- a/httpx/models.py +++ b/httpx/models.py @@ -112,6 +112,10 @@ class URL: @property def authority(self) -> str: + port_str = self._uri_reference.port + default_port_str = {"https": "443", "http": "80"}.get(self.scheme, "") + if port_str is None or port_str == default_port_str: + return self._uri_reference.host or "" return self._uri_reference.authority or "" @property @@ -203,9 +207,9 @@ class URL: authority = host if port is not None: authority += f":{port}" - if username is not None: + if username: userpass = username - if password is not None: + if password: userpass += f":{password}" authority = f"{userpass}@{authority}" diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py index 37a5683d..168b6a04 100755 --- a/tests/client/test_headers.py +++ b/tests/client/test_headers.py @@ -130,7 +130,12 @@ def test_header_does_not_exist(): @pytest.mark.asyncio -async def test_host_without_auth_in_header(): +async def test_host_with_auth_and_port_in_url(): + """ + The Host header should only include the hostname, or hostname:port + (for non-default ports only). Any userinfo or default port should not + be present. + """ url = "http://username:password@example.org:80/echo_headers" client = Client(dispatch=MockDispatch()) @@ -142,7 +147,31 @@ async def test_host_without_auth_in_header(): "accept": "*/*", "accept-encoding": "gzip, deflate, br", "connection": "keep-alive", - "host": "example.org:80", + "host": "example.org", + "user-agent": f"python-httpx/{__version__}", + "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + } + } + + +@pytest.mark.asyncio +async def test_host_with_non_default_port_in_url(): + """ + If the URL includes a non-default port, then it should be included in + the Host header. + """ + url = "http://username:password@example.org:123/echo_headers" + + client = Client(dispatch=MockDispatch()) + response = await client.get(url) + + assert response.status_code == 200 + assert response.json() == { + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate, br", + "connection": "keep-alive", + "host": "example.org:123", "user-agent": f"python-httpx/{__version__}", "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", } diff --git a/tests/models/test_url.py b/tests/models/test_url.py index f6c7ab4d..1415ee91 100644 --- a/tests/models/test_url.py +++ b/tests/models/test_url.py @@ -73,8 +73,8 @@ def test_url(): repr(url) == "URL('https://example.org:123/path/to/somewhere?abc=123#anchor')" ) - new = url.copy_with(scheme="http") - assert new == URL("http://example.org:123/path/to/somewhere?abc=123#anchor") + new = url.copy_with(scheme="http", port=None) + assert new == URL("http://example.org/path/to/somewhere?abc=123#anchor") assert new.scheme == "http"