diff --git a/twisted/internet/abstract.py b/twisted/internet/abstract.py index 64f77b0..c15158f 100644 --- a/twisted/internet/abstract.py +++ b/twisted/internet/abstract.py @@ -16,6 +16,7 @@ from twisted.python import log, reflect, failure from twisted.persisted import styles from twisted.internet import interfaces, main +import socket class FileDescriptor(object): """An object which can be operated on by select(). @@ -380,5 +381,16 @@ def isIPAddress(addr): return True return False +def isIPv6Address(addr): + if '%' in addr: + addr, scope = addr.split('%', 1) + try: + s = socket.inet_pton(socket.AF_INET6, addr) + except Exception, ex: + print ex + return False + else: + return True + __all__ = ["FileDescriptor"] diff --git a/twisted/internet/tcp.py b/twisted/internet/tcp.py index 23d56b8..e141c3d 100644 --- a/twisted/internet/tcp.py +++ b/twisted/internet/tcp.py @@ -602,16 +602,7 @@ class BaseClient(Connection): fdesc._setCloseOnExec(s.fileno()) return s - def resolveAddress(self): - if abstract.isIPAddress(self.addr[0]): - self._setRealAddress(self.addr[0]) - else: - d = self.reactor.resolve(self.addr[0]) - d.addCallbacks(self._setRealAddress, self.failIfNotConnected) - def _setRealAddress(self, address): - self.realAddress = (address, self.addr[1]) - self.doConnect() def doConnect(self): """I connect the socket. @@ -684,38 +675,85 @@ class Client(BaseClient): def __init__(self, host, port, bindAddress, connector, reactor=None): # BaseClient.__init__ is invoked later self.connector = connector + self.bindAddress = bindAddress + self.reactor = reactor self.addr = (host, port) - whenDone = self.resolveAddress err = None skt = None + self._start(host, port, True) + + def _start(self, host, port, resolve=False): + + if abstract.isIPAddress(host): + self.addressFamily = socket.AF_INET + self.realAddress = (host, port) + + elif abstract.isIPv6Address(host): + # we need to pass this through getaddrinfo to handle + # scope ID, but we don't want it to block, hence + # the flags.. + self.addressFamily = socket.AF_INET6 + replies = socket.getaddrinfo(host, port, + self.addressFamily, + self.socketType, + 0, + socket.AI_NUMERICHOST # numeric hosts - no DNS! + ) + if not replies: + err = error.DNSResolutionError('couldnt resolve v6 scope '+host) + self.failIfNotConnected(err) + return + # FIXME: what to do about multiple replies? + self.realAddress = replies[0][4] + + elif resolve: + # resolve is only set on 1st call from "init"; 2nd + # call is the answer, we set ourselves as callback + # FIXME: currently this only does IPv4 lookups + d = self.reactor.resolve(host) + d.addCallback(self._start, port).addErrback(self.failIfNotConnected) + return + + else: + # wtf? + err = error.DNSResolutionError('couldnt resolve '+self.addr[0]) + self.failIfNotConnected(err) + return + + # create our socket try: skt = self.createInternetSocket() except socket.error, se: err = error.ConnectBindError(se[0], se[1]) - whenDone = None - if whenDone and bindAddress is not None: + self._finishInit(None, skt, err, self.reactor) + return + + if self.bindAddress is not None: try: - skt.bind(bindAddress) + skt.bind(self.bindAddress) except socket.error, se: err = error.ConnectBindError(se[0], se[1]) - whenDone = None - self._finishInit(whenDone, skt, err, reactor) + self._finishInit(None, skt, err, self.reactor) + return + + self._finishInit(self.doConnect, skt, None, self.reactor) def getHost(self): """Returns an IPv4Address. This indicates the address from which I am connecting. """ - return address.IPv4Address('TCP', *(self.socket.getsockname() + ('INET',))) + addr = self.socket.getsockname() + return address.IPv4Address('TCP', addr[0], addr[1], 'INET') def getPeer(self): """Returns an IPv4Address. This indicates the address that I am connected to. """ - return address.IPv4Address('TCP', *(self.realAddress + ('INET',))) + return address.IPv4Address('TCP', self.realAddress[0], self.realAddress[1], 'INET') def __repr__(self): s = '<%s to %s at %x>' % (self.__class__, self.addr, unsignedID(self)) @@ -770,14 +808,15 @@ class Server(Connection): This indicates the server's address. """ - return address.IPv4Address('TCP', *(self.socket.getsockname() + ('INET',))) + addr = self.socket.getsockname() + return address.IPv4Address('TCP', addr[0], addr[1], 'INET') def getPeer(self): """Returns an IPv4Address. This indicates the client's address. """ - return address.IPv4Address('TCP', *(self.client + ('INET',))) + return address.IPv4Address('TCP', self.client[0], self.client[1], 'INET') class Port(base.BasePort, _SocketCloser): """ @@ -827,6 +866,8 @@ class Port(base.BasePort, _SocketCloser): self.factory = factory self.backlog = backlog self.interface = interface + if interface and abstract.isIPv6Address(interface): + self.addressFamily = socket.AF_INET6 def __repr__(self): if self._realPortNumber is not None: @@ -872,8 +913,8 @@ class Port(base.BasePort, _SocketCloser): self.startReading() - def _buildAddr(self, (host, port)): - return address._ServerFactoryIPv4Address('TCP', host, port) + def _buildAddr(self, addr): + return address._ServerFactoryIPv4Address('TCP', addr[0], addr[1]) def doRead(self): @@ -1005,7 +1046,8 @@ class Port(base.BasePort, _SocketCloser): This indicates the server's address. """ - return address.IPv4Address('TCP', *(self.socket.getsockname() + ('INET',))) + addr = self.socket.getsockname() + return address.IPv4Address('TCP', addr[0], addr[1], 'INET') class Connector(base.BaseConnector): def __init__(self, host, port, factory, timeout, bindAddress, reactor=None): diff --git a/twisted/internet/test/test_ipv6.py b/twisted/internet/test/test_ipv6.py new file mode 100644 index 0000000..b1aa1d5 --- /dev/null +++ b/twisted/internet/test/test_ipv6.py @@ -0,0 +1,205 @@ +# Copyright (c) 2008-2010 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for implementations of IPv6 +""" + +__metaclass__ = type + +import socket + +from zope.interface import implements +from zope.interface.verify import verifyObject + +from twisted.internet.test.reactormixins import ReactorBuilder +from twisted.internet.error import DNSLookupError +from twisted.internet.interfaces import IResolverSimple, IConnector, IListeningPort +from twisted.internet.address import IPv4Address +from twisted.internet.defer import succeed, fail, maybeDeferred +from twisted.internet.protocol import ServerFactory, ClientFactory, Protocol, DatagramProtocol +from twisted.python import log + +class Stop(ClientFactory): + """ + A client factory which stops a reactor when a connection attempt fails. + """ + def __init__(self, reactor): + self.reactor = reactor + + + def clientConnectionFailed(self, connector, reason): + self.reactor.stop() + +class v6TestsBuilder(ReactorBuilder): + def _freePort(self, interface='::1'): + probe = socket.socket(socket.AF_INET6) + try: + probe.bind((interface, 0)) + ip, port, flow, scope = probe.getsockname() + return (ip, port) + finally: + probe.close() + + + def test_clientConnectionFailedStopsReactor(self): + host, port = self._freePort() + + reactor = self.buildReactor() + + reactor.connectTCP('::1', port, Stop(reactor)) + reactor.run() + + + def test_addresses(self): + """ + A client's transport's C{getHost} and C{getPeer} return L{IPv4Address} + instances which give the dotted-quad string form of the local and + remote endpoints of the connection respectively. + """ + host, port = self._freePort() + reactor = self.buildReactor() + + serverFactory = ServerFactory() + serverFactory.protocol = Protocol + server = reactor.listenTCP(0, serverFactory, interface='::1') + serverAddress = server.getHost() + + addresses = {'host': None, 'peer': None} + class CheckAddress(Protocol): + def makeConnection(self, transport): + addresses['host'] = transport.getHost() + addresses['peer'] = transport.getPeer() + reactor.stop() + + clientFactory = Stop(reactor) + clientFactory.protocol = CheckAddress + reactor.connectTCP('::1', server.getHost().port, clientFactory, bindAddress=('::1', port)) + + reactor.run() # self.runReactor(reactor) + + self.assertEqual( + addresses['host'], + IPv4Address('TCP', '::1', port)) + self.assertEqual( + addresses['peer'], + IPv4Address('TCP', '::1', serverAddress.port)) + + def test_mixed4to6(self): + reactor = self.buildReactor() + + addresses = {'server': None, 'client': None, 'cserver': None} + + class SvProto(Protocol): + def connectionMade(self): + addresses['client'] = self.transport.getPeer() + addresses['server'] = self.transport.getHost() + + serverFactory = ServerFactory() + serverFactory.protocol = SvProto + host, sport = self._freePort() + server = reactor.listenTCP(sport, serverFactory, interface='::') + + class ClProto(Protocol): + def makeConnection(self, transport): + addresses['cserver'] = transport.getPeer() + reactor.stop() + + clientFactory = Stop(reactor) + clientFactory.protocol = ClProto + + host, cport = self._freePort() + server = reactor.connectTCP('127.0.0.1', sport, clientFactory, bindAddress=('', cport)) + + reactor.run() + + self.assertEqual( + addresses['client'], + IPv4Address('TCP', '::ffff:127.0.0.1', cport)) + self.assertEqual( + addresses['cserver'], + IPv4Address('TCP', '127.0.0.1', sport)) + + + +class UDP6ServerTestsBuilder(ReactorBuilder): + def _freePort(self, interface='::1'): + probe = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + try: + probe.bind((interface, 0)) + ip, port, flow, scope = probe.getsockname() + return (ip, port) + finally: + probe.close() + + def test_interface(self): + reactor = self.buildReactor() + port = reactor.listenUDP(0, DatagramProtocol(), interface='::') + self.assertTrue(verifyObject(IListeningPort, port)) + + def test_traffic(self): + reactor = self.buildReactor() + + dgs = [] + class Accum(DatagramProtocol): + def datagramReceived(self, data, addr): + dgs.append((addr, data)) + self.transport.write('r'+data, addr) + + class Xmit(DatagramProtocol): + def perform(self, addr, port): + self.transport.write('msg', (addr, port)) + def datagramReceived(self, data, addr): + dgs.append((addr, data)) + reactor.stop() + + ip, lport = self._freePort() + p = reactor.listenUDP(lport, Accum(), interface='::1') + + ip, xport = self._freePort() + xmit = Xmit() + p = reactor.listenUDP(xport, xmit, interface='::1') + + reactor.callLater(0, xmit.perform, '::1', lport) + reactor.callLater(10, reactor.stop) + reactor.run() + + self.assertEqual( + dgs, + [(('::1', xport), 'msg'), (('::1', lport), 'rmsg')] + ) + + def test_mixed4to6(self): + reactor = self.buildReactor() + + dgs = [] + class Accum(DatagramProtocol): + def datagramReceived(self, data, addr): + dgs.append((addr, data)) + self.transport.write('r'+data, addr) + + class Xmit(DatagramProtocol): + def perform(self, addr, port): + self.transport.write('msg', (addr, port)) + def datagramReceived(self, data, addr): + dgs.append((addr, data)) + reactor.stop() + + ip, lport = self._freePort() + p = reactor.listenUDP(lport, Accum(), interface='::') + + ip, xport = self._freePort() + xmit = Xmit() + p = reactor.listenUDP(xport, xmit) + + reactor.callLater(0, xmit.perform, '127.0.0.1', lport) + reactor.callLater(10, reactor.stop) + reactor.run() + + self.assertEqual( + dgs, + [(('::ffff:127.0.0.1', xport), 'msg'), (('127.0.0.1', lport), 'rmsg')] + ) + +globals().update(v6TestsBuilder.makeTestCaseClasses()) +globals().update(UDP6ServerTestsBuilder.makeTestCaseClasses()) diff --git a/twisted/internet/udp.py b/twisted/internet/udp.py index 3a21453..c5672c0 100644 --- a/twisted/internet/udp.py +++ b/twisted/internet/udp.py @@ -62,6 +62,8 @@ class Port(base.BasePort): self.interface = interface self.setLogStr() self._connectedAddr = None + if interface and abstract.isIPv6Address(interface): + self.addressFamily = socket.AF_INET6 def __repr__(self): if self._realPortNumber is not None: @@ -127,6 +129,7 @@ class Port(base.BasePort): else: read += len(data) try: + addr = (addr[0], addr[1]) self.protocol.datagramReceived(data, addr) except: log.err() @@ -160,7 +163,9 @@ class Port(base.BasePort): raise else: assert addr != None - if not addr[0].replace(".", "").isdigit() and addr[0] != "": + if abstract.isIPAddress(addr[0]) or abstract.isIPv6Address(addr[0]): + pass + elif addr[0] != "": warnings.warn("Please only pass IPs to write(), not hostnames", DeprecationWarning, stacklevel=2) try: @@ -188,7 +193,7 @@ class Port(base.BasePort): """ if self._connectedAddr: raise RuntimeError, "already connected, reconnecting is not currently supported (talk to itamar if you want this)" - if not abstract.isIPAddress(host): + if not abstract.isIPAddress(host) and not abstract.isIPv6Address(host): raise ValueError, "please pass only IP addresses, not domain names" self._connectedAddr = (host, port) self.socket.connect((host, port)) @@ -242,10 +247,11 @@ class Port(base.BasePort): This indicates the address from which I am connecting. """ - return address.IPv4Address('UDP', *(self.socket.getsockname() + ('INET_UDP',))) - + addr = self.socket.getsockname() + return address.IPv4Address('UDP', addr[0], addr[1], 'INET_UDP') +# FIXME: implement ipv6 multicast... class MulticastMixin: """ Implement multicast functionality.