fuck this thing and fuck it again

master
q3k 2015-03-16 22:48:35 +01:00
parent dbd8b53cfd
commit 9b19bb8a81
1 changed files with 118 additions and 56 deletions

174
at.py
View File

@ -7,135 +7,161 @@ import json
import requests import requests
import os import os
import logging import logging
from flask import Flask, render_template, abort, g, \ from flask import Flask, render_template, abort, g, \
redirect, session, request, flash, url_for redirect, session, request, flash, url_for, make_response
from werkzeug.contrib.fixers import ProxyFix
from datetime import datetime from datetime import datetime
from wsgiref import simple_server
from pesto import Response, dispatcher_app
from time import sleep, time, mktime from time import sleep, time, mktime
from collections import namedtuple from collections import namedtuple
from urllib import urlencode from urllib import urlencode
from hashlib import sha256
app = Flask('at') app = Flask('at')
app.config.from_pyfile('at.cfg') app.config.from_pyfile('at.cfg')
app.wsgi_app = ProxyFix(app.wsgi_app)
app.jinja_env.add_extension('jinja2.ext.i18n') app.jinja_env.add_extension('jinja2.ext.i18n')
app.jinja_env.install_null_translations() app.jinja_env.install_null_translations()
app.updater = None app.updater = None
from functools import wraps from functools import wraps
def v4addr():
r_addr = request.remote_addr
if r_addr.startswith('::ffff:'):
r_addr = r_addr[7:]
return r_addr
def restrict_ip(prefix='', exclude=[]): def restrict_ip(prefix='', exclude=[]):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def func(*a, **kw): def func(*a, **kw):
r_addr = request.remote_addr r_addr = v4addr()
if not r_addr.startswith(prefix) or r_addr in exclude: if not r_addr.startswith(prefix) or r_addr in exclude:
abort(403) abort(403)
return f(*a, **kw) return f(*a, **kw)
return func return func
return decorator return decorator
def req_to_ctx(): def req_to_ctx():
return dict(request.form.iteritems()) return dict(request.form.iteritems())
@app.template_filter('strfts') @app.template_filter('strfts')
def strfts(ts, format='%d/%m/%Y %H:%M'): def strfts(ts, format='%d/%m/%Y %H:%M'):
return datetime.fromtimestamp(ts).strftime(format) return datetime.fromtimestamp(ts).strftime(format)
@app.template_filter('wikiurl') @app.template_filter('wikiurl')
def wikiurl(user): def wikiurl(user):
return app.config['WIKI_URL'] % { 'login': user } return app.config['WIKI_URL'] % {'login': user}
@app.before_request @app.before_request
def make_connection(): def make_connection():
conn = sqlite3.connect(app.config['DB']) conn = sqlite3.connect(app.config['DB'])
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.isolation_level = None # for autocommit mode conn.isolation_level = None # for autocommit mode
g.db = conn g.db = conn
@app.teardown_request @app.teardown_request
def close_connection(exception): def close_connection(exception):
g.db.close() g.db.close()
DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'name', 'owner', 'ignored']) DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'name', 'owner', 'ignored'])
def get_device_info(conn, hwaddr): def get_device_info(conn, hwaddr):
return list(get_device_infos(conn, (hwaddrs,)))[0] return list(get_device_infos(conn, (hwaddr,)))[0]
def get_device_infos(conn, hwaddrs): def get_device_infos(conn, hwaddrs):
stmt = '''select hwaddr, name, ignored, owner from in_clause = '({})'.format(', '.join(['?'] * len(hwaddrs)))
devices where devices.hwaddr in (''' + ','.join(['?'] * len(hwaddrs)) + ')' stmt = '''select hwaddr, name, ignored, owner from
devices where devices.hwaddr in ''' + in_clause
for row in conn.execute(stmt, hwaddrs): for row in conn.execute(stmt, hwaddrs):
owner = row['owner'] or '' owner = row['owner'] or ''
ignored = row['ignored'] ignored = row['ignored']
yield DeviceInfo(row['hwaddr'], row['name'], owner, ignored) yield DeviceInfo(row['hwaddr'], row['name'], owner, ignored)
class Updater(threading.Thread): class Updater(threading.Thread):
def __init__(self, timeout, lease_offset = 0, *a, **kw): def __init__(self, timeout, lease_offset=0, *a, **kw):
self.timeout = timeout self.timeout = timeout
self.lock = threading.Lock() self.lock = threading.Lock()
self.lease_offset = lease_offset self.lease_offset = lease_offset
self.active = {} self.active = {}
threading.Thread.__init__(self, *a, **kw) threading.Thread.__init__(self, *a, **kw)
self.daemon = True self.daemon = True
def purge_stale(self): def purge_stale(self):
now = time() now = time()
for addr, (atime, ip, name) in self.active.items(): for addr, (atime, ip, name) in self.active.items():
if now - atime > self.timeout: if now - atime > self.timeout:
del self.active[addr] del self.active[addr]
def get_active_devices(self): def get_active_devices(self):
self.lock.acquire() self.lock.acquire()
self.purge_stale() self.purge_stale()
r = dict(self.active) r = dict(self.active)
self.lock.release() self.lock.release()
return r return r
def get_device(self, ip): def get_device(self, ip):
for hwaddr, (atime, dip, name) in \ active_devices = self.get_active_devices().iteritems()
self.get_active_devices().iteritems(): for hwaddr, (atime, dip, name) in active_devices:
if ip == dip: if ip == dip:
return hwaddr, name return hwaddr, name
return None, None return None, None
def update(self, hwaddr, atime = None, ip = None, name = None):
def update(self, hwaddr, atime=None, ip=None, name=None):
if atime: if atime:
atime -= self.lease_offset atime -= self.lease_offset
else: else:
atime = time() atime = time()
self.lock.acquire() self.lock.acquire()
self.active[hwaddr] = (atime, ip, name) self.active[hwaddr] = (atime, ip, name)
self.lock.release() self.lock.release()
app.logger.info('updated %s with atime %s and ip %s', app.logger.info('updated %s with atime %s and ip %s',
hwaddr, strfts(atime), ip) hwaddr, strfts(atime), ip)
class CapUpdater(Updater): class CapUpdater(Updater):
def __init__(self, cap_file, *a, **kw): def __init__(self, cap_file, *a, **kw):
self.cap_file = cap_file self.cap_file = cap_file
Updater.__init__(self, *a, **kw) Updater.__init__(self, *a, **kw)
def run(self): def run(self):
while True: while True:
try: try:
with open(self.cap_file, 'r', buffering=0) as f: with open(self.cap_file, 'r', buffering=0) as f:
app.logger.info('Updater ready on cap file %s', self.cap_file) app.logger.info('Updater ready on cap file %s',
while True: self.cap_file)
hwaddr = f.readline().strip() lines = [l.strip() for l in f.read().split('\n')]
if not hwaddr: for hwaddr in lines:
break if hwaddr:
self.update(hwaddr) self.update(hwaddr)
app.logger.warning('Cap file %s closed, reopening', self.cap_file) app.logger.warning('Cap file %s closed, reopening',
self.cap_file)
except Exception as e: except Exception as e:
app.logger.error('Updater got an exception:\n' + \ app.logger.error('Updater got an exception:\n' +
traceback.format_exc(e)) traceback.format_exc(e))
sleep(10.0) sleep(10.0)
class MtimeUpdater(Updater): class MtimeUpdater(Updater):
def __init__(self, lease_file, *a, **kw): def __init__(self, lease_file, *a, **kw):
self.lease_file = lease_file self.lease_file = lease_file
self.last_modified = 0 self.last_modified = 0
Updater.__init__(self, *a, **kw) Updater.__init__(self, *a, **kw)
def file_changed(self, f): def file_changed(self, f):
pass pass
def run(self): def run(self):
while True: while True:
try: try:
@ -147,19 +173,23 @@ class MtimeUpdater(Updater):
self.last_modified = mtime self.last_modified = mtime
sleep(3.0) sleep(3.0)
except Exception as e: except Exception as e:
app.logger.error('Updater got an exception:\n' + \ app.logger.error('Updater got an exception:\n' +
traceback.format_exc(e)) traceback.format_exc(e))
sleep(10.0) sleep(10.0)
class DnsmasqUpdater(MtimeUpdater): class DnsmasqUpdater(MtimeUpdater):
def file_changed(self, f): def file_changed(self, f):
for line in f: for line in f:
ts, hwaddr, ip, name, client_id = line.split(' ') ts, hwaddr, ip, name, client_id = line.split(' ')
self.update(hwaddr, int(ts), ip, name) self.update(hwaddr, int(ts), ip, name)
class DhcpdUpdater(MtimeUpdater): class DhcpdUpdater(MtimeUpdater):
def file_changed(self, f): def file_changed(self, f):
lease = False lease = False
# for use by next-line logic
ip = None
for line in f: for line in f:
line = line.split('#')[0] line = line.split('#')[0]
cmd = line.strip().split() cmd = line.strip().split()
@ -168,7 +198,8 @@ class DhcpdUpdater(MtimeUpdater):
if lease: if lease:
field = cmd[0] field = cmd[0]
if(field == 'starts'): if(field == 'starts'):
dt = datetime.strptime(' '.join(cmd[2:]), '%Y/%m/%d %H:%M:%S;') dt = datetime.strptime(' '.join(cmd[2:]),
'%Y/%m/%d %H:%M:%S;')
atime = mktime(dt.utctimetuple()) atime = mktime(dt.utctimetuple())
if(field == 'client-hostname'): if(field == 'client-hostname'):
name = cmd[1][1:-2] name = cmd[1][1:-2]
@ -182,14 +213,17 @@ class DhcpdUpdater(MtimeUpdater):
ip = cmd[1] ip = cmd[1]
name, hwaddr, atime = [None] * 3 name, hwaddr, atime = [None] * 3
lease = True lease = True
@app.route('/') @app.route('/')
def main_view(): def main_view():
return render_template('main.html', **now_at()) return render_template('main.html', **now_at())
@app.route('/api') @app.route('/api')
def list_all(): def list_all():
result = now_at() result = now_at()
def prettify_user((user, atime)): def prettify_user((user, atime)):
return { return {
'login': user, 'login': user,
@ -198,83 +232,105 @@ def list_all():
} }
result['users'] = map(prettify_user, result['users']) result['users'] = map(prettify_user, result['users'])
result['unknown'] = len(result['unknown']) result['unknown'] = len(result['unknown'])
return json.dumps(result) res = make_response(json.dumps(result), 200)
res.headers['Access-Control-Allow-Origin'] = '*'
return res
def now_at(): def now_at():
devices = app.updater.get_active_devices() devices = app.updater.get_active_devices()
device_infos = list(get_device_infos(g.db, devices.keys())) device_infos = list(get_device_infos(g.db, devices.keys()))
device_infos.sort(key=lambda di: devices.__getitem__) device_infos.sort(key=lambda di: devices.__getitem__)
users = list(dict((info.owner, devices[info.hwaddr][0]) for info in device_infos
if info.owner and not info.ignored).iteritems())
users.sort(key=lambda (u, a): a, reverse=True)
unknown = set(devices.keys()) - set(d.hwaddr for d in device_infos) unknown = set(devices.keys()) - set(d.hwaddr for d in device_infos)
return dict(users=users, unknown=unknown)
restrict_to_hs = restrict_ip(prefix=app.config['CLAIMABLE_PREFIX'], users = {}
exclude=app.config['CLAIMABLE_EXCLUDE']) for info in device_infos:
if info.owner not in users:
users[info.owner] = devices[info.hwaddr][0]
users_sorted = sorted(users.items(), key=lambda (u, a): a, reverse=True)
return dict(users=users_sorted, unknown=unknown)
restrict_to_hs = restrict_ip(prefix=app.config['CLAIMABLE_PREFIX'],
exclude=app.config['CLAIMABLE_EXCLUDE'])
@app.route('/login', methods=['GET']) @app.route('/login', methods=['GET'])
def login_form(): def login_form():
return render_template('login.html', **req_to_ctx()) return render_template('login.html', **req_to_ctx())
@app.route('/login', methods=['POST']) @app.route('/login', methods=['POST'])
def login(): def login():
login = request.form.get('login', '').lower() login = request.form.get('login', '').lower()
pwd = request.form.get('password', '') pwd = request.form.get('password', '')
goto = request.values.get('goto') or '/' goto = request.values.get('goto') or '/'
data = dict(login=login, password=pwd)
if requests.post('https://auth.hackerspace.pl', verify=False, if requests.post('https://auth.hackerspace.pl', verify=False,
data = { 'login': login, 'password': pwd }).status_code == 200: data=data).status_code == 200:
session['login'] = login session['login'] = login
return redirect(goto) return redirect(goto)
else: else:
flash('Username or password invalid', category='error') flash('Username or password invalid', category='error')
return login_form() return login_form()
@app.route('/logout') @app.route('/logout')
def logout(): def logout():
session.clear() session.clear()
return redirect('/') return redirect('/')
def login_required(f): def login_required(f):
@wraps(f) @wraps(f)
def func(*a, **kw): def func(*a, **kw):
if 'login' not in session: if 'login' not in session:
flash('You must log in to continue', category='error') flash('You must log in to continue', category='error')
return redirect('/login?' + return redirect('/login?' +
urlencode({'goto': request.path})) urlencode({'goto': request.path}))
return f(*a, **kw) return f(*a, **kw)
return func return func
@app.route('/claim', methods=['GET']) @app.route('/claim', methods=['GET'])
@restrict_to_hs @restrict_to_hs
@login_required @login_required
def claim_form(): def claim_form():
hwaddr, name = app.updater.get_device(request.remote_addr) hwaddr, name = app.updater.get_device(v4addr())
return render_template('claim.html', hwaddr=hwaddr, name=name) return render_template('claim.html', hwaddr=hwaddr, name=name)
@app.route('/claim', methods=['POST']) @app.route('/claim', methods=['POST'])
@restrict_to_hs @restrict_to_hs
@login_required @login_required
def claim(): def claim():
hwaddr, lease_name = app.updater.get_device(request.remote_addr) hwaddr, lease_name = app.updater.get_device(v4addr())
ctx = None ctx = None
if not hwaddr: if not hwaddr:
ctx = { 'error': 'Invalid device.' } ctx = dict(error='Invalid device.')
else: else:
login = session['login'] login = session['login']
try: try:
g.db.execute('insert into devices (hwaddr, name, owner, ignored)\ g.db.execute('''
values (?, ?, ?, ?)', [hwaddr, request.form['name'], login, False]) insert into devices (hwaddr, name, owner, ignored) values (?, ?, ?, ?)''',
[hwaddr, request.form['name'], login, False])
ctx = {} ctx = {}
except sqlite3.Error as e: except sqlite3.Error:
ctx = { 'error': 'Could not add device! Perhaps someone claimed it?' } error = 'Could not add device! Perhaps someone claimed it?'
ctx = dict(error=error)
return render_template('post_claim.html', **ctx) return render_template('post_claim.html', **ctx)
def get_user_devices(conn, user): def get_user_devices(conn, user):
devs = conn.execute('select hwaddr, name, ignored from devices where\ devs = conn.execute('select hwaddr, name, ignored from devices where\
owner = ?', [user]) owner = ?', [user])
return (DeviceInfo(row['hwaddr'], row['name'], user, row['ignored']) for device_info = []
row in devs) 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']) @app.route('/account', methods=['GET'])
@login_required @login_required
@ -282,13 +338,18 @@ def account():
devices = get_user_devices(g.db, session['login']) devices = get_user_devices(g.db, session['login'])
return render_template('account.html', devices=devices) return render_template('account.html', devices=devices)
def set_ignored(conn, hwaddr, user, value): def set_ignored(conn, hwaddr, user, value):
return conn.execute('update devices set ignored = ? where hwaddr = ? and owner = ?', return conn.execute('''
[value, hwaddr, user]) update devices set ignored = ? where hwaddr = ? and owner = ?''',
[value, hwaddr, user])
def delete_device(conn, hwaddr, user): def delete_device(conn, hwaddr, user):
return conn.execute('delete from devices where hwaddr = ? and owner = ?', return conn.execute('''
[hwaddr, user]) delete from devices where hwaddr = ? and owner = ?''',
[hwaddr, user])
@app.route('/devices/<id>/<action>/') @app.route('/devices/<id>/<action>/')
@login_required @login_required
@ -302,10 +363,11 @@ def device(id, action):
delete_device(g.db, id, user) delete_device(g.db, id, user)
return redirect(url_for('account')) return redirect(url_for('account'))
@app.before_first_request @app.before_first_request
def setup(): def setup():
updater = DhcpdUpdater(app.config['LEASE_FILE'], app.config['TIMEOUT'], updater = DhcpdUpdater(app.config['LEASE_FILE'], app.config['TIMEOUT'],
app.config['LEASE_OFFSET']) app.config['LEASE_OFFSET'])
updater.start() updater.start()
app.updater = updater app.updater = updater