From 49693121ea2e5702a0060574aaf976a2440ec6c5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 18 Oct 2013 15:30:17 -0700 Subject: [PATCH] use zones/nameserver IPs/update keys from DB, logging (thanks to asmaps) remove unneeded stuff from settings (we still need some in conftest.py for the tests, though) init DB for tests via conftest.py more update algorithm choices give origin zone (if we already know it) to dnstools functions new views: DomainOverview, DeleteDomain unify deletion templates using delete_object.html add django-extensions --- conftest.py | 22 +++++++ nsupdate/main/_tests/test_dnstools.py | 1 + nsupdate/main/dnstools.py | 14 +++-- nsupdate/main/forms.py | 8 ++- nsupdate/main/models.py | 25 ++++++-- .../{delete_host.html => delete_object.html} | 7 +-- .../main/templates/main/domain_overview.html | 61 ++++++++++++++++++ nsupdate/main/urls.py | 6 +- nsupdate/main/views.py | 62 +++++++++++++++++-- nsupdate/settings.py | 10 +-- nsupdate/templates/base.html | 1 + setup.py | 1 + 12 files changed, 191 insertions(+), 27 deletions(-) create mode 100644 conftest.py rename nsupdate/main/templates/main/{delete_host.html => delete_object.html} (71%) create mode 100644 nsupdate/main/templates/main/domain_overview.html diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..7deb88a --- /dev/null +++ b/conftest.py @@ -0,0 +1,22 @@ +import pytest + +from django.utils.translation import activate + + +# Note: fixture must be "function" scope (default), see https://github.com/pelme/pytest_django/issues/33 +@pytest.fixture(autouse=True) +def db_init(db): + """ + Init the database contents for testing, so we have a service domain, ... + """ + from nsupdate.main.models import Domain + from django.db import IntegrityError + Domain.objects.create(domain='nsupdate.info', + nameserver_ip='85.10.192.104', + nameserver_update_algorithm='HMAC_SHA512', + nameserver_update_key='YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==', + available_for_everyone=True) + + +def pytest_runtest_setup(item): + activate('en') diff --git a/nsupdate/main/_tests/test_dnstools.py b/nsupdate/main/_tests/test_dnstools.py index 186d6a5..df004f7 100644 --- a/nsupdate/main/_tests/test_dnstools.py +++ b/nsupdate/main/_tests/test_dnstools.py @@ -3,6 +3,7 @@ Tests for dnstools module. """ import pytest +pytestmark = pytest.mark.django_db from dns.resolver import NXDOMAIN diff --git a/nsupdate/main/dnstools.py b/nsupdate/main/dnstools.py index d393fd3..4654532 100644 --- a/nsupdate/main/dnstools.py +++ b/nsupdate/main/dnstools.py @@ -4,6 +4,9 @@ Misc. DNS related code: query, dynamic update, etc. Usually, higher level code wants to call the add/update/delete functions. """ +import logging +logger = logging.getLogger(__name__) + import dns.inet import dns.name import dns.resolver @@ -167,11 +170,10 @@ def get_ns_info(origin): :param origin: zone we are dealing with, must be with trailing dot :return: master nameserver, update key, update algo """ - # later look this up from Domain model: domain+'.': nameserver_ip, nameserver_update_key - ns_info = { - settings.BASEDOMAIN + '.': (settings.SERVER, settings.UPDATE_KEY, settings.UPDATE_ALGO), - } - return ns_info[origin] + from .models import Domain + d = Domain.objects.get(domain=origin.rstrip('.')) + algorithm = getattr(dns.tsig, d.nameserver_update_algorithm) + return d.nameserver_ip, d.nameserver_update_key, algorithm def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60): @@ -201,5 +203,7 @@ def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60): elif action == 'upd': assert ipaddr is not None upd.replace(name, ttl, rdtype, ipaddr) + logger.debug("performing %s for name %s and origin %s with rdtype %s and ipaddr %s" % ( + action, name, origin, rdtype, ipaddr)) response = dns.query.tcp(upd, nameserver) return response diff --git a/nsupdate/main/forms.py b/nsupdate/main/forms.py index e2ae1b9..0a916d4 100644 --- a/nsupdate/main/forms.py +++ b/nsupdate/main/forms.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from django import forms -from .models import Host +from .models import Host, Domain class CreateHostForm(forms.ModelForm): @@ -14,3 +14,9 @@ class EditHostForm(forms.ModelForm): class Meta(object): model = Host fields = ['comment'] + + +class CreateDomainForm(forms.ModelForm): + class Meta(object): + model = Domain + exclude = ['created_by'] diff --git a/nsupdate/main/models.py b/nsupdate/main/models.py index c9a29fa..59100d3 100644 --- a/nsupdate/main/models.py +++ b/nsupdate/main/models.py @@ -34,12 +34,25 @@ def domain_blacklist_validator(value): raise ValidationError(u'This domain is not allowed') +UPDATE_ALGORITHMS = ( + ('HMAC_SHA512', 'HMAC_SHA512'), + ('HMAC_SHA384', 'HMAC_SHA384'), + ('HMAC_SHA256', 'HMAC_SHA256'), + ('HMAC_SHA224', 'HMAC_SHA224'), + ('HMAC_SHA1', 'HMAC_SHA1'), + ('HMAC_MD5', 'HMAC_MD5'), +) + + class Domain(models.Model): domain = models.CharField(max_length=256, unique=True) nameserver_ip = models.GenericIPAddressField( max_length=256, - help_text="An IP where the nsupdates for this domain will be sent to") + help_text="IP where the dynamic updates for this domain will be sent to") nameserver_update_key = models.CharField(max_length=256) + nameserver_update_algorithm = models.CharField( + max_length=256, default='HMAC_SHA512', choices=UPDATE_ALGORITHMS) + available_for_everyone = models.BooleanField(default=False) last_update = models.DateTimeField(auto_now=True) created = models.DateTimeField(auto_now_add=True) @@ -88,14 +101,14 @@ class Host(models.Model): def getIPv4(self): try: - return dnstools.query_ns(self.get_fqdn(), 'A') - except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): + return dnstools.query_ns(self.get_fqdn(), 'A', origin=self.domain.domain) + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.resolver.Timeout): return '' def getIPv6(self): try: - return dnstools.query_ns(self.get_fqdn(), 'AAAA') - except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): + return dnstools.query_ns(self.get_fqdn(), 'AAAA', origin=self.domain.domain) + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.resolver.Timeout): return '' def poke(self): @@ -118,6 +131,6 @@ class Host(models.Model): def post_delete_host(sender, **kwargs): obj = kwargs['instance'] - dnstools.delete(obj.get_fqdn()) + dnstools.delete(obj.get_fqdn(), origin=obj.domain.domain) post_delete.connect(post_delete_host, sender=Host) diff --git a/nsupdate/main/templates/main/delete_host.html b/nsupdate/main/templates/main/delete_object.html similarity index 71% rename from nsupdate/main/templates/main/delete_host.html rename to nsupdate/main/templates/main/delete_object.html index 2d8c01e..b8808ce 100644 --- a/nsupdate/main/templates/main/delete_host.html +++ b/nsupdate/main/templates/main/delete_object.html @@ -2,10 +2,9 @@ {% load bootstrap %} {% block content %} -
-

Delete Host back to overview

+

Delete {{ object }}

{% csrf_token %}

Are you sure you want to delete "{{ object }}"?

@@ -13,6 +12,4 @@
- - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/nsupdate/main/templates/main/domain_overview.html b/nsupdate/main/templates/main/domain_overview.html new file mode 100644 index 0000000..061fc66 --- /dev/null +++ b/nsupdate/main/templates/main/domain_overview.html @@ -0,0 +1,61 @@ +{% extends "base.html" %} +{% load bootstrap %} + +{% block content %} +
+
+

Your Domains

+ + + + + + + + + + + + {% for domain in domains %} + + + + + + + + + {% empty %} + + {% endfor %} +
DomainNameserver IPUpdate keyAlgorithmPublic?Action
{{ domain.domain }}{{ domain.nameserver_ip }}{{ domain.nameserver_update_key }}{{ domain.get_nameserver_update_algorithm_display }}{{ domain.available_for_everyone|yesno }} + delete +
No domains yet.
+
+
+
+ +
+
+
+

Add a new Domain

+
+ {% csrf_token %} + {{ form|bootstrap }} + +
+
+
+
+
+

Help

+

Here you can add new domains (zones) which you control (and this is only useful if you + have some own zone which you can update automatically).

+

You need to configure the primary master nameserver of the zone so it accepts dynamic updates + if the correct update key is presented (which is just a shared secret between the nameserver + and this service). +

+
+
+
+{% endblock %} diff --git a/nsupdate/main/urls.py b/nsupdate/main/urls.py index 70e547e..f34d2e4 100644 --- a/nsupdate/main/urls.py +++ b/nsupdate/main/urls.py @@ -3,7 +3,7 @@ from django.views.generic import TemplateView from .views import ( HomeView, OverviewView, HostView, DeleteHostView, AboutView, HelpView, GenerateSecretView, - RobotsTxtView, ) + RobotsTxtView, DomainOverwievView, DeleteDomainView) from ..api.views import ( MyIpView, DetectIpView, NicUpdateView, AuthorizedNicUpdateView) @@ -20,6 +20,10 @@ urlpatterns = patterns( url(r'^generate_secret/(?P\d+)/$', GenerateSecretView.as_view(), name='generate_secret_view'), url(r'^host/(?P\d+)/delete/$', DeleteHostView.as_view(), name='delete_host'), + url(r'^domain_overview/$', + DomainOverwievView.as_view(), name='domain_overview'), + url(r'^domain/(?P\d+)/delete/$', + DeleteDomainView.as_view(), name='delete_domain'), url(r'^myip$', MyIpView), url(r'^detectip/$', DetectIpView), url(r'^detectip/(?P\w+)/$', DetectIpView), diff --git a/nsupdate/main/views.py b/nsupdate/main/views.py index 66657e7..6b19185 100644 --- a/nsupdate/main/views.py +++ b/nsupdate/main/views.py @@ -2,6 +2,7 @@ import dns.inet +from django.db.models import Q from django.views.generic import TemplateView, CreateView from django.views.generic.edit import UpdateView, DeleteView from django.http import HttpResponse, HttpResponseRedirect @@ -13,8 +14,8 @@ from django.core.exceptions import PermissionDenied import dnstools -from .forms import CreateHostForm, EditHostForm -from .models import Host +from .forms import CreateHostForm, EditHostForm, CreateDomainForm +from .models import Host, Domain class GenerateSecretView(UpdateView): @@ -87,11 +88,17 @@ class OverviewView(CreateView): def get_success_url(self): return reverse('generate_secret_view', args=(self.object.pk,)) + def get_form(self, form_class): + form = super(OverviewView, self).get_form(form_class) + form.fields['domain'].queryset = Domain.objects.filter( + Q(created_by=self.request.user) | Q(available_for_everyone=True)) + return form + def form_valid(self, form): self.object = form.save(commit=False) self.object.created_by = self.request.user self.object.save() - dnstools.add(self.object.get_fqdn(), self.request.META['REMOTE_ADDR']) + dnstools.add(self.object.get_fqdn(), self.request.META['REMOTE_ADDR'], origin=self.object.domain.domain) messages.add_message(self.request, messages.SUCCESS, 'Host added.') return HttpResponseRedirect(self.get_success_url()) @@ -136,8 +143,7 @@ class HostView(UpdateView): class DeleteHostView(DeleteView): model = Host - template_name = "main/delete_host.html" - form_class = EditHostForm + template_name = "main/delete_object.html" @method_decorator(login_required) def dispatch(self, *args, **kwargs): @@ -159,6 +165,52 @@ class DeleteHostView(DeleteView): return context +class DomainOverwievView(CreateView): + model = Domain + template_name = "main/domain_overview.html" + form_class = CreateDomainForm + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(DomainOverwievView, self).dispatch(*args, **kwargs) + + def get_success_url(self): + return reverse('domain_overview') + + def form_valid(self, form): + self.object = form.save(commit=False) + self.object.created_by = self.request.user + self.object.save() + messages.add_message(self.request, messages.SUCCESS, 'Domain added.') + return HttpResponseRedirect(self.get_success_url()) + + def get_context_data(self, *args, **kwargs): + context = super( + DomainOverwievView, self).get_context_data(*args, **kwargs) + context['nav_domains'] = True + context['domains'] = Domain.objects.filter( + created_by=self.request.user) + return context + + +class DeleteDomainView(DeleteView): + model = Domain + template_name = "main/delete_object.html" + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super(DeleteDomainView, self).dispatch(*args, **kwargs) + + def get_object(self, *args, **kwargs): + obj = super(DeleteDomainView, self).get_object(*args, **kwargs) + if obj.created_by != self.request.user: + raise PermissionDenied() # or Http404 + return obj + + def get_success_url(self): + return reverse('domain_overview') + + def RobotsTxtView(request): """ Dynamically serve robots.txt content. diff --git a/nsupdate/settings.py b/nsupdate/settings.py index 8c8a4c5..8bc6a6f 100644 --- a/nsupdate/settings.py +++ b/nsupdate/settings.py @@ -27,7 +27,6 @@ DATABASES = { } } -SERVER = '85.10.192.104' # ns1.thinkmo.de (master / dynamic upd server for nsupdate.info) BASEDOMAIN = 'nsupdate.info' NONEXISTING_HOST = 'nonexisting.' + BASEDOMAIN @@ -39,9 +38,6 @@ WWW_IPV6_IP = '2001:41d0:8:e00e::1' BAD_AGENTS = set() # useragent blacklist for /nic/update service -UPDATE_ALGO = dns.tsig.HMAC_SHA512 -UPDATE_KEY = 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==' - # Hosts/domain names that are valid for this site; required if DEBUG is False # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts ALLOWED_HOSTS = [WWW_HOST, WWW_IPV4_HOST, WWW_IPV6_HOST] @@ -156,6 +152,7 @@ INSTALLED_APPS = ( 'nsupdate.main', 'bootstrapform', 'registration', + 'django_extensions', ) # A sample logging configuration. The only tangible logging @@ -189,6 +186,11 @@ LOGGING = { 'level': 'DEBUG', 'propagate': True, }, + 'nsupdate.main.dnstools': { + 'handlers': ['stderr', ], + 'level': 'DEBUG', + 'propagate': True, + }, 'django.request': { 'handlers': ['mail_admins', ], 'level': 'ERROR', diff --git a/nsupdate/templates/base.html b/nsupdate/templates/base.html index 3d59ee8..97a6080 100644 --- a/nsupdate/templates/base.html +++ b/nsupdate/templates/base.html @@ -52,6 +52,7 @@