dyndns.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import base64
  2. import binascii
  3. from functools import cached_property
  4. from rest_framework import generics
  5. from rest_framework.authentication import get_authorization_header
  6. from rest_framework.exceptions import NotFound, ValidationError
  7. from rest_framework.response import Response
  8. from desecapi import metrics
  9. from desecapi.authentication import (
  10. BasicTokenAuthentication,
  11. TokenAuthentication,
  12. URLParamAuthentication,
  13. )
  14. from desecapi.exceptions import ConcurrencyException
  15. from desecapi.models import Domain
  16. from desecapi.pdns_change_tracker import PDNSChangeTracker
  17. from desecapi.permissions import TokenHasDomainDynDNSPermission
  18. from desecapi.renderers import PlainTextRenderer
  19. from desecapi.serializers import RRsetSerializer
  20. class DynDNS12UpdateView(generics.GenericAPIView):
  21. authentication_classes = (
  22. TokenAuthentication,
  23. BasicTokenAuthentication,
  24. URLParamAuthentication,
  25. )
  26. permission_classes = (TokenHasDomainDynDNSPermission,)
  27. renderer_classes = [PlainTextRenderer]
  28. serializer_class = RRsetSerializer
  29. throttle_scope = "dyndns"
  30. @property
  31. def throttle_scope_bucket(self):
  32. return self.domain.name
  33. def _find_ip(self, param_keys, separator):
  34. # Check URL parameters
  35. for param_key in param_keys:
  36. try:
  37. params = {
  38. param.strip()
  39. for param in self.request.query_params[param_key].split(",")
  40. if separator in param or param.strip() in ("", "preserve")
  41. }
  42. except KeyError:
  43. continue
  44. if len(params) > 1 and params & {"", "preserve"}:
  45. raise ValidationError(
  46. detail={
  47. "detail": f'IP parameter "{param_key}" cannot have addresses and "preserve" at the same time.',
  48. "code": "inconsistent-parameter",
  49. }
  50. )
  51. if params:
  52. return [] if "" in params else list(params)
  53. # Check remote IP address
  54. client_ip = self.request.META.get("REMOTE_ADDR")
  55. if separator in client_ip:
  56. return [client_ip]
  57. # give up
  58. return []
  59. @cached_property
  60. def qname(self):
  61. # hostname parameter
  62. try:
  63. if self.request.query_params["hostname"] != "YES":
  64. return self.request.query_params["hostname"].lower()
  65. except KeyError:
  66. pass
  67. # host_id parameter
  68. try:
  69. return self.request.query_params["host_id"].lower()
  70. except KeyError:
  71. pass
  72. # http basic auth username
  73. try:
  74. domain_name = (
  75. base64.b64decode(
  76. get_authorization_header(self.request)
  77. .decode()
  78. .split(" ")[1]
  79. .encode()
  80. )
  81. .decode()
  82. .split(":")[0]
  83. )
  84. if domain_name and "@" not in domain_name:
  85. return domain_name.lower()
  86. except (binascii.Error, IndexError, UnicodeDecodeError):
  87. pass
  88. # username parameter
  89. try:
  90. return self.request.query_params["username"].lower()
  91. except KeyError:
  92. pass
  93. # only domain associated with this user account
  94. try:
  95. return self.request.user.domains.get().name
  96. except Domain.MultipleObjectsReturned:
  97. raise ValidationError(
  98. detail={
  99. "detail": "Request does not properly specify domain for update.",
  100. "code": "domain-unspecified",
  101. }
  102. )
  103. except Domain.DoesNotExist:
  104. metrics.get("desecapi_dynDNS12_domain_not_found").inc()
  105. raise NotFound("nohost")
  106. @cached_property
  107. def domain(self):
  108. try:
  109. return Domain.objects.filter_qname(
  110. self.qname, owner=self.request.user
  111. ).order_by("-name_length")[0]
  112. except (IndexError, ValueError):
  113. raise NotFound("nohost")
  114. @property
  115. def subname(self):
  116. return self.qname.rpartition(f".{self.domain.name}")[0]
  117. def get_serializer_context(self):
  118. return {
  119. **super().get_serializer_context(),
  120. "domain": self.domain,
  121. "minimum_ttl": 60,
  122. }
  123. def get_queryset(self):
  124. return self.domain.rrset_set.filter(
  125. subname=self.subname, type__in=["A", "AAAA"]
  126. )
  127. def get(self, request, *args, **kwargs):
  128. instances = self.get_queryset().all()
  129. record_params = {
  130. "A": self._find_ip(["myip", "myipv4", "ip"], separator="."),
  131. "AAAA": self._find_ip(["myipv6", "ipv6", "myip", "ip"], separator=":"),
  132. }
  133. data = [
  134. {
  135. "type": type_,
  136. "subname": self.subname,
  137. "ttl": 60,
  138. "records": ip_params,
  139. }
  140. for type_, ip_params in record_params.items()
  141. if "preserve" not in ip_params
  142. ]
  143. serializer = self.get_serializer(instances, data=data, many=True, partial=True)
  144. try:
  145. serializer.is_valid(raise_exception=True)
  146. except ValidationError as e:
  147. if any(
  148. any(
  149. getattr(non_field_error, "code", "") == "unique"
  150. for non_field_error in err.get("non_field_errors", [])
  151. )
  152. for err in e.detail
  153. ):
  154. raise ConcurrencyException from e
  155. raise e
  156. with PDNSChangeTracker():
  157. serializer.save()
  158. return Response("good", content_type="text/plain")