views.py 14 KB

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