views.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. from __future__ import unicode_literals
  2. from django.core.mail import EmailMessage
  3. from desecapi.models import Domain, User, RRset, RR
  4. from desecapi.serializers import (
  5. DomainSerializer, RRsetSerializer, DonationSerializer)
  6. from rest_framework import generics
  7. from desecapi.permissions import IsOwner, IsDomainOwner
  8. from rest_framework import permissions
  9. from django.http import Http404, HttpResponseRedirect
  10. from rest_framework.views import APIView
  11. from rest_framework.response import Response
  12. from rest_framework.reverse import reverse
  13. from rest_framework.authentication import (
  14. TokenAuthentication, get_authorization_header)
  15. from rest_framework.renderers import StaticHTMLRenderer
  16. from dns import resolver
  17. from django.template.loader import get_template
  18. import desecapi.authentication as auth
  19. import base64
  20. from desecapi import settings
  21. from rest_framework.exceptions import (
  22. APIException, MethodNotAllowed, PermissionDenied, ValidationError)
  23. import django.core.exceptions
  24. from djoser import views, signals
  25. from rest_framework import status
  26. from datetime import timedelta
  27. from django.utils import timezone
  28. from desecapi.forms import UnlockForm
  29. from django.shortcuts import render
  30. from django.db.models import Q
  31. from desecapi.emails import send_account_lock_email, send_token_email
  32. import re
  33. import ipaddress, os
  34. from rest_framework_bulk import ListBulkCreateUpdateAPIView
  35. patternDyn = re.compile(r'^[A-Za-z-][A-Za-z0-9_-]*\.dedyn\.io$')
  36. patternNonDyn = re.compile(r'^([A-Za-z0-9-][A-Za-z0-9_-]*\.)+[A-Za-z]+$')
  37. def get_client_ip(request):
  38. return request.META.get('REMOTE_ADDR')
  39. class DomainList(generics.ListCreateAPIView):
  40. serializer_class = DomainSerializer
  41. permission_classes = (permissions.IsAuthenticated, IsOwner,)
  42. def get_queryset(self):
  43. return Domain.objects.filter(owner=self.request.user.pk)
  44. def perform_create(self, serializer):
  45. pattern = patternDyn if self.request.user.dyn else patternNonDyn
  46. if pattern.match(serializer.validated_data['name']) is None:
  47. ex = ValidationError(detail={"detail": "This domain name is not well-formed, by policy.", "code": "domain-illformed"})
  48. ex.status_code = status.HTTP_409_CONFLICT
  49. raise ex
  50. # Generate a list containing this and all higher-level domain names
  51. domain_name = serializer.validated_data['name']
  52. domain_parts = domain_name.split('.')
  53. domain_list = {'.'.join(domain_parts[i:]) for i in range(1, len(domain_parts))}
  54. # Remove public suffixes and then use this list to control registration
  55. public_suffixes = {'dedyn.io'}
  56. domain_list = domain_list - public_suffixes
  57. queryset = Domain.objects.filter(Q(name=domain_name) | (Q(name__in=domain_list) & ~Q(owner=self.request.user)))
  58. if queryset.exists():
  59. ex = ValidationError(detail={"detail": "This domain name is unavailable.", "code": "domain-unavailable"})
  60. ex.status_code = status.HTTP_409_CONFLICT
  61. raise ex
  62. if self.request.user.limit_domains is not None and self.request.user.domains.count() >= self.request.user.limit_domains:
  63. ex = ValidationError(detail={"detail": "You reached the maximum number of domains allowed for your account.", "code": "domain-limit"})
  64. ex.status_code = status.HTTP_403_FORBIDDEN
  65. raise ex
  66. try:
  67. obj = serializer.save(owner=self.request.user)
  68. except Exception as e:
  69. if str(e).endswith(' already exists'):
  70. ex = ValidationError(detail={"detail": "This domain name is unavailable.", "code": "domain-unavailable"})
  71. ex.status_code = status.HTTP_409_CONFLICT
  72. raise ex
  73. else:
  74. raise e
  75. def sendDynDnsEmail(domain):
  76. content_tmpl = get_template('emails/domain-dyndns/content.txt')
  77. subject_tmpl = get_template('emails/domain-dyndns/subject.txt')
  78. from_tmpl = get_template('emails/from.txt')
  79. context = {
  80. 'domain': domain.name,
  81. 'url': 'https://update.dedyn.io/',
  82. 'username': domain.name,
  83. 'password': self.request.auth.key
  84. }
  85. email = EmailMessage(subject_tmpl.render(context),
  86. content_tmpl.render(context),
  87. from_tmpl.render(context),
  88. [self.request.user.email])
  89. email.send()
  90. if obj.name.endswith('.dedyn.io'):
  91. sendDynDnsEmail(obj)
  92. class DomainDetail(generics.RetrieveUpdateDestroyAPIView):
  93. serializer_class = DomainSerializer
  94. permission_classes = (permissions.IsAuthenticated, IsOwner,)
  95. def delete(self, request, *args, **kwargs):
  96. try:
  97. super().delete(request, *args, **kwargs)
  98. except Http404:
  99. pass
  100. return Response(status=status.HTTP_204_NO_CONTENT)
  101. def get_queryset(self):
  102. return Domain.objects.filter(owner=self.request.user.pk)
  103. def update(self, request, *args, **kwargs):
  104. try:
  105. return super().update(request, *args, **kwargs)
  106. except django.core.exceptions.ValidationError as e:
  107. ex = ValidationError(detail={"detail": str(e)})
  108. ex.status_code = status.HTTP_409_CONFLICT
  109. raise ex
  110. class DomainDetailByName(DomainDetail):
  111. lookup_field = 'name'
  112. class RRsetDetail(generics.RetrieveUpdateDestroyAPIView):
  113. lookup_field = 'type'
  114. serializer_class = RRsetSerializer
  115. permission_classes = (permissions.IsAuthenticated, IsDomainOwner,)
  116. def delete(self, request, *args, **kwargs):
  117. if request.user.locked:
  118. detail = "You cannot delete RRsets while your account is locked."
  119. raise PermissionDenied(detail)
  120. try:
  121. super().delete(request, *args, **kwargs)
  122. except Http404:
  123. pass
  124. return Response(status=status.HTTP_204_NO_CONTENT)
  125. def get_queryset(self):
  126. name = self.kwargs['name']
  127. subname = self.kwargs['subname'].replace('=2F', '/')
  128. type_ = self.kwargs['type']
  129. if type_ in RRset.RESTRICTED_TYPES:
  130. raise PermissionDenied("You cannot tinker with the %s RRset." % type_)
  131. return RRset.objects.filter(
  132. domain__owner=self.request.user.pk,
  133. domain__name=name, subname=subname, type=type_)
  134. def update(self, request, *args, **kwargs):
  135. if request.data.get('records') == []:
  136. return self.delete(request, *args, **kwargs)
  137. for k in ('type', 'subname'):
  138. request.data[k] = request.data.pop(k, self.kwargs[k])
  139. try:
  140. return super().update(request, *args, **kwargs)
  141. except django.core.exceptions.ValidationError as e:
  142. ex = ValidationError(detail=e.message_dict)
  143. ex.status_code = status.HTTP_409_CONFLICT
  144. raise ex
  145. class RRsetList(ListBulkCreateUpdateAPIView):
  146. authentication_classes = (TokenAuthentication, auth.IPAuthentication,)
  147. serializer_class = RRsetSerializer
  148. permission_classes = (permissions.IsAuthenticated, IsDomainOwner,)
  149. def get_queryset(self):
  150. name = self.kwargs['name']
  151. rrsets = self.request.user.domains.get(name=name).rrset_set
  152. for filter_field in ('subname', 'type'):
  153. value = self.request.query_params.get(filter_field)
  154. if value is not None:
  155. if filter_field == 'type' and value in RRset.RESTRICTED_TYPES:
  156. raise PermissionDenied("You cannot tinker with the %s RRset." % value)
  157. rrsets = rrsets.filter(**{'%s__exact' % filter_field: value})
  158. return rrsets
  159. def create(self, request, *args, **kwargs):
  160. try:
  161. return super().create(request, *args, **kwargs)
  162. except Domain.DoesNotExist:
  163. raise Http404
  164. except ValidationError as e:
  165. if isinstance(e.detail, dict):
  166. detail = e.detail.get('__all__')
  167. if isinstance(detail, list) \
  168. and any(m.endswith(' already exists.') for m in detail):
  169. e.status_code = status.HTTP_409_CONFLICT
  170. raise e
  171. def perform_create(self, serializer):
  172. # For new RRsets without a subname, set it empty. We don't use
  173. # default='' in the serializer field definition so that during PUT, the
  174. # subname value is retained if omitted.
  175. if isinstance(self.request.data, list):
  176. serializer._validated_data = [{**{'subname': ''}, **data}
  177. for data in serializer.validated_data]
  178. else:
  179. serializer._validated_data = {**{'subname': ''}, **serializer.validated_data}
  180. # Associate RRset with proper domain
  181. domain = self.request.user.domains.get(name=self.kwargs['name'])
  182. serializer.save(domain=domain)
  183. def get(self, request, *args, **kwargs):
  184. name = self.kwargs['name']
  185. if not Domain.objects.filter(name=name, owner=self.request.user.pk):
  186. raise Http404
  187. return super().get(request, *args, **kwargs)
  188. class Root(APIView):
  189. def get(self, request, format=None):
  190. if self.request.user and self.request.user.is_authenticated():
  191. return Response({
  192. 'domains': reverse('domain-list'),
  193. 'user': reverse('user'),
  194. 'logout': reverse('token-destroy'), # TODO change interface to token-destroy, too?
  195. })
  196. else:
  197. return Response({
  198. 'login': reverse('token-create', request=request, format=format), # TODO change interface to token-create, too?
  199. 'register': reverse('register', request=request, format=format),
  200. })
  201. class DnsQuery(APIView):
  202. def get(self, request, format=None):
  203. desecio = resolver.Resolver()
  204. if not 'domain' in request.GET:
  205. return Response(status=status.HTTP_400_BAD_REQUEST)
  206. domain = str(request.GET['domain'])
  207. def getRecords(domain, type_):
  208. records = []
  209. try:
  210. for ip in desecio.query(domain, type_):
  211. records.append(str(ip))
  212. except resolver.NoAnswer:
  213. return []
  214. except resolver.NoNameservers:
  215. return []
  216. except resolver.NXDOMAIN:
  217. return []
  218. return records
  219. # find currently active NS records
  220. nsrecords = getRecords(domain, 'NS')
  221. # find desec.io nameserver IP address with standard nameserver
  222. ips = desecio.query('ns2.desec.io')
  223. desecio.nameservers = []
  224. for ip in ips:
  225. desecio.nameservers.append(str(ip))
  226. # query desec.io nameserver for A and AAAA records
  227. arecords = getRecords(domain, 'A')
  228. aaaarecords = getRecords(domain, 'AAAA')
  229. return Response({
  230. 'domain': domain,
  231. 'ns': nsrecords,
  232. 'a': arecords,
  233. 'aaaa': aaaarecords,
  234. '_nameserver': desecio.nameservers
  235. })
  236. class DynDNS12Update(APIView):
  237. authentication_classes = (TokenAuthentication, auth.BasicTokenAuthentication, auth.URLParamAuthentication,)
  238. renderer_classes = [StaticHTMLRenderer]
  239. def findDomain(self, request):
  240. def findDomainname(request):
  241. # 1. hostname parameter
  242. if 'hostname' in request.query_params and request.query_params['hostname'] != 'YES':
  243. return request.query_params['hostname']
  244. # 2. host_id parameter
  245. if 'host_id' in request.query_params:
  246. return request.query_params['host_id']
  247. # 3. http basic auth username
  248. try:
  249. domainname = base64.b64decode(get_authorization_header(request).decode().split(' ')[1].encode()).decode().split(':')[0]
  250. if domainname:
  251. return domainname
  252. except IndexError:
  253. pass
  254. except UnicodeDecodeError:
  255. pass
  256. # 4. username parameter
  257. if 'username' in request.query_params:
  258. return request.query_params['username']
  259. # 5. only domain associated with this user account
  260. if len(request.user.domains.all()) == 1:
  261. return request.user.domains.all()[0].name
  262. if len(request.user.domains.all()) > 1:
  263. ex = ValidationError(detail={"detail": "Request does not specify domain unambiguously.", "code": "domain-ambiguous"})
  264. ex.status_code = status.HTTP_409_CONFLICT
  265. raise ex
  266. return None
  267. name = findDomainname(request)
  268. try:
  269. return self.request.user.domains.get(name=name)
  270. except Domain.DoesNotExist:
  271. return None
  272. def findIP(self, request, params, version=4):
  273. if version == 4:
  274. lookfor = '.'
  275. elif version == 6:
  276. lookfor = ':'
  277. else:
  278. raise Exception
  279. # Check URL parameters
  280. for p in params:
  281. if p in request.query_params:
  282. if not len(request.query_params[p]):
  283. return None
  284. if lookfor in request.query_params[p]:
  285. return request.query_params[p]
  286. # Check remote IP address
  287. client_ip = get_client_ip(request)
  288. if lookfor in client_ip:
  289. return client_ip
  290. # give up
  291. return None
  292. def findIPv4(self, request):
  293. return self.findIP(request, ['myip', 'myipv4', 'ip'])
  294. def findIPv6(self, request):
  295. return self.findIP(request, ['myipv6', 'ipv6', 'myip', 'ip'], version=6)
  296. def get(self, request, format=None):
  297. domain = self.findDomain(request)
  298. if domain is None:
  299. raise Http404
  300. datas = {'A': self.findIPv4(request), 'AAAA': self.findIPv6(request)}
  301. rrsets = RRset.plain_to_RRsets(
  302. [{'subname': '', 'type': type_, 'ttl': 60,
  303. 'contents': [ip] if ip is not None else []}
  304. for type_, ip in datas.items()],
  305. domain=domain)
  306. domain.write_rrsets(rrsets)
  307. return Response('good')
  308. class DonationList(generics.CreateAPIView):
  309. serializer_class = DonationSerializer
  310. def perform_create(self, serializer):
  311. iban = serializer.validated_data['iban']
  312. obj = serializer.save()
  313. def sendDonationEmails(donation):
  314. context = {
  315. 'donation': donation,
  316. 'creditoridentifier': settings.SEPA['CREDITOR_ID'],
  317. 'creditorname': settings.SEPA['CREDITOR_NAME'],
  318. 'complete_iban': iban
  319. }
  320. # internal desec notification
  321. content_tmpl = get_template('emails/donation/desec-content.txt')
  322. subject_tmpl = get_template('emails/donation/desec-subject.txt')
  323. attachment_tmpl = get_template('emails/donation/desec-attachment-jameica.txt')
  324. from_tmpl = get_template('emails/from.txt')
  325. email = EmailMessage(subject_tmpl.render(context),
  326. content_tmpl.render(context),
  327. from_tmpl.render(context),
  328. ['donation@desec.io'],
  329. attachments=[
  330. ('jameica-directdebit.xml',
  331. attachment_tmpl.render(context),
  332. 'text/xml')
  333. ])
  334. email.send()
  335. # donor notification
  336. if donation.email:
  337. content_tmpl = get_template('emails/donation/donor-content.txt')
  338. subject_tmpl = get_template('emails/donation/donor-subject.txt')
  339. email = EmailMessage(subject_tmpl.render(context),
  340. content_tmpl.render(context),
  341. from_tmpl.render(context),
  342. [donation.email])
  343. email.send()
  344. # send emails
  345. sendDonationEmails(obj)
  346. class UserCreateView(views.UserCreateView):
  347. """
  348. Extends the djoser UserCreateView to record the remote IP address of any registration.
  349. """
  350. def create(self, request, *args, **kwargs):
  351. serializer = self.get_serializer(data=request.data)
  352. serializer.is_valid(raise_exception=True)
  353. self.perform_create(serializer, get_client_ip(request))
  354. headers = self.get_success_headers(serializer.data)
  355. return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  356. def perform_create(self, serializer, remote_ip):
  357. lock = (
  358. ipaddress.ip_address(remote_ip) not in ipaddress.IPv6Network(os.environ['DESECSTACK_IPV6_SUBNET'])
  359. and (
  360. User.objects.filter(
  361. created__gte=timezone.now()-timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS),
  362. registration_remote_ip=remote_ip
  363. ).count() >= settings.ABUSE_BY_REMOTE_IP_LIMIT
  364. or
  365. User.objects.filter(
  366. created__gte=timezone.now() - timedelta(hours=settings.ABUSE_BY_EMAIL_HOSTNAME_PERIOD_HRS),
  367. email__endswith='@{0}'.format(serializer.validated_data['email'].split('@')[-1])
  368. ).count() >= settings.ABUSE_BY_EMAIL_HOSTNAME_LIMIT
  369. )
  370. )
  371. user = serializer.save(registration_remote_ip=remote_ip, lock=lock)
  372. if user.locked:
  373. send_account_lock_email(self.request, user)
  374. elif not user.dyn:
  375. context = {'token': user.get_token()}
  376. send_token_email(context, user)
  377. signals.user_registered.send(sender=self.__class__, user=user, request=self.request)
  378. def unlock(request, email):
  379. # if this is a POST request we need to process the form data
  380. if request.method == 'POST':
  381. # create a form instance and populate it with data from the request:
  382. form = UnlockForm(request.POST)
  383. # check whether it's valid:
  384. if form.is_valid():
  385. try:
  386. user = User.objects.get(email=email)
  387. if user.locked:
  388. user.unlock()
  389. if not user.dyn:
  390. context = {'token': user.get_token()}
  391. send_token_email(context, user)
  392. except User.DoesNotExist:
  393. # fail silently, so people can't probe registered addresses
  394. pass
  395. return HttpResponseRedirect(reverse('unlock/done'))
  396. # if a GET (or any other method) we'll create a blank form
  397. else:
  398. form = UnlockForm()
  399. return render(request, 'unlock.html', {'form': form})
  400. def unlock_done(request):
  401. return render(request, 'unlock-done.html')