fuck this thing and fuck it again
parent
dbd8b53cfd
commit
9b19bb8a81
174
at.py
174
at.py
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue