views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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 unavailable.", "code": "domain-unavailable"})
  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. try:
  56. obj = serializer.save(owner=self.request.user)
  57. except Exception as e:
  58. if str(e).endswith(' already 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. else:
  63. raise e
  64. def sendDynDnsEmail(domain):
  65. content_tmpl = get_template('emails/domain-dyndns/content.txt')
  66. subject_tmpl = get_template('emails/domain-dyndns/subject.txt')
  67. from_tmpl = get_template('emails/from.txt')
  68. context = Context({
  69. 'domain': domain.name,
  70. 'url': 'https://update.dedyn.io/',
  71. 'username': domain.name,
  72. 'password': self.request.auth.key
  73. })
  74. email = EmailMessage(subject_tmpl.render(context),
  75. content_tmpl.render(context),
  76. from_tmpl.render(context),
  77. [self.request.user.email])
  78. email.send()
  79. if self.request.user.dyn:
  80. sendDynDnsEmail(obj)
  81. class DomainDetail(generics.RetrieveUpdateDestroyAPIView):
  82. serializer_class = DomainSerializer
  83. permission_classes = (permissions.IsAuthenticated, IsOwner,)
  84. def delete(self, request, *args, **kwargs):
  85. try:
  86. super(DomainDetail, self).delete(request, *args, **kwargs)
  87. except Http404:
  88. pass
  89. return Response(status=status.HTTP_204_NO_CONTENT)
  90. def get_queryset(self):
  91. return Domain.objects.filter(owner=self.request.user.pk)
  92. class DomainDetailByName(DomainDetail):
  93. lookup_field = 'name'
  94. class Root(APIView):
  95. def get(self, request, format=None):
  96. if self.request.user and self.request.user.is_authenticated():
  97. return Response({
  98. 'domains': reverse('domain-list'),
  99. 'user': reverse('user'),
  100. 'logout:': reverse('logout'),
  101. })
  102. else:
  103. return Response({
  104. 'login': reverse('login', request=request, format=format),
  105. 'register': reverse('register', request=request, format=format),
  106. })
  107. class DnsQuery(APIView):
  108. def get(self, request, format=None):
  109. desecio = resolver.Resolver()
  110. if not 'domain' in request.GET:
  111. return Response(status=status.HTTP_400_BAD_REQUEST)
  112. domain = str(request.GET['domain'])
  113. def getRecords(domain, type):
  114. records = []
  115. try:
  116. for ip in desecio.query(domain, type):
  117. records.append(str(ip))
  118. except resolver.NoAnswer:
  119. return []
  120. except resolver.NoNameservers:
  121. return []
  122. except resolver.NXDOMAIN:
  123. return []
  124. return records
  125. # find currently active NS records
  126. nsrecords = getRecords(domain, 'NS')
  127. # find desec.io nameserver IP address with standard nameserver
  128. ips = desecio.query('ns2.desec.io')
  129. desecio.nameservers = []
  130. for ip in ips:
  131. desecio.nameservers.append(str(ip))
  132. # query desec.io nameserver for A and AAAA records
  133. arecords = getRecords(domain, 'A')
  134. aaaarecords = getRecords(domain, 'AAAA')
  135. return Response({
  136. 'domain': domain,
  137. 'ns': nsrecords,
  138. 'a': arecords,
  139. 'aaaa': aaaarecords,
  140. '_nameserver': desecio.nameservers
  141. })
  142. class DynDNS12Update(APIView):
  143. authentication_classes = (TokenAuthentication, BasicTokenAuthentication, URLParamAuthentication,)
  144. renderer_classes = [StaticHTMLRenderer]
  145. def findDomain(self, request):
  146. def findDomainname(request):
  147. # 1. hostname parameter
  148. if 'hostname' in request.query_params and request.query_params['hostname'] != 'YES':
  149. return request.query_params['hostname']
  150. # 2. host_id parameter
  151. if 'host_id' in request.query_params:
  152. return request.query_params['host_id']
  153. # 3. http basic auth username
  154. try:
  155. domainname = base64.b64decode(get_authorization_header(request).decode().split(' ')[1].encode()).decode().split(':')[0]
  156. if domainname:
  157. return domainname
  158. except IndexError:
  159. pass
  160. except UnicodeDecodeError:
  161. pass
  162. # 4. username parameter
  163. if 'username' in request.query_params:
  164. return request.query_params['username']
  165. # 5. only domain associated with this user account
  166. if len(request.user.domains.all()) == 1:
  167. return request.user.domains.all()[0].name
  168. if len(request.user.domains.all()) > 1:
  169. ex = ValidationError(detail={"detail": "Request does not specify domain unambiguously.", "code": "domain-ambiguous"})
  170. ex.status_code = status.HTTP_409_CONFLICT
  171. raise ex
  172. return None
  173. domainname = findDomainname(request)
  174. domain = None
  175. # load and check permissions
  176. try:
  177. domain = Domain.objects.filter(owner=self.request.user.pk, name=domainname).all()[0]
  178. except:
  179. pass
  180. return domain
  181. def findIP(self, request, params, version=4):
  182. if version == 4:
  183. lookfor = '.'
  184. elif version == 6:
  185. lookfor = ':'
  186. else:
  187. raise Exception
  188. # Check URL parameters
  189. for p in params:
  190. if p in request.query_params and lookfor in request.query_params[p]:
  191. return request.query_params[p]
  192. # Check remote IP address
  193. client_ip = get_client_ip(request)
  194. if lookfor in client_ip:
  195. return client_ip
  196. # give up
  197. return ''
  198. def findIPv4(self, request):
  199. return self.findIP(request, ['myip', 'myipv4', 'ip'])
  200. def findIPv6(self, request):
  201. return self.findIP(request, ['myipv6', 'ipv6', 'myip', 'ip'], version=6)
  202. def get(self, request, format=None):
  203. domain = self.findDomain(request)
  204. if domain is None:
  205. raise Http404
  206. domain.arecord = self.findIPv4(request)
  207. domain.aaaarecord = self.findIPv6(request)
  208. domain.save()
  209. return Response('good')
  210. class DonationList(generics.CreateAPIView):
  211. serializer_class = DonationSerializer
  212. def perform_create(self, serializer):
  213. iban = serializer.validated_data['iban']
  214. obj = serializer.save()
  215. def sendDonationEmails(donation):
  216. context = Context({
  217. 'donation': donation,
  218. 'creditoridentifier': settings.SEPA['CREDITOR_ID'],
  219. 'complete_iban': iban
  220. })
  221. # internal desec notification
  222. content_tmpl = get_template('emails/donation/desec-content.txt')
  223. subject_tmpl = get_template('emails/donation/desec-subject.txt')
  224. attachment_tmpl = get_template('emails/donation/desec-attachment-jameica.txt')
  225. from_tmpl = get_template('emails/from.txt')
  226. email = EmailMessage(subject_tmpl.render(context),
  227. content_tmpl.render(context),
  228. from_tmpl.render(context),
  229. ['donation@desec.io'],
  230. attachments=[
  231. ('jameica-directdebit.xml',
  232. attachment_tmpl.render(context),
  233. 'text/xml')
  234. ])
  235. email.send()
  236. # donor notification
  237. if donation.email:
  238. content_tmpl = get_template('emails/donation/donor-content.txt')
  239. subject_tmpl = get_template('emails/donation/donor-subject.txt')
  240. test = content_tmpl.render(context)
  241. email = EmailMessage(subject_tmpl.render(context),
  242. content_tmpl.render(context),
  243. from_tmpl.render(context),
  244. [donation.email])
  245. email.send()
  246. # send emails
  247. sendDonationEmails(obj)
  248. class RegistrationView(views.RegistrationView):
  249. """
  250. Extends the djoser RegistrationView to record the remote IP address of any registration.
  251. """
  252. def create(self, request, *args, **kwargs):
  253. serializer = self.get_serializer(data=request.data)
  254. serializer.is_valid(raise_exception=True)
  255. self.perform_create(serializer, get_client_ip(request))
  256. headers = self.get_success_headers(serializer.data)
  257. return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  258. def perform_create(self, serializer, remote_ip):
  259. captcha = \
  260. (
  261. User.objects.filter(
  262. created__gte=timezone.now()-timedelta(hours=settings.ABUSE_BY_REMOTE_IP_PERIOD_HRS),
  263. registration_remote_ip=remote_ip
  264. ).count() >= settings.ABUSE_BY_REMOTE_IP_LIMIT
  265. or
  266. User.objects.filter(
  267. created__gte=timezone.now() - timedelta(hours=settings.ABUSE_BY_EMAIL_HOSTNAME_PERIOD_HRS),
  268. email__endswith=serializer.validated_data['email'].split('@')[-1]
  269. ).count() >= settings.ABUSE_BY_EMAIL_HOSTNAME_LIMIT
  270. )
  271. user = serializer.save(registration_remote_ip=remote_ip, captcha_required=captcha)
  272. if captcha:
  273. send_account_lock_email(self.request, user)
  274. signals.user_registered.send(sender=self.__class__, user=user, request=self.request)
  275. def unlock(request, email):
  276. # if this is a POST request we need to process the form data
  277. if request.method == 'POST':
  278. # create a form instance and populate it with data from the request:
  279. form = UnlockForm(request.POST)
  280. # check whether it's valid:
  281. if form.is_valid():
  282. try:
  283. User.objects.get(email=email).unlock()
  284. except User.DoesNotExist:
  285. pass # fail silently, otherwise people can find out if email addresses are registered with us
  286. return HttpResponseRedirect(reverse('unlock/done'))
  287. # if a GET (or any other method) we'll create a blank form
  288. else:
  289. form = UnlockForm()
  290. return render(request, 'unlock.html', {'form': form})
  291. def unlock_done(request):
  292. return render(request, 'unlock-done.html')