596 lines
20 KiB
Python
596 lines
20 KiB
Python
# 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, IReactorPluggableNameResolver,
|
|
IHostnameResolver,
|
|
)
|
|
|
|
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 (
|
|
PluggableResolverMixin,
|
|
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')
|
|
|
|
|
|
def test_portNumber(self):
|
|
"""
|
|
L{SimpleResolverComplexifier} preserves the C{port} argument passed to
|
|
C{resolveHostName} in its returned addresses.
|
|
"""
|
|
simple = SillyResolverSimple()
|
|
complex = SimpleResolverComplexifier(simple)
|
|
receiver = ResultHolder(self)
|
|
complex.resolveHostName(receiver, u"example.com", 4321)
|
|
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', 4321)])
|
|
self.assertEqual(receiver._ended, True)
|
|
|
|
|
|
|
|
class JustEnoughReactor(ReactorBase):
|
|
"""
|
|
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{PluggableResolverMixin} and L{ReactorBase} (from which all of Twisted's
|
|
reactor implementations derive).
|
|
"""
|
|
|
|
def test_interfaceCompliance(self):
|
|
"""
|
|
L{PluggableResolverMixin} (and its subclasses) implement both
|
|
L{IReactorPluggableNameResolver} and L{IReactorPluggableResolver}.
|
|
"""
|
|
reactor = PluggableResolverMixin()
|
|
verifyObject(IReactorPluggableNameResolver, reactor)
|
|
verifyObject(IResolverSimple, reactor.resolver)
|
|
verifyObject(IHostnameResolver, reactor.nameResolver)
|
|
|
|
|
|
def test_installingOldStyleResolver(self):
|
|
"""
|
|
L{PluggableResolverMixin} will wrap an L{IResolverSimple} in a
|
|
complexifier.
|
|
"""
|
|
reactor = PluggableResolverMixin()
|
|
it = SillyResolverSimple()
|
|
verifyObject(IResolverSimple, reactor.installResolver(it))
|
|
self.assertIsInstance(reactor.nameResolver, SimpleResolverComplexifier)
|
|
self.assertIs(reactor.nameResolver._simpleResolver, it)
|
|
|
|
|
|
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)
|