#!/usr/bin/env python # # $Id: czoink.py 11 2008-06-18 23:44:45Z respawned $ # # Copyright (c) 2008 Respwaned Fluff # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. import os, re, sys, urllib, logging, logging.handlers, getopt, base64, time import mcrypt # mcrypt usually needs to be installed. import ConfigParser, signal, socket CZ_VER = "1.0.1.3" # This is the version we're compatible with. CZ_KEY = "f52412c4ff1dacd2111f4951f3db1260" CZ_IV = "0e32f4c96203f892" def read_config(): global USER, PASSWD config = ConfigParser.ConfigParser() config.read("/etc/czoink.ini") USER = config.get("login", "user") PASSWD = config.get("login", "pass") def daemon_me(): return len(sys.argv) > 1 and sys.argv[1] == "-d" if daemon_me(): LOG_FORMAT = "%(name)s[%(levelname)s]: %(message)s" # %(asctime)s: LOG_LEVEL = logging.INFO else: LOG_FORMAT = "%(asctime)s: %(name)s[%(levelname)s]: %(message)s" LOG_LEVEL = logging.DEBUG def setup_log(): log = logging.getLogger("czoink") if daemon_me(): hand = logging.handlers.SysLogHandler('/dev/log') else: hand = logging.StreamHandler() fmt = logging.Formatter(LOG_FORMAT) hand.setFormatter(fmt) log.addHandler(hand) log.setLevel(LOG_LEVEL) # log filter hand.setLevel(LOG_LEVEL) # handler filter return log def beh64(data): return base64.encodestring(data).strip().replace("\n", "") def setup_aes(): try: from mcrypt import MCRYPT aes = MCRYPT("rijndael-128", "cbc") aes.init(CZ_KEY, CZ_IV) def cz_fun(strn): aes.reinit() return beh64(beh64(aes.encrypt(strn))) cry_prov = "mcrypt" except: from Crypto.Cipher import AES def cz_fun(strn): aes = AES.new(CZ_KEY, AES.MODE_CBC, CZ_IV) # no reinit pad = len(strn) % 16 if pad != 0: strn = strn + ('\x00' * (16 - pad)) return beh64(beh64(aes.encrypt(strn))) cry_prov = "pycrypto" global cz_arg cz_arg = cz_fun return cry_prov def check_ver(): try: fil = urllib.urlopen("https://acces.czone.ro/czone/versionlinux.txt") remote_ver = fil.readline() fil.close() if remote_ver != CZ_VER: log.warning("I may be too old. Seek newer version.") else: log.info("Version check passes.") except: log.error("Could not determine latest czone app version.") junkre = re.compile("" or "Warning...") while junkre.match(strn): strn = strn[strn.find("\x0a")+1:] return strn class CzRPCError(Exception): def __init__(self, url): self.url = url def __str__(self): return repr(self.url) def cz_rpc(cmd): url = "https://acces.czone.ro/czone/c-zone.php?Data=" + cz_arg(cmd) try: log.debug(url) fil = urllib.urlopen(url) res = fil.read() fil.close() equs = skip_warns(res.strip("\x09\x0a\x0d\x20")).split("\x0d") log.debug(equs) return dict([elm.split("=", 1) for elm in equs if elm]) except: log.error("RPC failed: " + url) raise CzRPCError(url) iline = re.compile(r'\d+: (\w+):') nline = re.compile(r'\s+inet ([0-9.]+)') sanre = re.compile(r'[0-9.]+') def sanit(strn): return sanre.match(strn).group() def do_sys(cmd): log.info("Executing: %s", cmd) status = os.system(cmd) if status: log.error("Command %s failed.", cmd) # TBD: error string return False return True def setup_ip(dct): iface = "" ip = "" pip = os.popen("ip addr show scope global") for line in pip: mi = iline.match(line) mn = nline.match(line) if mi: iface = mi.group(1) log.debug("IF: " + iface) elif mn: ip = mn.group(1) log.debug("IP: " + ip) if dct['YOURIP'] == ip: log.info("Detected interface: " + iface) break pip.close() if not iface: log.critical("Could not determine your czone interface.") return ip = sanit(dct['IP']) pip = os.popen("ipcalc -p %s %s" % (ip, sanit(dct['MASK']))) sla = pip.readline().lstrip("PREFIX=").replace("\n", "") log.debug("CIDR: /" + sla) if (#do_sys("ip addr flush dev %s scope global" % iface) and do_sys("ip addr add dev %s %s/%s brd + scope global" % (iface, ip, sla)) and do_sys("ip route replace default dev %s via %s proto static" % (iface, sanit(dct['GATE'])))): log.info("Interface %s configured successfully." % iface) return True # XXX: This won't configure the IPv6 link address return False must_die = False def death_handler(signum, frame): global must_die log.info('Terminating on signal %s.', signum) must_die = True def refresh_handler(signum, frame): log.info('Refreshing on signal %s.', signum) def setup_signals(): signal.signal(signal.SIGTERM, death_handler) signal.signal(signal.SIGINT, death_handler) signal.signal(signal.SIGUSR1, refresh_handler) signal.signal(signal.SIGALRM, refresh_handler) def cz_login(): nonce = cz_rpc("IP=unknown&COMMAND=NONCE&USERNAME=" + USER) login = cz_rpc(("IP=unknown&COMMAND=LOGIN&USERNAME=%s" + "&PASSWORD=%s&NONCE=%s") % (USER, PASSWD, nonce["NONCE"])) if login["MESSAGE"] == "INSTALL": if not setup_ip(login): log.error('Cannot complete IP setup. Aborting.') global must_die must_die = True return False return True elif login["MESSAGE"] == "CONNECTED": log.info("Connected.") huh = cz_rpc("IP=unknown&COMMAND=LINUX&USERNAME=" + USER + "&FORWARD=0&SNAT=0&TTL=0&MASQ=0") log.debug(huh) return True log.error("Oopsie: " + login) return False def reindent(s, numSpaces): leading_space = numSpaces * ' ' lines = [ leading_space + line.strip( ) for line in s.splitlines( ) ] return '\n'.join(lines) def dhcl_set_state(state): log.debug("dhclient state <- %d", state) (pi, po) = os.popen4("omshell") pi.write(reindent(""" port 7912 connect new control open set state = %d update """ % state, 0)) pi.close() po.close() def dhcl_stop(): log.info("Pausing dhclient.") dhcl_set_state(3) return False def dhcl_run(): log.info("Resuming dhclient.") dhcl_set_state(4) time.sleep(2) return True def daemon(): try: pid = os.fork() if pid > 0: # exit first parent sys.exit(0) except OSError, e: print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror) sys.exit(1) # decouple from parent environment os.chdir("/") #don't prevent unmounting.... os.setsid() os.umask(0) # do second fork try: pid = os.fork() if pid > 0: # exit from second parent, print eventual PID before #print "Daemon PID %d" % pid open("/var/run/czoink.pid", 'w').write("%d\n" % pid) sys.exit(0) except OSError, e: print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror) sys.exit(1) def setup_monitor(): return os.popen("ip monitor link route") fire = re.compile(r"(.*state DOWN)|(default via)") def wait_route_event(pmon): while True: try: line = pmon.readline() if fire.match(line): log.info("Event: " + line.replace("\n", "")) return line else: log.debug("Event filtered.") except IOError: log.debug("Signal?") return None drre = re.compile(r"default via ([0-9.]+) dev (\S+)") def get_route_state(): pip = os.popen("ip route show |grep default") result = drre.match(pip.readline()) pip.close() if result: result = result.groups() return result nsre = re.compile(r"search ([-a-zA-Z0-9.]+)") def get_domain(): pip = os.popen("grep search /etc/resolv.conf") result = nsre.match(pip.readline()) pip.close() if result: result = '.'.join(result.group(1).split('.')[-2:]) return result class Zone: ALONE = 0 ZPRIV = 1 ZPUBL = 2 OTHER = 3 def get_zone(): rt = get_route_state() if not rt: return Zone.ALONE time.sleep(2) if get_domain() == 'czone.ro': if rt[0][:2] == '10': return Zone.ZPRIV else: return Zone.ZPUBL return Zone.OTHER if __name__ == "__main__": log = setup_log() aes = setup_aes() setup_signals() read_config() if daemon_me(): daemon() log.info("Startup using %s AES provider.", aes) pmon = setup_monitor() verchk = True dhclgo = True while not must_die: zone = get_zone() log.debug("Before we are in zone: %d", zone) if zone in [Zone.ZPRIV, Zone.ZPUBL]: if verchk: check_ver() verchk = False dhclgo = dhcl_stop() try: if cz_login(): signal.alarm(600) else: dhclgo = dhcl_run() # last hope: restart DHCP and try again except CzRPCError, e: dhclgo = dhcl_run() else: if not dhclgo: dhclgo = dhcl_run() zone = get_zone() log.debug("After we are in zone: %d", zone) wait_route_event(pmon) dhcl_run()