diff options
author | vuko <vuko@hackerspace.pl> | 2020-06-13 12:03:53 +0200 |
---|---|---|
committer | vuko <vuko@hackerspace.pl> | 2020-06-13 12:03:53 +0200 |
commit | de59a1eb03c24a04a18be38bb4ef025cd7e3d4e7 (patch) | |
tree | 60fb120322d0e0bc4283f259b5296b72ddbddb5c /at | |
parent | 21f8891b0f954c531770fd6ec44270d5787eeb0e (diff) | |
download | checkinator-de59a1eb03c24a04a18be38bb4ef025cd7e3d4e7.tar.gz checkinator-de59a1eb03c24a04a18be38bb4ef025cd7e3d4e7.tar.bz2 checkinator-de59a1eb03c24a04a18be38bb4ef025cd7e3d4e7.zip |
split at/__init__.py into dhcpd.py and web.py
Diffstat (limited to 'at')
-rw-r--r-- | at/__init__.py | 422 | ||||
-rw-r--r-- | at/dhcp.py | 187 | ||||
-rw-r--r-- | at/templates/account.html | 34 | ||||
-rw-r--r-- | at/templates/admin.html | 13 | ||||
-rw-r--r-- | at/templates/basic.html | 24 | ||||
-rw-r--r-- | at/templates/claim.html | 21 | ||||
-rw-r--r-- | at/templates/login.html | 17 | ||||
-rw-r--r-- | at/templates/main.html | 34 | ||||
-rw-r--r-- | at/templates/post_claim.html | 11 | ||||
-rw-r--r-- | at/templates/register.html | 36 | ||||
-rw-r--r-- | at/web.py | 240 |
11 files changed, 617 insertions, 422 deletions
diff --git a/at/__init__.py b/at/__init__.py index 5a06ef2..e69de29 100644 --- a/at/__init__.py +++ b/at/__init__.py @@ -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/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/at/templates/account.html b/at/templates/account.html new file mode 100644 index 0000000..5e8272a --- /dev/null +++ b/at/templates/account.html @@ -0,0 +1,34 @@ +{% extends "basic.html" %} +{% block content %} +<a href="/">Back to homepage</a> +<h2>Account settings</h2> +{% for msg in get_flashed_messages(True) %} + <p class="{{ msg[0] }}">{{ msg[1] }}</p> +{% endfor %} +<h3>Claimed devices</h3> +<table class="devices"> + <tr> + <th>MAC</th> + <th>Device name</th> + <th>Visible</th> + <th>Toggle visibility</th> + <th>Delete</th> + </tr> +{% for device in devices %} + <tr> + <td>{{ device.hwaddr }}</td> + <td>{{ device.name }}</td> + {% if device.ignored %} + <td class="invisible">invisible</td> + <td><a href="devices/{{ device.hwaddr }}/show">make visible</a></td> + {% else %} + <td class="visible">visible</td> + <td><a href="devices/{{ device.hwaddr }}/hide">make invisible</a></td> + {% endif %} + <td><a href="devices/{{ device.hwaddr }}/delete">delete device</a></td> + </tr> + </tbody> +{% endfor%} +</table> +<p><a href="/claim">claim this device</a> +{% endblock %} diff --git a/at/templates/admin.html b/at/templates/admin.html new file mode 100644 index 0000000..c2fcb4b --- /dev/null +++ b/at/templates/admin.html @@ -0,0 +1,13 @@ +{% extends "basic.html" %} +{% block content %} +<table class="devices"> + <tr> + <th>MAC</th> + <th>Device type</th> + </tr> + {% for key, l in data.items() %} + {% for item in l %} + <tr><td>{{ item }}</td><td>{{ key }}</td></tr> + {% endfor %} + {% endfor %} +{% endblock %} diff --git a/at/templates/basic.html b/at/templates/basic.html new file mode 100644 index 0000000..4f27050 --- /dev/null +++ b/at/templates/basic.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> + <head> + {% block head %} + <link rel="stylesheet" type="text/css" href="/static/css/basic.css"> + <title>{% block title %}Now at hackerspace{% endblock %}</title> + {% endblock %} + </head> + <body> + {% block body %} + <div class="login"> + {% if current_user.is_authenticated %} + logged in as {{ current_user.id }} | + <a href="account">account</a> | + <a href="{{ url_for('spaceauth.logout') }}">log out</a> + {% else %} + <a href="{{ url_for('spaceauth.login') }}">login</a> + {% endif %} + </div> + {% block content %} + {% endblock %} + {% endblock %} + </body> +</html> diff --git a/at/templates/claim.html b/at/templates/claim.html new file mode 100644 index 0000000..05f2e0b --- /dev/null +++ b/at/templates/claim.html @@ -0,0 +1,21 @@ +{% extends "basic.html" %} +{% block content %} + <h2>Claiming a device</h2> + {% if not hwaddr %} + <p class="error">Unknown MAC. Are you sure you're in the hackerspace?</p> + {% else %} + You are about to claim <strong>{{ hwaddr }}</strong> as <strong>{{ current_user.id }}</strong>. Do you wish to continue? + <table> + <form action="" method="post"> + <label><tr> + <td>Device name (optional):</td> + <td><input type="text" name="name" value="{{ name }}"></td> + </tr></label> + <tr> + <td><input type="submit" value="yes"></td> + </form> + <td><a href="/"><button>no</button></button></td> + </tr> + </table> + {% endif %} +{% endblock %} diff --git a/at/templates/login.html b/at/templates/login.html new file mode 100644 index 0000000..b4174b8 --- /dev/null +++ b/at/templates/login.html @@ -0,0 +1,17 @@ +<html> +<body> +<h2>Login</h2> +{% for error in get_flashed_messages() %} +<p class="error">{{ error }}</p> +{% endfor %} +<form action="" method="POST"> +<table> +<label><tr><td>login</td><td><input type="text" name="login" value="{{ login }}"></td></tr></label> +<label><tr><td>password</td><td><input type="password" name="password"></td></tr></label> +{% if goto %} +<input type="hidden" name="goto" value="{{ goto }}"> +{% endif %} +<tr><td></td><td><input type="submit" value="login"></input></td></tr> +</form> +</body> +</html> diff --git a/at/templates/main.html b/at/templates/main.html new file mode 100644 index 0000000..3ac5c50 --- /dev/null +++ b/at/templates/main.html @@ -0,0 +1,34 @@ +{% extends "basic.html" %} +{% block title %} +Now at hackerspace +{% endblock %} +{% block content %} + <h2>Now at hackerspace!</h2> + Recently at <a href="http://www.hackerspace.pl">hackerspace</a>: + <ul> + {% for user, timestamp in users %} + <li> + <a href="{{ user | wikiurl }}"> + {{ user }} ({{ timestamp|strfts() }}) + </a> + </li> + {% endfor %} + </ul> + {% trans n_unk=unknown|length %} + There is {{ n_unk }} unknown device operating. + {% pluralize %} + There are {{ n_unk }} unknown devices operating. + {% endtrans %} + {% trans n_kek=kektops|length %} + There is {{ n_kek }} unknown kektop operating. + {% pluralize %} + There are {{ n_kek }} unknown kektops operating. + {% endtrans %} + {% trans n_esp=esps|length %} + There is {{ n_esp }} unknown ESP operating. + {% pluralize %} + There are {{ n_esp }} unknown ESPs operating. + {% endtrans %} + <hr> + <a href="claim">Claim this device!</a> +{% endblock %} diff --git a/at/templates/post_claim.html b/at/templates/post_claim.html new file mode 100644 index 0000000..858323b --- /dev/null +++ b/at/templates/post_claim.html @@ -0,0 +1,11 @@ +{% extends "basic.html" %} +{% block content %} +{% if error %} +<h2>Error!</h2> +<p class="error">{{ error }}</p> +{% else %} +<h2>Success!</h2> +Congratulations, you just claimed this device! +<a href="/">go back</a> +{% endif %} +{% endblock %} diff --git a/at/templates/register.html b/at/templates/register.html new file mode 100644 index 0000000..9c6399f --- /dev/null +++ b/at/templates/register.html @@ -0,0 +1,36 @@ +{% extends "basic.html" %} +{% block content %} +<h2>Register a new account</h2> +{% for error in get_flashed_messages() %} +<p class="error">{{ error }}</p> +{% endfor %} +<form action="" method="POST"> + <table> + <label><tr> + <td>login</td> + <td><input type="text" name="login" value="{{ login }}"></td> + </tr></label> + <label><tr> + <td>password</td> + <td><input type="password" name="password"></td> + </tr></label> + <label><tr> + <td>confirm password</td> + <td><input type="password" name="password2"></td> + </tr></label> + <label><tr> + <td>homepage url</td> + <td><input type="text" name="url" value="{{ url }}"></td> + </tr></label> + <label><tr> + <td>use wiki page as url</td> + <td><input type="checkbox" name="wiki" + value="yes" {% if wiki == 'yes' %}checked="yes"{% endif %}></td> + </tr</label> + <tr> + <td></td> + <td><input type="submit" value="register"></input></td> + </tr> + </table> +</form> +{% endblock %} 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) |