Repository: Pylons/webob

Issue:
IPv6 support
Parts of WebOb haven't been adapted to work in IPv6 environment:
- request.domain will split IPv6 addresses incorrectly
- request.host_port will split IPv6 addresses incorrectly
- request.host_url will split IPv6 addresses incorrectly
- .. maybe more places, I haven't checked all sources

This issue is similar to #174 however there it's about mitigating potential vulnerabilities and this issue is only about making API work correctly. Software using WebOb can workaround this by parsing the Host header manually, but it's just wasteful because everybody has to repeat the same work, that should be done by WebOb (as promised by the API)



Comments:

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 4b5784a..ce5397f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -44,6 +44,10 @@ These features are experimental and may change at any point in the future.
 Bugfix
 ~~~~~~
 
+- Request.host_url, Request.host_port, Request.domain correctly parse IPv6 Host
+  headers as provided by a browser. See
+  https://github.com/Pylons/webob/pull/332
+
 - Request.authorization would raise ValueError for unusual or malformed header
   values. See https://github.com/Pylons/webob/issues/231
 
diff --git a/src/webob/request.py b/src/webob/request.py
index 011617c..b9f45d9 100644
--- a/src/webob/request.py
+++ b/src/webob/request.py
@@ -413,8 +413,8 @@ class BaseRequest(object):
         e = self.environ
         host = e.get('HTTP_HOST')
         if host is not None:
-            if ':' in host:
-                host, port = host.split(':', 1)
+            if ':' in host and host[-1] != ']':
+                host, port = host.rsplit(':', 1)
             else:
                 url_scheme = e['wsgi.url_scheme']
                 if url_scheme == 'https':
@@ -435,8 +435,8 @@ class BaseRequest(object):
         url = scheme + '://'
         host = e.get('HTTP_HOST')
         if host is not None:
-            if ':' in host:
-                host, port = host.split(':', 1)
+            if ':' in host and host[-1] != ']':
+                host, port = host.rsplit(':', 1)
             else:
                 port = None
         else:
@@ -667,8 +667,8 @@ class BaseRequest(object):
         .. code-block:: python
 
            domain = request.host
-           if ':' in domain:
-               domain = domain.split(':', 1)[0]
+           if ':' in domain and domain[-1] != ']': # Check for ] because of IPv6
+               domain = domain.rsplit(':', 1)[0]
 
         This will be equivalent to the domain portion of the ``HTTP_HOST``
         value in the environment if it exists, or the ``SERVER_NAME`` value in
@@ -680,8 +680,8 @@ class BaseRequest(object):
         value use :meth:`webob.request.Request.host` instead.
         """
         domain = self.host
-        if ':' in domain:
-            domain = domain.split(':', 1)[0]
+        if ':' in domain and domain[-1] != ']':
+            domain = domain.rsplit(':', 1)[0]
         return domain
 
     @property


Test patch:
diff --git a/tests/test_request.py b/tests/test_request.py
index 85e6047..c0f932d 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -1639,6 +1639,16 @@ class TestBaseRequest(object):
         req = self._makeOne(environ)
         assert req.domain == 'example.com'
 
+    def test_domain_with_ipv6(self):
+        environ = {'HTTP_HOST': '[2001:DB8::1]:6453'}
+        req = self._makeOne(environ)
+        assert req.domain == '[2001:DB8::1]'
+
+    def test_domain_with_ipv6_no_port(self):
+        environ = {'HTTP_HOST': '[2001:DB8::1]'}
+        req = self._makeOne(environ)
+        assert req.domain == '[2001:DB8::1]'
+
     def test_encget_raises_without_default(self):
         inst = self._makeOne({})
         with pytest.raises(KeyError):
@@ -1965,6 +1975,18 @@ class TestLegacyRequest(object):
         req = self._makeOne(environ)
         assert req.host_port == '4333'
 
+    def test_host_port_ipv6(self):
+        environ = {'HTTP_HOST': '[2001:DB8::1]:6453'}
+        req = self._makeOne(environ)
+        assert req.host_port == '6453'
+
+    def test_host_port_ipv6(self):
+        environ = {'wsgi.url_scheme': 'https',
+                   'HTTP_HOST': '[2001:DB8::1]'
+                  }
+        req = self._makeOne(environ)
+        assert req.host_port == '443'
+
     def test_host_url_w_http_host_and_no_port(self):
         environ = {'wsgi.url_scheme': 'http',
                    'HTTP_HOST': 'example.com',
@@ -2015,6 +2037,20 @@ class TestLegacyRequest(object):
         req = self._makeOne(environ)
         assert req.host_url == 'https://example.com:4333'
 
+    def test_host_url_http_ipv6_host(self):
+        environ = {'wsgi.url_scheme': 'https',
+                   'HTTP_HOST': '[2001:DB8::1]:6453'
+                  }
+        req = self._makeOne(environ)
+        assert req.host_url == 'https://[2001:DB8::1]:6453'
+
+    def test_host_url_http_ipv6_host_no_port(self):
+        environ = {'wsgi.url_scheme': 'https',
+                   'HTTP_HOST': '[2001:DB8::1]'
+                  }
+        req = self._makeOne(environ)
+        assert req.host_url == 'https://[2001:DB8::1]'
+
     @py2only
     def test_application_url_py2(self):
         inst = self._blankOne('/%C3%AB')