]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Correctly handle ipv6 addresses as a part of URL (#1349)
authorcdeler <serj.krotov@gmail.com>
Thu, 8 Oct 2020 08:37:13 +0000 (11:37 +0300)
committerGitHub <noreply@github.com>
Thu, 8 Oct 2020 08:37:13 +0000 (11:37 +0300)
* Correctly handle ipv6 addresses as a host

* Fixed typo

* Added an extra rfc reference

* Update tests/models/test_url.py

* Update tests/models/test_url.py

httpx/_models.py
tests/models/test_url.py

index 7bad1c9eb93fcb4dd4af3b81e712ce2a3d39d7ad..c981c740bf235d0a7c41dd5538f6c6155cd1190f 100644 (file)
@@ -108,6 +108,11 @@ class URL:
                 raw_scheme, raw_host, port, raw_path = url
                 scheme = raw_scheme.decode("ascii")
                 host = raw_host.decode("ascii")
+                if host and ":" in host and host[0] != "[":
+                    # it's an IPv6 address, so it should be enclosed in "[" and "]"
+                    # ref: https://tools.ietf.org/html/rfc2732#section-2
+                    # ref: https://tools.ietf.org/html/rfc3986#section-3.2.2
+                    host = f"[{host}]"
                 port_str = "" if port is None else f":{port}"
                 path = raw_path.decode("ascii")
                 url = f"{scheme}://{host}{port_str}{path}"
@@ -186,8 +191,17 @@ class URL:
 
         url = httpx.URL("http://中国.icom.museum")
         assert url.host == "xn--fiqs8s.icom.museum"
+
+        url = httpx.URL("https://[::ffff:192.168.0.1]")
+        assert url.host == "::ffff:192.168.0.1"
         """
-        return self._uri_reference.host or ""
+        host: str = self._uri_reference.host
+
+        if host and ":" in host and host[0] == "[":
+            # it's an IPv6 address
+            host = host.lstrip("[").rstrip("]")
+
+        return host or ""
 
     @property
     def port(self) -> typing.Optional[int]:
@@ -336,6 +350,11 @@ class URL:
             # Consolidate host and port into  netloc.
             host = kwargs.pop("host", self.host) or ""
             port = kwargs.pop("port", self.port)
+
+            if host and ":" in host and host[0] != "[":
+                # it's an IPv6 address, so it should be hidden under bracket
+                host = f"[{host}]"
+
             kwargs["netloc"] = f"{host}:{port}" if port is not None else host
 
         if "userinfo" in kwargs or "netloc" in kwargs:
index fcd81ba50ca1c879d5fdb01aaade8fa292d7ea0e..9d67618b5b8f8757bf256f2583e80b3c4e067784 100644 (file)
@@ -287,3 +287,37 @@ def test_url_with_url_encoded_path():
     assert url.path == "/path to somewhere"
     assert url.query == b""
     assert url.raw_path == b"/path%20to%20somewhere"
+
+
+def test_ipv6_url():
+    url = httpx.URL("http://[::ffff:192.168.0.1]:5678/")
+
+    assert url.host == "::ffff:192.168.0.1"
+    assert url.netloc == "[::ffff:192.168.0.1]:5678"
+
+
+@pytest.mark.parametrize(
+    "url_str",
+    [
+        "http://127.0.0.1:1234",
+        "http://example.com:1234",
+        "http://[::ffff:127.0.0.1]:1234",
+    ],
+)
+@pytest.mark.parametrize("new_host", ["[::ffff:192.168.0.1]", "::ffff:192.168.0.1"])
+def test_ipv6_url_copy_with_host(url_str, new_host):
+    url = httpx.URL(url_str).copy_with(host=new_host)
+
+    assert url.host == "::ffff:192.168.0.1"
+    assert url.netloc == "[::ffff:192.168.0.1]:1234"
+    assert str(url) == "http://[::ffff:192.168.0.1]:1234"
+
+
+@pytest.mark.parametrize("host", [b"[::ffff:192.168.0.1]", b"::ffff:192.168.0.1"])
+def test_ipv6_url_from_raw_url(host):
+    raw_url = (b"https", host, 443, b"/")
+    url = httpx.URL(raw_url)
+
+    assert url.host == "::ffff:192.168.0.1"
+    assert url.netloc == "[::ffff:192.168.0.1]:443"
+    assert str(url) == "https://[::ffff:192.168.0.1]:443/"