#!/usr/bin/env python2 # -*- coding: utf-8 -*- import sqlite3 import threading import traceback import json import requests import os import logging from flask import Flask, render_template, abort, g, \ redirect, request, flash, url_for, make_response from datetime import datetime from time import sleep, time, mktime from collections import namedtuple from urllib import urlencode from spaceauth import SpaceAuth, login_required, current_user, cap_required app = Flask('at') app.config.from_pyfile('at.cfg') app.jinja_env.add_extension('jinja2.ext.i18n') app.jinja_env.install_null_translations() app.updater = None if app.config.get('PROXY_FIX'): from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) auth = SpaceAuth(app) from functools import wraps def v4addr(): if request.headers.getlist("X-Forwarded-For"): r_addr = request.headers.getlist("X-Forwarded-For")[-1] else: r_addr = request.remote_addr if r_addr.startswith('::ffff:'): r_addr = r_addr[7:] return r_addr def restrict_ip(prefix='', exclude=[]): def decorator(f): @wraps(f) def func(*a, **kw): r_addr = v4addr() if not r_addr.startswith(prefix) or r_addr in exclude: app.logger.info('got IP %s, rejecting', r_addr) abort(403) return f(*a, **kw) return func return decorator def req_to_ctx(): return dict(request.form.iteritems()) @app.template_filter('strfts') def strfts(ts, format='%d/%m/%Y %H:%M'): return datetime.fromtimestamp(ts).strftime(format) @app.template_filter('wikiurl') def wikiurl(user): return app.config['WIKI_URL'] % {'login': user} @app.before_request def make_connection(): conn = sqlite3.connect(app.config['DB']) conn.row_factory = sqlite3.Row conn.isolation_level = None # for autocommit mode g.db = conn @app.teardown_request def close_connection(exception): g.db.close() DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'name', 'owner', 'ignored']) def get_device_info(conn, hwaddr): return list(get_device_infos(conn, (hwaddr,)))[0] def get_device_infos(conn, hwaddrs): in_clause = '({})'.format(', '.join(['?'] * len(hwaddrs))) stmt = '''select hwaddr, name, ignored, owner from devices where devices.hwaddr in ''' + in_clause for row in conn.execute(stmt, hwaddrs): owner = row['owner'] or '' ignored = row['ignored'] yield DeviceInfo(row['hwaddr'], row['name'], owner, ignored) class Updater(threading.Thread): def __init__(self, timeout, lease_offset=0, *a, **kw): self.timeout = timeout self.lock = threading.Lock() self.lease_offset = lease_offset self.active = {} threading.Thread.__init__(self, *a, **kw) self.daemon = True def purge_stale(self): now = time() for addr, (atime, ip, name) in self.active.items(): if now - atime > self.timeout: del self.active[addr] def get_active_devices(self): self.lock.acquire() self.purge_stale() r = dict(self.active) self.lock.release() return r def get_device(self, ip): active_devices = self.get_active_devices().iteritems() for hwaddr, (atime, dip, name) in active_devices: if ip == dip: return hwaddr, name return None, None def update(self, hwaddr, atime=None, ip=None, name=None): if atime: atime -= self.lease_offset else: atime = time() self.lock.acquire() if hwaddr not in self.active or self.active[hwaddr][0] < atime: self.active[hwaddr] = (atime, ip, name) app.logger.info('updated %s with atime %s and ip %s', hwaddr, strfts(atime), ip) self.lock.release() 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: with open(self.cap_file, 'r', buffering=0) as f: app.logger.info('Updater ready on cap file %s', self.cap_file) lines = [l.strip() for l in f.read().split('\n')] for hwaddr in lines: if hwaddr: self.update(hwaddr) app.logger.warning('Cap file %s closed, reopening', self.cap_file) except Exception as e: app.logger.error('Updater got an exception:\n' + traceback.format_exc(e)) sleep(10.0) class MtimeUpdater(Updater): def __init__(self, lease_file, *a, **kw): self.lease_file = lease_file self.last_modified = 0 Updater.__init__(self, *a, **kw) def file_changed(self, f): pass def _trigger_update(self): app.logger.info('Lease file changed, updating') with open(self.lease_file, 'r') as f: self.file_changed(f) def run(self): while True: try: mtime = os.stat(self.lease_file).st_mtime if mtime > self.last_modified: self._trigger_update() self.last_modified = mtime sleep(3.0) except Exception as e: app.logger.error('Updater got an exception:\n' + traceback.format_exc(e)) sleep(10.0) class DnsmasqUpdater(MtimeUpdater): def file_changed(self, f): for line in f: ts, hwaddr, ip, name, client_id = line.split(' ') self.update(hwaddr, int(ts), ip, name) class DhcpdUpdater(MtimeUpdater): def file_changed(self, f): lease = False # for use by next-line logic ip = None hwaddr = None atime = None for line in f: line = line.split('#')[0] cmd = line.strip().split() if not cmd: continue if lease: field = cmd[0] if(field == 'starts'): dt = datetime.strptime(' '.join(cmd[2:]), '%Y/%m/%d %H:%M:%S;') atime = mktime(dt.utctimetuple()) if(field == 'client-hostname'): name = cmd[1][1:-2] if(field == 'hardware'): hwaddr = cmd[2][:-1] if(field.startswith('}')): lease = False if hwaddr is not None and atime is not None: self.update(hwaddr, atime, ip, name) hwaddr, atime = None, None elif cmd[0] == 'lease': ip = cmd[1] name, hwaddr, atime = [None] * 3 lease = True @app.route('/') def main_view(): return render_template('main.html', **now_at()) @app.route('/api') def list_all(): data = now_at() def prettify_user((user, atime)): return { 'login': user, 'timestamp': atime, 'pretty_time': strfts(atime), } result = {} result['users'] = map(prettify_user, data.pop('users')) result.update((k, len(v)) for k, v in data.items()) res = make_response(json.dumps(result), 200) res.headers['Access-Control-Allow-Origin'] = '*' return res def now_at(): result = dict() devices = app.updater.get_active_devices() device_infos = list(get_device_infos(g.db, devices.keys())) device_infos.sort(key=lambda di: devices.__getitem__) unknown = set(devices.keys()) - set(d.hwaddr for d in device_infos) # das kektop sorting maschine for name, prefixes in app.config['SPECIAL_DEVICES'].items(): result[name] = set() for u in unknown.copy(): if u.startswith(prefixes): result[name].add(u) unknown.discard(u) result['unknown'] = unknown users = {} for info in device_infos: if info.owner not in users: users[info.owner] = devices[info.hwaddr][0] result['users'] = sorted(users.items(), key=lambda (u, a): a, reverse=True) return result restrict_to_hs = restrict_ip(prefix=app.config['CLAIMABLE_PREFIX'], exclude=app.config['CLAIMABLE_EXCLUDE']) @app.route('/claim', methods=['GET']) @restrict_to_hs @login_required def claim_form(): hwaddr, name = app.updater.get_device(v4addr()) return render_template('claim.html', hwaddr=hwaddr, name=name) @app.route('/claim', methods=['POST']) @restrict_to_hs @login_required def claim(): hwaddr, lease_name = app.updater.get_device(v4addr()) ctx = None if not hwaddr: ctx = dict(error='Invalid device.') else: login = current_user.id try: g.db.execute(''' insert into devices (hwaddr, name, owner, ignored) values (?, ?, ?, ?)''', [hwaddr, request.form['name'], login, False]) ctx = {} except sqlite3.Error: error = 'Could not add device! Perhaps someone claimed it?' ctx = dict(error=error) return render_template('post_claim.html', **ctx) def get_user_devices(conn, user): devs = conn.execute('select hwaddr, name, ignored from devices where\ owner = ?', [user]) device_info = [] for row in devs: di = DeviceInfo(row['hwaddr'], row['name'], user, row['ignored']) device_info.append(di) return device_info @app.route('/account', methods=['GET']) @login_required def account(): devices = get_user_devices(g.db, current_user.id) return render_template('account.html', devices=devices) def set_ignored(conn, hwaddr, user, value): return conn.execute(''' update devices set ignored = ? where hwaddr = ? and owner = ?''', [value, hwaddr, user]) def delete_device(conn, hwaddr, user): return conn.execute(''' delete from devices where hwaddr = ? and owner = ?''', [hwaddr, user]) @app.route('/devices///') @login_required def device(id, action): user = current_user.id if action == 'hide': set_ignored(g.db, id, user, True) if action == 'show': set_ignored(g.db, id, user, False) if action == 'delete': delete_device(g.db, id, user) return redirect(url_for('account')) @app.route('/admin') @cap_required('staff') def admin(): data = now_at() return render_template('admin.html', data=data) @app.before_first_request def setup(): updater = DhcpdUpdater(app.config['LEASE_FILE'], app.config['TIMEOUT'], app.config['LEASE_OFFSET']) updater.start() app.updater = updater port = 8080 if __name__ == '__main__': app.logger.setLevel(logging.DEBUG) app.run('0.0.0.0', 8080, debug=True)