#! /usr/bin/python '''Python client to update your dynamic DNS entry. Usage: dyndns.py <userid> <password> [ip=<my-ip>] [wildcard=ON] dyndns.py -f <filename>|- dyndns.py -h|-v The defaults are the server-detected IP address and no wildcards, so the only mandatory arguments are <userid> and <password>. The format of the configuration file is as follows: <userid> <password> [ip=<aaa.bbb.ccc.ddd>] [wildcard=ON] [MX=...] ... userid and password must be the first two lines of the file, the remaining parameters depend on the respective dynamic DNS provider. The file name "-" means to read the configuration form standard input. This is useful if you have the configuration in an encrypted file, like this: decrypt dyndns.conf | dyndns.py -f - See also www.dyndns.org, www.yi.org, eyep.net, gnudip.cheapnet.net''' import sys, os, time, string, re, urllib, httplib, base64 import nettools def day(): '''returns the number of days since the Epoch (1970-01-01)''' return int(time.time() / 24 / 3600) ################################################################ class DynamicDNS: '''This is the abstract base class for all dynamic DNS clients.''' version = 'dyndns.py 0.2/2000-04-14 <beat.bolli@earthling.net>' server = '' # host [:port] to send request to url = '' # base request URL request = 'GET' # or POST command_ip = '' # name of IP argument to server command_host = '' # name of host name argument to server commands = [] # list of other known commands arguments = {} # hash of arguments and their values str_success = '' # unique string indicating success str_error = '' # unique string indicating error policy = 0 # 0: update always # -1: update when IP address changes only # n: update when IP address changes, # but at least every n days (n>0) verbose = 1 def __init__(self): self.name = self.__class__.__name__ self.ip = nettools.localaddr() self.host = '' self.cfg = [] try: home = os.environ['HOME'] except KeyError: home = '/etc' if not os.path.isdir(home): home = '' self.cfg_file = home + '/.dyndns' self.state = 3, 'not finished' def read_config(self): try: f = open(self.cfg_file) self.cfg = f.readlines() f.close() except: print 'Can not read configuration file' self.cfg = [] for i in range(len(self.cfg)): c = self.cfg[i].split(':') if len(c) >= 4 \ and c[0] == self.name and c[1] == self.host: del self.cfg[i] return c[2], int(c[3]) return '*', 0 def write_config(self): c = ':'.join( (self.name, self.host, self.ip, str(day())) ) self.cfg.insert(0, c + '\n') try: f = open(self.cfg_file, 'w') f.writelines(self.cfg) f.close() except: print 'Can not write configuration file' def update(self, userid, password, args): self.userid = userid self.password = password # scan the arguments for arg in args: arg = arg.strip() if '=' not in arg or arg[:1] == '#': continue key, val = arg.split('=', 1) key, val = key.strip().lower(), val.strip() if not val: continue if key == 'ip': if self.command_ip: self.arguments[self.command_ip] = self.ip = val else: print 'Ignoring IP address', val elif key == 'host': if self.command_host: self.arguments[self.command_host] = self.host = val else: print 'Ignoring host name', val elif key in self.commands: self.arguments[key] = val else: print 'Ignoring argument', arg # check the policy last_ip, last_update = self.read_config() if last_ip == self.ip: # policy applies only unless address has changed if self.policy == -1 \ or self.policy > 0 \ and last_update + self.policy >= day(): if self.verbose: print self.name, 'policy says update is not neccessary.' self.state = 0, 'update not neccessary' return # talk to the server errcode, errmsg, headers, reply = self.protocol() if self.verbose: print errcode, errmsg # print headers print reply # check for success if errcode == 200: if re.search(self.str_success, reply): self.state = 0, 'update successful' self.write_config() elif self.str_error and re.search(self.str_error, reply): self.state = 1, 'error:\n\n' + reply else: self.state = 2, 'not OK' else: self.state = -errcode, errmsg def protocol(self): '''The default HTTP protocol''' # allow some special treatment by the subclass self.url_hook() if self.url is None: if self.verbose: print 'Cancelled...' return 0, 'Cancel', '', '' # stick the arguments onto the URL content = urllib.urlencode(self.arguments) if self.request == 'GET': self.url = self.url + content content = None # authenticate auth = self.auth() # are we going through a proxy? try: server = os.environ['http_proxy'] self.url = 'http://' + self.server + self.url except KeyError: server = self.server if self.verbose: print 'Server:', server if auth: print 'Authorization:', auth print self.request, self.url print # send the request h = httplib.HTTP(server) h.putrequest(self.request, self.url) h.putheader('Host', self.server) h.putheader('Connection', 'close') h.putheader('Accept', '*/*') h.putheader('User-Agent', self.version) if auth: h.putheader('Authorization', auth) if content: h.putheader('Content-Type', 'application/x-www-form-urlencoded') h.putheader('Content-Length', '%d' % len(content)) h.endheaders() if content: if self.verbose: print content h.send(content) # return the results errcode, errmsg, headers = h.getreply() reply = h.getfile().read() return errcode, errmsg, headers, reply def url_hook(self): pass def auth_none(self): return '' def auth_basic(self): a = base64.encodestring(self.userid + ':' + self.password) return 'Basic ' + a.strip() # def auth_xxx(sel): # to be defined... auth = auth_basic # default authentication scheme def __str__(self): return self.name + ': %d (%s)' % self.state def ok(self): return self.state[0] == 0 ################################################################ class dyndns_org(DynamicDNS): command_ip = 'myip' command_host = 'hostname' commands = ['wildcard', 'mx', 'backmx'] str_success = 'good ' server = 'members.dyndns.org' url = '/nic/update?' policy = 20 ################################################################ class yi_org(DynamicDNS): command_ip = 'ipaddr' str_success = 'STATUS:OK' str_error = 'STATUS:ERROR' server = 'www.yi.org' url = '/bin/dyndns.fcgi?' policy = 1 ################################################################ class eyep_net(DynamicDNS): command_ip = 'ip' command_host = 'host' server = 'eyep.net' url = '/update-new.php3/' str_success = 'RETURN4' auth = DynamicDNS.auth_none def url_hook(self): self.url = self.url + self.userid + '/' + \ self.password + '/' + self.arguments['host'] + '/' if self.arguments.has_key('ip'): self.url = self.url + self.arguments['ip'] + '/' self.arguments = {} ################################################################ class ez_ip_net(DynamicDNS): command_ip = 'ipaddress' commands = ['mx', 'wildcard', 'preferred'] arguments = {'mode': 'update'} server = 'www.ez-ip.net' url = '/members/update/?' str_success = 'OK' def url_hook(self): if self.arguments.has_key('preferred'): self.url = '/members/preferred/update/?' del self.arguments['preferred'] ################################################################ class gnudip(DynamicDNS): commands = ['domain'] arguments = { 'updatehost': 'Go' } server = 'www.cheapnet.net' url = '/cgi-bin/gnudip2.cgi/' request = 'POST' def url_hook(self): self.str_success = string.replace(self.ip, '.', r'\.') self.arguments['username'] = self.userid self.arguments['password'] = self.password ################################################################ class gnudip_MD5(DynamicDNS): server = 'gnudip.cheapnet.net' port = 3495 command_host = 'domain' str_success = '0' str_error = '1' verbose = 1 def digest(self, s): '''returns a hex representation of s's MD5 hash''' import md5 d = md5.new(s).digest() return string.join(map( lambda c: string.zfill(hex(ord(c))[2:], 2), d ), '') def protocol(self): '''This server uses a propietary protocol.''' import socket # connect to the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect( (self.server, self.port) ) # read the server challenge (10 random characters) challenge = s.recv(64)[:10] if self.verbose: print 'Challenge:', challenge # calculate the digest of our password's digest plus the challenge digest = self.digest(self.password) secret = self.digest(digest + '.' + challenge) # send the update command cmd = string.join( (self.userid, secret, self.host, '0'), ':') if self.verbose: print 'Command:', cmd s.send(cmd + '\n') # read the reply reply = str(s.recv(1024)) return 200, 'OK', '', reply ################################################################ class uptime_net(DynamicDNS): command_host = 'hid' server = 'data.uptimes.net' url = '/server.html' request = 'POST' string_success = 'UP4: 000' def procread(self, file): return open('/proc/' + file).read().split() def url_hook(self): args = self.arguments import os # uptime and idle percentage try: up, idle = map(float, self.procread('uptime')[:2]) except IOError: self.url = None return args['uptime'] = '%d' % (up / 60) args['idle'] = '%d' % (100 * idle / up) # load try: args['load'] = self.procread('loadavg')[0] except IOError: pass # OS, OS level and CPU type if hasattr(os, 'uname'): u = os.uname() args['os'] = u[0] args['oslevel'] = u[2] args['cpu'] = u[4] ################################################################ def update(DynamicDNSProvider, userid, password, *args): p = DynamicDNSProvider() # class parameter! p.update(userid, password, args) print p if not p.ok(): print 'Press <Enter> to continue' sys.stdin.readline() # wait print '-' * 64 def main(): argv = sys.argv[1:] argc = len(argv) if argc == 0 or argv[0] == '-h': print __doc__ return elif argv[0] == '-v': print DynDNS.version return elif argv[0][:2] == '-f': if argc == 1 and argv[0][2:]: file = argv[0][2:] elif argc == 2: file = argv[1] else: print 'Missing filename' return if file == '-': fp = sys.stdin else: try: fp = open(file) except IOError: print 'Error opening', file return argv = fp.read().split() fp.close() elif argc < 2: print 'Missing argument' return update(dyndns_org, *argv) ################################################################ update(dyndns_org, 'test', 'test', 'host=test.ath.cx') # DynDNS test account print 'Done.'