views.py 18 KB

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