views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. from __future__ import unicode_literals
  2. from django.core.mail import EmailMessage
  3. from desecapi.models import Domain, User
  4. from desecapi.serializers import DomainSerializer, DonationSerializer
  5. from rest_framework import generics
  6. from desecapi.permissions import IsOwner
  7. from rest_framework import permissions
  8. from django.http import Http404
  9. from rest_framework.views import APIView
  10. from rest_framework.response import Response
  11. from rest_framework.reverse import reverse
  12. from rest_framework.authentication import TokenAuthentication, get_authorization_header
  13. from rest_framework.renderers import StaticHTMLRenderer
  14. from dns import resolver
  15. from django.template.loader import get_template
  16. from django.template import Context
  17. from desecapi.authentication import BasicTokenAuthentication, URLParamAuthentication
  18. import base64
  19. from desecapi import settings
  20. from rest_framework.exceptions import ValidationError
  21. from djoser import views, signals
  22. from rest_framework import status
  23. from datetime import datetime, timedelta
  24. from django.utils import timezone
  25. from desecapi.forms import UnlockForm
  26. from django.shortcuts import render
  27. from django.http import HttpResponseRedirect
  28. from desecapi.emails import send_account_lock_email
  29. import re
  30. # TODO Generalize?
  31. patternDyn = re.compile(r'^[A-Za-z][A-Za-z0-9-]*\.dedyn\.io$')
  32. patternNonDyn = re.compile(r'^([A-Za-z][A-Za-z0-9-]*\.)+[A-Za-z]+$')
  33. def get_client_ip(request):
  34. return request.META.get('REMOTE_ADDR')
  35. class DomainList(generics.ListCreateAPIView):
  36. serializer_class = DomainSerializer
  37. permission_classes = (permissions.IsAuthenticated, IsOwner,)
  38. def get_queryset(self):
  39. return Domain.objects.filter(owner=self.request.user.pk)
  40. def perform_create(self, serializer):
  41. pattern = patternDyn if self.request.user.dyn else patternNonDyn
  42. if pattern.match(serializer.validated_data['name']) is None or "--" in serializer.validated_data['name']:
  43. ex = ValidationError(detail={"detail": "This domain name is not well-formed, by policy.", "code": "domain-illformed"})
  44. ex.status_code = status.HTTP_409_CONFLICT
  45. raise ex
  46. queryset = Domain.objects.filter(name=serializer.validated_data['name'])
  47. if queryset.exists():
  48. ex = ValidationError(detail={"detail": "This domain name is already registered.", "code": "domain-taken"})
  49. ex.status_code = status.HTTP_409_CONFLICT
  50. raise ex
  51. if self.request.user.limit_domains is not None and self.request.user.domains.count() >= self.request.user.limit_domains:
  52. ex = ValidationError(detail={"detail": "You reached the maximum number of domains allowed for your account.", "code": "domain-limit"})
  53. ex.status_code = status.HTTP_403_FORBIDDEN
  54. raise ex
  55. obj = serializer.save(owner=self.request.user)
  56. def sendDynDnsEmail(domain):
  57. content_tmpl = get_template('emails/domain-dyndns/content.txt')
  58. subject_tmpl = get_template('emails/domain-dyndns/subject.txt')
  59. from_tmpl = get_template('emails/from.txt')
  60. context = Context({
  61. 'domain': domain.name,
  62. 'url': 'https://update.dedyn.io/',
  63. 'username': domain.name,
  64. 'password': self.request.auth.key
  65. })
  66. email = EmailMessage(subject_tmpl.render(context),
  67. content_tmpl.render(context),
  68. from_tmpl.render(context),
  69. [self.request.user.email])
  70. email.send()
  71. if self.request.user.dyn:
  72. sendDynDnsEmail(obj)
  73. class DomainDetail(generics.RetrieveUpdateDestroyAPIView):
  74. serializer_class = DomainSerializer
  75. permission_classes = (permissions.IsAuthenticated, IsOwner,)
  76. def get_queryset(self):
  77. return Domain.objects.filter(owner=self.request.user.pk)
  78. def put(self, request, pk, format=None):
  79. # Don't accept PUT requests for non-existent or non-owned domains.
  80. domain = Domain.objects.filter(owner=self.request.user.pk, pk=pk)
  81. if len(domain) is 0:
  82. raise Http404
  83. return super(DomainDetail, self).put(request, pk, format)
  84. class DomainDetailByName(DomainDetail):
  85. lookup_field = 'name'
  86. class Root(APIView):
  87. def get(self, request, format=None):
  88. if self.request.user and self.request.user.is_authenticated():
  89. return Response({
  90. 'domains': reverse('domain-list'),
  91. 'user': reverse('user'),
  92. 'logout:': reverse('logout'),
  93. })
  94. else:
  95. return Response({
  96. 'login': reverse('login', request=request, format=format),
  97. 'register': reverse('register', request=request, format=format),
  98. })
  99. class DnsQuery(APIView):
  100. def get(self, request, format=None):
  101. desecio = resolver.Resolver()
  102. if not 'domain' in request.GET:
  103. return Response(status=status.HTTP_400_BAD_REQUEST)
  104. domain = str(request.GET['domain'])
  105. def getRecords(domain, type):
  106. records = []
  107. try:
  108. for ip in desecio.query(domain, type):
  109. records.append(str(ip))
  110. except resolver.NoAnswer:
  111. return []
  112. except resolver.NoNameservers:
  113. return []
  114. except resolver.NXDOMAIN:
  115. return []
  116. return records
  117. # find currently active NS records
  118. nsrecords = getRecords(domain, 'NS')
  119. # find desec.io nameserver IP address with standard nameserver
  120. ips = desecio.query('ns2.desec.io')
  121. desecio.nameservers = []
  122. for ip in ips:
  123. desecio.nameservers.append(str(ip))
  124. # query desec.io nameserver for A and AAAA records
  125. arecords = getRecords(domain, 'A')
  126. aaaarecords = getRecords(domain, 'AAAA')
  127. return Response({
  128. 'domain': domain,
  129. 'ns': nsrecords,
  130. 'a': arecords,
  131. 'aaaa': aaaarecords,
  132. '_nameserver': desecio.nameservers
  133. })
  134. class DynDNS12Update(APIView):
  135. authentication_classes = (TokenAuthentication, BasicTokenAuthentication, URLParamAuthentication,)
  136. renderer_classes = [StaticHTMLRenderer]
  137. def findDomain(self, request):
  138. def findDomainname(request):
  139. # 1. hostname parameter
  140. if 'hostname' in request.query_params and request.query_params['hostname'] != 'YES':
  141. return request.query_params['hostname']
  142. # 2. host_id parameter
  143. if 'host_id' in request.query_params:
  144. return request.query_params['host_id']
  145. # 3. http basic auth username
  146. try:
  147. return base64.b64decode(get_authorization_header(request).decode().split(' ')[1].encode()).decode().split(':')[0]
  148. except IndexError:
  149. pass
  150. except UnicodeDecodeError:
  151. pass
  152. # 4. username parameter
  153. if 'username' in request.query_params:
  154. return request.query_params['username']
  155. # 5. only domain associated with this user account
  156. if len(request.user.domains.all()) == 1:
  157. return request.user.domains.all()[0].name
  158. return None
  159. domainname = findDomainname(request)
  160. domain = None
  161. # load and check permissions
  162. try:
  163. domain = Domain.objects.filter(owner=self.request.user.pk, name=domainname).all()[0]
  164. except:
  165. pass
  166. return domain
  167. def findIP(self, request, params, version=4):
  168. if version == 4:
  169. lookfor = '.'
  170. elif version == 6:
  171. lookfor = ':'
  172. else:
  173. raise Exception
  174. # Check URL parameters
  175. for p in params:
  176. if p in request.query_params and lookfor in request.query_params[p]:
  177. return request.query_params[p]
  178. # Check remote IP address
  179. client_ip = get_client_ip(request)
  180. if lookfor in client_ip:
  181. return client_ip
  182. # give up
  183. return ''
  184. def findIPv4(self, request):
  185. return self.findIP(request, ['myip', 'myipv4', 'ip'])
  186. def findIPv6(self, request):
  187. return self.findIP(request, ['myipv6', 'ipv6', 'myip', 'ip'], version=6)
  188. def get(self, request, format=None):
  189. domain = self.findDomain(request)
  190. if domain is None:
  191. raise Http404
  192. domain.arecord = self.findIPv4(request)
  193. domain.aaaarecord = self.findIPv6(request)
  194. domain.save()
  195. return Response('good')
  196. class DonationList(generics.CreateAPIView):
  197. serializer_class = DonationSerializer
  198. def perform_create(self, serializer):
  199. iban = serializer.validated_data['iban']
  200. obj = serializer.save()
  201. def sendDonationEmails(donation):
  202. context = Context({
  203. 'donation': donation,
  204. 'creditoridentifier': settings.SEPA['CREDITOR_ID'],
  205. 'complete_iban': iban
  206. })
  207. # internal desec notification
  208. content_tmpl = get_template('emails/donation/desec-content.txt')
  209. subject_tmpl = get_template('emails/donation/desec-subject.txt')
  210. attachment_tmpl = get_template('emails/donation/desec-attachment-jameica.txt')
  211. from_tmpl = get_template('emails/from.txt')
  212. email = EmailMessage(subject_tmpl.render(context),
  213. content_tmpl.render(context),
  214. from_tmpl.render(context),
  215. ['donation@desec.io'],
  216. attachments=[
  217. ('jameica-directdebit.xml',
  218. attachment_tmpl.render(context),
  219. 'text/xml')
  220. ])
  221. email.send()
  222. # donor notification
  223. if donation.email:
  224. content_tmpl = get_template('emails/donation/donor-content.txt')
  225. subject_tmpl = get_template('emails/donation/donor-subject.txt')
  226. test = content_tmpl.render(context)
  227. email = EmailMessage(subject_tmpl.render(context),
  228. content_tmpl.render(context),
  229. from_tmpl.render(context),
  230. [donation.email])
  231. email.send()
  232. # send emails
  233. sendDonationEmails(obj)
  234. class RegistrationView(views.RegistrationView):
  235. """
  236. Extends the djoser RegistrationView to record the remote IP address of any registration.
  237. """
  238. def create(self, request, *args, **kwargs):
  239. serializer = self.get_serializer(data=request.data)
  240. serializer.is_valid(raise_exception=True)
  241. self.perform_create(serializer, get_client_ip(request))
  242. headers = self.get_success_headers(serializer.data)
  243. return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  244. def perform_create(self, serializer, remote_ip):
  245. captcha = \
  246. (
  247. User.objects.filter(
  248. created__gte=timezone.now()-timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS),
  249. registration_remote_ip=remote_ip
  250. ).count() >= settings.ABUSE_BY_REMOTE_IP_LIMIT
  251. or
  252. User.objects.filter(
  253. created__gte=timezone.now() - timedelta(hours=settings.ABUSE_BY_EMAIL_HOSTNAME_PERIOD_HRS),
  254. email__endswith=serializer.validated_data['email'].split('@')[-1]
  255. ).count() >= settings.ABUSE_BY_EMAIL_HOSTNAME_LIMIT
  256. )
  257. user = serializer.save(registration_remote_ip=remote_ip, captcha_required=captcha)
  258. if captcha:
  259. send_account_lock_email(self.request, user)
  260. signals.user_registered.send(sender=self.__class__, user=user, request=self.request)
  261. def unlock(request, email):
  262. # if this is a POST request we need to process the form data
  263. if request.method == 'POST':
  264. # create a form instance and populate it with data from the request:
  265. form = UnlockForm(request.POST)
  266. # check whether it's valid:
  267. if form.is_valid():
  268. try:
  269. User.objects.get(email=email).unlock()
  270. except User.DoesNotExist:
  271. pass # fail silently, otherwise people can find out if email addresses are registered with us
  272. return HttpResponseRedirect(reverse('unlock/done'))
  273. # if a GET (or any other method) we'll create a blank form
  274. else:
  275. form = UnlockForm()
  276. return render(request, 'unlock.html', {'form': form})
  277. def unlock_done(request):
  278. return render(request, 'unlock-done.html')