2013-09-28 18:46:31 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2013-12-15 17:09:22 +01:00
|
|
|
"""
|
|
|
|
views for the (usually non-interactive, automated) web api
|
|
|
|
"""
|
2013-09-28 21:41:47 +02:00
|
|
|
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2013-11-01 06:03:36 +01:00
|
|
|
import json
|
|
|
|
|
2013-09-28 18:46:31 +02:00
|
|
|
from django.http import HttpResponse
|
|
|
|
from django.conf import settings
|
2013-11-05 00:24:02 +01:00
|
|
|
from django.views.generic.base import View
|
2013-09-29 00:34:26 +02:00
|
|
|
from django.contrib.auth.hashers import check_password
|
2013-09-29 02:07:58 +02:00
|
|
|
from django.contrib.auth.decorators import login_required
|
2013-09-29 14:33:24 +02:00
|
|
|
from django.contrib.sessions.backends.db import SessionStore
|
2013-11-05 00:24:02 +01:00
|
|
|
from django.utils.decorators import method_decorator
|
2013-09-29 00:34:26 +02:00
|
|
|
|
2013-11-26 08:10:05 +01:00
|
|
|
from ..utils import log, ddns_client
|
2013-10-17 20:50:44 +00:00
|
|
|
from ..main.models import Host
|
2013-11-14 02:11:44 +01:00
|
|
|
from ..main.dnstools import update, SameIpError, DnsUpdateError, NameServerNotAvailable, check_ip, put_ip_into_session
|
2013-09-28 20:35:30 +02:00
|
|
|
|
|
|
|
|
2013-10-03 19:38:07 +02:00
|
|
|
def Response(content):
|
|
|
|
"""
|
|
|
|
shortcut for text/plain HttpResponse
|
|
|
|
|
|
|
|
:param content: plain text content for the response
|
|
|
|
:return: HttpResonse object
|
|
|
|
"""
|
|
|
|
return HttpResponse(content, content_type='text/plain')
|
|
|
|
|
|
|
|
|
2013-11-09 07:17:05 +01:00
|
|
|
@log.logger(__name__)
|
|
|
|
def myip_view(request, logger=None):
|
2013-09-29 15:59:16 +02:00
|
|
|
"""
|
|
|
|
return the IP address (can be v4 or v6) of the client requesting this view.
|
|
|
|
|
|
|
|
:param request: django request object
|
|
|
|
:return: HttpResponse object
|
|
|
|
"""
|
2013-11-05 00:24:02 +01:00
|
|
|
# Note: keeping this as a function-based view, as it is frequently used -
|
|
|
|
# maybe it is slightly more efficient than class-based.
|
2013-11-09 07:17:05 +01:00
|
|
|
ipaddr = request.META['REMOTE_ADDR']
|
|
|
|
logger.debug("detected remote ip address: %s" % ipaddr)
|
|
|
|
return Response(ipaddr)
|
2013-09-28 19:01:40 +02:00
|
|
|
|
2013-09-28 20:35:30 +02:00
|
|
|
|
2013-11-05 00:24:02 +01:00
|
|
|
class DetectIpView(View):
|
2013-11-09 07:17:05 +01:00
|
|
|
@log.logger(__name__)
|
|
|
|
def get(self, request, sessionid, logger=None):
|
2013-11-05 00:24:02 +01:00
|
|
|
"""
|
|
|
|
Put the IP address (can be v4 or v6) of the client requesting this view
|
|
|
|
into the client's session.
|
|
|
|
|
|
|
|
:param request: django request object
|
|
|
|
:param sessionid: sessionid from url used to find the correct session w/o session cookie
|
|
|
|
:return: HttpResponse object
|
|
|
|
"""
|
|
|
|
# we do not have the session as usual, as this is a different host,
|
|
|
|
# so the session cookie is not received here - thus we access it via
|
|
|
|
# the sessionid:
|
|
|
|
s = SessionStore(session_key=sessionid)
|
|
|
|
ipaddr = request.META['REMOTE_ADDR']
|
2013-11-14 02:11:44 +01:00
|
|
|
# as this is NOT the session automatically established and
|
|
|
|
# also saved by the framework, we need to use save=True here
|
|
|
|
put_ip_into_session(s, ipaddr, save=True)
|
|
|
|
logger.debug("detected remote address: %s for session %s" % (ipaddr, sessionid))
|
2013-11-05 00:24:02 +01:00
|
|
|
return HttpResponse(status=204)
|
|
|
|
|
|
|
|
|
|
|
|
class AjaxGetIps(View):
|
2013-11-09 07:17:05 +01:00
|
|
|
@log.logger(__name__)
|
|
|
|
def get(self, request, logger=None):
|
2013-11-05 00:24:02 +01:00
|
|
|
"""
|
|
|
|
Get the IP addresses of the client from the session via AJAX
|
|
|
|
(so we don't need to reload the view in case we just invalidated stale IPs
|
|
|
|
and triggered new detection).
|
|
|
|
|
|
|
|
:param request: django request object
|
|
|
|
:return: HttpResponse object
|
|
|
|
"""
|
|
|
|
response = dict(
|
|
|
|
ipv4=request.session.get('ipv4', ''),
|
2013-11-27 08:16:49 +01:00
|
|
|
ipv4_rdns=request.session.get('ipv4_rdns', ''),
|
2013-11-05 00:24:02 +01:00
|
|
|
ipv6=request.session.get('ipv6', ''),
|
2013-11-27 08:16:49 +01:00
|
|
|
ipv6_rdns=request.session.get('ipv6_rdns', ''),
|
2013-11-05 00:24:02 +01:00
|
|
|
)
|
|
|
|
logger.debug("ajax_get_ips response: %r" % (response, ))
|
|
|
|
return HttpResponse(json.dumps(response), content_type='application/json')
|
2013-11-01 06:03:36 +01:00
|
|
|
|
|
|
|
|
2013-09-28 23:32:15 +02:00
|
|
|
def basic_challenge(realm, content='Authorization Required'):
|
|
|
|
"""
|
|
|
|
Construct a 401 response requesting http basic auth.
|
|
|
|
|
|
|
|
:param realm: realm string (displayed by the browser)
|
|
|
|
:param content: request body content
|
|
|
|
:return: HttpResponse object
|
|
|
|
"""
|
2013-10-03 19:38:07 +02:00
|
|
|
response = Response(content)
|
2013-09-28 20:35:30 +02:00
|
|
|
response['WWW-Authenticate'] = 'Basic realm="%s"' % (realm, )
|
|
|
|
response.status_code = 401
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
def basic_authenticate(auth):
|
2013-09-28 23:32:15 +02:00
|
|
|
"""
|
|
|
|
Get username and password from http basic auth string.
|
|
|
|
|
|
|
|
:param auth: http basic auth string
|
|
|
|
:return: username, password
|
|
|
|
"""
|
2013-09-28 20:35:30 +02:00
|
|
|
authmeth, auth = auth.split(' ', 1)
|
|
|
|
if authmeth.lower() != 'basic':
|
|
|
|
return
|
|
|
|
auth = auth.strip().decode('base64')
|
|
|
|
username, password = auth.split(':', 1)
|
|
|
|
return username, password
|
|
|
|
|
|
|
|
|
2013-09-29 02:42:20 +02:00
|
|
|
def check_api_auth(username, password):
|
2013-09-28 23:32:15 +02:00
|
|
|
"""
|
|
|
|
Check username and password against our database.
|
|
|
|
|
|
|
|
:param username: http basic auth username (== fqdn)
|
|
|
|
:param password: update password
|
2013-11-24 11:09:38 +01:00
|
|
|
:return: host object if authenticated, None otherwise.
|
2013-09-28 23:32:15 +02:00
|
|
|
"""
|
2013-09-29 00:34:26 +02:00
|
|
|
fqdn = username
|
2013-11-06 13:17:23 +01:00
|
|
|
try:
|
2013-11-24 10:43:15 +01:00
|
|
|
host = Host.filter_by_fqdn(fqdn)
|
|
|
|
except ValueError:
|
2013-11-24 11:09:38 +01:00
|
|
|
return None
|
|
|
|
if host is None or not check_password(password, host.update_secret):
|
|
|
|
return None
|
|
|
|
return host
|
2013-09-28 20:35:30 +02:00
|
|
|
|
|
|
|
|
2013-09-29 02:42:20 +02:00
|
|
|
def check_session_auth(user, hostname):
|
|
|
|
"""
|
|
|
|
Check our database whether the hostname is owned by the user.
|
|
|
|
|
|
|
|
:param user: django user object
|
|
|
|
:param hostname: fqdn
|
2013-11-24 11:09:38 +01:00
|
|
|
:return: host object if hostname is owned by this user, None otherwise.
|
2013-09-29 02:42:20 +02:00
|
|
|
"""
|
2013-09-29 18:15:15 +02:00
|
|
|
fqdn = hostname
|
2013-11-24 10:43:15 +01:00
|
|
|
try:
|
|
|
|
host = Host.filter_by_fqdn(fqdn, created_by=user)
|
|
|
|
except ValueError:
|
2013-11-24 11:09:38 +01:00
|
|
|
return None
|
|
|
|
# we have specifically looked for a host of the logged in user,
|
|
|
|
# we either have one now and return it, or we have None and return that.
|
|
|
|
return host
|
2013-09-29 02:42:20 +02:00
|
|
|
|
|
|
|
|
2013-11-05 00:24:02 +01:00
|
|
|
class NicUpdateView(View):
|
2013-11-09 07:17:05 +01:00
|
|
|
@log.logger(__name__)
|
|
|
|
def get(self, request, logger=None):
|
2013-11-05 00:24:02 +01:00
|
|
|
"""
|
|
|
|
dyndns2 compatible /nic/update API.
|
|
|
|
|
|
|
|
Example URLs:
|
|
|
|
|
|
|
|
Will request username (fqdn) and password (secret) from user,
|
|
|
|
for interactive testing / updating:
|
|
|
|
https://nsupdate.info/nic/update
|
|
|
|
|
|
|
|
You can put it also into the url, so the browser will automatically
|
|
|
|
send the http basic auth with the request:
|
|
|
|
https://fqdn:secret@nsupdate.info/nic/update
|
|
|
|
|
|
|
|
If the request does not come from the correct IP, you can give it as
|
2013-11-24 06:20:23 +01:00
|
|
|
a query parameter.
|
|
|
|
You can also give the hostname/fqdn as a query parameter (this is
|
|
|
|
supported for api compatibility only), but then it MUST match the fqdn
|
|
|
|
used for http basic auth's username part, because the secret only
|
|
|
|
allows you to update this single fqdn).
|
2013-11-05 00:24:02 +01:00
|
|
|
https://fqdn:secret@nsupdate.info/nic/update?hostname=fqdn&myip=1.2.3.4
|
|
|
|
|
|
|
|
:param request: django request object
|
|
|
|
:return: HttpResponse object
|
|
|
|
"""
|
|
|
|
hostname = request.GET.get('hostname')
|
|
|
|
auth = request.META.get('HTTP_AUTHORIZATION')
|
|
|
|
if auth is None:
|
2013-11-09 07:17:05 +01:00
|
|
|
logger.warning('%s - received no auth' % (hostname, ))
|
2013-11-16 06:09:56 +01:00
|
|
|
return basic_challenge("authenticate to update DNS", 'badauth')
|
2013-11-05 00:24:02 +01:00
|
|
|
username, password = basic_authenticate(auth)
|
2013-11-24 06:42:55 +01:00
|
|
|
if '.' not in username: # username MUST be the fqdn
|
|
|
|
# specifically point to configuration errors on client side
|
|
|
|
return Response('notfqdn')
|
2013-11-24 11:09:38 +01:00
|
|
|
host = check_api_auth(username, password)
|
|
|
|
if host is None:
|
2013-11-10 13:59:40 +01:00
|
|
|
logger.warning('%s - received bad credentials, username: %s' % (hostname, username, ))
|
2013-11-05 00:24:02 +01:00
|
|
|
return basic_challenge("authenticate to update DNS", 'badauth')
|
2013-11-10 08:03:36 +01:00
|
|
|
logger.info("authenticated by update secret for host %s" % username)
|
2013-11-05 00:24:02 +01:00
|
|
|
if hostname is None:
|
|
|
|
# as we use update_username == hostname, we can fall back to that:
|
|
|
|
hostname = username
|
2013-11-10 07:57:40 +01:00
|
|
|
elif hostname != username:
|
2013-11-24 06:42:55 +01:00
|
|
|
if '.' not in hostname:
|
|
|
|
# specifically point to configuration errors on client side
|
|
|
|
result = 'notfqdn'
|
|
|
|
else:
|
|
|
|
# maybe this host is owned by same person, but we can't know.
|
|
|
|
result = 'nohost' # or 'badauth'?
|
2013-11-10 13:59:40 +01:00
|
|
|
logger.warning("rejecting to update wrong host %s (given in query string) "
|
|
|
|
"[instead of %s (given in basic auth)]" % (hostname, username))
|
2013-11-24 11:42:59 +01:00
|
|
|
host.register_client_fault()
|
2013-11-24 06:42:55 +01:00
|
|
|
return Response(result)
|
2013-11-15 13:22:06 +01:00
|
|
|
agent = request.META.get('HTTP_USER_AGENT', 'unknown')
|
2013-11-05 00:24:02 +01:00
|
|
|
if agent in settings.BAD_AGENTS:
|
2013-11-10 13:59:40 +01:00
|
|
|
logger.warning('%s - received update from bad user agent' % (hostname, ))
|
2013-11-24 11:42:59 +01:00
|
|
|
host.register_client_fault()
|
2013-11-05 00:24:02 +01:00
|
|
|
return Response('badagent')
|
2013-11-15 13:22:06 +01:00
|
|
|
ipaddr = request.GET.get('myip')
|
|
|
|
if ipaddr is None:
|
|
|
|
ipaddr = request.META.get('REMOTE_ADDR')
|
|
|
|
ssl = request.is_secure()
|
2013-12-15 18:12:10 +01:00
|
|
|
return _update(host, hostname, ipaddr, ssl, logger=logger)
|
2013-11-05 00:24:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
class AuthorizedNicUpdateView(View):
|
|
|
|
|
|
|
|
@method_decorator(login_required)
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(AuthorizedNicUpdateView, self).dispatch(*args, **kwargs)
|
|
|
|
|
2013-11-09 07:17:05 +01:00
|
|
|
@log.logger(__name__)
|
|
|
|
def get(self, request, logger=None):
|
2013-11-05 00:24:02 +01:00
|
|
|
"""
|
|
|
|
similar to NicUpdateView, but the client is not a router or other dyndns client,
|
|
|
|
but the admin browser who is currently logged into the nsupdate.info site.
|
|
|
|
|
|
|
|
Example URLs:
|
|
|
|
|
|
|
|
https://nsupdate.info/nic/update?hostname=fqdn&myip=1.2.3.4
|
|
|
|
|
|
|
|
:param request: django request object
|
|
|
|
:return: HttpResponse object
|
|
|
|
"""
|
|
|
|
hostname = request.GET.get('hostname')
|
|
|
|
if hostname is None:
|
|
|
|
return Response('nohost')
|
2013-11-24 11:09:38 +01:00
|
|
|
host = check_session_auth(request.user, hostname)
|
|
|
|
if host is None:
|
2013-11-10 13:59:40 +01:00
|
|
|
logger.warning('%s - is not owned by user: %s' % (hostname, request.user.username, ))
|
2013-11-05 00:24:02 +01:00
|
|
|
return Response('nohost')
|
2013-11-10 08:03:36 +01:00
|
|
|
logger.info("authenticated by session as user %s, creator of host %s" % (request.user.username, hostname))
|
2013-12-15 18:12:10 +01:00
|
|
|
# note: we do not check the user agent here as this is interactive
|
|
|
|
# and logged-in usage - thus misbehaved user agents are no problem.
|
2013-11-05 00:24:02 +01:00
|
|
|
ipaddr = request.GET.get('myip')
|
2013-11-21 04:10:04 +01:00
|
|
|
if not ipaddr: # None or empty string
|
2013-11-05 00:24:02 +01:00
|
|
|
ipaddr = request.META.get('REMOTE_ADDR')
|
2013-11-15 13:22:06 +01:00
|
|
|
ssl = request.is_secure()
|
2013-12-15 18:12:10 +01:00
|
|
|
return _update(host, hostname, ipaddr, ssl, logger=logger)
|
2013-09-29 02:42:20 +02:00
|
|
|
|
|
|
|
|
2013-12-15 18:12:10 +01:00
|
|
|
def _update(host, hostname, ipaddr, ssl=False, logger=None):
|
2013-12-15 17:29:55 +01:00
|
|
|
"""
|
|
|
|
common code shared by the 2 update views
|
|
|
|
|
|
|
|
:param host: host object
|
|
|
|
:param hostname: hostname (fqdn)
|
|
|
|
:param ipaddr: new ip addr (v4 or v6)
|
|
|
|
:param ssl: True if we use SSL/https
|
|
|
|
:param logger: a logger object
|
|
|
|
:return: Response object with dyndns2 response
|
|
|
|
"""
|
2013-11-30 12:32:03 +01:00
|
|
|
# we are doing abuse / available checks rather late, so the client might
|
|
|
|
# get more specific responses (like 'badagent' or 'notfqdn') by earlier
|
|
|
|
# checks. it also avoids some code duplication if done here:
|
|
|
|
if host.abuse or host.abuse_blocked:
|
|
|
|
return Response('abuse')
|
|
|
|
if not host.available:
|
|
|
|
# not available is like it doesn't exist
|
|
|
|
return Response('nohost')
|
2013-10-03 17:25:10 +02:00
|
|
|
ipaddr = str(ipaddr) # bug in dnspython: crashes if ipaddr is unicode, wants a str!
|
|
|
|
# https://github.com/rthalley/dnspython/issues/41
|
|
|
|
# TODO: reproduce and submit traceback to issue 41
|
2013-10-27 13:09:46 +01:00
|
|
|
kind = check_ip(ipaddr, ('ipv4', 'ipv6'))
|
2013-11-24 10:43:15 +01:00
|
|
|
host.poke(kind, ssl)
|
2013-09-28 20:35:30 +02:00
|
|
|
try:
|
|
|
|
update(hostname, ipaddr)
|
2013-11-09 07:17:05 +01:00
|
|
|
logger.info('%s - received good update -> ip: %s ssl: %r' % (hostname, ipaddr, ssl))
|
2013-11-26 08:10:05 +01:00
|
|
|
# now check if there are other services we shall relay updates to:
|
2013-11-29 02:11:55 +01:00
|
|
|
for hc in host.serviceupdaterhostconfigs.all():
|
|
|
|
if (kind == 'ipv4' and hc.give_ipv4 and hc.service.accept_ipv4
|
|
|
|
or
|
|
|
|
kind == 'ipv6' and hc.give_ipv6 and hc.service.accept_ipv6):
|
|
|
|
kwargs = dict(
|
|
|
|
name=hc.name, password=hc.password,
|
|
|
|
hostname=hc.hostname, myip=ipaddr,
|
|
|
|
server=hc.service.server, path=hc.service.path, secure=hc.service.secure,
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
ddns_client.dyndns2_update(**kwargs)
|
|
|
|
except Exception:
|
|
|
|
# we never want to crash here
|
|
|
|
kwargs.pop('password')
|
|
|
|
logger.exception("the dyndns2 updater raised an exception [%r]" % kwargs)
|
2013-09-28 20:35:30 +02:00
|
|
|
return Response('good %s' % ipaddr)
|
|
|
|
except SameIpError:
|
2013-11-09 07:17:05 +01:00
|
|
|
logger.warning('%s - received no-change update, ip: %s ssl: %r' % (hostname, ipaddr, ssl))
|
2013-11-24 11:42:59 +01:00
|
|
|
host.register_client_fault()
|
2013-09-28 20:35:30 +02:00
|
|
|
return Response('nochg %s' % ipaddr)
|
2013-11-10 07:04:46 +01:00
|
|
|
except (DnsUpdateError, NameServerNotAvailable) as e:
|
2013-11-10 06:29:33 +01:00
|
|
|
msg = str(e)
|
2013-11-10 13:59:40 +01:00
|
|
|
logger.error('%s - received update that resulted in a dns error [%s], ip: %s ssl: %r' % (
|
|
|
|
hostname, msg, ipaddr, ssl))
|
2013-11-24 11:42:59 +01:00
|
|
|
host.register_server_fault()
|
2013-11-10 06:29:33 +01:00
|
|
|
return Response('dnserr %s' % msg)
|