summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorvuko <vuko@hackerspace.pl>2020-06-13 12:13:19 +0200
committervuko <vuko@hackerspace.pl>2020-06-13 12:13:19 +0200
commit68508440d10143fe7429a5d6cc6ae31264cfecf2 (patch)
tree0e9c177cb432a91549b636254062aeca2adc8c48
parentf5cde26030a66608628c3581784074753b6ad679 (diff)
parentbc53e2fe6b20e8d6642c369ceec819b7c7221e3a (diff)
downloadcheckinator-master.tar.gz
checkinator-master.tar.bz2
checkinator-master.zip
preparation for parsing and webapp splitHEADmaster
-rw-r--r--.gitignore4
-rw-r--r--README.rst32
-rw-r--r--at.cfg.dist17
-rw-r--r--at.py422
-rw-r--r--at/__init__.py0
-rw-r--r--at/dhcp.py187
-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.py240
-rw-r--r--run.py5
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
diff --git a/at.py b/at.py
deleted file mode 100644
index 5a06ef2..0000000
--- a/at.py
+++ /dev/null
@@ -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)
diff --git a/run.py b/run.py
new file mode 100644
index 0000000..ff961ec
--- /dev/null
+++ b/run.py
@@ -0,0 +1,5 @@
+"""Entry point for running flask application"""
+
+import at.web
+from pathlib import Path
+app = at.web.app(Path(__file__).parent)