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 @@