From dbe9db0e4ace032734256f90773c039f590dfcba Mon Sep 17 00:00:00 2001 From: Tomek Dubrownik Date: Fri, 27 Jan 2012 08:31:32 +0100 Subject: [PATCH] dnsmasq.leases based updater Also a self-claim module --- at.py | 182 ++++++++++++++++++++++++++++++++++---- config.py | 8 ++ templates/claim.html | 14 +++ templates/login.html | 13 +++ templates/main.html | 11 ++- templates/post_claim.html | 10 +++ templates/register.html | 14 +++ 7 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 templates/claim.html create mode 100644 templates/login.html create mode 100644 templates/post_claim.html create mode 100644 templates/register.html diff --git a/at.py b/at.py index e7e58fe..4a3b09c 100644 --- a/at.py +++ b/at.py @@ -7,15 +7,22 @@ import traceback from datetime import datetime from wsgiref import simple_server from pesto import Response, dispatcher_app +from pesto.session import session_middleware +from pesto.session.memorysessionmanager import MemorySessionManager from time import sleep, time from collections import namedtuple from jinja2 import Environment, FileSystemLoader +from urllib import urlencode +from hashlib import sha256 import config dispatcher = dispatcher_app() +app = session_middleware(MemorySessionManager())(dispatcher) logger = logging.getLogger() -env = Environment(loader=FileSystemLoader('templates')) +env = Environment(loader=FileSystemLoader('templates'), + autoescape='html', + extensions=['jinja2.ext.autoescape']) conn = None updater = None @@ -30,6 +37,17 @@ def render(filepath): return func return decorator +def restrict_ip(prefix='', exclude=[], fail_response=Response(status=403)): + def decorator(f): + @wraps(f) + def func(request, *a, **kw): + r_addr = request.remote_addr + if not r_addr.startswith(prefix) or r_addr in exclude: + return fail_response + return f(request, *a, **kw) + return func + return decorator + def strfts(ts, format='%d/%m/%Y %H:%M'): return datetime.fromtimestamp(ts).strftime(format) env.filters['strfts'] = strfts @@ -37,6 +55,7 @@ env.filters['strfts'] = strfts def setup_db(): conn = sqlite3.connect(config.db) conn.row_factory = sqlite3.Row + conn.isolation_level = None # for autocommit mode return conn DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'owner', 'ignored']) @@ -55,15 +74,14 @@ def get_device_infos(conn, hwaddrs): yield DeviceInfo(row['hwaddr'], owner, ignored) class Updater(threading.Thread): - def __init__(self, cap_file, timeout, *a, **kw): - self.cap_file = cap_file + def __init__(self, timeout, *a, **kw): self.timeout = timeout self.lock = threading.Lock() self.active = {} threading.Thread.__init__(self, *a, **kw) def purge_stale(self): now = time() - for addr, atime in self.active.items(): + for addr, (atime, ip, name) in self.active.items(): if now - atime > self.timeout: del self.active[addr] def get_active_devices(self): @@ -72,38 +90,166 @@ class Updater(threading.Thread): r = dict(self.active) self.lock.release() return r - def update(self, hwaddr): + def get_device(self, ip): + for hwaddr, (atime, dip, name) in \ + self.get_active_devices().iteritems(): + if ip == dip: + return hwaddr, name + def update(self, hwaddr, atime = None, ip = None, name = None): + atime = atime or time() self.lock.acquire() - self.active[hwaddr] = time() + self.active[hwaddr] = (atime, ip, name) self.lock.release() + logger.info('updated %s with atime %s and ip %s', + hwaddr, strfts(atime), ip) + +class CapUpdater(Updater): + def __init__(self, cap_file, *a, **kw): + self.cap_file = cap_file + Updater.__init__(self, *a, **kw) def run(self): while True: try: - f = open(self.cap_file, 'r', buffering=0) - logger.info('Updater ready on cap file %s', self.cap_file) - while True: - hwaddr = f.readline().strip() - if not hwaddr: - break - self.update(hwaddr) - logger.info('logged dhcp request from %s', hwaddr) + with open(self.cap_file, 'r', buffering=0) as f: + logger.info('Updater ready on cap file %s', self.cap_file) + while True: + hwaddr = f.readline().strip() + if not hwaddr: + break + self.update(hwaddr) logging.warning('Cap file %s closed, reopening', self.cap_file) except Exception as e: logging.error('Updater got an exception:\n' + \ traceback.format_exc(e)) sleep(10.0) +class DnsmasqUpdater(Updater): + def __init__(self, lease_file, lease_offset, *a, **kw): + self.lease_file = lease_file + self.lease_offset = lease_offset + self.last_modified = 0 + Updater.__init__(self, *a, **kw) + def run(self): + import os + while True: + try: + mtime = os.stat(self.lease_file).st_mtime + if mtime > self.last_modified: + logger.info('Lease file changed, updating') + with open(self.lease_file, 'r') as f: + for line in f: + ts, hwaddr, ip, name, client_id = line.split(' ') + self.update(hwaddr, int(ts) - self.lease_offset, ip, name) + self.last_modified = mtime + sleep(3.0) + except Exception as e: + logging.error('Updater got an exception:\n' + \ + traceback.format_exc(e)) + sleep(10.0) + + @dispatcher.match('/', 'GET') @render('main.html') def now_at(request): devices = updater.get_active_devices() device_infos = list(get_device_infos(conn, devices.keys())) device_infos.sort(key=lambda di: devices.__getitem__) - users = list(dict((info.owner, devices[info.hwaddr]) for info in device_infos + users = list(dict((info.owner, devices[info.hwaddr][0]) for info in device_infos if info.owner and not info.ignored).iteritems()) users.sort(key=lambda (u, a): a, reverse=True) unknown = set(devices.keys()) - set(d.hwaddr for d in device_infos) - return dict(users=users, unknown=unknown) + return dict(users=users, unknown=unknown, login=request.session.get('login')) + +restrict_to_hs = restrict_ip(prefix=config.claimable_prefix, + exclude=config.claimable_exclude) + +@dispatcher.match('/register', 'GET') +@restrict_to_hs +@render('register.html') +def register_form(request): + return request.form + +@dispatcher.match('/register', 'POST') +@restrict_to_hs +def register(request): + login = request['login'] + url = request['url'] + if 'wiki' in request.form: + url = config.wiki_url % { 'login': login } + try: + conn.execute('insert into users (login, url, pass) values (?, ?, ?)', + [login, url, sha256(request['password']).hexdigest()]) + return Response.redirect('/') + except sqlite3.Error as e: + request.form['error'] = 'Cannot add user - username taken?' + return register_form(request) + +@dispatcher.match('/login', 'GET') +@restrict_to_hs +@render('login.html') +def login_form(request): + return request.form + +def get_credentials(login, password): + row = conn.execute('select userid from users where login = ? and pass = ?', + [login, sha256(password).hexdigest()]).fetchone() + return row and row['userid'] + +@dispatcher.match('/login', 'POST') +@restrict_to_hs +def login(request): + login = request.get('login') + pwd = request.get('password') + goto = request.get('goto') or '/' + userid = get_credentials(login, pwd) + if userid: + request.session['userid'] = userid + request.session['login'] = login + return Response.redirect(goto) + else: + request.form['error'] = 'Username or password invalid' + return login_form(request) + +@dispatcher.match('/logout', 'GET') +@restrict_to_hs +def logout(request): + request.session.clear() + return Response.redirect('/') + +def login_required(f): + @wraps(f) + def func(request, *a, **kw): + if 'userid' not in request.session: + return Response.redirect('/login?' + + urlencode({'goto': request.path_info, + 'error': 'You must log in to continue'})) + return f(request, *a, **kw) + return func + +@dispatcher.match('/claim', 'GET') +@restrict_to_hs +@login_required +@render('claim.html') +def claim_form(request): + hwaddr, name = updater.get_device(request.remote_addr) + return { 'hwaddr': hwaddr, 'name': name, + 'login': request.session['login'] } + +@dispatcher.match('/claim', 'POST') +@restrict_to_hs +@login_required +@render('post_claim.html') +def claim(request): + hwaddr, lease_name = updater.get_device(request.remote_addr) + if not hwaddr: + return { 'error': 'Invalid device.' } + userid = request.session['userid'] + try: + conn.execute('insert into devices (hwaddr, name, owner, ignored)\ + values (?, ?, ?, ?)', [hwaddr, request['name'], userid, False]) + return {} + except sqlite3.Error as e: + return { 'error': 'Could not add device! Perhaps someone claimed it?' } port = 8080 if __name__ == '__main__': @@ -111,7 +257,7 @@ if __name__ == '__main__': logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler()) conn = setup_db() - updater = Updater(config.cap_file, config.timeout) + updater = DnsmasqUpdater(config.lease_file, config.lease_offset, config.timeout) updater.start() - server = simple_server.make_server('', port, dispatcher) + server = simple_server.make_server('', port, app) server.serve_forever() diff --git a/config.py b/config.py index f3d3033..b08cf21 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,12 @@ db = './at.db' cap_file = './dhcp-cap' +lease_file = './leases' +lease_offset = 60 * 20 timeout = 300 +wiki_url = 'http://hackerspace.pl/wiki/doku.php?id=people:%(login)s:start' + +claimable_prefix = '' +claimable_exclude = [ +# '127.0.0.1', +] diff --git a/templates/claim.html b/templates/claim.html new file mode 100644 index 0000000..ab4243b --- /dev/null +++ b/templates/claim.html @@ -0,0 +1,14 @@ + + +{% if not hwaddr %} +

Unknown MAC. Are you sure you're in the hackerspace?

+{% else %} +You are about to claim {{ hwaddr }} as {{ login }}. Do you wish to continue? +
+ + +
+ +{% endif %} + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..2a3d65a --- /dev/null +++ b/templates/login.html @@ -0,0 +1,13 @@ + + +

{{ error }}

+
+ + +{% if goto %} + +{% endif %} + +
+ + diff --git a/templates/main.html b/templates/main.html index 9b0769c..d647833 100644 --- a/templates/main.html +++ b/templates/main.html @@ -1,4 +1,12 @@ + +
+{% if login %} +logged in as {{ login }} | log out +{% else %} +login | register +{% endif %} +
Recently at hackerspace:
-There are {{ unknown|length }} unknown devices operating. +There are {{ unknown|length }} unknown devices operating. Claim this device! + diff --git a/templates/post_claim.html b/templates/post_claim.html new file mode 100644 index 0000000..cd24469 --- /dev/null +++ b/templates/post_claim.html @@ -0,0 +1,10 @@ + + +{% if error %} +

{{ error }}

+{% else %} +Congratulations, you just claimed this device! +go back +{% endif %} + + diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..bbe9626 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,14 @@ + + +

{{ error }}

+
+ + + + + + +
+ +