From ef1d2ca6c9d81254892f6841236650b140157cbd Mon Sep 17 00:00:00 2001 From: Aaron Guise Date: Mon, 14 Oct 2019 17:00:55 +1300 Subject: [PATCH] Initial Import --- .gitignore | 133 +++++++++++++++++++++++++++++++++++++++++ ddns_updater.py | 62 +++++++++++++++++++ lib/__init__.py | 0 lib/directadmin.py | 146 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 7 +++ 5 files changed, 348 insertions(+) create mode 100644 .gitignore create mode 100644 ddns_updater.py create mode 100644 lib/__init__.py create mode 100644 lib/directadmin.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54e0fc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Created by .ignore support plugin (hsz.mobi) +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + diff --git a/ddns_updater.py b/ddns_updater.py new file mode 100644 index 0000000..9240419 --- /dev/null +++ b/ddns_updater.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +import socket + +import click +import requests +import tldextract + +from lib.directadmin import DirectAdminClient, DirectAdminClientException + + +def get_zone_and_name(record_domain): + """Find a suitable zone for a domain + :param str record_name: the domain name + :returns: (the zone, the name in the zone) + :rtype: tuple + """ + (subdomain, domain, suffix) = tldextract.extract(record_domain) + + directadmin_zone = ".".join((domain, suffix)) + directadmin_name = subdomain + + return directadmin_zone, directadmin_name + + +def get_directadmin_client(url, username, password): + return DirectAdminClient( + url, + username, + password) + + +@click.command() +@click.option('--hostname', required=False, help='The FQDN you wish to update for DynamicDNS on DirectAdmin server') +@click.option('--url', required=False, help='The FQDN to access DirectAdmin server e.g https://my.directadmin.com:2222') +@click.option('--username', required=False, help='The username for your account on DirectAdmin server') +@click.option('--password', required=False, help='The password for your account on DirectAdmin server') +def main(hostname, url, username, password): + click.echo('Updating DNS for ' + hostname) + (directadmin_zone, directadmin_name) = get_zone_and_name(hostname) + + da_client = get_directadmin_client(url, username, password) + current_ip = requests.get('https://icanhazip.com').text.strip() + hostname_resolves_to = socket.gethostbyname(hostname).strip() + + click.echo(current_ip) + click.echo(hostname_resolves_to) + + if current_ip != hostname_resolves_to: + try: + response = da_client.update_dns_record(directadmin_zone, + "A", + directadmin_name, + hostname_resolves_to, + current_ip, 30) + if int(response['error']) == 0: + click.echo("Successfully updated A record for " + hostname) + except DirectAdminClientException as e: + click.echo("Error updating A record: %s" % e) + + +if __name__ == '__main__': + main() diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/directadmin.py b/lib/directadmin.py new file mode 100644 index 0000000..176c2e3 --- /dev/null +++ b/lib/directadmin.py @@ -0,0 +1,146 @@ +import base64 +from collections import OrderedDict +import requests + +# noinspection PyUnresolvedReferences +try: + # python 3 + from urllib.request import urlopen, Request + from urllib.parse import urlencode, parse_qs +except ImportError: + # python 2 + from urllib import urlencode + from urllib2 import urlopen, Request + from cgi import parse_qs + + +class DirectAdminClient: + + def __init__(self, url, username, password): + + self.version = "0.0.5" + self.client = requests.session() + self.headers = {'user-agent': 'pyDirectAdmin/' + str(self.version), + 'Authorization': 'Basic %s' % base64.b64encode(("%s:%s" % + (username, password)) + .encode()).decode('utf8'), + } + self.url = url + + def make_request(self, endpoint, data=None): + response = None # Empty response variable + + if data is not None: + # Data is present add the fields to call + response = urlopen( + Request( + "%s?%s" % (self.url + '/{}'.format(endpoint), urlencode(data)), + headers=self.headers, + ) + ) + elif data is None: + # Data is not present so we don't need addition field. + response = urlopen( + Request( + "%s?" % (self.url + '/{}'.format(endpoint)), + headers=self.headers, + ) + ) + + return response + + @staticmethod + def __process_response__(response): + if int(response['error'][0]) > 0: + # There was an error + raise DirectAdminClientException(response['text'][0]) + elif int(response['error'][0]) == 0: + # Everything succeeded + return {'error': response['error'][0], + 'message': response['text'][0] + } + + def get_domain_list(self): + r = self.make_request('CMD_API_SHOW_DOMAINS') + + domains = parse_qs(r.read().decode('utf8'), + keep_blank_values=0, + strict_parsing=1) + response = list() + for domain in domains.values(): + response.append(domain[0]) + return response + + def get_zone_list(self, domain): + params = OrderedDict([('domain', domain)]) + r = self.make_request('CMD_API_DNS_CONTROL', params) + + return r.read() + + def add_dns_record(self, domain, record_type, record_name, record_value, record_ttl=None): + + params = OrderedDict([('domain', domain), + ('action', 'add'), + ('type', record_type.upper()), + ('name', record_name), + ('value', record_value)]) + + if record_ttl is not None: + params.update({'ttl': record_ttl}) + + response = self.make_request('CMD_API_DNS_CONTROL', data=params) + response = parse_qs(response.read().decode('utf8'), + keep_blank_values=0, + strict_parsing=1) + + return self.__process_response__(response) + + def update_dns_record(self, domain, record_type, record_name, record_value_old, record_value_new, record_ttl=None): + + params = OrderedDict([('domain', domain), + ('action', 'edit'), + ('type', record_type.upper()), + (record_type.lower() + "recs0", 'name={}&value={}'.format(record_name, record_value_old)), + ('name', record_name), + ('value', record_value_new)]) + + if record_ttl is not None: + params.update({'ttl': record_ttl}) + + response = self.make_request('CMD_API_DNS_CONTROL', data=params) + response = parse_qs(response.read().decode('utf8'), + keep_blank_values=0, + strict_parsing=1) + + return self.__process_response__(response) + + def delete_dns_record(self, domain, record_type, record_name, record_value): + params = OrderedDict([('domain', domain), + ('action', 'select'), + (record_type.lower() + "recs0", 'name={}&value={}'.format(record_name, record_value)) + ]) + + response = self.make_request('CMD_API_DNS_CONTROL', data=params) + response = parse_qs(response.read().decode('utf8'), + keep_blank_values=0, + strict_parsing=1) + + return self.__process_response__(response) + + def override_domain_ttl(self, domain, record_name, ttl): + params = OrderedDict([('domain', domain), + ('action', 'ttl'), + ('ttl_select', 'custom'), + ('name', record_name), + ('ttl', ttl)]) + + response = self.make_request('CMD_API_DNS_CONTROL', data=params) + response = parse_qs(response.read().decode('utf8'), + keep_blank_values=0, + strict_parsing=1) + + return self.__process_response__(response) + + +class DirectAdminClientException(Exception): + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..057ade4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +certifi==2019.9.11 +chardet==3.0.4 +Click==7.0 +idna==2.8 +requests==2.22.0 +urllib3==1.25.6 +tldextract==2.2.1 \ No newline at end of file