diff --git a/bifrost/digital_ocean.py b/bifrost/digital_ocean.py new file mode 100644 index 0000000..195beee --- /dev/null +++ b/bifrost/digital_ocean.py @@ -0,0 +1,98 @@ +import json +import requests + + +def create_domain_a_record(domain, name, ip_address, api_url_base, api_token, logger): + headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer {0}'.format(api_token)} + api_url = f"{api_url_base}/domains/{domain}/records" + + data = {"type": "A", "name": name, "data": ip_address, "priority": None, "port": None, "ttl": 1800, "weight": None, + "flags": None, "tag": None} + response = requests.post(api_url, headers=headers, json=data) + + if response.status_code > 499: + logger.error(f"[!] [{response.status_code}] Server Error") + return None + elif response.status_code == 404: + logger.error(f"[!] [{response.status_code}] URL not found: [{api_url}]") + return None + elif response.status_code == 401: + logger.error(f"[!] [{response.status_code}] Authentication Failed") + return None + elif response.status_code > 399: + logger.error(f"[!] [{response.status_code}] Bad Request") + logger.error(f"Domain: {domain}, Name: {name}, New IP Address: {ip_address}") + logger.error(response.content) + return None + elif response.status_code > 299: + logger.error(f"[!] [{response.status_code}] Unexpected redirect") + return None + elif response.status_code == 201: + logger.info(f'{domain} updated to {ip_address}') + return json.loads(response.content) + else: + logger.error(f"[?] Unexpected Error: [HTTP {response.status_code}]: Content: {response.content}") + return None + + +def update_domain_a_record(domain, record_id, new_ip_address, api_url_base, api_token, logger): + headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer {0}'.format(api_token)} + api_url = f"{api_url_base}/domains/{domain}/records/{record_id}" + + data = {"data": new_ip_address} + response = requests.put(api_url, headers=headers, json=data) + + if response.status_code > 499: + logger.error(f"[!] [{response.status_code}] Server Error") + return None + elif response.status_code == 404: + logger.error(f"[!] [{response.status_code}] URL not found: [{api_url}]") + return None + elif response.status_code == 401: + logger.error(f"[!] [{response.status_code}] Authentication Failed") + return None + elif response.status_code > 399: + logger.error(f"[!] [{response.status_code}] Bad Request") + logger.error(f"Domain: {domain}, Record ID: {record_id}, New IP Address: {new_ip_address}") + logger.error(response.content) + return None + elif response.status_code > 299: + logger.error(f"[!] [{response.status_code}] Unexpected redirect") + return None + elif response.status_code == 201: + logger.info(f'{domain} updated to {new_ip_address}') + return json.loads(response.content) + else: + logger.error(f"[?] Unexpected Error: [HTTP {response.status_code}]: Content: {response.content}") + return None + + +def list_all_domain_records(domain, api_url_base, api_token, logger): + headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer {0}'.format(api_token)} + api_url = f"{api_url_base}/domains/{domain}/records" + response = requests.get(api_url, headers=headers) + if response.status_code == 200: + return json.loads(response.content.decode('utf-8'))["domain_records"] + else: + logger.error(f"[?] Unexpected Error: [HTTP {response.status_code}]: Content: {response.content}") + return None + + +def get_domain_a_record(domain, name, api_url_base, api_token, logger): + for item in list_all_domain_records(domain, api_url_base, api_token, logger): + if item["name"] == name and item["type"] == "A": + return item + else: + return None + + +def update_domain(domain, name, new_ip_address, api_url_base, api_token, logger): + record = get_domain_a_record(domain, name, api_url_base, api_token, logger) + if record is None: + logger.info(f'Creating domain a record for {domain} with ip address {new_ip_address}') + create_domain_a_record(domain, name, new_ip_address, api_url_base, api_token, logger) + elif record["data"] != new_ip_address: + logger.info(f'Updating domain a record for {domain}. Old ip address {record["data"]}, new ip address {new_ip_address}') + update_domain_a_record(domain, record["id"], new_ip_address, api_url_base, api_token, logger) + else: + logger.info(f'Not updating domain a record for {domain}. IP address {record["data"]} is current') diff --git a/bifrost/views.py b/bifrost/views.py index 0b5ad9c..4a5cb50 100644 --- a/bifrost/views.py +++ b/bifrost/views.py @@ -1,20 +1,17 @@ import logging -import os -import re -import xmlrpc.client from pyramid.httpexceptions import HTTPUnauthorized from pyramid.security import forget, Authenticated from pyramid.view import view_config, forbidden_view_config +from bifrost.digital_ocean import update_domain log = logging.getLogger('bifrost.updater') +api_url_base = 'https://api.digitalocean.com/v2' +api_token = None @view_config(route_name='update', request_param='domain', renderer='json', permission=Authenticated) def update_view(request): - file = request.registry.settings['biforst.file'] - username = request.registry.settings['webfaction.username'] - password = request.registry.settings['webfaction.password'] current_ip = request.GET.get('ip', None) if current_ip is None: if 'X-Forwarded-For' in request.headers: @@ -22,43 +19,11 @@ def update_view(request): else: current_ip = request.remote_addr - domain = request.GET['domain'] - db = load(file) - - if domain in db and db[domain] != current_ip: - server = xmlrpc.client.ServerProxy('https://api.webfaction.com/') - session_id, account = server.login(username, password) - server.delete_dns_override(session_id, domain) - server.create_dns_override(session_id, domain, current_ip, '', '', '', '') - db[domain] = current_ip - update(file, db) - - log.info('{0} updated to {1}'.format(domain, current_ip)) - else: - log.info('{0} not updated'.format(domain)) + name, domain = request.GET['domain'].split('.', maxsplit=1) + update_domain(domain, name, current_ip, api_url_base, api_token, log) return {} -def load(file): - exp = re.compile(r"^([a-z1-9.]+):([0-9]{1,3}(?:\.[0-9]{1,3}){3})$", re.I) - if not os.path.isfile(file): - return {} - - current = {} - with open(file) as f: - for line in f: - match = exp.match(line) - if match: - domain, ip = match.groups() - current[domain] = ip - return current - - -def update(file, db): - with open(file, 'w') as f: - f.writelines(['{0}:{1}\n'.format(key, value) for key, value in db.items()]) - - @forbidden_view_config() def basic_challenge(request): response = HTTPUnauthorized() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ff4f41f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyramid +waitress +requests diff --git a/setup.py b/setup.py index 19792de..23967fd 100644 --- a/setup.py +++ b/setup.py @@ -9,10 +9,11 @@ CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() requires = [ 'pyramid', 'waitress', + 'requests' ] setup(name='bifrost', - version='0.0', + version='1.0', description='bifrost', long_description=README + '\n\n' + CHANGES, classifiers=[ @@ -21,9 +22,9 @@ setup(name='bifrost', "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], - author='', - author_email='', - url='', + author='tanshu', + author_email='programming@tanshu.com', + url='https://git.tanshu.com/tanshu', keywords='web pyramid pylons', packages=find_packages(), include_package_data=True,