views.py 17 KB

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