update.py 5.46 KB
#!/usr/bin/env python3

import http.client
import datetime
import requests
import json
import argparse
import sys

CURRENT_VERSION = "1.1-imanolbarba"
USER_AGENT_STRING = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36"
DEBUG = 0

API_URL="https://domains.google.com/nic/update"

TYPE_INFO = 0
TYPE_WARNING = 1
TYPE_ERROR = 2
TYPE_DEBUG = 3

BASH_YLW_TEXT = '\033[33m'
BASH_RED_TEXT = '\033[31m'
BASH_RESET_TEXT = '\033[0m'

# Message logging function
# TODO: log to file
def msgLog(string,type = TYPE_INFO):
    time = str(datetime.datetime.now())

    if(type == TYPE_ERROR):
        print("[{}]: {}ERROR: {}{}".format(time, BASH_RED_TEXT, string, BASH_RESET_TEXT))
    elif(type == TYPE_WARNING):
        print("[{}]: {}WARNING: {}{}".format(time, BASH_YLW_TEXT, string, BASH_RESET_TEXT))
    elif(type == TYPE_INFO):
        print("[{}]: INFO: {}".format(time, string))
    elif(type == TYPE_DEBUG):
        if(DEBUG):
            print("[{}]: DEBUG: {}".format(time, string))
    else:
        print(string)

def parseResponse(body):
    # format: response arg
    repFields = body.split(" ")
    if(len(repFields) > 2):
        msgLog("Response exceeded 2 arguments. No currently implemented responses exceed that, so it must be new.\nResponse was:\n\n{}".format(body), TYPE_WARNING)
    repVerb = repFields[0]
    repArg = repFields[-1] # If response is only 1 argument (the verb), this will also be the verb, saving length logic ;)
    if(repVerb == "good"):
        msgLog("Successfully updated to {}!".format(repArg), TYPE_INFO)
        return True

    if(repVerb == "nochg"):
        msgLog("Record was already updated to address {}".format(repArg), TYPE_INFO)
        return True

    if(repVerb == "nohost"):
        msgLog("Hostname doesn't exist, or does not have Dynamic DNS enabled", TYPE_ERROR)
        return False

    if(repVerb == "badauth"):
        msgLog("Invalid credentials for specified host", TYPE_ERROR)
        return False

    if(repVerb == "notfqdn"):
        msgLog("Hostname is not a valid FQDN", TYPE_ERROR)
        return False

    if(repVerb == "badagent"):
        msgLog("Invalid request. Ensure the user agent is set in the request", TYPE_ERROR)
        return False

    if(repVerb == "abuse"):
        msgLog("Dynamic DNS access for the hostname has been blocked due to failure to interpret previous responses correctly", TYPE_ERROR)
        return False

    if(repVerb == "911"):
        msgLog("Google Domains failure. Try again later", TYPE_ERROR)
        return False

    if(repVerb == "conflict"):
        msgLog("Conflicting {} record found. Delete the indicated resource record within DNS settings page and try the update again".format(repArg), TYPE_ERROR)
        return False

    msgLog("Unknown response:\n\n{}".format(body), TYPE_ERROR)
    return False

def updateRecord(username, password, hostname, address, offline):
    msgLog("Updating {} to {}".format(hostname, ("current IP address" if address == None else address)),TYPE_INFO)
    headers = {'User-Agent': USER_AGENT_STRING, 'Content-Type': 'application/x-www-form-urlencoded'}
    updateParams = {"hostname" : hostname, "offline": ("yes" if offline == True else "no")}
    if(address != None):
        updateParams["myip"] = address
    req = requests.Request('POST', API_URL, auth=(username,password), headers=headers, data=updateParams)
    prep = req.prepare()
    msgLog('Request:\n\n{} {}\n{}\n\n{}'.format(prep.method, prep.url,'\n'.join('{}: {}'.format(k, v) for k, v in prep.headers.items()),prep.body), TYPE_DEBUG)
    rep = requests.Session().send(prep)
    msgLog('Response:\n\n{} {}\n{}\n\n{}'.format(rep.status_code, http.client.responses[rep.status_code],'\n'.join('{}: {}'.format(k, v) for k, v in rep.headers.items()),rep.content.decode()), TYPE_DEBUG)
    if(rep.status_code != 200):
        msgLog("API returned non-success code: {}\n".format(rep.status_code), TYPE_ERROR)
        return False
    return parseResponse(rep.content.decode())


parser = argparse.ArgumentParser(description="Update Google Domain Dynamic DNS records")
parser.add_argument('--verbose', action='store_true', dest="verbose", help="Enable debugging messages")
parser.add_argument('--version', action='version', version=str("GoogleDDNS v" + CURRENT_VERSION))

parser.add_argument("--hostname", action="store", dest="hostname", help="Hostname to be updated", nargs=1)
parser.add_argument("--ip", action="store", dest="ip", help="IPv4/6 address to update the hostname to (Optional for IPv4, Google uses the requestor IP address otherwise)", nargs=1)
parser.add_argument("--username", action="store", dest="username", help="Username for this specific hostname (NOT YOUR GOOGLE ACCOUNT USERNAME)", nargs=1)
parser.add_argument("--password", action="store", dest="password", help="Password for this specific hostname (NOT YOUR GOOGLE ACCOUNT PASSWORD)", nargs=1)
parser.add_argument("--offline", action="store_true", dest="offline", help="Put the hostname offline until further updates")

argv = parser.parse_args()
if(argv.verbose):
    DEBUG = 1

if(argv.hostname != None):
    if(argv.username != None):
        if(argv.password != None):
            if(updateRecord(argv.username[0], argv.password[0], argv.hostname[0], (argv.ip[0] if argv.ip != None else None), argv.offline) == False):
                exit(1)
        else:
            msgLog("Password not specified", TYPE_ERROR)
    else:
        msgLog("Username not specified", TYPE_ERROR)
else:
    msgLog("Hostname not specified", TYPE_ERROR)

exit(0)