test_rrsets.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. from ipaddress import IPv4Network
  2. import re
  3. from itertools import product
  4. from django.conf import settings
  5. from django.core.exceptions import ValidationError
  6. from django.core.management import call_command
  7. from rest_framework import status
  8. from desecapi.models import Domain, RRset, RR_SET_TYPES_AUTOMATIC, RR_SET_TYPES_UNSUPPORTED
  9. from desecapi.tests.base import DesecTestCase, AuthenticatedRRSetBaseTestCase
  10. class UnauthenticatedRRSetTestCase(DesecTestCase):
  11. def test_unauthorized_access(self):
  12. url = self.reverse('v1:rrsets', name='example.com')
  13. for method in [
  14. self.client.get,
  15. self.client.post,
  16. self.client.put,
  17. self.client.delete,
  18. self.client.patch
  19. ]:
  20. response = method(url)
  21. self.assertStatus(response, status.HTTP_401_UNAUTHORIZED)
  22. class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
  23. def test_subname_validity(self):
  24. for subname in [
  25. 'aEroport',
  26. 'AEROPORT',
  27. 'aéroport'
  28. ]:
  29. with self.assertRaises(ValidationError):
  30. RRset(domain=self.my_domain, subname=subname, ttl=60, type='A').save()
  31. RRset(domain=self.my_domain, subname='aeroport', ttl=60, type='A').save()
  32. def test_retrieve_my_rr_sets(self):
  33. for response in [
  34. self.client.get_rr_sets(self.my_domain.name),
  35. self.client.get_rr_sets(self.my_domain.name, subname=''),
  36. ]:
  37. self.assertStatus(response, status.HTTP_200_OK)
  38. self.assertEqual(len(response.data), 1, response.data)
  39. def test_retrieve_my_rr_sets_pagination(self):
  40. def convert_links(links):
  41. mapping = {}
  42. for link in links.split(', '):
  43. _url, label = link.split('; ')
  44. label = re.search('rel="(.*)"', label).group(1)
  45. _url = _url[1:-1]
  46. assert label not in mapping
  47. mapping[label] = _url
  48. return mapping
  49. def assertPaginationResponse(response, expected_length, expected_directional_links=[]):
  50. self.assertStatus(response, status.HTTP_200_OK)
  51. self.assertEqual(len(response.data), expected_length)
  52. _links = convert_links(response['Link'])
  53. self.assertEqual(len(_links), len(expected_directional_links) + 1) # directional links, plus "first"
  54. self.assertTrue(_links['first'].endswith('/?cursor='))
  55. for directional_link in expected_directional_links:
  56. self.assertEqual(_links['first'].find('/?cursor='), _links[directional_link].find('/?cursor='))
  57. self.assertTrue(len(_links[directional_link]) > len(_links['first']))
  58. # Prepare extra records so that we get three pages (total: n + 1)
  59. n = int(settings.REST_FRAMEWORK['PAGE_SIZE'] * 2.5)
  60. RRset.objects.bulk_create(
  61. [RRset(domain=self.my_domain, subname=str(i), ttl=123, type='A') for i in range(n)]
  62. )
  63. # No pagination
  64. response = self.client.get_rr_sets(self.my_domain.name)
  65. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  66. self.assertEqual(response.data['detail'],
  67. f'Pagination required. You can query up to {settings.REST_FRAMEWORK["PAGE_SIZE"]} items at a time ({n+1} total). '
  68. 'Please use the `first` page link (see Link header).')
  69. links = convert_links(response['Link'])
  70. self.assertEqual(len(links), 1)
  71. self.assertTrue(links['first'].endswith('/?cursor='))
  72. # First page
  73. url = links['first']
  74. response = self.client.get(url)
  75. assertPaginationResponse(response, settings.REST_FRAMEWORK['PAGE_SIZE'], ['next'])
  76. # Next
  77. url = convert_links(response['Link'])['next']
  78. response = self.client.get(url)
  79. assertPaginationResponse(response, settings.REST_FRAMEWORK['PAGE_SIZE'], ['next', 'prev'])
  80. data_next = response.data.copy()
  81. # Next-next (last) page
  82. url = convert_links(response['Link'])['next']
  83. response = self.client.get(url)
  84. assertPaginationResponse(response, n/5 + 1, ['prev'])
  85. # Prev
  86. url = convert_links(response['Link'])['prev']
  87. response = self.client.get(url)
  88. assertPaginationResponse(response, settings.REST_FRAMEWORK['PAGE_SIZE'], ['next', 'prev'])
  89. # Make sure that one step forward equals two steps forward and one step back
  90. self.assertEqual(response.data, data_next)
  91. def test_retrieve_other_rr_sets(self):
  92. self.assertStatus(self.client.get_rr_sets(self.other_domain.name), status.HTTP_404_NOT_FOUND)
  93. self.assertStatus(self.client.get_rr_sets(self.other_domain.name, subname='test'), status.HTTP_404_NOT_FOUND)
  94. self.assertStatus(self.client.get_rr_sets(self.other_domain.name, type='A'), status.HTTP_404_NOT_FOUND)
  95. def test_retrieve_my_rr_sets_filter(self):
  96. response = self.client.get_rr_sets(self.my_rr_set_domain.name, query='?cursor=')
  97. self.assertStatus(response, status.HTTP_200_OK)
  98. expected_number_of_rrsets = min(len(self._test_rr_sets()), settings.REST_FRAMEWORK['PAGE_SIZE'])
  99. self.assertEqual(len(response.data), expected_number_of_rrsets)
  100. for subname in self.SUBNAMES:
  101. response = self.client.get_rr_sets(self.my_rr_set_domain.name, subname=subname)
  102. self.assertStatus(response, status.HTTP_200_OK)
  103. self.assertRRSetsCount(response.data, [dict(subname=subname)],
  104. count=len(self._test_rr_sets(subname=subname)))
  105. for type_ in self.ALLOWED_TYPES:
  106. response = self.client.get_rr_sets(self.my_rr_set_domain.name, type=type_)
  107. self.assertStatus(response, status.HTTP_200_OK)
  108. def test_create_my_rr_sets(self):
  109. for subname in [None, 'create-my-rr-sets', 'foo.create-my-rr-sets', 'bar.baz.foo.create-my-rr-sets']:
  110. for data in [
  111. {'subname': subname, 'records': ['1.2.3.4'], 'ttl': 3660, 'type': 'A'},
  112. {'subname': '' if subname is None else subname, 'records': ['desec.io.'], 'ttl': 36900, 'type': 'PTR'},
  113. {'subname': '' if subname is None else subname, 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
  114. {'subname': f'{subname}.cname'.lower(), 'ttl': 3600, 'type': 'CNAME', 'records': ['example.com.']},
  115. ]:
  116. # Try POST with missing subname
  117. if data['subname'] is None:
  118. data.pop('subname')
  119. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  120. response = self.client.post_rr_set(domain_name=self.my_empty_domain.name, **data)
  121. self.assertStatus(response, status.HTTP_201_CREATED)
  122. self.assertTrue(all(field in response.data for field in
  123. ['created', 'domain', 'subname', 'name', 'records', 'ttl', 'type', 'touched']))
  124. self.assertEqual(self.my_empty_domain.touched,
  125. max(rrset.touched for rrset in self.my_empty_domain.rrset_set.all()))
  126. # Check for uniqueness on second attempt
  127. response = self.client.post_rr_set(domain_name=self.my_empty_domain.name, **data)
  128. self.assertContains(response, 'Another RRset with the same subdomain and type exists for this domain.',
  129. status_code=status.HTTP_400_BAD_REQUEST)
  130. response = self.client.get_rr_sets(self.my_empty_domain.name)
  131. self.assertStatus(response, status.HTTP_200_OK)
  132. self.assertRRSetsCount(response.data, [data])
  133. response = self.client.get_rr_set(self.my_empty_domain.name, data.get('subname', ''), data['type'])
  134. self.assertStatus(response, status.HTTP_200_OK)
  135. self.assertRRSet(response.data, **data)
  136. def test_create_my_rr_sets_type_restriction(self):
  137. for subname in ['', 'create-my-rr-sets', 'foo.create-my-rr-sets', 'bar.baz.foo.create-my-rr-sets']:
  138. for data in [
  139. {'subname': subname, 'ttl': 60, 'type': 'a'},
  140. {'subname': subname, 'records': ['10 example.com.'], 'ttl': 60, 'type': 'txt'}
  141. ] + [
  142. {'subname': subname, 'records': ['10 example.com.'], 'ttl': 60, 'type': type_}
  143. for type_ in self.UNSUPPORTED_TYPES
  144. ] + [
  145. {'subname': subname, 'records': ['set.an.example. get.desec.io. 2584 10800 3600 604800 60'],
  146. 'ttl': 60, 'type': type_}
  147. for type_ in self.AUTOMATIC_TYPES
  148. ]:
  149. response = self.client.post_rr_set(self.my_domain.name, **data)
  150. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  151. response = self.client.get_rr_sets(self.my_domain.name)
  152. self.assertStatus(response, status.HTTP_200_OK)
  153. self.assertRRSetsCount(response.data, [data], count=0)
  154. def test_create_my_rr_sets_cname_at_apex(self):
  155. data = {'subname': '', 'ttl': 3600, 'type': 'CNAME', 'records': ['foobar.com.']}
  156. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  157. self.assertContains(response, 'CNAME RRset cannot have empty subname', status_code=status.HTTP_400_BAD_REQUEST)
  158. def test_create_my_rr_sets_cname_exclusivity(self):
  159. self.create_rr_set(self.my_domain, ['1.2.3.4'], type='A', ttl=3600, subname='a')
  160. self.create_rr_set(self.my_domain, ['example.com.'], type='CNAME', ttl=3600, subname='cname')
  161. # Can't add a CNAME where something else is
  162. data = {'subname': 'a', 'ttl': 3600, 'type': 'CNAME', 'records': ['foobar.com.']}
  163. response = self.client.post_rr_set(self.my_domain.name, **data)
  164. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  165. # Can't add something else where a CNAME is
  166. data = {'subname': 'cname', 'ttl': 3600, 'type': 'A', 'records': ['4.3.2.1']}
  167. response = self.client.post_rr_set(self.my_domain.name, **data)
  168. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  169. def test_create_my_rr_sets_without_records(self):
  170. for subname in ['', 'create-my-rr-sets', 'foo.create-my-rr-sets', 'bar.baz.foo.create-my-rr-sets']:
  171. for data in [
  172. {'subname': subname, 'records': [], 'ttl': 60, 'type': 'A'},
  173. {'subname': subname, 'ttl': 60, 'type': 'A'},
  174. ]:
  175. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  176. self.assertStatus(
  177. response,
  178. status.HTTP_400_BAD_REQUEST
  179. )
  180. response = self.client.get_rr_sets(self.my_empty_domain.name)
  181. self.assertStatus(response, status.HTTP_200_OK)
  182. self.assertRRSetsCount(response.data, [], count=0)
  183. def test_create_other_rr_sets(self):
  184. data = {'records': ['1.2.3.4'], 'ttl': 60, 'type': 'A'}
  185. response = self.client.post_rr_set(self.other_domain.name, **data)
  186. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  187. @staticmethod
  188. def _create_test_txt_record(record, type_='TXT'):
  189. return {'records': [f'{record}'], 'ttl': 3600, 'type': type_, 'subname': f'name{len(record)}'}
  190. def test_create_my_rr_sets_chunk_too_long(self):
  191. for l, t in product([1, 255, 256, 498], ['TXT', 'SPF']):
  192. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
  193. response = self.client.post_rr_set(
  194. self.my_empty_domain.name,
  195. **self._create_test_txt_record(f'"{"A" * l}"', t)
  196. )
  197. self.assertStatus(response, status.HTTP_201_CREATED)
  198. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
  199. self.client.delete_rr_set(self.my_empty_domain.name, type_=t, subname=f'name{l+2}')
  200. def test_create_my_rr_sets_too_long_content(self):
  201. for t in ['SPF', 'TXT']:
  202. response = self.client.post_rr_set(
  203. self.my_empty_domain.name,
  204. # record of wire length 501 bytes in chunks of max 255 each (RFC 4408)
  205. **self._create_test_txt_record(f'"{"A" * 255}" "{"A" * 244}"', t)
  206. )
  207. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  208. self.assertIn(
  209. 'Ensure this value has no more than 500 byte in wire format (it has 501).',
  210. str(response.data)
  211. )
  212. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
  213. response = self.client.post_rr_set(
  214. self.my_empty_domain.name,
  215. # record of wire length 500 bytes in chunks of max 255 each (RFC 4408)
  216. ** self._create_test_txt_record(f'"{"A" * 255}" "{"A" * 243}"')
  217. )
  218. self.assertStatus(response, status.HTTP_201_CREATED)
  219. def test_create_my_rr_sets_too_large_rrset(self):
  220. network = IPv4Network('127.0.0.0/20') # size: 4096 IP addresses
  221. data = {'records': [str(ip) for ip in network], 'ttl': 3600, 'type': 'A', 'subname': 'name'}
  222. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  223. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  224. excess_length = 28743 + len(self.my_empty_domain.name)
  225. self.assertIn(f'Total length of RRset exceeds limit by {excess_length} bytes.', str(response.data))
  226. def test_create_my_rr_sets_twice(self):
  227. data = {'records': ['1.2.3.4'], 'ttl': 3660, 'type': 'A'}
  228. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
  229. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  230. self.assertStatus(response, status.HTTP_201_CREATED)
  231. data['records'][0] = '3.2.2.1'
  232. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  233. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  234. def test_create_my_rr_sets_duplicate_content(self):
  235. for records in [
  236. ['::1', '0::1'],
  237. # TODO add more examples
  238. ]:
  239. data = {'records': records, 'ttl': 3660, 'type': 'AAAA'}
  240. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  241. self.assertContains(response, 'Duplicate', status_code=status.HTTP_400_BAD_REQUEST)
  242. def test_create_my_rr_sets_upper_case(self):
  243. for subname in ['asdF', 'cAse', 'asdf.FOO', '--F', 'ALLCAPS']:
  244. data = {'records': ['1.2.3.4'], 'ttl': 60, 'type': 'A', 'subname': subname}
  245. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  246. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  247. self.assertIn('Subname can only use (lowercase)', str(response.data))
  248. def test_create_my_rr_sets_subname_too_many_dots(self):
  249. for subname in ['dottest.', '.dottest', 'dot..test']:
  250. data = {'subname': subname, 'records': ['10 example.com.'], 'ttl': 3600, 'type': 'MX'}
  251. response = self.client.post_rr_set(self.my_domain.name, **data)
  252. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  253. response = self.client.get_rr_sets(self.my_domain.name)
  254. self.assertStatus(response, status.HTTP_200_OK)
  255. self.assertRRSetsCount(response.data, [data], count=0)
  256. def test_create_my_rr_sets_empty_payload(self):
  257. response = self.client.post_rr_set(self.my_empty_domain.name)
  258. self.assertContains(response, 'No data provided', status_code=status.HTTP_400_BAD_REQUEST)
  259. def test_create_my_rr_sets_cname_two_records(self):
  260. data = {'subname': 'sub', 'records': ['example.com.', 'example.org.'], 'ttl': 3600, 'type': 'CNAME'}
  261. response = self.client.post_rr_set(self.my_domain.name, **data)
  262. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  263. def test_create_my_rr_sets_canonical_content(self):
  264. # TODO fill in more examples
  265. datas = [
  266. # record type: (non-canonical input, canonical output expectation)
  267. ('A', ('127.0.0.1', '127.0.0.1')),
  268. ('AAAA', ('0000::0000:0001', '::1')),
  269. ('AFSDB', ('02 turquoise.FEMTO.edu.', '2 turquoise.femto.edu.')),
  270. ('CAA', ('0128 "issue" "letsencrypt.org"', '128 issue "letsencrypt.org"')),
  271. ('CERT', ('06 00 00 sadfdd==', '6 0 0 sadfdQ==')),
  272. ('CNAME', ('EXAMPLE.COM.', 'example.com.')),
  273. ('DHCID', ('xxxx', 'xxxx')),
  274. ('DLV', ('6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
  275. '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA10DF1F520'.lower())),
  276. ('DLV', ('6454 8 2 5C BA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
  277. '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA10DF1F520'.lower())),
  278. ('DS', ('6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
  279. '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA10DF1F520'.lower())),
  280. ('DS', ('6454 8 2 5C BA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA1 0DF1F520',
  281. '6454 8 2 5CBA665A006F6487625C6218522F09BD3673C25FA10F25CB18459AA10DF1F520'.lower())),
  282. ('EUI48', ('AA-BB-CC-DD-EE-FF', 'aa-bb-cc-dd-ee-ff')),
  283. ('EUI64', ('AA-BB-CC-DD-EE-FF-aa-aa', 'aa-bb-cc-dd-ee-ff-aa-aa')),
  284. ('HINFO', ('cpu os', '"cpu" "os"')),
  285. ('HINFO', ('"cpu" "os"', '"cpu" "os"')),
  286. # ('IPSECKEY', ('01 00 02 . ASDFAA==', '1 0 2 . ASDFAF==')),
  287. # ('IPSECKEY', ('01 00 02 . 00000w==', '1 0 2 . 000000==')),
  288. ('KX', ('010 example.com.', '10 example.com.')),
  289. ('LOC', ('023 012 59 N 042 022 48.500 W 65.00m 20.00m 10.00m 10.00m',
  290. '23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m')),
  291. ('MX', ('10 010.1.1.1.', '10 010.1.1.1.')),
  292. ('MX', ('010 010.1.1.2.', '10 010.1.1.2.')),
  293. ('NAPTR', ('100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.',
  294. '100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.')),
  295. ('NS', ('EXaMPLE.COM.', 'example.com.')),
  296. ('OPENPGPKEY', ('mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tS xLFJYhX+uabSgMrhOqUizJhkLx82',
  297. 'mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tSxLFJYhX+uabSgMrhOqUizJhkLx82')),
  298. ('PTR', ('EXAMPLE.COM.', 'example.com.')),
  299. ('RP', ('hostmaster.EXAMPLE.com. .', 'hostmaster.example.com. .')),
  300. # ('SMIMEA', ('3 01 0 aaBBccddeeff', '3 1 0 aabbccddeeff')),
  301. ('SPF', ('"v=spf1 ip4:10.1" ".1.1 ip4:127" ".0.0.0/16 ip4:192.168.0.0/27 include:example.com -all"',
  302. '"v=spf1 ip4:10.1" ".1.1 ip4:127" ".0.0.0/16 ip4:192.168.0.0/27 include:example.com -all"')),
  303. ('SPF', ('"foo" "bar"', '"foo" "bar"')),
  304. ('SPF', ('"foobar"', '"foobar"')),
  305. ('SRV', ('0 000 0 .', '0 0 0 .')),
  306. # ('SRV', ('100 1 5061 EXAMPLE.com.', '100 1 5061 example.com.')), # TODO fixed in dnspython 5c58601
  307. ('SRV', ('100 1 5061 example.com.', '100 1 5061 example.com.')),
  308. ('SSHFP', ('2 2 aabbccEEddff', '2 2 aabbcceeddff')),
  309. ('TLSA', ('3 0001 1 000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', '3 1 1 000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')),
  310. ('TXT', ('"foo" "bar"', '"foo" "bar"')),
  311. ('TXT', ('"foobar"', '"foobar"')),
  312. ('TXT', ('"foo" "" "bar"', '"foo" "" "bar"')),
  313. ('TXT', ('"" "" "foo" "" "bar"', '"" "" "foo" "" "bar"')),
  314. ('URI', ('10 01 "ftp://ftp1.example.com/public"', '10 1 "ftp://ftp1.example.com/public"')),
  315. ]
  316. for t, (record, canonical_record) in datas:
  317. if not record:
  318. continue
  319. data = {'records': [record], 'ttl': 3660, 'type': t, 'subname': 'test'}
  320. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  321. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  322. self.assertStatus(response, status.HTTP_201_CREATED)
  323. self.assertEqual(canonical_record, response.data['records'][0],
  324. f'For RR set type {t}, expected \'{canonical_record}\' to be the canonical form of '
  325. f'\'{record}\', but saw \'{response.data["records"][0]}\'.')
  326. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  327. response = self.client.delete_rr_set(self.my_empty_domain.name, subname='test', type_=t)
  328. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  329. self.assertAllSupportedRRSetTypes(set(t for t, _ in datas))
  330. def test_create_my_rr_sets_known_type_benign(self):
  331. # TODO fill in more examples
  332. datas = {
  333. 'A': ['127.0.0.1', '127.0.0.2'],
  334. 'AAAA': ['::1', '::2'],
  335. 'AFSDB': ['2 turquoise.femto.edu.'],
  336. 'CAA': ['128 issue "letsencrypt.org"', '128 iodef "mailto:desec@example.com"', '1 issue "letsencrypt.org"'],
  337. 'CERT': ['6 0 0 sadfdd=='],
  338. 'CNAME': ['example.com.'],
  339. 'DHCID': ['aaaaaaaaaaaa', 'aa aaa aaaa a a a'],
  340. 'DLV': ['39556 13 1 aabbccddeeff'],
  341. 'DS': ['39556 13 1 aabbccddeeff'],
  342. 'EUI48': ['aa-bb-cc-dd-ee-ff', 'AA-BB-CC-DD-EE-FF'],
  343. 'EUI64': ['aa-bb-cc-dd-ee-ff-00-11', 'AA-BB-CC-DD-EE-FF-00-11'],
  344. 'HINFO': ['"ARMv8-A" "Linux"'],
  345. # 'IPSECKEY': ['12 0 2 . asdfdf==', '03 1 1 127.0.00.1 asdfdf==', '12 3 1 example.com. asdfdf==',],
  346. 'KX': ['4 example.com.', '28 io.'],
  347. 'LOC': ['23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m'],
  348. 'MX': ['10 example.com.', '20 1.1.1.1.'],
  349. 'NAPTR': ['100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.'],
  350. 'NS': ['ns1.example.com.'],
  351. 'OPENPGPKEY': [
  352. 'mG8EXtVIsRMFK4EEACIDAwQSZPNqE4tSxLFJYhX+uabSgMrhOqUizJhkLx82', # key incomplete
  353. 'YWFh\xf0\x9f\x92\xa9YWFh', # valid as non-alphabet bytes will be ignored
  354. ],
  355. 'PTR': ['example.com.', '*.example.com.'],
  356. 'RP': ['hostmaster.example.com. .'],
  357. # 'SMIMEA': ['3 1 0 aabbccddeeff'],
  358. 'SPF': ['"v=spf1 include:example.com ~all"',
  359. '"v=spf1 ip4:10.1.1.1 ip4:127.0.0.0/16 ip4:192.168.0.0/27 include:example.com -all"',
  360. '"spf2.0/pra,mfrom ip6:2001:558:fe14:76:68:87:28:0/120 -all"'],
  361. 'SRV': ['0 0 0 .', '100 1 5061 example.com.'],
  362. 'SSHFP': ['2 2 aabbcceeddff'],
  363. 'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
  364. 'TXT': ['"foobar"', '"foo" "bar"', '"“红色联合”对“四·二八兵团”总部大楼的攻击已持续了两天"', '"new\\010line"'
  365. '"🧥 👚 👕 👖 👔 👗 👙 👘 👠 👡 👢 👞 👟 🥾 🥿 🧦 🧤 🧣 🎩 🧢 👒 🎓 ⛑ 👑 👝 👛 👜 💼 🎒 👓 🕶 🥽 🥼 🌂 🧵"'],
  366. 'URI': ['10 1 "ftp://ftp1.example.com/public"'],
  367. }
  368. self.assertAllSupportedRRSetTypes(set(datas.keys()))
  369. for t, records in datas.items():
  370. for r in records:
  371. data = {'records': [r], 'ttl': 3660, 'type': t, 'subname': 'test'}
  372. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  373. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  374. self.assertStatus(response, status.HTTP_201_CREATED)
  375. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  376. response = self.client.delete_rr_set(self.my_empty_domain.name, subname='test', type_=t)
  377. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  378. def test_create_my_rr_sets_known_type_invalid(self):
  379. # TODO fill in more examples
  380. datas = {
  381. # recordtype: [list of examples expected to be rejected, individually]
  382. 'A': ['127.0.0.999', '127.000.0.01', '127.0.0.256', '::1', 'foobar', '10.0.1', '10!'],
  383. 'AAAA': ['::g', '1:1:1:1:1:1:1:1:', '1:1:1:1:1:1:1:1:1'],
  384. 'AFSDB': ['example.com.', '1 1', '1 de'],
  385. 'CAA': ['43235 issue "letsencrypt.org"'],
  386. 'CERT': ['6 0 sadfdd=='],
  387. 'CNAME': ['example.com', '10 example.com.'],
  388. 'DHCID': ['x', 'xx', 'xxx'],
  389. 'DLV': ['-34 13 1 aabbccddeeff'],
  390. 'DS': ['-34 13 1 aabbccddeeff'],
  391. 'EUI48': ['aa-bb-ccdd-ee-ff', 'AA-BB-CC-DD-EE-GG'],
  392. 'EUI64': ['aa-bb-cc-dd-ee-ff-gg-11', 'AA-BB-C C-DD-EE-FF-00-11'],
  393. 'HINFO': ['"ARMv8-A"', f'"a" "{"b"*256}"'],
  394. # 'IPSECKEY': [],
  395. 'KX': ['-1 example.com', '10 example.com'],
  396. 'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
  397. 'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.'],
  398. 'NAPTR': ['100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu',
  399. '100 50 "s" "" _z3950._tcp.gatech.edu.',
  400. '100 50 3 2 "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.'],
  401. 'NS': ['ns1.example.com', '127.0.0.1'],
  402. 'OPENPGPKEY': ['1 2 3'],
  403. 'PTR': ['"example.com."', '10 *.example.com.'],
  404. 'RP': ['hostmaster.example.com.', '10 foo.'],
  405. # 'SMIMEA': ['3 1 0 aGVsbG8gd29ybGQh'],
  406. 'SPF': ['"v=spf1', 'v=spf1 include:example.com ~all'],
  407. 'SRV': ['0 0 0 0', '100 5061 example.com.'],
  408. 'SSHFP': ['aabbcceeddff'],
  409. 'TLSA': ['3 1 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'],
  410. 'TXT': ['foob"ar', 'v=spf1 include:example.com ~all', '"foo\nbar"', '"\x00" "NUL byte yo"'],
  411. 'URI': ['"1" "2" "3"'],
  412. }
  413. self.assertAllSupportedRRSetTypes(set(datas.keys()))
  414. for t, records in datas.items():
  415. for r in records:
  416. data = {'records': [r], 'ttl': 3660, 'type': t, 'subname': ''}
  417. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  418. self.assertNotContains(response, 'Duplicate', status_code=status.HTTP_400_BAD_REQUEST)
  419. def test_create_my_rr_sets_txt_splitting(self):
  420. for t in ['TXT', 'SPF']:
  421. for l in [200, 255, 256, 300, 400]:
  422. data = {'records': [f'"{"a"*l}"'], 'ttl': 3660, 'type': t, 'subname': f'x{l}'}
  423. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  424. response = self.client.post_rr_set(self.my_empty_domain.name, **data)
  425. self.assertStatus(response, status.HTTP_201_CREATED)
  426. response = self.client.get_rr_set(self.my_empty_domain.name, f'x{l}', t)
  427. num_tokens = response.data['records'][0].count(' ') + 1
  428. num_tokens_expected = l // 256 + 1
  429. self.assertEqual(num_tokens, num_tokens_expected,
  430. f'For a {t} record with a token of length of {l}, expected to see '
  431. f'{num_tokens_expected} tokens in the canonical format, but saw {num_tokens}.')
  432. self.assertEqual("".join(r.strip('" ') for r in response.data['records'][0]), 'a'*l)
  433. def test_create_my_rr_sets_unknown_type(self):
  434. for _type in ['AA', 'ASDF'] + list(RR_SET_TYPES_AUTOMATIC | RR_SET_TYPES_UNSUPPORTED):
  435. response = self.client.post_rr_set(self.my_domain.name, records=['1234'], ttl=3660, type=_type)
  436. self.assertContains(
  437. response,
  438. text='managed automatically' if _type in RR_SET_TYPES_AUTOMATIC else 'type is currently unsupported',
  439. status_code=status.HTTP_400_BAD_REQUEST
  440. )
  441. def test_create_my_rr_sets_insufficient_ttl(self):
  442. ttl = settings.MINIMUM_TTL_DEFAULT - 1
  443. response = self.client.post_rr_set(self.my_empty_domain.name, records=['1.2.3.4'], ttl=ttl, type='A')
  444. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  445. detail = f'Ensure this value is greater than or equal to {self.my_empty_domain.minimum_ttl}.'
  446. self.assertEqual(response.data['ttl'][0], detail)
  447. ttl += 1
  448. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
  449. response = self.client.post_rr_set(self.my_empty_domain.name, records=['1.2.23.4'], ttl=ttl, type='A')
  450. self.assertStatus(response, status.HTTP_201_CREATED)
  451. def test_retrieve_my_rr_sets_apex(self):
  452. response = self.client.get_rr_set(self.my_rr_set_domain.name, subname='', type_='A')
  453. self.assertStatus(response, status.HTTP_200_OK)
  454. self.assertEqual(response.data['records'][0], '1.2.3.4')
  455. self.assertEqual(response.data['ttl'], 3620)
  456. def test_retrieve_my_rr_sets_restricted_types(self):
  457. for type_ in self.AUTOMATIC_TYPES:
  458. response = self.client.get_rr_sets(self.my_domain.name, type=type_)
  459. self.assertStatus(response, status.HTTP_403_FORBIDDEN)
  460. response = self.client.get_rr_sets(self.my_domain.name, type=type_, subname='')
  461. self.assertStatus(response, status.HTTP_403_FORBIDDEN)
  462. def test_update_my_rr_sets(self):
  463. for subname in self.SUBNAMES:
  464. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
  465. data = {'records': ['2.2.3.4'], 'ttl': 3630, 'type': 'A', 'subname': subname}
  466. response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
  467. self.assertStatus(response, status.HTTP_200_OK)
  468. response = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A')
  469. self.assertStatus(response, status.HTTP_200_OK)
  470. self.assertEqual(response.data['records'], ['2.2.3.4'])
  471. self.assertEqual(response.data['ttl'], 3630)
  472. response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'records': ['2.2.3.5']})
  473. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  474. response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'ttl': 3637})
  475. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  476. def test_update_my_rr_set_with_invalid_payload_type(self):
  477. for subname in self.SUBNAMES:
  478. data = [{'records': ['2.2.3.4'], 'ttl': 30, 'type': 'A', 'subname': subname}]
  479. response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
  480. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  481. self.assertEquals(response.data['non_field_errors'][0],
  482. 'Invalid data. Expected a dictionary, but got list.')
  483. data = 'foobar'
  484. response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
  485. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  486. self.assertEquals(response.data['non_field_errors'][0],
  487. 'Invalid data. Expected a dictionary, but got str.')
  488. def test_partially_update_my_rr_sets(self):
  489. for subname in self.SUBNAMES:
  490. current_rr_set = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A').data
  491. for data in [
  492. {'records': ['2.2.3.4'], 'ttl': 3630},
  493. {'records': ['3.2.3.4']},
  494. {'records': ['3.2.3.4', '9.8.8.7']},
  495. {'ttl': 3637},
  496. ]:
  497. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
  498. response = self.client.patch_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
  499. self.assertStatus(response, status.HTTP_200_OK)
  500. response = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A')
  501. self.assertStatus(response, status.HTTP_200_OK)
  502. current_rr_set.update(data)
  503. self.assertEqual(response.data['records'], current_rr_set['records'])
  504. self.assertEqual(response.data['ttl'], current_rr_set['ttl'])
  505. response = self.client.patch_rr_set(self.my_rr_set_domain.name, subname, 'A', {})
  506. self.assertStatus(response, status.HTTP_200_OK)
  507. def test_rr_sets_touched_if_noop(self):
  508. for subname in self.SUBNAMES:
  509. touched_old = RRset.objects.get(domain=self.my_rr_set_domain, type='A', subname=subname).touched
  510. response = self.client.patch_rr_set(self.my_rr_set_domain.name, subname, 'A', {})
  511. self.assertStatus(response, status.HTTP_200_OK)
  512. touched_new = RRset.objects.get(domain=self.my_rr_set_domain, type='A', subname=subname).touched
  513. self.assertGreater(touched_new, touched_old)
  514. self.assertEqual(Domain.objects.get(name=self.my_rr_set_domain.name).touched, touched_new)
  515. def test_partially_update_other_rr_sets(self):
  516. data = {'records': ['3.2.3.4'], 'ttl': 334}
  517. for subname in self.SUBNAMES:
  518. response = self.client.patch_rr_set(self.other_rr_set_domain.name, subname, 'A', data)
  519. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  520. def test_update_other_rr_sets(self):
  521. data = {'ttl': 305}
  522. for subname in self.SUBNAMES:
  523. response = self.client.patch_rr_set(self.other_rr_set_domain.name, subname, 'A', data)
  524. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  525. def test_update_essential_properties(self):
  526. # Changing the subname is expected to cause an error
  527. url = self.reverse('v1:rrset', name=self.my_rr_set_domain.name, subname='test', type='A')
  528. data = {'records': ['3.2.3.4'], 'ttl': 3620, 'subname': 'test2', 'type': 'A'}
  529. response = self.client.patch(url, data)
  530. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  531. self.assertEquals(response.data['subname'][0].code, 'read-only-on-update')
  532. response = self.client.put(url, data)
  533. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  534. self.assertEquals(response.data['subname'][0].code, 'read-only-on-update')
  535. # Changing the type is expected to cause an error
  536. data = {'records': ['3.2.3.4'], 'ttl': 3620, 'subname': 'test', 'type': 'TXT'}
  537. response = self.client.patch(url, data)
  538. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  539. self.assertEquals(response.data['type'][0].code, 'read-only-on-update')
  540. response = self.client.put(url, data)
  541. self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
  542. self.assertEquals(response.data['type'][0].code, 'read-only-on-update')
  543. # Changing "created" is no-op
  544. response = self.client.get(url)
  545. data = response.data
  546. created = data['created']
  547. data['created'] = '2019-07-19T17:22:49.575717Z'
  548. response = self.client.patch(url, data)
  549. self.assertStatus(response, status.HTTP_200_OK)
  550. response = self.client.put(url, data)
  551. self.assertStatus(response, status.HTTP_200_OK)
  552. # Check that nothing changed
  553. response = self.client.get(url)
  554. self.assertStatus(response, status.HTTP_200_OK)
  555. self.assertEqual(response.data['records'][0], '2.2.3.4')
  556. self.assertEqual(response.data['ttl'], 3620)
  557. self.assertEqual(response.data['name'], 'test.' + self.my_rr_set_domain.name + '.')
  558. self.assertEqual(response.data['subname'], 'test')
  559. self.assertEqual(response.data['type'], 'A')
  560. self.assertEqual(response.data['created'], created)
  561. # This is expected to work, but the fields are ignored
  562. data = {'records': ['3.2.3.4'], 'name': 'example.com.', 'domain': 'example.com'}
  563. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
  564. response = self.client.patch(url, data)
  565. self.assertStatus(response, status.HTTP_200_OK)
  566. response = self.client.get(url)
  567. self.assertStatus(response, status.HTTP_200_OK)
  568. self.assertEqual(response.data['records'][0], '3.2.3.4')
  569. self.assertEqual(response.data['domain'], self.my_rr_set_domain.name)
  570. self.assertEqual(response.data['name'], 'test.' + self.my_rr_set_domain.name + '.')
  571. def test_update_unknown_rrset(self):
  572. url = self.reverse('v1:rrset', name=self.my_rr_set_domain.name, subname='doesnotexist', type='A')
  573. data = {'records': ['3.2.3.4'], 'ttl': 3620}
  574. response = self.client.patch(url, data)
  575. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  576. response = self.client.put(url, data)
  577. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  578. def test_delete_my_rr_sets_with_patch(self):
  579. data = {'records': []}
  580. for subname in self.SUBNAMES:
  581. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
  582. response = self.client.patch_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
  583. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  584. # Deletion is only idempotent via DELETE. For PATCH/PUT, the view raises 404 if the instance does not
  585. # exist. By that time, the view has not parsed the payload yet and does not know it is a deletion.
  586. response = self.client.patch_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
  587. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  588. response = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A')
  589. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  590. def test_delete_my_rr_sets_with_delete(self):
  591. for subname in self.SUBNAMES:
  592. with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
  593. response = self.client.delete_rr_set(self.my_rr_set_domain.name, subname=subname, type_='A')
  594. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  595. domain = Domain.objects.get(name=self.my_rr_set_domain.name)
  596. self.assertEqual(domain.touched, domain.published)
  597. response = self.client.delete_rr_set(self.my_rr_set_domain.name, subname=subname, type_='A')
  598. self.assertStatus(response, status.HTTP_204_NO_CONTENT)
  599. response = self.client.get_rr_set(self.my_rr_set_domain.name, subname=subname, type_='A')
  600. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  601. def test_delete_other_rr_sets(self):
  602. data = {'records': []}
  603. for subname in self.SUBNAMES:
  604. # Try PATCH empty
  605. response = self.client.patch_rr_set(self.other_rr_set_domain.name, subname, 'A', data)
  606. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  607. # Try DELETE
  608. response = self.client.delete_rr_set(self.other_rr_set_domain.name, subname, 'A')
  609. self.assertStatus(response, status.HTTP_404_NOT_FOUND)
  610. # Make sure it actually is still there
  611. self.assertGreater(len(self.other_rr_set_domain.rrset_set.filter(subname=subname, type='A')), 0)
  612. def test_import_rr_sets(self):
  613. with self.assertPdnsRequests(self.request_pdns_zone_retrieve(name=self.my_domain.name)):
  614. call_command('sync-from-pdns', self.my_domain.name)
  615. for response in [
  616. self.client.get_rr_sets(self.my_domain.name),
  617. self.client.get_rr_sets(self.my_domain.name, subname=''),
  618. ]:
  619. self.assertStatus(response, status.HTTP_200_OK)
  620. self.assertEqual(len(response.data), 1, response.data)
  621. self.assertContainsRRSets(response.data, [dict(subname='', records=settings.DEFAULT_NS, type='NS')])