def fix_twisted_web_http_Request(): """Fix broken IPv6 handling in twisted.web.http.request.Request.""" from netaddr import IPAddress from netaddr.core import AddrFormatError from twisted.internet import address from twisted.python.compat import intToBytes, networkString import twisted.web.http from twisted.web.server import Request from twisted.web.test.requesthelper import DummyChannel def new_getRequestHostname(self): # Unlike upstream, support/require IPv6 addresses to be # [ip:v6:add:ress]:port, with :port being optional. # IPv6 IP addresses are wrapped in [], to disambigate port numbers. host = self.getHeader(b"host") if host: if host.startswith(b"[") and b"]" in host: if host.find(b"]") < host.rfind(b":"): # The format is: [ip:add:ress]:port. return host[:host.rfind(b":")] else: # no :port after [...] return host # No brackets, so it must be host:port or IPv4:port. return host.split(b":", 1)[0] host = self.getHost().host try: if isinstance(host, str): ip = IPAddress(host) else: ip = IPAddress(host.decode("idna")) except AddrFormatError: # If we could not convert the hostname to an IPAddress, assume that # it is a hostname. return networkString(host) if ip.version == 4: return networkString(host) else: return networkString("[" + host + "]") def new_setHost(self, host, port, ssl=0): try: ip = IPAddress(host.decode("idna")) except AddrFormatError: ip = None # `host` is a host or domain name. self._forceSSL = ssl # set first so isSecure will work if self.isSecure(): default = 443 else: default = 80 if ip is None: hostHeader = host elif ip.version == 4: hostHeader = host else: hostHeader = b"[" + host + b"]" if port != default: hostHeader += b":" + intToBytes(port) self.requestHeaders.setRawHeaders(b"host", [hostHeader]) if ip is None: # Pretend that a host or domain name is an IPv4 address. self.host = address.IPv4Address("TCP", host, port) elif ip.version == 4: self.host = address.IPv4Address("TCP", host, port) else: self.host = address.IPv6Address("TCP", host, port) request = Request(DummyChannel(), False) request.client = address.IPv6Address("TCP", "fe80::1", "80") request.setHost(b"fe80::1", 1234) if isinstance(request.host, address.IPv4Address): # Buggy code calls fe80::1 an IPv4Address. twisted.web.http.Request.setHost = new_setHost if request.getRequestHostname() == b"fe80": # The fe80::1 test address above was incorrectly interpreted as # address='fe80', port = ':1', because it does host.split(':', 1)[0]. twisted.web.http.Request.getRequestHostname = new_getRequestHostname
def fix_twisted_web_http_Request(): """Add ipv6 support to Request.getClientIP() Specifically, IPv6 IP addresses need to be wrapped in [], and return address.IPv6Address when needed. See https://bugs.launchpad.net/ubuntu/+source/twisted/+bug/1604608 """ from netaddr import IPAddress from netaddr.core import AddrFormatError from twisted.internet import address from twisted.python.compat import ( intToBytes, networkString, ) import twisted.web.http from twisted.web.server import Request from twisted.web.test.requesthelper import DummyChannel def new_getClientIP(self): from twisted.internet import address # upstream doesn't check for address.IPv6Address if isinstance(self.client, address.IPv4Address): return self.client.host elif isinstance(self.client, address.IPv6Address): return self.client.host else: return None def new_getRequestHostname(self): # Unlike upstream, support/require IPv6 addresses to be # [ip:v6:add:ress]:port, with :port being optional. # IPv6 IP addresses are wrapped in [], to disambigate port numbers. host = self.getHeader(b'host') if host: if host.startswith(b'[') and b']' in host: if host.find(b']') < host.rfind(b':'): # The format is: [ip:add:ress]:port. return host[:host.rfind(b':')] else: # no :port after [...] return host # No brackets, so it must be host:port or IPv4:port. return host.split(b':', 1)[0] host = self.getHost().host try: if isinstance(host, str): ip = IPAddress(host) else: ip = IPAddress(host.decode("idna")) except AddrFormatError: # If we could not convert the hostname to an IPAddress, assume that # it is a hostname. return networkString(host) if ip.version == 4: return networkString(host) else: return networkString('[' + host + ']') def new_setHost(self, host, port, ssl=0): try: ip = IPAddress(host.decode("idna")) except AddrFormatError: ip = None # `host` is a host or domain name. self._forceSSL = ssl # set first so isSecure will work if self.isSecure(): default = 443 else: default = 80 if ip is None: hostHeader = host elif ip.version == 4: hostHeader = host else: hostHeader = b"[" + host + b"]" if port != default: hostHeader += b":" + intToBytes(port) self.requestHeaders.setRawHeaders(b"host", [hostHeader]) if ip is None: # Pretend that a host or domain name is an IPv4 address. self.host = address.IPv4Address("TCP", host, port) elif ip.version == 4: self.host = address.IPv4Address("TCP", host, port) else: self.host = address.IPv6Address("TCP", host, port) request = Request(DummyChannel(), False) request.client = address.IPv6Address('TCP', 'fe80::1', '80') request.setHost(b"fe80::1", 1234) if request.getClientIP() is None: # Buggy code returns None for IPv6 addresses. twisted.web.http.Request.getClientIP = new_getClientIP if isinstance(request.host, address.IPv4Address): # Buggy code calls fe80::1 an IPv4Address. twisted.web.http.Request.setHost = new_setHost if request.getRequestHostname() == b'fe80': # The fe80::1 test address above was incorrectly interpreted as # address='fe80', port = ':1', because it does host.split(':', 1)[0]. twisted.web.http.Request.getRequestHostname = new_getRequestHostname