Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

srvconnect.py 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. # -*- test-case-name: twisted.names.test.test_srvconnect -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. import random
  5. from zope.interface import implementer
  6. from twisted.internet import error, interfaces
  7. from twisted.names import client, dns
  8. from twisted.names.error import DNSNameError
  9. from twisted.python.compat import nativeString
  10. class _SRVConnector_ClientFactoryWrapper:
  11. def __init__(self, connector, wrappedFactory):
  12. self.__connector = connector
  13. self.__wrappedFactory = wrappedFactory
  14. def startedConnecting(self, connector):
  15. self.__wrappedFactory.startedConnecting(self.__connector)
  16. def clientConnectionFailed(self, connector, reason):
  17. self.__connector.connectionFailed(reason)
  18. def clientConnectionLost(self, connector, reason):
  19. self.__connector.connectionLost(reason)
  20. def __getattr__(self, key):
  21. return getattr(self.__wrappedFactory, key)
  22. @implementer(interfaces.IConnector)
  23. class SRVConnector:
  24. """
  25. A connector that looks up DNS SRV records.
  26. RFC 2782 details how SRV records should be interpreted and selected
  27. for subsequent connection attempts. The algorithm for using the records'
  28. priority and weight is implemented in L{pickServer}.
  29. @ivar servers: List of candidate server records for future connection
  30. attempts.
  31. @type servers: L{list} of L{dns.Record_SRV}
  32. @ivar orderedServers: List of server records that have already been tried
  33. in this round of connection attempts.
  34. @type orderedServers: L{list} of L{dns.Record_SRV}
  35. """
  36. stopAfterDNS = 0
  37. def __init__(
  38. self,
  39. reactor,
  40. service,
  41. domain,
  42. factory,
  43. protocol="tcp",
  44. connectFuncName="connectTCP",
  45. connectFuncArgs=(),
  46. connectFuncKwArgs={},
  47. defaultPort=None,
  48. ):
  49. """
  50. @param domain: The domain to connect to. If passed as a text
  51. string, it will be encoded using C{idna} encoding.
  52. @type domain: L{bytes} or L{str}
  53. @param defaultPort: Optional default port number to be used when SRV
  54. lookup fails and the service name is unknown. This should be the
  55. port number associated with the service name as defined by the IANA
  56. registry.
  57. @type defaultPort: L{int}
  58. """
  59. self.reactor = reactor
  60. self.service = service
  61. self.domain = None if domain is None else dns.domainString(domain)
  62. self.factory = factory
  63. self.protocol = protocol
  64. self.connectFuncName = connectFuncName
  65. self.connectFuncArgs = connectFuncArgs
  66. self.connectFuncKwArgs = connectFuncKwArgs
  67. self._defaultPort = defaultPort
  68. self.connector = None
  69. self.servers = None
  70. # list of servers already used in this round:
  71. self.orderedServers = None
  72. def connect(self):
  73. """Start connection to remote server."""
  74. self.factory.doStart()
  75. self.factory.startedConnecting(self)
  76. if not self.servers:
  77. if self.domain is None:
  78. self.connectionFailed(
  79. error.DNSLookupError("Domain is not defined."),
  80. )
  81. return
  82. d = client.lookupService(
  83. "_%s._%s.%s"
  84. % (
  85. nativeString(self.service),
  86. nativeString(self.protocol),
  87. nativeString(self.domain),
  88. ),
  89. )
  90. d.addCallbacks(self._cbGotServers, self._ebGotServers)
  91. d.addCallback(lambda x, self=self: self._reallyConnect())
  92. if self._defaultPort:
  93. d.addErrback(self._ebServiceUnknown)
  94. d.addErrback(self.connectionFailed)
  95. elif self.connector is None:
  96. self._reallyConnect()
  97. else:
  98. self.connector.connect()
  99. def _ebGotServers(self, failure):
  100. failure.trap(DNSNameError)
  101. # Some DNS servers reply with NXDOMAIN when in fact there are
  102. # just no SRV records for that domain. Act as if we just got an
  103. # empty response and use fallback.
  104. self.servers = []
  105. self.orderedServers = []
  106. def _cbGotServers(self, result):
  107. answers, auth, add = result
  108. if (
  109. len(answers) == 1
  110. and answers[0].type == dns.SRV
  111. and answers[0].payload
  112. and answers[0].payload.target == dns.Name(b".")
  113. ):
  114. # decidedly not available
  115. raise error.DNSLookupError(
  116. "Service %s not available for domain %s."
  117. % (repr(self.service), repr(self.domain))
  118. )
  119. self.servers = []
  120. self.orderedServers = []
  121. for a in answers:
  122. if a.type != dns.SRV or not a.payload:
  123. continue
  124. self.orderedServers.append(a.payload)
  125. def _ebServiceUnknown(self, failure):
  126. """
  127. Connect to the default port when the service name is unknown.
  128. If no SRV records were found, the service name will be passed as the
  129. port. If resolving the name fails with
  130. L{error.ServiceNameUnknownError}, a final attempt is done using the
  131. default port.
  132. """
  133. failure.trap(error.ServiceNameUnknownError)
  134. self.servers = [dns.Record_SRV(0, 0, self._defaultPort, self.domain)]
  135. self.orderedServers = []
  136. self.connect()
  137. def pickServer(self):
  138. """
  139. Pick the next server.
  140. This selects the next server from the list of SRV records according
  141. to their priority and weight values, as set out by the default
  142. algorithm specified in RFC 2782.
  143. At the beginning of a round, L{servers} is populated with
  144. L{orderedServers}, and the latter is made empty. L{servers}
  145. is the list of candidates, and L{orderedServers} is the list of servers
  146. that have already been tried.
  147. First, all records are ordered by priority and weight in ascending
  148. order. Then for each priority level, a running sum is calculated
  149. over the sorted list of records for that priority. Then a random value
  150. between 0 and the final sum is compared to each record in order. The
  151. first record that is greater than or equal to that random value is
  152. chosen and removed from the list of candidates for this round.
  153. @return: A tuple of target hostname and port from the chosen DNS SRV
  154. record.
  155. @rtype: L{tuple} of native L{str} and L{int}
  156. """
  157. assert self.servers is not None
  158. assert self.orderedServers is not None
  159. if not self.servers and not self.orderedServers:
  160. # no SRV record, fall back..
  161. return nativeString(self.domain), self.service
  162. if not self.servers and self.orderedServers:
  163. # start new round
  164. self.servers = self.orderedServers
  165. self.orderedServers = []
  166. assert self.servers
  167. self.servers.sort(key=lambda record: (record.priority, record.weight))
  168. minPriority = self.servers[0].priority
  169. index = 0
  170. weightSum = 0
  171. weightIndex = []
  172. for x in self.servers:
  173. if x.priority == minPriority:
  174. weightSum += x.weight
  175. weightIndex.append((index, weightSum))
  176. index += 1
  177. rand = random.randint(0, weightSum)
  178. for index, weight in weightIndex:
  179. if weight >= rand:
  180. chosen = self.servers[index]
  181. del self.servers[index]
  182. self.orderedServers.append(chosen)
  183. return str(chosen.target), chosen.port
  184. raise RuntimeError(f"Impossible {self.__class__.__name__} pickServer result.")
  185. def _reallyConnect(self):
  186. if self.stopAfterDNS:
  187. self.stopAfterDNS = 0
  188. return
  189. self.host, self.port = self.pickServer()
  190. assert self.host is not None, "Must have a host to connect to."
  191. assert self.port is not None, "Must have a port to connect to."
  192. connectFunc = getattr(self.reactor, self.connectFuncName)
  193. self.connector = connectFunc(
  194. self.host,
  195. self.port,
  196. _SRVConnector_ClientFactoryWrapper(self, self.factory),
  197. *self.connectFuncArgs,
  198. **self.connectFuncKwArgs,
  199. )
  200. def stopConnecting(self):
  201. """Stop attempting to connect."""
  202. if self.connector:
  203. self.connector.stopConnecting()
  204. else:
  205. self.stopAfterDNS = 1
  206. def disconnect(self):
  207. """Disconnect whatever our are state is."""
  208. if self.connector is not None:
  209. self.connector.disconnect()
  210. else:
  211. self.stopConnecting()
  212. def getDestination(self):
  213. assert self.connector
  214. return self.connector.getDestination()
  215. def connectionFailed(self, reason):
  216. self.factory.clientConnectionFailed(self, reason)
  217. self.factory.doStop()
  218. def connectionLost(self, reason):
  219. self.factory.clientConnectionLost(self, reason)
  220. self.factory.doStop()