diff --git a/src/twisted/internet/_resolver.py b/src/twisted/internet/_resolver.py new file mode 100644 index 00000000000..fc68d958edc --- /dev/null +++ b/src/twisted/internet/_resolver.py @@ -0,0 +1,263 @@ +# -*- test-case-name: twisted.internet.test.test_resolver -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +IPv6-aware hostname resolution. + +@see: L{IHostnameResolver} +""" + +from __future__ import division, absolute_import + +__metaclass__ = type + +from socket import (getaddrinfo, AF_INET, AF_INET6, AF_UNSPEC, SOCK_STREAM, + SOCK_DGRAM, gaierror) + +from zope.interface import implementer + +from twisted.internet.interfaces import (IHostnameResolver, IHostResolution, + IResolverSimple, IResolutionReceiver) +from twisted.internet.error import DNSLookupError +from twisted.internet.defer import Deferred +from twisted.internet.threads import deferToThreadPool +from twisted.internet.address import IPv4Address, IPv6Address +from twisted.logger import Logger + + +@implementer(IHostResolution) +class HostResolution(object): + """ + The in-progress resolution of a given hostname. + """ + + def __init__(self, name): + """ + Create a L{HostResolution} with the given name. + """ + self.name = name + + + +_any = frozenset([IPv4Address, IPv6Address]) + +_typesToAF = { + frozenset([IPv4Address]): AF_INET, + frozenset([IPv6Address]): AF_INET6, + _any: AF_UNSPEC, +} + +_afToType = { + AF_INET: IPv4Address, + AF_INET6: IPv6Address, +} + +_transportToSocket = { + 'TCP': SOCK_STREAM, + 'UDP': SOCK_DGRAM, +} + +_socktypeToType = { + SOCK_STREAM: 'TCP', + SOCK_DGRAM: 'UDP', +} + + + +@implementer(IHostnameResolver) +class GAIResolver(object): + """ + L{IHostnameResolver} implementation that resolves hostnames by calling + L{getaddrinfo} in a thread. + """ + + def __init__(self, reactor, getThreadPool=None, getaddrinfo=getaddrinfo): + """ + Create a L{GAIResolver}. + + @param reactor: the reactor to schedule result-delivery on + @type reactor: L{IReactorThreads} + + @param getThreadPool: a function to retrieve the thread pool to use for + scheduling name resolutions. If not supplied, the use the given + C{reactor}'s thread pool. + @type getThreadPool: 0-argument callable returning a + L{twisted.python.threadpool.ThreadPool} + + @param getaddrinfo: a reference to the L{getaddrinfo} to use - mainly + parameterized for testing. + @type getaddrinfo: callable with the same signature as L{getaddrinfo} + """ + self._reactor = reactor + self._getThreadPool = (reactor.getThreadPool if getThreadPool is None + else getThreadPool) + self._getaddrinfo = getaddrinfo + + + def resolveHostName(self, resolutionReceiver, hostName, portNumber=0, + addressTypes=None, transportSemantics='TCP'): + """ + See L{IHostnameResolver.resolveHostName} + + @param resolutionReceiver: see interface + + @param hostName: see interface + + @param portNumber: see interface + + @param addressTypes: see interface + + @param transportSemantics: see interface + + @return: see interface + """ + pool = self._getThreadPool() + addressFamily = _typesToAF[_any if addressTypes is None + else frozenset(addressTypes)] + socketType = _transportToSocket[transportSemantics] + def get(): + try: + return self._getaddrinfo(hostName, portNumber, addressFamily, + socketType) + except gaierror: + return [] + d = deferToThreadPool(self._reactor, pool, get) + resolution = HostResolution(hostName) + resolutionReceiver.resolutionBegan(resolution) + @d.addCallback + def deliverResults(result): + for family, socktype, proto, cannoname, sockaddr in result: + addrType = _afToType[family] + resolutionReceiver.addressResolved( + addrType(_socktypeToType.get(socktype, 'TCP'), *sockaddr) + ) + resolutionReceiver.resolutionComplete() + return resolution + + + +@implementer(IHostnameResolver) +class SimpleResolverComplexifier(object): + """ + A converter from L{IResolverSimple} to L{IHostnameResolver}. + """ + + _log = Logger() + + def __init__(self, simpleResolver): + """ + Construct a L{SimpleResolverComplexifier} with an L{IResolverSimple}. + """ + self._simpleResolver = simpleResolver + + + def resolveHostName(self, resolutionReceiver, hostName, portNumber=0, + addressTypes=None, transportSemantics='TCP'): + """ + See L{IHostnameResolver.resolveHostName} + + @param resolutionReceiver: see interface + + @param hostName: see interface + + @param portNumber: see interface + + @param addressTypes: see interface + + @param transportSemantics: see interface + + @return: see interface + """ + resolution = HostResolution(hostName) + resolutionReceiver.resolutionBegan(resolution) + onAddress = self._simpleResolver.getHostByName(hostName) + def addressReceived(address): + resolutionReceiver.addressResolved(IPv4Address('TCP', address, 0)) + def errorReceived(error): + if not error.check(DNSLookupError): + self._log.failure("while looking up {name} with {resolver}", + error, name=hostName, + resolver=self._simpleResolver) + onAddress.addCallbacks(addressReceived, errorReceived) + def finish(result): + resolutionReceiver.resolutionComplete() + onAddress.addCallback(finish) + return resolution + + + +@implementer(IResolutionReceiver) +class FirstOneWins(object): + """ + An L{IResolutionReceiver} which fires a L{Deferred} with its first result. + """ + + def __init__(self, deferred): + """ + @param deferred: The L{Deferred} to fire when the first resolution + result arrives. + """ + self._deferred = deferred + self._resolved = False + + + def resolutionBegan(self, resolution): + """ + See L{IResolutionReceiver.resolutionBegan} + + @param resolution: See L{IResolutionReceiver.resolutionBegan} + """ + self._resolution = resolution + + + def addressResolved(self, address): + """ + See L{IResolutionReceiver.addressResolved} + + @param address: See L{IResolutionReceiver.addressResolved} + """ + if self._resolved: + return + self._resolved = True + self._deferred.callback(address.host) + + + def resolutionComplete(self): + """ + See L{IResolutionReceiver.resolutionComplete} + """ + if self._resolved: + return + self._deferred.errback(DNSLookupError(self._resolution.name)) + + + +@implementer(IResolverSimple) +class ComplexResolverSimplifier(object): + """ + A converter from L{IHostnameResolver} to L{IResolverSimple} + """ + def __init__(self, nameResolver): + """ + Create a L{ComplexResolverSimplifier} with an L{IHostnameResolver}. + + @param nameResolver: The L{IHostnameResolver} to use. + """ + self._nameResolver = nameResolver + + + def getHostByName(self, name, timeouts=()): + """ + See L{IResolverSimple.getHostByName} + + @param name: see L{IResolverSimple.getHostByName} + + @param timeouts: see L{IResolverSimple.getHostByName} + + @return: see L{IResolverSimple.getHostByName} + """ + result = Deferred() + self._nameResolver.resolveHostName(FirstOneWins(result), name, 0, + [IPv4Address]) + return result diff --git a/src/twisted/internet/address.py b/src/twisted/internet/address.py index 2b6338ee4c9..2c50b35af77 100644 --- a/src/twisted/internet/address.py +++ b/src/twisted/internet/address.py @@ -79,8 +79,23 @@ class IPv6Address(_IPAddress): @ivar host: A string containing a colon-separated, hexadecimal formatted IPv6 address; for example, "::1". @type host: C{str} + + @ivar flowInfo: the IPv6 flow label. This can be used by QoS routers to + identify flows of traffic; you may generally safely ignore it. + @type flowInfo: L{int} + + @ivar scopeID: the IPv6 scope identifier - roughly analagous to what + interface traffic destined for this address must be transmitted over. + @type scopeID: L{int} """ + compareAttributes = ('type', 'host', 'port', 'flowInfo', 'scopeID') + + def __init__(self, type, host, port, flowInfo=0, scopeID=0): + super(IPv6Address, self).__init__(type, host, port) + self.flowInfo = flowInfo + self.scopeID = scopeID + @implementer(IAddress) diff --git a/src/twisted/internet/base.py b/src/twisted/internet/base.py index 09931aec09f..7a99a0f1c8c 100644 --- a/src/twisted/internet/base.py +++ b/src/twisted/internet/base.py @@ -21,6 +21,11 @@ from twisted.internet.interfaces import IResolverSimple, IReactorPluggableResolver from twisted.internet.interfaces import IConnector, IDelayedCall from twisted.internet import fdesc, main, error, abstract, defer, threads +from twisted.internet._resolver import ( + GAIResolver as _GAIResolver, + ComplexResolverSimplifier as _ComplexResolverSimplifier, + SimpleResolverComplexifier as _SimpleResolverComplexifier, +) from twisted.python import log, failure, reflect from twisted.python.compat import unicode, iteritems from twisted.python.runtime import seconds as runtimeSeconds, platform @@ -487,6 +492,7 @@ def __init__(self): self._startedBefore = False # reactor internal readers, e.g. the waker. self._internalReaders = set() + self._nameResolver = None self.waker = None # Arrange for the running attribute to change to True at the right time @@ -509,12 +515,45 @@ def installWaker(self): raise NotImplementedError( reflect.qual(self.__class__) + " did not implement installWaker") + def installResolver(self, resolver): + """ + See L{IReactorPluggableResolver}. + + @param resolver: see L{IReactorPluggableResolver}. + + @return: see L{IReactorPluggableResolver}. + """ assert IResolverSimple.providedBy(resolver) oldResolver = self.resolver self.resolver = resolver + self._nameResolver = _SimpleResolverComplexifier(resolver) return oldResolver + + def installNameResolver(self, resolver): + """ + See L{IReactorPluggableNameResolver}. + + @param resolver: See L{IReactorPluggableNameResolver}. + + @return: see L{IReactorPluggableNameResolver}. + """ + previousNameResolver = self._nameResolver + self._nameResolver = resolver + self.resolver = _ComplexResolverSimplifier(resolver) + return previousNameResolver + + + @property + def nameResolver(self): + """ + Implementation of read-only + L{IReactorPluggableNameResolver.nameResolver}. + """ + return self._nameResolver + + def wakeUp(self): """ Wake up the event loop. @@ -938,8 +977,9 @@ def argChecker(arg): threadpoolShutdownID = None def _initThreads(self): + self.installNameResolver(_GAIResolver(self, self.getThreadPool)) self.usingThreads = True - self.resolver = ThreadedResolver(self) + def callFromThread(self, f, *args, **kw): """ diff --git a/src/twisted/internet/interfaces.py b/src/twisted/internet/interfaces.py index f6fa9589926..62e8bfcfc3a 100644 --- a/src/twisted/internet/interfaces.py +++ b/src/twisted/internet/interfaces.py @@ -84,6 +84,110 @@ def getHostByName(name, timeout = (1, 3, 11, 45)): +class IHostResolution(Interface): + """ + An L{IHostResolution} represents represents an in-progress recursive query + for a DNS name. + + @since: 16.7 + """ + + name = Attribute( + """ + L{unicode}; the name of the host being resolved. + """ + ) + + def cancel(): + """ + Stop the hostname resolution in progress. + """ + + + +class IResolutionReceiver(Interface): + """ + An L{IResolutionReceiver} receives the results of a hostname resolution in + progress, initiated by an L{IHostnameResolver}. + + @since: 16.7 + """ + + def resolutionBegan(resolutionInProgress): + """ + A hostname resolution began. + + @param resolutionInProgress: an L{IHostResolution}. + """ + + + def addressResolved(address): + """ + An internet address. This is called when an address for the given name + is discovered. In the current implementation this practically means + L{IPv4Address} or L{IPv6Address}, but implementations of this interface + should be lenient to other types being passed to this interface as + well, for future-proofing. + + @param address: An address object. + @type address: L{IAddress} + """ + + + def resolutionComplete(): + """ + Resolution has completed; no further addresses will be relayed to + L{IResolutionReceiver.addressResolved}. + """ + + + +class IHostnameResolver(Interface): + """ + An L{IHostnameResolver} can resolve a host name and port number into a + series of L{IAddress} objects. + + @since: 16.7 + """ + + def resolveHostName(resolutionReceiver, hostName, portNumber=0, + addressTypes=None, transportSemantics='TCP'): + """ + Initiate a hostname resolution. + + @param resolutionReceiver: an object that will receive each resolved + address as it arrives. + @type resolutionReceiver: L{IResolutionReceiver} + + @param hostName: The name of the host to resolve. If this contains + non-ASCII code points, they will be converted to IDNA first. + @type hostName: L{unicode} + + @param portNumber: The port number that the returned addresses should + include. + @type portNumber: L{int} greater than or equal to 0 and less than 65536 + + @param addressTypes: An iterable of implementors of L{IAddress} that + are acceptable values for C{resolutionReceiver} to receive to its + L{addressResolved }. In + practice, this means an iterable containing + L{twisted.internet.address.IPv4Address}, + L{twisted.internet.address.IPv6Address}, both, or neither. + @type addressTypes: L{collections.Iterable} of L{type} + + @param transportSemantics: A string describing the semantics of the + transport; either C{'TCP'} for stream-oriented transports or + C{'UDP'} for datagram-oriented; see + L{twisted.internet.address.IPv6Address.type} and + L{twisted.internet.address.IPv4Address.type}. + @type transportSemantics: native L{str} + + @return: The resolution in progress. + @rtype: L{IResolutionReceiver} + """ + + + class IResolver(IResolverSimple): def query(query, timeout=None): """ @@ -1413,9 +1517,14 @@ def callWhenRunning(callable, *args, **kw): """ + class IReactorPluggableResolver(Interface): """ - A reactor with a pluggable name resolver interface. + An L{IReactorPluggableResolver} is a reactor which can be customized with + an L{IResolverSimple}. This is a fairly limited interface, that supports + only IPv4; you should use L{IReactorPluggableNameResolver} instead. + + @see: L{IReactorPluggableNameResolver} """ def installResolver(resolver): @@ -1426,9 +1535,37 @@ def installResolver(resolver): @param resolver: The new resolver to use. @return: The previously installed resolver. + @rtype: L{IResolverSimple} """ + +class IReactorPluggableNameResolver(Interface): + """ + An L{IReactorPluggableNameResolver} is a reactor whose name resolver can be + set to a user-supplied object. + """ + + nameResolver = Attribute( + """ + Read-only attribute; the resolver installed with L{installResolver}. + An L{IHostnameResolver}. + """ + ) + + def installNameResolver(resolver): + """ + Set the internal resolver to use for name lookups. + + @type resolver: An object providing the L{IHostnameResolver} interface. + @param resolver: The new resolver to use. + + @return: The previously installed resolver. + @rtype: L{IHostnameResolver} + """ + + + class IReactorDaemonize(Interface): """ A reactor which provides hooks that need to be called before and after diff --git a/src/twisted/internet/test/test_resolver.py b/src/twisted/internet/test/test_resolver.py new file mode 100644 index 00000000000..a08c67eaf3f --- /dev/null +++ b/src/twisted/internet/test/test_resolver.py @@ -0,0 +1,559 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for implementations of L{IHostnameResolver} and their interactions with +reactor implementations. +""" + +from __future__ import division, absolute_import + +__metaclass__ = type + +from collections import defaultdict + +from socket import ( + getaddrinfo, gaierror, EAI_NONAME, AF_INET, AF_INET6, AF_UNSPEC, + SOCK_STREAM, SOCK_DGRAM, IPPROTO_TCP +) +from threading import local, Lock + +from zope.interface import implementer +from zope.interface.verify import verifyObject + +from twisted.internet.interfaces import IResolutionReceiver, IResolverSimple + +from twisted.trial.unittest import ( + SynchronousTestCase as UnitTest +) + +from twisted.python.threadpool import ThreadPool +from twisted._threads import createMemoryWorker, Team, LockWorker + +from twisted.internet.address import IPv4Address, IPv6Address +from twisted.internet._resolver import ( + GAIResolver, SimpleResolverComplexifier, ComplexResolverSimplifier +) + +from twisted.internet.defer import Deferred +from twisted.internet.error import DNSLookupError +from twisted.internet.base import ReactorBase + + +class DeterministicThreadPool(ThreadPool, object): + """ + Create a deterministic L{ThreadPool} object. + """ + def __init__(self, team): + """ + Create a L{DeterministicThreadPool} from a L{Team}. + """ + self.min = 1 + self.max = 1 + self.name = None + self.threads = [] + self._team = team + + + +def deterministicPool(): + """ + Create a deterministic threadpool. + + @return: 2-tuple of L{ThreadPool}, 0-argument C{work} callable; when + C{work} is called, do the work. + """ + worker, doer = createMemoryWorker() + return ( + DeterministicThreadPool(Team(LockWorker(Lock(), local()), + (lambda: worker), lambda: None)), + doer + ) + + + +def deterministicReactorThreads(): + """ + Create a deterministic L{IReactorThreads} + + @return: a 2-tuple consisting of an L{IReactorThreads}-like object and a + 0-argument callable that will perform one unit of work invoked via that + object's C{callFromThread} method. + """ + worker, doer = createMemoryWorker() + class CFT(object): + def callFromThread(self, f, *a, **k): + worker.do(lambda: f(*a, **k)) + return CFT(), doer + + + +class FakeAddrInfoGetter(object): + """ + Test object implementing getaddrinfo. + """ + + def __init__(self): + """ + Create a L{FakeAddrInfoGetter}. + """ + self.calls = [] + self.results = defaultdict(list) + + + def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): + """ + Mock for L{socket.getaddrinfo}. + + @param host: see L{socket.getaddrinfo} + + @param port: see L{socket.getaddrinfo} + + @param family: see L{socket.getaddrinfo} + + @param socktype: see L{socket.getaddrinfo} + + @param proto: see L{socket.getaddrinfo} + + @param flags: see L{socket.getaddrinfo} + + @return: L{socket.getaddrinfo} + """ + self.calls.append((host, port, family, socktype, proto, flags)) + results = self.results[host] + if results: + return results + else: + raise gaierror(EAI_NONAME, + 'nodename nor servname provided, or not known') + + + def addResultForHost(self, host, sockaddr, family=AF_INET, + socktype=SOCK_STREAM, proto=IPPROTO_TCP, + canonname=b""): + """ + Add a result for a given hostname. When this hostname is resolved, the + result will be a L{list} of all results C{addResultForHost} has been + called with using that hostname so far. + + @param host: The hostname to give this result for. This will be the + next result from L{FakeAddrInfoGetter.getaddrinfo} when passed this + host. + + @type canonname: native L{str} + + @param sockaddr: The resulting socket address; should be a 2-tuple for + IPv4 or a 4-tuple for IPv6. + + @param family: An C{AF_*} constant that will be returned from + C{getaddrinfo}. + + @param socktype: A C{SOCK_*} constant that will be returned from + C{getaddrinfo}. + + @param proto: An C{IPPROTO_*} constant that will be returned from + C{getaddrinfo}. + + @param canonname: A canonical name that will be returned from + C{getaddrinfo}. + @type canonname: native L{str} + """ + self.results[host].append( + (family, socktype, proto, canonname, sockaddr) + ) + + + +@implementer(IResolutionReceiver) +class ResultHolder(object): + """ + A resolution receiver which holds onto the results it received. + """ + _started = False + _ended = False + + def __init__(self, testCase): + """ + Create a L{ResultHolder} with a L{UnitTest}. + """ + self._testCase = testCase + + + def resolutionBegan(self, hostResolution): + """ + Hostname resolution began. + + @param hostResolution: see L{IResolutionReceiver} + """ + self._started = True + self._resolution = hostResolution + self._addresses = [] + + + def addressResolved(self, address): + """ + An address was resolved. + + @param address: see L{IResolutionReceiver} + """ + self._addresses.append(address) + + + def resolutionComplete(self): + """ + Hostname resolution is complete. + """ + self._ended = True + + + +class HelperTests(UnitTest): + """ + Tests for error cases of helpers used in this module. + """ + + def test_logErrorsInThreads(self): + """ + L{DeterministicThreadPool} will log any exceptions that its "thread" + workers encounter. + """ + self.pool, self.doThreadWork = deterministicPool() + def divideByZero(): + return 1 / 0 + self.pool.callInThread(divideByZero) + self.doThreadWork() + self.assertEqual(len(self.flushLoggedErrors(ZeroDivisionError)), 1) + + + +class HostnameResolutionTests(UnitTest): + """ + Tests for hostname resolution. + """ + + def setUp(self): + """ + Set up a L{GAIResolver}. + """ + self.pool, self.doThreadWork = deterministicPool() + self.reactor, self.doReactorWork = deterministicReactorThreads() + self.getter = FakeAddrInfoGetter() + self.resolver = GAIResolver(self.reactor, lambda: self.pool, + self.getter.getaddrinfo) + + + def test_resolveOneHost(self): + """ + Resolving an individual hostname that results in one address from + getaddrinfo results in a single call each to C{resolutionBegan}, + C{addressResolved}, and C{resolutionComplete}. + """ + receiver = ResultHolder(self) + self.getter.addResultForHost(u"sample.example.com", ("4.3.2.1", 0)) + resolution = self.resolver.resolveHostName(receiver, + u"sample.example.com") + self.assertIs(receiver._resolution, resolution) + self.assertEqual(receiver._started, True) + self.assertEqual(receiver._ended, False) + self.doThreadWork() + self.doReactorWork() + self.assertEqual(receiver._ended, True) + self.assertEqual(receiver._addresses, + [IPv4Address('TCP', '4.3.2.1', 0)]) + + + def test_resolveOneIPv6Host(self): + """ + Resolving an individual hostname that results in one address from + getaddrinfo results in a single call each to C{resolutionBegan}, + C{addressResolved}, and C{resolutionComplete}; C{addressResolved} will + receive an L{IPv6Address}. + """ + receiver = ResultHolder(self) + flowInfo = 1 + scopeID = 2 + self.getter.addResultForHost(u"sample.example.com", + ("::1", 0, flowInfo, scopeID), + family=AF_INET6) + resolution = self.resolver.resolveHostName(receiver, + u"sample.example.com") + self.assertIs(receiver._resolution, resolution) + self.assertEqual(receiver._started, True) + self.assertEqual(receiver._ended, False) + self.doThreadWork() + self.doReactorWork() + self.assertEqual(receiver._ended, True) + self.assertEqual(receiver._addresses, + [IPv6Address('TCP', '::1', 0, flowInfo, scopeID)]) + + + def test_gaierror(self): + """ + Resolving a hostname that results in C{getaddrinfo} raising a + L{gaierror} will result in the L{IResolutionReceiver} receiving a call + to C{resolutionComplete} with no C{addressResolved} calls in between; + no failure is logged. + """ + receiver = ResultHolder(self) + resolution = self.resolver.resolveHostName(receiver, + u"sample.example.com") + self.assertIs(receiver._resolution, resolution) + self.doThreadWork() + self.doReactorWork() + self.assertEqual(receiver._started, True) + self.assertEqual(receiver._ended, True) + self.assertEqual(receiver._addresses, []) + + + def _resolveOnlyTest(self, addrTypes, expectedAF): + """ + Verify that the given set of address types results in the given C{AF_} + constant being passed to C{getaddrinfo}. + + @param addrTypes: iterable of L{IAddress} implementers + + @param expectedAF: an C{AF_*} constant + """ + receiver = ResultHolder(self) + resolution = self.resolver.resolveHostName( + receiver, u"sample.example.com", addressTypes=addrTypes + ) + self.assertIs(receiver._resolution, resolution) + self.doThreadWork() + self.doReactorWork() + host, port, family, socktype, proto, flags = self.getter.calls[0] + self.assertEqual(family, expectedAF) + + + def test_resolveOnlyIPv4(self): + """ + When passed an C{addressTypes} parameter containing only + L{IPv4Address}, L{GAIResolver} will pass C{AF_INET} to C{getaddrinfo}. + """ + self._resolveOnlyTest([IPv4Address], AF_INET) + + + def test_resolveOnlyIPv6(self): + """ + When passed an C{addressTypes} parameter containing only + L{IPv6Address}, L{GAIResolver} will pass C{AF_INET6} to C{getaddrinfo}. + """ + self._resolveOnlyTest([IPv6Address], AF_INET6) + + + def test_resolveBoth(self): + """ + When passed an C{addressTypes} parameter containing both L{IPv4Address} + and L{IPv6Address} (or the default of C{None}, which carries the same + meaning), L{GAIResolver} will pass C{AF_UNSPEC} to C{getaddrinfo}. + """ + self._resolveOnlyTest([IPv4Address, IPv6Address], AF_UNSPEC) + self._resolveOnlyTest(None, AF_UNSPEC) + + + def test_transportSemanticsToSocketType(self): + """ + When passed a C{transportSemantics} paramter, C{'TCP'} (the value + present in L{IPv4Address.type} to indicate a stream transport) maps to + C{SOCK_STREAM} and C{'UDP'} maps to C{SOCK_DGRAM}. + """ + receiver = ResultHolder(self) + self.resolver.resolveHostName(receiver, u"example.com", + transportSemantics='TCP') + receiver2 = ResultHolder(self) + self.resolver.resolveHostName(receiver2, u"example.com", + transportSemantics='UDP') + self.doThreadWork() + self.doReactorWork() + self.doThreadWork() + self.doReactorWork() + host, port, family, socktypeT, proto, flags = self.getter.calls[0] + host, port, family, socktypeU, proto, flags = self.getter.calls[1] + self.assertEqual(socktypeT, SOCK_STREAM) + self.assertEqual(socktypeU, SOCK_DGRAM) + + + def test_socketTypeToAddressType(self): + """ + When L{GAIResolver} receives a C{SOCK_DGRAM} result from + C{getaddrinfo}, it returns a C{'TCP'} L{IPv4Address} or L{IPv6Address}; + if it receives C{SOCK_STREAM} then it returns a C{'UDP'} type of same. + """ + receiver = ResultHolder(self) + flowInfo = 1 + scopeID = 2 + for socktype in SOCK_STREAM, SOCK_DGRAM: + self.getter.addResultForHost( + "example.com", ("::1", 0, flowInfo, scopeID), family=AF_INET6, + socktype=socktype + ) + self.getter.addResultForHost( + "example.com", ("127.0.0.3", 0), family=AF_INET, + socktype=socktype + ) + self.resolver.resolveHostName(receiver, u"example.com") + self.doThreadWork() + self.doReactorWork() + stream4, stream6, dgram4, dgram6 = receiver._addresses + self.assertEqual(stream4.type, 'TCP') + self.assertEqual(stream6.type, 'TCP') + self.assertEqual(dgram4.type, 'UDP') + self.assertEqual(dgram6.type, 'UDP') + + + +@implementer(IResolverSimple) +class SillyResolverSimple(object): + """ + Trivial implementation of L{IResolverSimple} + """ + def __init__(self): + """ + Create a L{SillyResolverSimple} with a queue of requests it is working + on. + """ + self._requests = [] + + + def getHostByName(self, name, timeout=()): + """ + Implement L{IResolverSimple.getHostByName}. + + @param name: see L{IResolverSimple.getHostByName}. + + @param timeout: see L{IResolverSimple.getHostByName}. + + @return: see L{IResolverSimple.getHostByName}. + """ + self._requests.append(Deferred()) + return self._requests[-1] + + + +class LegacyCompatibilityTests(UnitTest, object): + """ + Older applications may supply an object to the reactor via + C{installResolver} that only provides L{IResolverSimple}. + L{SimpleResolverComplexifier} is a wrapper for an L{IResolverSimple}. + """ + + def test_success(self): + """ + L{SimpleResolverComplexifier} translates C{resolveHostName} into + L{IResolutionReceiver.addressResolved}. + """ + simple = SillyResolverSimple() + complex = SimpleResolverComplexifier(simple) + receiver = ResultHolder(self) + self.assertEqual(receiver._started, False) + complex.resolveHostName(receiver, u"example.com") + self.assertEqual(receiver._started, True) + self.assertEqual(receiver._ended, False) + self.assertEqual(receiver._addresses, []) + simple._requests[0].callback("192.168.1.1") + self.assertEqual(receiver._addresses, + [IPv4Address('TCP', '192.168.1.1', 0)]) + self.assertEqual(receiver._ended, True) + + + def test_failure(self): + """ + L{SimpleResolverComplexifier} translates a known error result from + L{IResolverSimple.resolveHostName} into an empty result. + """ + simple = SillyResolverSimple() + complex = SimpleResolverComplexifier(simple) + receiver = ResultHolder(self) + self.assertEqual(receiver._started, False) + complex.resolveHostName(receiver, u"example.com") + self.assertEqual(receiver._started, True) + self.assertEqual(receiver._ended, False) + self.assertEqual(receiver._addresses, []) + simple._requests[0].errback(DNSLookupError("nope")) + self.assertEqual(receiver._ended, True) + self.assertEqual(receiver._addresses, []) + + + def test_error(self): + """ + L{SimpleResolverComplexifier} translates an unknown error result from + L{IResolverSimple.resolveHostName} into an empty result and a logged + error. + """ + simple = SillyResolverSimple() + complex = SimpleResolverComplexifier(simple) + receiver = ResultHolder(self) + self.assertEqual(receiver._started, False) + complex.resolveHostName(receiver, u"example.com") + self.assertEqual(receiver._started, True) + self.assertEqual(receiver._ended, False) + self.assertEqual(receiver._addresses, []) + simple._requests[0].errback(ZeroDivisionError("zow")) + self.assertEqual(len(self.flushLoggedErrors(ZeroDivisionError)), 1) + self.assertEqual(receiver._ended, True) + self.assertEqual(receiver._addresses, []) + + + def test_simplifier(self): + """ + L{ComplexResolverSimplifier} translates an L{IHostnameResolver} into an + L{IResolverSimple} for applications that still expect the old + interfaces to be in place. + """ + self.pool, self.doThreadWork = deterministicPool() + self.reactor, self.doReactorWork = deterministicReactorThreads() + self.getter = FakeAddrInfoGetter() + self.resolver = GAIResolver(self.reactor, lambda: self.pool, + self.getter.getaddrinfo) + simpleResolver = ComplexResolverSimplifier(self.resolver) + self.getter.addResultForHost('example.com', ('192.168.3.4', 4321)) + success = simpleResolver.getHostByName('example.com') + failure = simpleResolver.getHostByName('nx.example.com') + self.doThreadWork() + self.doReactorWork() + self.doThreadWork() + self.doReactorWork() + self.assertEqual(self.failureResultOf(failure).type, DNSLookupError) + self.assertEqual(self.successResultOf(success), '192.168.3.4') + + + +class JustEnoughReactor(ReactorBase, object): + """ + Just enough subclass implementation to be a valid L{ReactorBase} subclass. + """ + def installWaker(self): + """ + Do nothing. + """ + + + +class ReactorInstallationTests(UnitTest, object): + """ + Tests for installing old and new resolvers onto a L{ReactorBase} (from + which all of Twisted's reactor implementations derive). + """ + + def test_defaultToGAIResolver(self): + """ + L{ReactorBase} defaults to using a L{GAIResolver}. + """ + reactor = JustEnoughReactor() + self.assertIsInstance(reactor.nameResolver, GAIResolver) + self.assertIs(reactor.nameResolver._getaddrinfo, getaddrinfo) + self.assertIsInstance(reactor.resolver, ComplexResolverSimplifier) + self.assertIs(reactor.nameResolver._reactor, reactor) + self.assertIs(reactor.resolver._nameResolver, reactor.nameResolver) + + + def test_installingOldStyleResolver(self): + """ + L{ReactorBase} will wrap an L{IResolverSimple} in a complexifier. + """ + reactor = JustEnoughReactor() + it = SillyResolverSimple() + verifyObject(IResolverSimple, reactor.installResolver(it)) + self.assertIsInstance(reactor.nameResolver, SimpleResolverComplexifier) + self.assertIs(reactor.nameResolver._simpleResolver, it) + diff --git a/src/twisted/topfiles/4362.feature b/src/twisted/topfiles/4362.feature new file mode 100644 index 00000000000..0baa6da1a6e --- /dev/null +++ b/src/twisted/topfiles/4362.feature @@ -0,0 +1,4 @@ +Added a new interface, twisted.internet.interfaces.IHostnameResolver, which is +an improvement to twisted.internet.interfaces.IResolverSimple that supports +resolving multiple addresses as well as resolving IPv6 addresses. This is a +native, asynchronous, Twisted analogue to getaddrinfo.