From 1cf21ea086877c5e1d6a08feb741a33482bfe9a3 Mon Sep 17 00:00:00 2001 From: rune Date: Tue, 7 Mar 2023 17:49:38 +0100 Subject: [PATCH] Inital 0.1 release --- ddns.py | 303 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 245 insertions(+), 58 deletions(-) diff --git a/ddns.py b/ddns.py index 75224da..c802ac3 100755 --- a/ddns.py +++ b/ddns.py @@ -9,6 +9,9 @@ import argparse import requests import os import time +from string import ascii_letters, digits +from rich import print +from argparse import RawTextHelpFormatter homefilepath = Path.home() filepath = homefilepath.joinpath('.config/ddns') @@ -58,7 +61,9 @@ def connect_database(): c.execute('''CREATE TABLE IF NOT EXISTS subdomains (id integer PRIMARY KEY, main_id integer NOT NULL, - name text)''') + name text, + current_ip4 text NOT NULL, + current_ip6 text NULL)''') return conn @@ -89,54 +94,103 @@ def api(api_value): def add_domian(domain): + apikey = get_api() cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM domains WHERE name like ?',(domain,)) count = cursor.fetchone()[0] - if count == 0: - cursor.execute('INSERT INTO domains values(?,?)', (None, domain,)) - print('The domain %s has been added to the DB' % (domain)) - conn.commit() + if count != 0: + print('[red]Error:[/red] Domain name (%s) already in database!' % (domain)) else: - print('Error: Domain name (%s) already in database!' % (domain)) + if apikey != None: + headers = {'Authorization': 'Bearer ' + apikey, "Content-Type": "application/json"} + response = requests.get('https://api.digitalocean.com/v2/domains/' + domain, headers=headers) + response_data = response.json() + + if 'id' in response_data: + print('[red]Error: [/red]The domain does not exist in your DigitalOcean account.\nPlease add the domain from your control panel [b]https://cloud.digitalocean.com/networking/domains/[/b]') + else: + cursor.execute('INSERT INTO domains values(?,?)', (None, domain,)) + print('The domain [b]%s[/b] has been added to the DB' % (domain)) + conn.commit() -def add_subdomain(sub,top): - apikey = get_api() - if apikey == None: - print("Missing APIkey. Please add one!") +def add_subdomain(domain): + if set(domain).difference(ascii_letters + '.' + digits): + print('[red]Error:[/red] Give the domain name in simple form e.g. [b]test.domain.com[/b]') else: - ip = get_ip() - if ip == None or 'urlopen error' in ip: - print('Failed to get public IP. Do you have a typo in your URI? Error %s' % (ip)) + parts = domain.split('.') + sub = parts[0] + top = parts[1] + '.' + parts[2] + apikey = get_api() + if apikey == None: + print("[red]Error:[/red] Missing APIkey. Please add one!") else: - cursor = conn.cursor() - cursor.execute('SELECT COUNT(*) FROM domains WHERE name like ?',(top,)) + ip = get_ip() + if ip == None or 'urlopen error' in ip: + print('[red]Error:[/red] Failed to get public IP. Do you have a typo in your URI? [red]Error %s.[/red]' % (ip)) + else: + cursor = conn.cursor() + cursor.execute('SELECT COUNT(*) FROM domains WHERE name like ?',(top,)) + count = cursor.fetchone()[0] + if count == 0: + print('[red]Error:[/red] Top domain [bold]%s[/bold] does not exist in the DB. Please add it with [i]ddns -t %s[/i].' % (top,top)) + else: + cursor.execute('SELECT id FROM domains WHERE name LIKE ?',(top,)) + topdomain_id = cursor.fetchone() + topdomain_id = topdomain_id[0] + cursor.execute('SELECT count(*) FROM subdomains WHERE main_id LIKE ? AND name like ?',(topdomain_id,sub,)) + count = cursor.fetchone()[0] + if count != 0: + print('[red]Error:[/red] [bold]%s[/bold] already exists.' % (domain)) + else: + data = {'name': sub,'data': ip,'type': "A",'ttl': 3600} + headers = {'Authorization': 'Bearer ' + apikey, "Content-Type": "application/json"} + response = requests.post('https://api.digitalocean.com/v2/domains/' + top + '/records', + data=json.dumps(data), headers=headers) + if str(response) == '': + if response != 'Fail': + response_data = response.json() + domainid = str(response_data['domain_record']['id']) + cursor.execute('INSERT INTO subdomains values(?,?,?,?,?)',(domainid,topdomain_id,sub,ip,None,)) + conn.commit() + print('The domain %s has been added.' % (domain)) + else: + return '[red]Error: %s [/red]' % (str(response)) + + +def remove_subdomain(domain): + if set(domain).difference(ascii_letters + '.' + digits): + print('[red]Error:[/red] Give the domain name in simple form e.g. [b]test.domain.com[/b]') + else: + parts = domain.split('.') + sub = parts[0] + top = parts[1] + '.' + parts[2] + cursor = conn.cursor() + cursor.execute('SELECT COUNT(*) FROM domains WHERE name like ?',(top,)) + count = cursor.fetchone()[0] + if count == 0: + print('[red]Error:[/red] Top domain [bold]%s[/bold] does not exist in the DB. So I\'m giving up!.' % (top)) + else: + cursor.execute('SELECT COUNT(*) FROM subdomains WHERE name like ? and main_id=(SELECT id from domains WHERE name=?)',(sub,top,)) count = cursor.fetchone()[0] if count == 0: - print('Error: Top domain %s does not exist in the DB. Please add it with ddns -t %s ' % (top,top)) + print('[red]Error:[/red] Domain [bold]%s[/bold] does not exist in the DB. So I\'m giving up!.' % (domain)) else: - cursor.execute('SELECT id FROM domains WHERE name LIKE ?',(top,)) - topdomain_id = cursor.fetchone() - topdomain_id = topdomain_id[0] - cursor.execute('SELECT count(*) FROM subdomains WHERE main_id LIKE ? AND name like ?',(topdomain_id,sub,)) - count = cursor.fetchone()[0] - if count != 0: - print('The subdomain %s already exists for the domain %s.' % (sub,top)) + apikey = get_api() + if apikey == None: + print("[red]Error:[/red] Missing APIkey. Please add one!") else: - data = {'name': sub,'data': ip,'type': "A",'ttl': 3600} + cursor.execute('SELECT id FROM subdomains WHERE name like ? and main_id=(SELECT id from domains WHERE name=?)',(sub,top,)) + subdomain_id = str(cursor.fetchone()[0]) headers = {'Authorization': 'Bearer ' + apikey, "Content-Type": "application/json"} - response = requests.post('https://api.digitalocean.com/v2/domains/' + top + '/records', - data=json.dumps(data), headers=headers) - if str(response) == '': - if response != 'Fail': - response_data = response.json() - domainid = str(response_data['domain_record']['id']) - cursor.execute('INSERT INTO subdomains values(?,?,?)',(domainid,topdomain_id,sub,)) - conn.commit() - print('The subdomain %s for the domain %s has been added.' % (sub,top)) + response = requests.delete('https://api.digitalocean.com/v2/domains/'+top+'/records/' + subdomain_id, headers=headers) + if str(response) == '': + cursor.execute('DELETE from subdomains where id=?',(subdomain_id,)) + conn.commit() else: - return 'Error: %s ' % (str(response)) + print('[red]Error: [/red]An error occurred! Please try again later!') + def show_all_top_domains(): @@ -155,36 +209,86 @@ def show_all_top_domains(): cursor.execute('SELECT COUNT(*) FROM domains WHERE name like ?',(k['name'],)) count = cursor.fetchone()[0] if count != 0: - print('Name : '+k['name']+ ' *') + print('Name : [bold]'+k['name']+ ' [*][/bold]') else: print('Name : '+k['name']) else: - print("Missing APIkey. Please add one!") + print("[red]Error:[/red] Missing APIkey. Please add one!") def list_sub_domains(domain): - # TODO: Finish this :) apikey = get_api() cursor = conn.cursor() if apikey == None: - print("Missing APIkey. Please add one!") + print("[red]Error:[/red] Missing APIkey. Please add one!") else: cursor.execute('SELECT COUNT(*) FROM domains WHERE name LIKE ?',(domain,)) count = cursor.fetchone()[0] if count == 0: - print("Error: No such domain. Check spelling or use ddns -d to show all top domains.") + print("[red]Error: [/red]No such domain. Check spelling or use ddns -d to show all top domains.") else: - cursor.execute('SELECT * FROM domains WHERE name LIKE ?', (domain,)) - row = cursor.fetchone() - print(row) + print('\n\nCurrent sub domains for [b]%s[/b]' % (domain)) + print('=====================================================================') + cursor.execute('SELECT id FROM domains WHERE name LIKE ?', (domain,)) + topdomain_id = cursor.fetchone()[0] + cursor.execute('SELECT COUNT(*) FROM subdomains WHERE main_id LIKE ?',(topdomain_id,)) + count = cursor.fetchone()[0] + if count == 0: + print('[red]Error:[/red] No sub domains for [b]%s[/b]' % (domain)) + else: + cursor.execute('SELECT name FROM subdomains WHERE main_id LIKE ?',(topdomain_id,) ) + subdomains = cursor.fetchall() + for i in subdomains: + print(i[0]+'.'+domain) + print('\n') + + +def list_do_sub_domains(domain): + apikey = get_api() + cursor = conn.cursor() + if apikey == None: + print("[red]Error:[/red] Missing APIkey. Please add one!") + else: + req = urllib.request.Request('https://api.digitalocean.com/v2/domains/'+domain+'/records?type="A"/?per_page=200') + req.add_header('Content-Type', 'application/json') + req.add_header('Authorization', 'Bearer ' + apikey) + current = urllib.request.urlopen(req) + remote = current.read().decode('utf-8') + remoteData = json.loads(remote) + print('Domains in your DigitalOcean account not in ddns DB for [b]%s[/b]' % (domain)) + print('============================================================================') + for k in remoteData["domain_records"]: + if k['type'] == 'A': + cursor.execute('SELECT COUNT(*) FROM subdomains WHERE id like ?',(str(k['id']),)) + count = cursor.fetchone()[0] + if count == 0: + print(k['name']+'.'+domain+' ID : '+str(k['id'])) + + - print("List -> " + domain) def domaininfo(domain): - print("domaininfo") + apikey = get_api() + local_ip = get_ip() + cursor = conn.cursor() + if set(domain).difference(ascii_letters + '.'): + print('[red]Error:[/red]. Give the domain name in simple form e.g. [bold]test.domain.com[/bold]') + else: + parts = domain.split('.') + topdomain = parts[1]+'.'+parts[2] + cursor.execute('SELECT id FROM domains WHERE name like ?', (topdomain,)) + domainid = cursor.fetchone()[0] + cursor.execute('SELECT * FROM subdomains WHERE main_id like ?', (domainid,)) + domains = cursor.fetchall() + if local_ip != domains[0][3]: + localip = '[red]%s[/red]' % (local_ip) + else: + localip = local_ip + print ('The domain [bold]%s[/bold] has the IP [bold]%s[/bold]. Your public IP is [bold]%s[/bold]' % (domain,domains[0][3],localip)) + def show_current_info(): @@ -194,19 +298,27 @@ def show_current_info(): cursor.execute('SELECT COUNT(ip4_server) FROM ipservers') count = cursor.fetchone()[0] if count == 0: - ipserver = 'No IP resolvers in DB' + ipserver = '[red]Error:[/red] No IP resolvers in DB' else: cursor.execute('SELECT * FROM ipservers') ipserver = cursor.fetchall()[0][1] if API == None: - API = 'API key not stored in DB' + API = '[red]Error:[/red] API key not stored in DB' + + cursor.execute('SELECT COUNT(*) FROM domains') + topdomains = cursor.fetchone()[0] + cursor.execute('SELECT COUNT(*) FROM subdomains') + subdomains = cursor.fetchone()[0] + print('Current info.') print('==========================================') - print('API key : %s' % (API)) - print('IP resolver : %s' % (ipserver)) - print('App version : %s' % (app_version)) + print('API key : %s' % (API)) + print('IP resolver : %s' % (ipserver)) + print('App version : %s (https://gitlab.pm/rune/ddns)' % (app_version)) + print('Top domains : %s' % (topdomains)) + print('sub domains : %s' % (subdomains)) print('') print('IPv6 is not supported and not listed here.') @@ -239,16 +351,77 @@ def ip_server(ipserver, ip_type): def updateip(): - print("updateip") + apikey = get_api() + current_ip = get_ip() + cursor = conn.cursor() + cursor.execute('SELECT COUNT(*) FROM subdomains') + count = cursor.fetchone()[0] + if count == 0: + print('[red]Error: [/red]There are no dynamic domains active.'\ + ' Start by adding a new domain with [i]ddns -s test.example.com[/i]') + else: + cursor.execute('SELECT id FROM subdomains') + rows = cursor.fetchall() + for i in rows: + cursor.execute('SELECT name FROM domains WHERE id like (SELECT main_id from subdomains WHERE id = ?)',(i[0],)) + domain_name = str(cursor.fetchone()[0]) + subdomain_id = str(i[0]) + data = {'type': 'A', 'data': current_ip} + #print(domain_name, subdomain_id,current_ip,data) + headers = {'Authorization': 'Bearer ' + apikey, "Content-Type": "application/json"} + response = requests.patch('https://api.digitalocean.com/v2/domains/'+domain_name+'/records/' + subdomain_id, data=json.dumps(data), headers=headers) + # response = response.json() + if str(response) != '': + print('[red]Error: ' + str(response.json)) + else: + cursor.execute('UPDATE subdomains SET current_ip4=? WHERE id = ?',(current_ip,subdomain_id,)) + conn.commit() +def local_add_subdomain(domain,domainid): + if set(domain).difference(ascii_letters + '.' + digits + '-'): + print('[red]Error:[/red] Give the domain name in simple form e.g. [b]test.domain.com[/b]') + else: + parts = domain.split('.') + sub = parts[0] + top = parts[1] + '.' + parts[2] + apikey = get_api() + if apikey == None: + print("[red]Error:[/red] Missing APIkey. Please add one!") + else: + ip = get_ip() + if ip == None or 'urlopen error' in ip: + print('[red]Error:[/red] Failed to get public IP. Do you have a typo in your URI? [red]Error %s.[/red]' % (ip)) + else: + cursor = conn.cursor() + cursor.execute('SELECT COUNT(*) FROM domains WHERE name like ?',(top,)) + count = cursor.fetchone()[0] + if count == 0: + print('[red]Error:[/red] Top domain [bold]%s[/bold] does not exist in the DB. Please add it with [i]ddns -t %s[/i].' % (top,top)) + else: + cursor.execute('SELECT id FROM domains WHERE name LIKE ?',(top,)) + topdomain_id = cursor.fetchone() + topdomain_id = topdomain_id[0] + cursor.execute('SELECT count(*) FROM subdomains WHERE main_id LIKE ? AND name like ?',(topdomain_id,sub,)) + count = cursor.fetchone()[0] + if count != 0: + print('[red]Error:[/red] [bold]%s[/bold] already exists.' % (domain)) + else: + cursor.execute('INSERT INTO subdomains values(?,?,?,?,?)',(domainid,topdomain_id,sub,ip,None,)) + conn.commit() + print('The domain %s has been added.' % (domain)) + + # Commandline arguments conn = connect_database() parser = argparse.ArgumentParser(prog='ddns', - description='Application to use domains from DigitalOcean account as dynamic DNS domain(s).\r\nThe app only supports IP4. IPv6 is planned for a later release!', + description='Application to use domains from DigitalOcean account as dynamic '\ + 'DNS domain(s).\nThe app only supports IP4. IPv6 is planned for a later release!'\ + '\nYou\'ll always find the latest version on https://gitlab.pm/rune/ddns', + formatter_class=RawTextHelpFormatter, epilog='Making Selfhosting easier...') parser.add_argument('-a', '--api', help='Add/Change API key.', @@ -257,7 +430,10 @@ parser.add_argument('-a', '--api', help='Add/Change API key.', parser.add_argument('-l', '--list', help='List subdomains for supplied domain.', nargs=1, metavar=('domain'), required=False, action="append") -parser.add_argument('-d', '--domains', help='List top domains on your DigitalOcean account.', +parser.add_argument('-o', '--serverdomains', help='List subdomains for supplied domain not in ddns DB.', + nargs=1, metavar=('domain'), required=False, action="append") + +parser.add_argument('-d', '--domains', help='List top domains in your DigitalOcean account.', required=False, action="store_true") parser.add_argument('-c', '--current', help='List the current IP address for the sub-domain given', @@ -266,8 +442,14 @@ parser.add_argument('-c', '--current', help='List the current IP address for the parser.add_argument('-t', '--top', help='Add a new domain from your DigitalOcean account to use as a dynamic DNS domain', required=False, nargs=1, metavar=('domain'), action='append') -parser.add_argument('-s', '--sub', help='Add a new subdomain to dynamic DNS DO domain', - required=False, nargs=2, metavar=('subdomain', 'domain'), action='append') +parser.add_argument('-s', '--sub', help='Add a new subdomain to your DigitalOcean account and use as dynamic DNS.\n', + required=False, nargs=1, metavar=('domain'), action='append') + +parser.add_argument('-k', '--local', help='Add an existing DigitalOcean domain to your ddns DB and use as dynamic DNS. Get ', + required=False, nargs=2, metavar=('domain','domainid'), action='append') + +parser.add_argument('-r', '--remove', help='Remove a subdomain from your DigitalOcean account and ddns.', + required=False, nargs=1, metavar=('domain'), action='append') parser.add_argument('-i', '--info', help='Show current config info', required=False, action='store_true') @@ -280,23 +462,28 @@ if args['list']: list_sub_domains(args['list'][0][0]) elif args['domains']: show_all_top_domains() +elif args['serverdomains']: + list_do_sub_domains(args['serverdomains'][0][0]) elif args['current']: domaininfo(args['current'][0][0]) elif args['top']: add_domian(args['top'][0][0]) elif args['sub']: - add_subdomain(args['sub'][0][0],args['sub'][0][1]) - #add_subdomain(args['sub'][0][0]) + add_subdomain(args['sub'][0][0]) elif args['info']: show_current_info() elif args['ipserver']: ip_server(args['ipserver'][0][0],args['ipserver'][0][1]) elif args['api']: api(args['api'][0][0]) +elif args['remove']: + remove_subdomain(args['remove'][0][0]) +elif args['local']: + local_add_subdomain(args['local'][0][0],args['local'][0][1]) else: - get_ip() + # get_ip() # get_api() - + updateip()