domains.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import dns.name
  2. import dns.zone
  3. from rest_framework import serializers
  4. from api import settings
  5. from desecapi.models import Domain, RR_SET_TYPES_AUTOMATIC
  6. from desecapi.validators import ReadOnlyOnUpdateValidator
  7. from .records import RRsetSerializer
  8. class DomainSerializer(serializers.ModelSerializer):
  9. default_error_messages = {
  10. **serializers.Serializer.default_error_messages,
  11. 'name_unavailable': 'This domain name conflicts with an existing zone, or is disallowed by policy.',
  12. }
  13. zonefile = serializers.CharField(write_only=True, required=False, allow_blank=True)
  14. class Meta:
  15. model = Domain
  16. fields = ('created', 'published', 'name', 'keys', 'minimum_ttl', 'touched', 'zonefile')
  17. read_only_fields = ('published', 'minimum_ttl',)
  18. extra_kwargs = {
  19. 'name': {'trim_whitespace': False},
  20. }
  21. def __init__(self, *args, include_keys=False, **kwargs):
  22. self.include_keys = include_keys
  23. self.import_zone = None
  24. super().__init__(*args, **kwargs)
  25. def get_fields(self):
  26. fields = super().get_fields()
  27. if not self.include_keys:
  28. fields.pop('keys')
  29. fields['name'].validators.append(ReadOnlyOnUpdateValidator())
  30. return fields
  31. def validate_name(self, value):
  32. if not Domain(name=value, owner=self.context['request'].user).is_registrable():
  33. raise serializers.ValidationError(self.default_error_messages['name_unavailable'], code='name_unavailable')
  34. return value
  35. def parse_zonefile(self, domain_name: str, zonefile: str):
  36. try:
  37. self.import_zone = dns.zone.from_text(
  38. zonefile,
  39. origin=dns.name.from_text(domain_name),
  40. allow_include=False,
  41. check_origin=False,
  42. relativize=False,
  43. )
  44. except dns.zonefile.CNAMEAndOtherData:
  45. raise serializers.ValidationError(
  46. {'zonefile': ['No other records with the same name are allowed alongside a CNAME record.']})
  47. except ValueError as e:
  48. if 'has non-origin SOA' in str(e):
  49. raise serializers.ValidationError(
  50. {'zonefile': [f'Zonefile includes an SOA record for a name different from {domain_name}.']})
  51. raise e
  52. except dns.exception.SyntaxError as e:
  53. try:
  54. line = str(e).split(':')[1]
  55. raise serializers.ValidationError({'zonefile': [f'Zonefile contains syntax error in line {line}.']})
  56. except IndexError:
  57. raise serializers.ValidationError({'zonefile': [f'Could not parse zonefile: {str(e)}']})
  58. def validate(self, attrs):
  59. if attrs.get('zonefile') is not None:
  60. self.parse_zonefile(attrs.get('name'), attrs.pop('zonefile'))
  61. return super().validate(attrs)
  62. def create(self, validated_data):
  63. # save domain
  64. if 'minimum_ttl' not in validated_data and Domain(name=validated_data['name']).is_locally_registrable:
  65. validated_data.update(minimum_ttl=60)
  66. domain: Domain = super().create(validated_data)
  67. # save RRsets if zonefile was given
  68. nodes = getattr(self.import_zone, 'nodes', None)
  69. if nodes:
  70. zone_name = dns.name.from_text(validated_data['name'])
  71. min_ttl, max_ttl = domain.minimum_ttl, settings.MAXIMUM_TTL
  72. data = [
  73. {
  74. 'type': dns.rdatatype.to_text(rrset.rdtype),
  75. 'ttl': max(min_ttl, min(max_ttl, rrset.ttl)),
  76. 'subname': (owner_name - zone_name).to_text() if owner_name - zone_name != dns.name.empty else '',
  77. 'records': [rr.to_text() for rr in rrset],
  78. }
  79. for owner_name, node in nodes.items()
  80. for rrset in node.rdatasets
  81. if (
  82. dns.rdatatype.to_text(rrset.rdtype) not in (
  83. RR_SET_TYPES_AUTOMATIC | # do not import automatically managed record types
  84. {'CDS', 'CDNSKEY', 'DNSKEY'} # do not import these, as this would likely be unexpected
  85. )
  86. and not (owner_name - zone_name == dns.name.empty and rrset.rdtype == dns.rdatatype.NS) # ignore apex NS
  87. )
  88. ]
  89. rrset_list_serializer = RRsetSerializer(data=data, context=dict(domain=domain), many=True)
  90. # The following line raises if data passed validation by dnspython during zone file parsing,
  91. # but is rejected by validation in RRsetSerializer. See also
  92. # test_create_domain_zonefile_import_validation
  93. try:
  94. rrset_list_serializer.is_valid(raise_exception=True)
  95. except serializers.ValidationError as e:
  96. if isinstance(e.detail, serializers.ReturnList):
  97. # match the order of error messages with the RRsets provided to the
  98. # serializer to make sense to the client
  99. def fqdn(idx): return (data[idx]['subname'] + "." + domain.name).lstrip('.')
  100. raise serializers.ValidationError({
  101. 'zonefile': [
  102. f"{fqdn(idx)}/{data[idx]['type']}: {err}"
  103. for idx, d in enumerate(e.detail)
  104. for _, errs in d.items()
  105. for err in errs
  106. ]
  107. })
  108. raise e
  109. rrset_list_serializer.save()
  110. return domain