dyndns.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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 BasicTokenAuthentication, TokenAuthentication, URLParamAuthentication
  10. from desecapi.exceptions import ConcurrencyException
  11. from desecapi.models import Domain
  12. from desecapi.pdns_change_tracker import PDNSChangeTracker
  13. from desecapi.permissions import TokenHasDomainDynDNSPermission
  14. from desecapi.renderers import PlainTextRenderer
  15. from desecapi.serializers import RRsetSerializer
  16. class DynDNS12UpdateView(generics.GenericAPIView):
  17. authentication_classes = (TokenAuthentication, BasicTokenAuthentication, URLParamAuthentication,)
  18. permission_classes = (TokenHasDomainDynDNSPermission,)
  19. renderer_classes = [PlainTextRenderer]
  20. serializer_class = RRsetSerializer
  21. throttle_scope = 'dyndns'
  22. @property
  23. def throttle_scope_bucket(self):
  24. return self.domain.name
  25. def _find_ip(self, params, version):
  26. if version == 4:
  27. look_for = '.'
  28. elif version == 6:
  29. look_for = ':'
  30. else:
  31. raise Exception
  32. # Check URL parameters
  33. for p in params:
  34. if p in self.request.query_params:
  35. if not len(self.request.query_params[p]):
  36. return None
  37. if look_for in self.request.query_params[p]:
  38. return self.request.query_params[p]
  39. # Check remote IP address
  40. client_ip = self.request.META.get('REMOTE_ADDR')
  41. if look_for in client_ip:
  42. return client_ip
  43. # give up
  44. return None
  45. @cached_property
  46. def qname(self):
  47. # hostname parameter
  48. try:
  49. if self.request.query_params['hostname'] != 'YES':
  50. return self.request.query_params['hostname'].lower()
  51. except KeyError:
  52. pass
  53. # host_id parameter
  54. try:
  55. return self.request.query_params['host_id'].lower()
  56. except KeyError:
  57. pass
  58. # http basic auth username
  59. try:
  60. domain_name = base64.b64decode(
  61. get_authorization_header(self.request).decode().split(' ')[1].encode()).decode().split(':')[0]
  62. if domain_name and '@' not in domain_name:
  63. return domain_name.lower()
  64. except (binascii.Error, IndexError, UnicodeDecodeError):
  65. pass
  66. # username parameter
  67. try:
  68. return self.request.query_params['username'].lower()
  69. except KeyError:
  70. pass
  71. # only domain associated with this user account
  72. try:
  73. return self.request.user.domains.get().name
  74. except Domain.MultipleObjectsReturned:
  75. raise ValidationError(detail={
  76. "detail": "Request does not properly specify domain for update.",
  77. "code": "domain-unspecified"
  78. })
  79. except Domain.DoesNotExist:
  80. metrics.get('desecapi_dynDNS12_domain_not_found').inc()
  81. raise NotFound('nohost')
  82. @cached_property
  83. def domain(self):
  84. try:
  85. return Domain.objects.filter_qname(self.qname, owner=self.request.user).order_by('-name_length')[0]
  86. except (IndexError, ValueError):
  87. raise NotFound('nohost')
  88. @property
  89. def subname(self):
  90. return self.qname.rpartition(f'.{self.domain.name}')[0]
  91. def get_serializer_context(self):
  92. return {**super().get_serializer_context(), 'domain': self.domain, 'minimum_ttl': 60}
  93. def get_queryset(self):
  94. return self.domain.rrset_set.filter(subname=self.subname, type__in=['A', 'AAAA'])
  95. def get(self, request, *args, **kwargs):
  96. instances = self.get_queryset().all()
  97. ipv4 = self._find_ip(['myip', 'myipv4', 'ip'], version=4)
  98. ipv6 = self._find_ip(['myipv6', 'ipv6', 'myip', 'ip'], version=6)
  99. data = [
  100. {'type': 'A', 'subname': self.subname, 'ttl': 60, 'records': [ipv4] if ipv4 else []},
  101. {'type': 'AAAA', 'subname': self.subname, 'ttl': 60, 'records': [ipv6] if ipv6 else []},
  102. ]
  103. serializer = self.get_serializer(instances, data=data, many=True, partial=True)
  104. try:
  105. serializer.is_valid(raise_exception=True)
  106. except ValidationError as e:
  107. if any(
  108. any(
  109. getattr(non_field_error, 'code', '') == 'unique'
  110. for non_field_error
  111. in err.get('non_field_errors', [])
  112. )
  113. for err in e.detail
  114. ):
  115. raise ConcurrencyException from e
  116. raise e
  117. with PDNSChangeTracker():
  118. serializer.save()
  119. return Response('good', content_type='text/plain')