pdns.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import json
  2. import re
  3. from hashlib import sha1
  4. import requests
  5. from django.conf import settings
  6. from django.core.exceptions import SuspiciousOperation
  7. from desecapi import metrics
  8. from desecapi.exceptions import PDNSException, RequestEntityTooLarge
  9. SUPPORTED_RRSET_TYPES = {
  10. # https://doc.powerdns.com/authoritative/appendices/types.html
  11. # "major" types
  12. "A",
  13. "AAAA",
  14. "AFSDB",
  15. "ALIAS",
  16. "APL",
  17. "CAA",
  18. "CERT",
  19. "CDNSKEY",
  20. "CDS",
  21. "CNAME",
  22. "CSYNC",
  23. "DNSKEY",
  24. "DNAME",
  25. "DS",
  26. "HINFO",
  27. "HTTPS",
  28. "KEY",
  29. "LOC",
  30. "MX",
  31. "NAPTR",
  32. "NS",
  33. "NSEC",
  34. "NSEC3",
  35. "NSEC3PARAM",
  36. "OPENPGPKEY",
  37. "PTR",
  38. "RP",
  39. "RRSIG",
  40. "SOA",
  41. "SPF",
  42. "SSHFP",
  43. "SRV",
  44. "SVCB",
  45. "TLSA",
  46. "SMIMEA",
  47. "TXT",
  48. "URI",
  49. # "additional" types, without obsolete ones
  50. "DHCID",
  51. "DLV",
  52. "EUI48",
  53. "EUI64",
  54. "IPSECKEY",
  55. "KX",
  56. "MINFO",
  57. "MR",
  58. "RKEY",
  59. "WKS",
  60. # https://doc.powerdns.com/authoritative/changelog/4.5.html#change-4.5.0-alpha1-New-Features
  61. "NID",
  62. "L32",
  63. "L64",
  64. "LP",
  65. }
  66. NSLORD = object()
  67. NSMASTER = object()
  68. _config = {
  69. NSLORD: {
  70. "base_url": settings.NSLORD_PDNS_API,
  71. "headers": {
  72. "Accept": "application/json",
  73. "User-Agent": "desecapi",
  74. "X-API-Key": settings.NSLORD_PDNS_API_TOKEN,
  75. },
  76. },
  77. NSMASTER: {
  78. "base_url": settings.NSMASTER_PDNS_API,
  79. "headers": {
  80. "Accept": "application/json",
  81. "User-Agent": "desecapi",
  82. "X-API-Key": settings.NSMASTER_PDNS_API_TOKEN,
  83. },
  84. },
  85. }
  86. def _pdns_request(method, *, server, path, data=None):
  87. if data is not None:
  88. data = json.dumps(data)
  89. if data is not None and len(data) > settings.PDNS_MAX_BODY_SIZE:
  90. raise RequestEntityTooLarge
  91. r = requests.request(
  92. method,
  93. _config[server]["base_url"] + path,
  94. data=data,
  95. headers=_config[server]["headers"],
  96. )
  97. if r.status_code not in range(200, 300):
  98. raise PDNSException(response=r)
  99. metrics.get("desecapi_pdns_request_success").labels(method, r.status_code).inc()
  100. return r
  101. def _pdns_post(server, path, data):
  102. return _pdns_request("post", server=server, path=path, data=data)
  103. def _pdns_patch(server, path, data):
  104. return _pdns_request("patch", server=server, path=path, data=data)
  105. def _pdns_get(server, path):
  106. return _pdns_request("get", server=server, path=path)
  107. def _pdns_put(server, path):
  108. return _pdns_request("put", server=server, path=path)
  109. def _pdns_delete(server, path):
  110. return _pdns_request("delete", server=server, path=path)
  111. def pdns_id(name):
  112. # See also pdns code, apiZoneNameToId() in ws-api.cc (with the exception of forward slash)
  113. if not re.match(r"^[a-zA-Z0-9_.-]+$", name):
  114. raise SuspiciousOperation("Invalid hostname " + name)
  115. name = name.translate(str.maketrans({"/": "=2F", "_": "=5F"}))
  116. return name.rstrip(".") + "."
  117. def get_keys(domain):
  118. """
  119. Retrieves a dict representation of the DNSSEC key information
  120. """
  121. r = _pdns_get(NSLORD, "/zones/%s/cryptokeys" % pdns_id(domain.name))
  122. metrics.get("desecapi_pdns_keys_fetched").inc()
  123. field_map = {
  124. "dnskey": "dnskey",
  125. "cds": "ds",
  126. "flags": "flags", # deprecated
  127. "keytype": "keytype", # deprecated
  128. }
  129. return [
  130. {v: key.get(k, []) for k, v in field_map.items()}
  131. for key in r.json()
  132. if key["published"]
  133. ]
  134. def get_zone(domain):
  135. """
  136. Retrieves a dict representation of the zone from pdns
  137. """
  138. r = _pdns_get(NSLORD, "/zones/" + pdns_id(domain.name))
  139. return r.json()
  140. def get_rrset_datas(domain):
  141. """
  142. Retrieves a dict representation of the RRsets in a given zone
  143. """
  144. return [
  145. {
  146. "domain": domain,
  147. "subname": rrset["name"][: -(len(domain.name) + 2)],
  148. "type": rrset["type"],
  149. "records": [record["content"] for record in rrset["records"]],
  150. "ttl": rrset["ttl"],
  151. }
  152. for rrset in get_zone(domain)["rrsets"]
  153. ]
  154. def construct_catalog_rrset(
  155. zone=None, delete=False, subname=None, qtype="PTR", rdata=None
  156. ):
  157. # subname can be generated from zone for convenience; exactly one needs to be given
  158. assert (zone is None) ^ (subname is None)
  159. # sanity check: one can't delete an rrset and give record data at the same time
  160. assert not (delete and rdata)
  161. if subname is None:
  162. zone = zone.rstrip(".") + "."
  163. m_unique = sha1(zone.encode()).hexdigest()
  164. subname = f"{m_unique}.zones"
  165. if rdata is None:
  166. rdata = zone
  167. return {
  168. "name": f"{subname}.{settings.CATALOG_ZONE}".strip(".") + ".",
  169. "type": qtype,
  170. "ttl": 0, # as per the specification
  171. "changetype": "REPLACE",
  172. "records": [] if delete else [{"content": rdata, "disabled": False}],
  173. }
  174. def get_serials():
  175. return {
  176. zone["name"]: zone["edited_serial"]
  177. for zone in _pdns_get(NSMASTER, "/zones").json()
  178. }