#!/usr/local/bin/python # # Wireless Leidencaptive portal for (embedded) nodes # Rick van der Zwet import BaseHTTPServer import getopt import os import sys from signal import signal, SIGINFO from subprocess import Popen,PIPE, call from socket import gethostname from threading import Thread from time import asctime, time, sleep from urlparse import urlparse, parse_qs HOST_NAME = '' # Serve on all ip's PORT_NUMBER = 8081 # Capture port ## Number seconds before re-auth again # On if debugging, set this number to 10 sec CLIENT_TIMEOUT = 24 * 60 * 60 # 1 day ## Wait time between checks for gone clients, increase on host with many clients # and less IP addresses CHECK_INTERVAL = 60 # Template file to use for displaying purposes current_dir = os.path.dirname(os.path.abspath(__file__)) INDEX_FILE = current_dir + '/index.html.tmpl' # ipfw specifics (80 clients allowed) RULES_START = 10010 # ipfw rules starting point RULES_END = 10099 # ipfw last rule allowed # List ip ip addreses which are cleared for access # key : ip_addr # value : mac_addr hosts_allowed = {} # UNIX timestamp when host will be pushed back to portal again # key : ip_addr # value : expire time in epoch host_expire = {} # ifpw specifics # key : ip_addr # value : ipfw rule host_to_rule = {} free_rules = range(RULES_START, RULES_END) def get_mac_addr(ip_addr): """Return mac_addr string""" # XXX: Error checking if no mac is found # Get arp addr, for safefity, to check later on ## ChangeMe# arp -n 10.0.42.2 ## ? (10.0.42.2) at 00:22:41:26:ec:4e on vr1 [ethernet] output = Popen(["arp", "-n", ip_addr], stdout=PIPE).communicate()[0] return output.split(' ')[3] def add_host(ip_addr): """Add host to firewall""" global hosts_allowed, host_expire, host_to_rule, free_rules #XXX: Always assumped to be OK # Bookkeeping hosts_allowed[ip_addr] = get_mac_addr(ip_addr) host_expire[ip_addr] = time() + CLIENT_TIMEOUT # Rule book keeping rule_nr = free_rules.pop(0) host_to_rule[ip_addr] = rule_nr # Rule adding to firewall cmd = ["ipfw", "-q", "add" , str(rule_nr), "allow", "tcp", "from", ip_addr, "to", "not", "172.16.0.0/12", "dst-port", "80"] call(cmd) def delete_host(ip_addr): """Delete host from firewall""" global hosts_allowed, host_expire,host_to_rule, free_rules # Rule remove from firewall rule_nr = host_to_rule[ip_addr] cmd = ["ipfw", "delete", str(rule_nr)] call(cmd) # Book keeping del hosts_allowed[ip_addr] del host_expire[ip_addr] del host_to_rule[ip_addr] free_rules.append(rule_nr) def clear_firewall(): """Delete all custom rules from firewall to gain consistency""" global hosts_allowed, host_expire,host_to_rule, free_rules global RULES_START, RULES_END # Delete all rules ##ChangeMe# ipfw show 10002-10009 ##10002 0 0 allow tcp from 10.0.42.2 to not 172.16.0.0/12 dst-port 80 ##10003 0 0 allow tcp from 10.0.42.2 to not 172.16.0.0/12 dst-port 80 output = Popen(["ipfw", "show", str(RULES_START) + '-' + str(RULES_END)], stdout=PIPE,stderr=open('/dev/null', 'w')).communicate()[0].strip() if output: for line in output.split('\n'): cmd = ["ipfw", "delete", line.split(' ')[0]] call(cmd) # Clear all internal variables free_rules = range(RULES_START,RULES_END) hosts_allowed = {} host_expire = {} host_to_rule = {} class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(s): global INDEX_FILE """Respond to a GET request.""" # Locate specifics of URL request_uri = 'http://' + s.headers.get('host') + s.path o = urlparse(request_uri) query = parse_qs(o.query) if 'uri' in query: redirect_uri = query['uri'][0] else: redirect_uri = 'http://www.wirelessleiden.nl' if query.has_key('action'): if query['action'][0] == "accept": add_host(s.client_address[0]) s.send_response(307) s.send_header("Location", redirect_uri) s.send_header("Content-type", "text/html") s.end_headers() s.wfile.write("Accept") elif query['action'][0] == "reject": s.send_response(200) s.send_header("Content-type", "text/html") s.end_headers() s.wfile.write("reject") else: s.send_response(200) s.send_header("Content-type", "text/html") s.end_headers() s.wfile.write("ERROR") else: s.send_response(200) s.send_header("Content-type", "text/html") s.end_headers() index_fd = open(INDEX_FILE, 'r') template = { 'URI' : request_uri, 'IP' : s.client_address[0], 'MAC' : get_mac_addr(s.client_address[0]), 'HOSTNAME' : gethostname(), } s.wfile.write(index_fd.read() % template ) index_fd.close() class FirewallControl(Thread): """Cancel 'hack' is used for threading termination""" def __init__(self): self.cancelled = False Thread.__init__(self) def cancel(self): """Make sure to clean firewall on quit""" clear_firewall() self.cancelled = True def check_firewall(self): """Check for any deletions to be done""" global hosts_allowed, host_expire ip2mac = {} # Find current ip_addr to arp_addr listing ## ChangeMe# arp -na ## ? (10.0.42.2) at 00:22:41:26:ec:4e on vr1 [ethernet] ## ? (192.168.42.1) at 00:14:bf:a5:21:d5 on vr0 [ethernet] output = Popen(["arp", "-na"], stdout=PIPE).communicate()[0].strip() if output: for line in output.split('\n'): ip2mac[line.split(' ')[1].strip('()')] = line.split(' ')[3] # Find hosts to be deleted curr_time = time() for ip_addr in hosts_allowed.keys(): mac_addr = hosts_allowed[ip_addr] if not ip2mac.has_key(ip_addr): delete_host(ip_addr) # New mac_addr under stored ip_addr elif mac_addr != ip2mac[ip_addr]: delete_host(ip_addr) # Time is up! elif curr_time > host_expire[ip_addr]: delete_host(ip_addr) def run(self): global CHECK_INTERVAL while not self.cancelled: self.check_firewall() timer = CHECK_INTERVAL while not self.cancelled and timer > 0: sleep(1) timer =- 1 #XXX: Might want to do custom bookkeeping, e.g. preserve currents on boot ##ChangeMe# ipfw add 10002 allow tcp from 10.0.42.2 to not 172.16.0.0/12 dst-port 80 ##10002 allow tcp from 10.0.42.2 to not 172.16.0.0/12 dst-port 80 ## def siginfo_handler(signum, frame): """Signal handler for debug information""" global hosts_allowed, host_expire, host_to_rule, free_rules curr_time = time() print "Free Rules : ", free_rules print "Current time : ", curr_time print "Overview : " print "ip_addr,mac_addr,expire,rule,seconds,sec,left" for ip_addr, mac_addr in hosts_allowed.iteritems(): sec_left = int(host_expire[ip_addr] - curr_time); print ",".join([ip_addr, mac_addr,str(host_expire[ip_addr]), str(host_to_rule[ip_addr]), str(sec_left)]) def main(): """Hard working class""" server_class = BaseHTTPServer.HTTPServer httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler) print asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER) signal(SIGINFO, siginfo_handler) clear_firewall() worker = FirewallControl() worker.start() try: # Seems buggy, not allowing recurrent signal handling #httpd.serve_forever() while True: httpd._handle_request_noblock() except KeyboardInterrupt: pass httpd.server_close() worker.cancel() print asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER) def usage(): sys.stderr.write(''' [-h|--help] [-f|--foreground] [-l|--logfile] [-p|--pidfile] ''') if __name__ == '__main__': try: opts, args = getopt.getopt(sys.argv[1:], "hfl:p:", ["help", "foreground", "logfile=", "pidfile="]) except getopt.GetoptError, err: # print help information and exit: print str(err) # will print something like "option -a not recognized" usage() sys.exit(128) logfile = "/var/log/wlportal.log" pidfile = "/var/run/wlportal.pid" foreground = False for opt,value in opts: if opt in ("-h", "--help"): usage() sys.exit() elif opt in ("-f", "--foreground"): foreground = True elif opt in ("-l", "--logfile"): logfile = value elif opt in ("-p", "--pidfile"): logfile = value else: assert False, "unhandled option" if not foreground: # Some fork magic http://code.activestate.com/recipes/66012/, but without the error checking pid = os.fork() if pid > 0: sys.exit(0) os.chdir("/") os.setsid() os.umask(0) pid = os.fork() if pid > 0: sys.exit(0) # Set logfile logfile_fd = open(logfile, 'a+',0) sys.stdout = logfile_fd sys.stderr = logfile_fd # write pid pidfile_fd = open(pidfile, 'w',0) pidfile_fd.write(str(os.getpid())) pidfile_fd.close() # Goto the worker main()