diff options
author | vuko <vuko@hackerspace.pl> | 2020-06-13 12:13:19 +0200 |
---|---|---|
committer | vuko <vuko@hackerspace.pl> | 2020-06-13 12:13:19 +0200 |
commit | 68508440d10143fe7429a5d6cc6ae31264cfecf2 (patch) | |
tree | 0e9c177cb432a91549b636254062aeca2adc8c48 | |
parent | f5cde26030a66608628c3581784074753b6ad679 (diff) | |
parent | bc53e2fe6b20e8d6642c369ceec819b7c7221e3a (diff) | |
download | checkinator-master.tar.gz checkinator-master.tar.bz2 checkinator-master.tar.xz checkinator-master.zip |
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | README.rst | 32 | ||||
-rw-r--r-- | at.cfg.dist | 17 | ||||
-rw-r--r-- | at.py | 422 | ||||
-rw-r--r-- | at/__init__.py | 0 | ||||
-rw-r--r-- | at/dhcp.py | 187 | ||||
-rw-r--r-- | at/templates/account.html (renamed from templates/account.html) | 0 | ||||
-rw-r--r-- | at/templates/admin.html (renamed from templates/admin.html) | 0 | ||||
-rw-r--r-- | at/templates/basic.html (renamed from templates/basic.html) | 0 | ||||
-rw-r--r-- | at/templates/claim.html (renamed from templates/claim.html) | 0 | ||||
-rw-r--r-- | at/templates/login.html (renamed from templates/login.html) | 0 | ||||
-rw-r--r-- | at/templates/main.html (renamed from templates/main.html) | 0 | ||||
-rw-r--r-- | at/templates/post_claim.html (renamed from templates/post_claim.html) | 0 | ||||
-rw-r--r-- | at/templates/register.html (renamed from templates/register.html) | 0 | ||||
-rw-r--r-- | at/web.py | 240 | ||||
-rw-r--r-- | run.py | 5 |
16 files changed, 479 insertions, 428 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f2e0f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +at.cfg +at.db +*.egg-info +venv diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..28e4f8b --- /dev/null +++ b/README.rst @@ -0,0 +1,32 @@ +`Warsaw Hackerspace`_ presence tracker hosted on https://at.hackersapce.pl. It +uses dhcpd.leases file to track MAC adressess of devices connected to hs LAN +network. + +.. _Warsaw Hackerspace: https://hackerspace.pl + +Setup +----- +.. code:: bash + + cp at.cfg.dist at.dist + + # edit config file using your favourite editor + $EDITOR at.cfg + + # create new database file (or copy existing one) + sqlite3 at.db < dbsetup.sql + + # create python virtual environment + python3 -m venv vevnv + ./venv/bin/python3 -m pip install -r requirements + ./venv/bin/python3 -m pip install gunicorn + +Running +------- +.. code:: bash + + ./venv/bin/gunicorn run:app + +When running on OpenBSD make sure to pass '--no-sendfile' argument to gunicorn +command. This will prevent AttributeError on os.sendfile that seems to be +missing in this marvelous OS-es python3 stdlib. diff --git a/at.cfg.dist b/at.cfg.dist index b2e0720..3f32ce2 100644 --- a/at.cfg.dist +++ b/at.cfg.dist @@ -1,21 +1,26 @@ DB = './at.db' +DEBUG = False CAP_FILE = './dhcp-cap' LEASE_FILE = './dhcpd.leases' LEASE_OFFSET = -60 * 60 TIMEOUT = 1500 -WIKI_URL = 'http://hackerspace.pl/wiki/doku.php?id=people:%(login)s:start' +WIKI_URL = 'https://wiki.hackerspace.pl/people:%(login)s:start' -CLAIMABLE_PREFIX = '10.8.0.' #'192.168.1.' +CLAIMABLE_PREFIX = '10.8.0.' CLAIMABLE_EXCLUDE = [ -# '127.0.0.1', + # '127.0.0.1', ] +SECRET_KEY = 'CHANGEME' + +SPACEAUTH_CONSUMER_KEY = 'checkinator' +SPACEAUTH_CONSUMER_SECRET = 'CHANGEME' + SPECIAL_DEVICES = { 'kektops': ('90:e6:ba:84'), - 'esps': ('ec:fa:bc', 'dc:4f:22', 'd8:a0:1d', 'b4:e6:2d', 'ac:d0:74', 'a4:7b:9d', 'a0:20:a6', '90:97:d5', '68:c6:3a', '60:01:94', '5c:cf:7f', '54:5a:a6', '30:ae:a4', '2c:3a:e8', '24:b2:de', '24:0a:c4', '18:fe:34'), + 'esps': ('ec:fa:bc', 'dc:4f:22', 'd8:a0:1d', 'b4:e6:2d', 'ac:d0:74', 'a4:7b:9d', 'a0:20:a6', '90:97:d5', '68:c6:3a', '60:01:94', '5c:cf:7f', '54:5a:a6', '30:ae:a4', '2c:3a:e8', '24:b2:de', '24:0a:c4', '18:fe:34', '38:2b:78', 'bc:dd:c2:'), + 'vms': ('52:54:00'), # craptrap VMs } -SECRET_KEY = 'adaba' - PROXY_FIX = False @@ -1,422 +0,0 @@ -#!/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.parse 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(iter(request.form.items())) - - -@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 list(self.active.items()): - if now - atime > self.timeout: - del self.active[addr] - - def get_active_devices(self): - with self.lock: - self.purge_stale() - r = dict(self.active) - return r - - def get_device(self, ip): - active_devices = iter(self.get_active_devices().items()) - 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() - with self.lock: - 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) - - -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.position = 0 - self.last_modified = 0 - Updater.__init__(self, *a, **kw) - - def file_changed(self, f): - """Callback on changed lease file - - Args: - f: Lease file. File offset can be used to skip already parsed lines. - - Returns: New byte offset pointing after last parsed byte. - """ - return f.tell() - - def _trigger_update(self): - app.logger.info('Lease file changed, updating') - with open(self.lease_file, 'r') as f: - f.seek(self.position) - self.position = self.file_changed(f) - - def run(self): - """Periodicaly check if file has changed - - From ISC DHCPD manual: - - New leases are appended to the end of the dhcpd.leases file. In - order to prevent the file from becoming arbitrarily large, from - time to time dhcpd creates a new dhcpd.leases file from its in-core - lease database. Once this file has been written to disk, the old - file is renamed dhcpd.leases~, and the new file is renamed - dhcpd.leases. - """ - while True: - try: - stat = os.stat(self.lease_file) - mtime = stat.st_mtime - size = stat.st_size - if size < self.position: - app.logger.info('leases file changed - reseting pointer') - self.position = 0 - try: - # checking if DHCPD performed cleanup - # cleanup during operation seems to be currently broken - # on customs so this could never execute - purge_time = os.stat(self.lease_file + '~').st_mtime - if purge_time > self.last_modified: - app.logger.info('leases file purged - reseting pointer') - self.position = 0 - except FileNotFoundError: - pass - if mtime > self.last_modified: - self._trigger_update() - self.last_modified = mtime - sleep(5.0) - except Exception as e: - app.logger.exception('Exception in updater') - sleep(10.0) - - -class DnsmasqUpdater(MtimeUpdater): - def file_changed(self, f): - raise NotImplementedError( - "This was not tested after adding differential update") - for line in f: - ts, hwaddr, ip, name, client_id = line.split(' ') - self.update(hwaddr, int(ts), ip, name) - return f.tell() - - -class DhcpdUpdater(MtimeUpdater): - def file_changed(self, f): - lease = False - # for use by next-line logic - ip = None - hwaddr = None - atime = None - offset = f.tell() - while True: - # using readline because iter(file) blocks file.tell usage - line = f.readline() - if not line: - return offset - 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('}')): - offset = f.tell() - 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(xxx_todo_changeme): - (user, atime) = xxx_todo_changeme - if user == 'greenmaker': - user = 'dreammaker' - return { - 'login': user, - 'timestamp': atime, - 'pretty_time': strfts(atime), - } - result = {} - result['users'] = list(map(prettify_user, data.pop('users'))) - result.update((k, len(v)) for k, v in list(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, list(devices.keys()))) - device_infos.sort(key=lambda di: devices[di.hwaddr]) - unknown = set(devices.keys()) - set(d.hwaddr for d in device_infos) - # das kektop sorting maschine - for name, prefixes in list(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 and not info.ignored: - users[info.owner] = devices[info.hwaddr][0] - result['users'] = sorted(list(users.items()), key=lambda u_a: u_a[1], 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/<id>/<action>/') -@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) diff --git a/at/__init__.py b/at/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/at/__init__.py diff --git a/at/dhcp.py b/at/dhcp.py new file mode 100644 index 0000000..2874e27 --- /dev/null +++ b/at/dhcp.py @@ -0,0 +1,187 @@ +import threading +import traceback +import os + +import logging + +from time import sleep, time, mktime +from datetime import datetime + +logger = logging.getLogger(__name__) + +def strfts(ts, format='%d/%m/%Y %H:%M'): + return datetime.fromtimestamp(ts).strftime(format) + +class Updater(threading.Thread): + def __init__(self, timeout, lease_offset=0, logger=logger, *a, **kw): + self.timeout = timeout + self.lock = threading.Lock() + self.lease_offset = lease_offset + self.logger = logger + self.active = {} + threading.Thread.__init__(self, *a, **kw) + self.daemon = True + + def purge_stale(self): + now = time() + for addr, (atime, ip, name) in list(self.active.items()): + if now - atime > self.timeout: + del self.active[addr] + + def get_active_devices(self): + with self.lock: + self.purge_stale() + r = dict(self.active) + return r + + def get_device(self, ip): + active_devices = iter(self.get_active_devices().items()) + 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() + with self.lock: + if hwaddr not in self.active or self.active[hwaddr][0] < atime: + self.active[hwaddr] = (atime, ip, name) + self.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: + with open(self.cap_file, 'r', buffering=0) as f: + self.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) + self.logger.warning('Cap file %s closed, reopening', + self.cap_file) + except Exception as e: + self.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.position = 0 + self.last_modified = 0 + Updater.__init__(self, *a, **kw) + + def file_changed(self, f): + """Callback on changed lease file + + Args: + f: Lease file. File offset can be used to skip already parsed lines. + + Returns: New byte offset pointing after last parsed byte. + """ + return f.tell() + + def _trigger_update(self): + self.logger.info('Lease file changed, updating') + with open(self.lease_file, 'r') as f: + f.seek(self.position) + self.position = self.file_changed(f) + + def run(self): + """Periodicaly check if file has changed + + From ISC DHCPD manual: + + New leases are appended to the end of the dhcpd.leases file. In + order to prevent the file from becoming arbitrarily large, from + time to time dhcpd creates a new dhcpd.leases file from its in-core + lease database. Once this file has been written to disk, the old + file is renamed dhcpd.leases~, and the new file is renamed + dhcpd.leases. + """ + while True: + try: + stat = os.stat(self.lease_file) + mtime = stat.st_mtime + size = stat.st_size + if size < self.position: + self.logger.info('leases file changed - reseting pointer') + self.position = 0 + try: + # checking if DHCPD performed cleanup + # cleanup during operation seems to be currently broken + # on customs so this could never execute + purge_time = os.stat(self.lease_file + '~').st_mtime + if purge_time > self.last_modified: + self.logger.info('leases file purged - reseting pointer') + self.position = 0 + except FileNotFoundError: + pass + if mtime > self.last_modified: + self._trigger_update() + self.last_modified = mtime + sleep(5.0) + except Exception as e: + self.logger.exception('Exception in updater') + sleep(10.0) + + +class DnsmasqUpdater(MtimeUpdater): + def file_changed(self, f): + raise NotImplementedError( + "This was not tested after adding differential update") + for line in f: + ts, hwaddr, ip, name, client_id = line.split(' ') + self.update(hwaddr, int(ts), ip, name) + return f.tell() + + +class DhcpdUpdater(MtimeUpdater): + def file_changed(self, f): + lease = False + # for use by next-line logic + ip = None + hwaddr = None + atime = None + offset = f.tell() + while True: + # using readline because iter(file) blocks file.tell usage + line = f.readline() + if not line: + return offset + 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('}')): + offset = f.tell() + 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 diff --git a/templates/account.html b/at/templates/account.html index 5e8272a..5e8272a 100644 --- a/templates/account.html +++ b/at/templates/account.html diff --git a/templates/admin.html b/at/templates/admin.html index c2fcb4b..c2fcb4b 100644 --- a/templates/admin.html +++ b/at/templates/admin.html diff --git a/templates/basic.html b/at/templates/basic.html index 4f27050..4f27050 100644 --- a/templates/basic.html +++ b/at/templates/basic.html diff --git a/templates/claim.html b/at/templates/claim.html index 05f2e0b..05f2e0b 100644 --- a/templates/claim.html +++ b/at/templates/claim.html diff --git a/templates/login.html b/at/templates/login.html index b4174b8..b4174b8 100644 --- a/templates/login.html +++ b/at/templates/login.html diff --git a/templates/main.html b/at/templates/main.html index 3ac5c50..3ac5c50 100644 --- a/templates/main.html +++ b/at/templates/main.html diff --git a/templates/post_claim.html b/at/templates/post_claim.html index 858323b..858323b 100644 --- a/templates/post_claim.html +++ b/at/templates/post_claim.html diff --git a/templates/register.html b/at/templates/register.html index 9c6399f..9c6399f 100644 --- a/templates/register.html +++ b/at/templates/register.html diff --git a/at/web.py b/at/web.py new file mode 100644 index 0000000..288a5bc --- /dev/null +++ b/at/web.py @@ -0,0 +1,240 @@ +import json +import requests +import sqlite3 +from datetime import datetime +from collections import namedtuple +from urllib.parse import urlencode +from functools import wraps +from flask import Flask, render_template, abort, g, \ + redirect, request, flash, url_for, make_response + +from .dhcp import DhcpdUpdater + +from spaceauth import SpaceAuth, login_required, current_user, cap_required + +DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'name', 'owner', 'ignored']) + +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 req_to_ctx(): + return dict(iter(request.form.items())) + +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) + +def app(instance_path): + app = Flask('at', instance_path=instance_path, instance_relative_config=True) + 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) + + 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 + + + @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() + + + @app.route('/') + def main_view(): + return render_template('main.html', **now_at()) + + + @app.route('/api') + def list_all(): + data = now_at() + + def prettify_user(xxx_todo_changeme): + (user, atime) = xxx_todo_changeme + if user == 'greenmaker': + user = 'dreammaker' + return { + 'login': user, + 'timestamp': atime, + 'pretty_time': strfts(atime), + } + result = {} + result['users'] = list(map(prettify_user, data.pop('users'))) + result.update((k, len(v)) for k, v in list(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, list(devices.keys()))) + device_infos.sort(key=lambda di: devices[di.hwaddr]) + unknown = set(devices.keys()) - set(d.hwaddr for d in device_infos) + # das kektop sorting maschine + for name, prefixes in list(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 and not info.ignored: + users[info.owner] = devices[info.hwaddr][0] + result['users'] = sorted(list(users.items()), key=lambda u_a: u_a[1], 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/<id>/<action>/') + @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']) + app.logger.info('parsing leases file for the first time') + updater._trigger_update() + app.logger.info('leases file parsed') + updater.start() + app.updater = updater + + setup() + + return app + +if __name__ == '__main__': + app.logger.setLevel(logging.DEBUG) + app.run('0.0.0.0', 8080, debug=True) @@ -0,0 +1,5 @@ +"""Entry point for running flask application""" + +import at.web +from pathlib import Path +app = at.web.app(Path(__file__).parent) |