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.

authority.py 16KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. # -*- test-case-name: twisted.names.test.test_names -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Authoritative resolvers.
  6. """
  7. import os
  8. import time
  9. from twisted.internet import defer
  10. from twisted.names import common, dns, error
  11. from twisted.python import failure
  12. from twisted.python.compat import execfile, nativeString
  13. from twisted.python.filepath import FilePath
  14. def getSerial(filename="/tmp/twisted-names.serial"):
  15. """
  16. Return a monotonically increasing (across program runs) integer.
  17. State is stored in the given file. If it does not exist, it is
  18. created with rw-/---/--- permissions.
  19. This manipulates process-global state by calling C{os.umask()}, so it isn't
  20. thread-safe.
  21. @param filename: Path to a file that is used to store the state across
  22. program runs.
  23. @type filename: L{str}
  24. @return: a monotonically increasing number
  25. @rtype: L{str}
  26. """
  27. serial = time.strftime("%Y%m%d")
  28. o = os.umask(0o177)
  29. try:
  30. if not os.path.exists(filename):
  31. with open(filename, "w") as f:
  32. f.write(serial + " 0")
  33. finally:
  34. os.umask(o)
  35. with open(filename) as serialFile:
  36. lastSerial, zoneID = serialFile.readline().split()
  37. zoneID = (lastSerial == serial) and (int(zoneID) + 1) or 0
  38. with open(filename, "w") as serialFile:
  39. serialFile.write("%s %d" % (serial, zoneID))
  40. serial = serial + ("%02d" % (zoneID,))
  41. return serial
  42. class FileAuthority(common.ResolverBase):
  43. """
  44. An Authority that is loaded from a file.
  45. This is an abstract class that implements record search logic. To create
  46. a functional resolver, subclass it and override the L{loadFile} method.
  47. @ivar _ADDITIONAL_PROCESSING_TYPES: Record types for which additional
  48. processing will be done.
  49. @ivar _ADDRESS_TYPES: Record types which are useful for inclusion in the
  50. additional section generated during additional processing.
  51. @ivar soa: A 2-tuple containing the SOA domain name as a L{bytes} and a
  52. L{dns.Record_SOA}.
  53. @ivar records: A mapping of domains (as lowercased L{bytes}) to records.
  54. @type records: L{dict} with L{bytes} keys
  55. """
  56. # See https://twistedmatrix.com/trac/ticket/6650
  57. _ADDITIONAL_PROCESSING_TYPES = (dns.CNAME, dns.MX, dns.NS)
  58. _ADDRESS_TYPES = (dns.A, dns.AAAA)
  59. soa = None
  60. records = None
  61. def __init__(self, filename):
  62. common.ResolverBase.__init__(self)
  63. self.loadFile(filename)
  64. self._cache = {}
  65. def __setstate__(self, state):
  66. self.__dict__ = state
  67. def loadFile(self, filename):
  68. """
  69. Load DNS records from a file.
  70. This method populates the I{soa} and I{records} attributes. It must be
  71. overridden in a subclass. It is called once from the initializer.
  72. @param filename: The I{filename} parameter that was passed to the
  73. initilizer.
  74. @returns: L{None} -- the return value is ignored
  75. """
  76. def _additionalRecords(self, answer, authority, ttl):
  77. """
  78. Find locally known information that could be useful to the consumer of
  79. the response and construct appropriate records to include in the
  80. I{additional} section of that response.
  81. Essentially, implement RFC 1034 section 4.3.2 step 6.
  82. @param answer: A L{list} of the records which will be included in the
  83. I{answer} section of the response.
  84. @param authority: A L{list} of the records which will be included in
  85. the I{authority} section of the response.
  86. @param ttl: The default TTL for records for which this is not otherwise
  87. specified.
  88. @return: A generator of L{dns.RRHeader} instances for inclusion in the
  89. I{additional} section. These instances represent extra information
  90. about the records in C{answer} and C{authority}.
  91. """
  92. for record in answer + authority:
  93. if record.type in self._ADDITIONAL_PROCESSING_TYPES:
  94. name = record.payload.name.name
  95. for rec in self.records.get(name.lower(), ()):
  96. if rec.TYPE in self._ADDRESS_TYPES:
  97. yield dns.RRHeader(
  98. name, rec.TYPE, dns.IN, rec.ttl or ttl, rec, auth=True
  99. )
  100. def _lookup(self, name, cls, type, timeout=None):
  101. """
  102. Determine a response to a particular DNS query.
  103. @param name: The name which is being queried and for which to lookup a
  104. response.
  105. @type name: L{bytes}
  106. @param cls: The class which is being queried. Only I{IN} is
  107. implemented here and this value is presently disregarded.
  108. @type cls: L{int}
  109. @param type: The type of records being queried. See the types defined
  110. in L{twisted.names.dns}.
  111. @type type: L{int}
  112. @param timeout: All processing is done locally and a result is
  113. available immediately, so the timeout value is ignored.
  114. @return: A L{Deferred} that fires with a L{tuple} of three sets of
  115. response records (to comprise the I{answer}, I{authority}, and
  116. I{additional} sections of a DNS response) or with a L{Failure} if
  117. there is a problem processing the query.
  118. """
  119. cnames = []
  120. results = []
  121. authority = []
  122. additional = []
  123. default_ttl = max(self.soa[1].minimum, self.soa[1].expire)
  124. domain_records = self.records.get(name.lower())
  125. if domain_records:
  126. for record in domain_records:
  127. if record.ttl is not None:
  128. ttl = record.ttl
  129. else:
  130. ttl = default_ttl
  131. if record.TYPE == dns.NS and name.lower() != self.soa[0].lower():
  132. # NS record belong to a child zone: this is a referral. As
  133. # NS records are authoritative in the child zone, ours here
  134. # are not. RFC 2181, section 6.1.
  135. authority.append(
  136. dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=False)
  137. )
  138. elif record.TYPE == type or type == dns.ALL_RECORDS:
  139. results.append(
  140. dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=True)
  141. )
  142. if record.TYPE == dns.CNAME:
  143. cnames.append(
  144. dns.RRHeader(name, record.TYPE, dns.IN, ttl, record, auth=True)
  145. )
  146. if not results:
  147. results = cnames
  148. # Sort of https://tools.ietf.org/html/rfc1034#section-4.3.2 .
  149. # See https://twistedmatrix.com/trac/ticket/6732
  150. additionalInformation = self._additionalRecords(
  151. results, authority, default_ttl
  152. )
  153. if cnames:
  154. results.extend(additionalInformation)
  155. else:
  156. additional.extend(additionalInformation)
  157. if not results and not authority:
  158. # Empty response. Include SOA record to allow clients to cache
  159. # this response. RFC 1034, sections 3.7 and 4.3.4, and RFC 2181
  160. # section 7.1.
  161. authority.append(
  162. dns.RRHeader(
  163. self.soa[0], dns.SOA, dns.IN, ttl, self.soa[1], auth=True
  164. )
  165. )
  166. return defer.succeed((results, authority, additional))
  167. else:
  168. if dns._isSubdomainOf(name, self.soa[0]):
  169. # We may be the authority and we didn't find it.
  170. # XXX: The QNAME may also be in a delegated child zone. See
  171. # #6581 and #6580
  172. return defer.fail(failure.Failure(dns.AuthoritativeDomainError(name)))
  173. else:
  174. # The QNAME is not a descendant of this zone. Fail with
  175. # DomainError so that the next chained authority or
  176. # resolver will be queried.
  177. return defer.fail(failure.Failure(error.DomainError(name)))
  178. def lookupZone(self, name, timeout=10):
  179. name = dns.domainString(name)
  180. if self.soa[0].lower() == name.lower():
  181. # Wee hee hee hooo yea
  182. default_ttl = max(self.soa[1].minimum, self.soa[1].expire)
  183. if self.soa[1].ttl is not None:
  184. soa_ttl = self.soa[1].ttl
  185. else:
  186. soa_ttl = default_ttl
  187. results = [
  188. dns.RRHeader(
  189. self.soa[0], dns.SOA, dns.IN, soa_ttl, self.soa[1], auth=True
  190. )
  191. ]
  192. for (k, r) in self.records.items():
  193. for rec in r:
  194. if rec.ttl is not None:
  195. ttl = rec.ttl
  196. else:
  197. ttl = default_ttl
  198. if rec.TYPE != dns.SOA:
  199. results.append(
  200. dns.RRHeader(k, rec.TYPE, dns.IN, ttl, rec, auth=True)
  201. )
  202. results.append(results[0])
  203. return defer.succeed((results, (), ()))
  204. return defer.fail(failure.Failure(dns.DomainError(name)))
  205. def _cbAllRecords(self, results):
  206. ans, auth, add = [], [], []
  207. for res in results:
  208. if res[0]:
  209. ans.extend(res[1][0])
  210. auth.extend(res[1][1])
  211. add.extend(res[1][2])
  212. return ans, auth, add
  213. class PySourceAuthority(FileAuthority):
  214. """
  215. A FileAuthority that is built up from Python source code.
  216. """
  217. def loadFile(self, filename):
  218. g, l = self.setupConfigNamespace(), {}
  219. execfile(filename, g, l)
  220. if "zone" not in l:
  221. raise ValueError("No zone defined in " + filename)
  222. self.records = {}
  223. for rr in l["zone"]:
  224. if isinstance(rr[1], dns.Record_SOA):
  225. self.soa = rr
  226. self.records.setdefault(rr[0].lower(), []).append(rr[1])
  227. def wrapRecord(self, type):
  228. def wrapRecordFunc(name, *arg, **kw):
  229. return (dns.domainString(name), type(*arg, **kw))
  230. return wrapRecordFunc
  231. def setupConfigNamespace(self):
  232. r = {}
  233. items = dns.__dict__.keys()
  234. for record in [x for x in items if x.startswith("Record_")]:
  235. type = getattr(dns, record)
  236. f = self.wrapRecord(type)
  237. r[record[len("Record_") :]] = f
  238. return r
  239. class BindAuthority(FileAuthority):
  240. """
  241. An Authority that loads U{BIND zone files
  242. <https://en.wikipedia.org/wiki/Zone_file>}.
  243. Supports only C{$ORIGIN} and C{$TTL} directives.
  244. """
  245. def loadFile(self, filename):
  246. """
  247. Load records from C{filename}.
  248. @param filename: file to read from
  249. @type filename: L{bytes}
  250. """
  251. fp = FilePath(filename)
  252. # Not the best way to set an origin. It can be set using $ORIGIN
  253. # though.
  254. self.origin = nativeString(fp.basename() + b".")
  255. lines = fp.getContent().splitlines(True)
  256. lines = self.stripComments(lines)
  257. lines = self.collapseContinuations(lines)
  258. self.parseLines(lines)
  259. def stripComments(self, lines):
  260. """
  261. Strip comments from C{lines}.
  262. @param lines: lines to work on
  263. @type lines: iterable of L{bytes}
  264. @return: C{lines} sans comments.
  265. """
  266. return (
  267. a.find(b";") == -1 and a or a[: a.find(b";")]
  268. for a in [b.strip() for b in lines]
  269. )
  270. def collapseContinuations(self, lines):
  271. """
  272. Transform multiline statements into single lines.
  273. @param lines: lines to work on
  274. @type lines: iterable of L{bytes}
  275. @return: iterable of continuous lines
  276. """
  277. l = []
  278. state = 0
  279. for line in lines:
  280. if state == 0:
  281. if line.find(b"(") == -1:
  282. l.append(line)
  283. else:
  284. l.append(line[: line.find(b"(")])
  285. state = 1
  286. else:
  287. if line.find(b")") != -1:
  288. l[-1] += b" " + line[: line.find(b")")]
  289. state = 0
  290. else:
  291. l[-1] += b" " + line
  292. return filter(None, (line.split() for line in l))
  293. def parseLines(self, lines):
  294. """
  295. Parse C{lines}.
  296. @param lines: lines to work on
  297. @type lines: iterable of L{bytes}
  298. """
  299. ttl = 60 * 60 * 3
  300. origin = self.origin
  301. self.records = {}
  302. for line in lines:
  303. if line[0] == b"$TTL":
  304. ttl = dns.str2time(line[1])
  305. elif line[0] == b"$ORIGIN":
  306. origin = line[1]
  307. elif line[0] == b"$INCLUDE":
  308. raise NotImplementedError("$INCLUDE directive not implemented")
  309. elif line[0] == b"$GENERATE":
  310. raise NotImplementedError("$GENERATE directive not implemented")
  311. else:
  312. self.parseRecordLine(origin, ttl, line)
  313. # If the origin changed, reflect that within the instance.
  314. self.origin = origin
  315. def addRecord(self, owner, ttl, type, domain, cls, rdata):
  316. """
  317. Add a record to our authority. Expand domain with origin if necessary.
  318. @param owner: origin?
  319. @type owner: L{bytes}
  320. @param ttl: time to live for the record
  321. @type ttl: L{int}
  322. @param domain: the domain for which the record is to be added
  323. @type domain: L{bytes}
  324. @param type: record type
  325. @type type: L{str}
  326. @param cls: record class
  327. @type cls: L{str}
  328. @param rdata: record data
  329. @type rdata: L{list} of L{bytes}
  330. """
  331. if not domain.endswith(b"."):
  332. domain = domain + b"." + owner[:-1]
  333. else:
  334. domain = domain[:-1]
  335. f = getattr(self, f"class_{cls}", None)
  336. if f:
  337. f(ttl, type, domain, rdata)
  338. else:
  339. raise NotImplementedError(f"Record class {cls!r} not supported")
  340. def class_IN(self, ttl, type, domain, rdata):
  341. """
  342. Simulate a class IN and recurse into the actual class.
  343. @param ttl: time to live for the record
  344. @type ttl: L{int}
  345. @param type: record type
  346. @type type: str
  347. @param domain: the domain
  348. @type domain: bytes
  349. @param rdata:
  350. @type rdata: bytes
  351. """
  352. record = getattr(dns, f"Record_{nativeString(type)}", None)
  353. if record:
  354. r = record(*rdata)
  355. r.ttl = ttl
  356. self.records.setdefault(domain.lower(), []).append(r)
  357. if type == "SOA":
  358. self.soa = (domain, r)
  359. else:
  360. raise NotImplementedError(
  361. f"Record type {nativeString(type)!r} not supported"
  362. )
  363. def parseRecordLine(self, origin, ttl, line):
  364. """
  365. Parse a C{line} from a zone file respecting C{origin} and C{ttl}.
  366. Add resulting records to authority.
  367. @param origin: starting point for the zone
  368. @type origin: L{bytes}
  369. @param ttl: time to live for the record
  370. @type ttl: L{int}
  371. @param line: zone file line to parse; split by word
  372. @type line: L{list} of L{bytes}
  373. """
  374. queryClasses = {qc.encode("ascii") for qc in dns.QUERY_CLASSES.values()}
  375. queryTypes = {qt.encode("ascii") for qt in dns.QUERY_TYPES.values()}
  376. markers = queryClasses | queryTypes
  377. cls = b"IN"
  378. owner = origin
  379. if line[0] == b"@":
  380. line = line[1:]
  381. owner = origin
  382. elif not line[0].isdigit() and line[0] not in markers:
  383. owner = line[0]
  384. line = line[1:]
  385. if line[0].isdigit() or line[0] in markers:
  386. domain = owner
  387. owner = origin
  388. else:
  389. domain = line[0]
  390. line = line[1:]
  391. if line[0] in queryClasses:
  392. cls = line[0]
  393. line = line[1:]
  394. if line[0].isdigit():
  395. ttl = int(line[0])
  396. line = line[1:]
  397. elif line[0].isdigit():
  398. ttl = int(line[0])
  399. line = line[1:]
  400. if line[0] in queryClasses:
  401. cls = line[0]
  402. line = line[1:]
  403. type = line[0]
  404. rdata = line[1:]
  405. self.addRecord(owner, ttl, nativeString(type), domain, nativeString(cls), rdata)