2012-01-26 18:58:09 +00:00
|
|
|
#!/usr/bin/env python2
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import logging
|
|
|
|
import sqlite3
|
|
|
|
import threading
|
|
|
|
import traceback
|
2012-01-27 09:19:04 +00:00
|
|
|
import json
|
2012-01-27 17:46:30 +00:00
|
|
|
from flask import Flask, render_template, abort, redirect, session, request, flash, g
|
2012-01-26 18:58:09 +00:00
|
|
|
from datetime import datetime
|
|
|
|
from wsgiref import simple_server
|
|
|
|
from pesto import Response, dispatcher_app
|
|
|
|
from time import sleep, time
|
|
|
|
from collections import namedtuple
|
2012-01-27 07:31:32 +00:00
|
|
|
from urllib import urlencode
|
|
|
|
from hashlib import sha256
|
2012-01-26 18:58:09 +00:00
|
|
|
|
|
|
|
import config
|
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
app = Flask('at')
|
|
|
|
app.secret_key = config.secret_key
|
2012-01-26 18:58:09 +00:00
|
|
|
logger = logging.getLogger()
|
|
|
|
conn = None
|
|
|
|
updater = None
|
|
|
|
|
|
|
|
from functools import wraps
|
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
def restrict_ip(prefix='', exclude=[]):
|
2012-01-27 07:31:32 +00:00
|
|
|
def decorator(f):
|
|
|
|
@wraps(f)
|
2012-01-27 17:46:30 +00:00
|
|
|
def func(*a, **kw):
|
2012-01-27 07:31:32 +00:00
|
|
|
r_addr = request.remote_addr
|
|
|
|
if not r_addr.startswith(prefix) or r_addr in exclude:
|
2012-01-27 17:46:30 +00:00
|
|
|
abort(403)
|
|
|
|
return f(*a, **kw)
|
2012-01-27 07:31:32 +00:00
|
|
|
return func
|
|
|
|
return decorator
|
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
def req_to_ctx():
|
|
|
|
return dict(request.form.iteritems())
|
|
|
|
|
|
|
|
@app.template_filter('strfts')
|
2012-01-26 18:58:09 +00:00
|
|
|
def strfts(ts, format='%d/%m/%Y %H:%M'):
|
|
|
|
return datetime.fromtimestamp(ts).strftime(format)
|
|
|
|
|
|
|
|
def setup_db():
|
|
|
|
conn = sqlite3.connect(config.db)
|
|
|
|
conn.row_factory = sqlite3.Row
|
2012-01-27 07:31:32 +00:00
|
|
|
conn.isolation_level = None # for autocommit mode
|
2012-01-26 18:58:09 +00:00
|
|
|
return conn
|
|
|
|
|
|
|
|
DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'owner', 'ignored'])
|
|
|
|
User = namedtuple('User', ['login', 'passwd', 'url'])
|
|
|
|
|
|
|
|
def get_device_info(conn, hwaddr):
|
|
|
|
return list(get_device_infos(conn, (hwaddrs,)))[0]
|
|
|
|
|
|
|
|
def get_device_infos(conn, hwaddrs):
|
|
|
|
stmt = '''select hwaddr, name, ignored, login, url from
|
|
|
|
devices left join users on devices.owner = users.userid
|
|
|
|
where devices.hwaddr in (''' + ','.join(['?'] * len(hwaddrs)) + ')'
|
|
|
|
for row in conn.execute(stmt, hwaddrs):
|
|
|
|
owner = User(row['login'], None, row['url']) if row['login'] else None
|
|
|
|
ignored = row['ignored']
|
|
|
|
yield DeviceInfo(row['hwaddr'], owner, ignored)
|
|
|
|
|
|
|
|
class Updater(threading.Thread):
|
2012-01-27 07:31:32 +00:00
|
|
|
def __init__(self, timeout, *a, **kw):
|
2012-01-26 18:58:09 +00:00
|
|
|
self.timeout = timeout
|
|
|
|
self.lock = threading.Lock()
|
|
|
|
self.active = {}
|
|
|
|
threading.Thread.__init__(self, *a, **kw)
|
|
|
|
def purge_stale(self):
|
|
|
|
now = time()
|
2012-01-27 07:31:32 +00:00
|
|
|
for addr, (atime, ip, name) in self.active.items():
|
2012-01-26 18:58:09 +00:00
|
|
|
if now - atime > self.timeout:
|
|
|
|
del self.active[addr]
|
|
|
|
def get_active_devices(self):
|
|
|
|
self.lock.acquire()
|
|
|
|
self.purge_stale()
|
|
|
|
r = dict(self.active)
|
|
|
|
self.lock.release()
|
|
|
|
return r
|
2012-01-27 07:31:32 +00:00
|
|
|
def get_device(self, ip):
|
|
|
|
for hwaddr, (atime, dip, name) in \
|
|
|
|
self.get_active_devices().iteritems():
|
|
|
|
if ip == dip:
|
|
|
|
return hwaddr, name
|
2012-01-27 17:46:30 +00:00
|
|
|
return None, None
|
2012-01-27 07:31:32 +00:00
|
|
|
def update(self, hwaddr, atime = None, ip = None, name = None):
|
|
|
|
atime = atime or time()
|
2012-01-26 18:58:09 +00:00
|
|
|
self.lock.acquire()
|
2012-01-27 07:31:32 +00:00
|
|
|
self.active[hwaddr] = (atime, ip, name)
|
2012-01-26 18:58:09 +00:00
|
|
|
self.lock.release()
|
2012-01-27 07:31:32 +00:00
|
|
|
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)
|
2012-01-26 18:58:09 +00:00
|
|
|
def run(self):
|
|
|
|
while True:
|
|
|
|
try:
|
2012-01-27 07:31:32 +00:00
|
|
|
with open(self.cap_file, 'r', buffering=0) as f:
|
|
|
|
logger.info('Updater ready on cap file %s', self.cap_file)
|
|
|
|
while True:
|
|
|
|
hwaddr = f.readline().strip()
|
|
|
|
if not hwaddr:
|
|
|
|
break
|
|
|
|
self.update(hwaddr)
|
2012-01-26 18:58:09 +00:00
|
|
|
logging.warning('Cap file %s closed, reopening', self.cap_file)
|
|
|
|
except Exception as e:
|
|
|
|
logging.error('Updater got an exception:\n' + \
|
|
|
|
traceback.format_exc(e))
|
|
|
|
sleep(10.0)
|
|
|
|
|
2012-01-27 07:31:32 +00:00
|
|
|
class DnsmasqUpdater(Updater):
|
|
|
|
def __init__(self, lease_file, lease_offset, *a, **kw):
|
|
|
|
self.lease_file = lease_file
|
|
|
|
self.lease_offset = lease_offset
|
|
|
|
self.last_modified = 0
|
|
|
|
Updater.__init__(self, *a, **kw)
|
|
|
|
def run(self):
|
|
|
|
import os
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
mtime = os.stat(self.lease_file).st_mtime
|
|
|
|
if mtime > self.last_modified:
|
|
|
|
logger.info('Lease file changed, updating')
|
|
|
|
with open(self.lease_file, 'r') as f:
|
|
|
|
for line in f:
|
|
|
|
ts, hwaddr, ip, name, client_id = line.split(' ')
|
|
|
|
self.update(hwaddr, int(ts) - self.lease_offset, ip, name)
|
|
|
|
self.last_modified = mtime
|
|
|
|
sleep(3.0)
|
|
|
|
except Exception as e:
|
|
|
|
logging.error('Updater got an exception:\n' + \
|
|
|
|
traceback.format_exc(e))
|
|
|
|
sleep(10.0)
|
2012-01-27 17:46:30 +00:00
|
|
|
|
|
|
|
@app.route('/')
|
|
|
|
def main_view():
|
|
|
|
return render_template('main.html', **now_at())
|
2012-01-27 07:31:32 +00:00
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/api')
|
|
|
|
def list_all():
|
|
|
|
result = now_at()
|
2012-01-27 09:19:04 +00:00
|
|
|
def prettify_user((user, atime)):
|
|
|
|
return {
|
|
|
|
'login': user.login,
|
|
|
|
'timestamp': atime,
|
|
|
|
'pretty_time': strfts(atime),
|
|
|
|
'url': user.url,
|
|
|
|
}
|
|
|
|
result['users'] = map(prettify_user, result['users'])
|
|
|
|
result['unknown'] = len(result['unknown'])
|
|
|
|
del result['login']
|
2012-01-27 17:46:30 +00:00
|
|
|
return json.dumps(result)
|
2012-01-27 09:19:04 +00:00
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
def now_at():
|
2012-01-26 18:58:09 +00:00
|
|
|
devices = updater.get_active_devices()
|
|
|
|
device_infos = list(get_device_infos(conn, devices.keys()))
|
|
|
|
device_infos.sort(key=lambda di: devices.__getitem__)
|
2012-01-27 07:31:32 +00:00
|
|
|
users = list(dict((info.owner, devices[info.hwaddr][0]) for info in device_infos
|
2012-01-26 18:58:09 +00:00
|
|
|
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)
|
2012-01-27 17:46:30 +00:00
|
|
|
return dict(users=users, unknown=unknown, login=session.get('login'))
|
2012-01-27 07:31:32 +00:00
|
|
|
|
|
|
|
restrict_to_hs = restrict_ip(prefix=config.claimable_prefix,
|
|
|
|
exclude=config.claimable_exclude)
|
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/register', methods=['GET'])
|
2012-01-27 07:31:32 +00:00
|
|
|
@restrict_to_hs
|
2012-01-27 17:46:30 +00:00
|
|
|
def register_form():
|
|
|
|
return render_template('register.html', **req_to_ctx())
|
2012-01-27 07:31:32 +00:00
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/register', methods=['POST'])
|
2012-01-27 07:31:32 +00:00
|
|
|
@restrict_to_hs
|
2012-01-27 17:46:30 +00:00
|
|
|
def register():
|
|
|
|
login = request.form['login']
|
|
|
|
url = request.form['url']
|
2012-01-27 07:31:32 +00:00
|
|
|
if 'wiki' in request.form:
|
|
|
|
url = config.wiki_url % { 'login': login }
|
|
|
|
try:
|
|
|
|
conn.execute('insert into users (login, url, pass) values (?, ?, ?)',
|
2012-01-27 17:46:30 +00:00
|
|
|
[login, url, sha256(request.form['password']).hexdigest()])
|
|
|
|
return redirect('/')
|
2012-01-27 07:31:32 +00:00
|
|
|
except sqlite3.Error as e:
|
2012-01-27 17:46:30 +00:00
|
|
|
flash('Cannot add user - username taken?', category='error')
|
|
|
|
return register_form()
|
2012-01-27 07:31:32 +00:00
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/login', methods=['GET'])
|
2012-01-27 07:31:32 +00:00
|
|
|
@restrict_to_hs
|
2012-01-27 17:46:30 +00:00
|
|
|
def login_form():
|
|
|
|
return render_template('login.html', **req_to_ctx())
|
2012-01-27 07:31:32 +00:00
|
|
|
|
|
|
|
def get_credentials(login, password):
|
|
|
|
row = conn.execute('select userid from users where login = ? and pass = ?',
|
|
|
|
[login, sha256(password).hexdigest()]).fetchone()
|
|
|
|
return row and row['userid']
|
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/login', methods=['POST'])
|
2012-01-27 07:31:32 +00:00
|
|
|
@restrict_to_hs
|
2012-01-27 17:46:30 +00:00
|
|
|
def login():
|
|
|
|
login = request.form.get('login', '')
|
|
|
|
pwd = request.form.get('password', '')
|
|
|
|
goto = request.values.get('goto') or '/'
|
2012-01-27 07:31:32 +00:00
|
|
|
userid = get_credentials(login, pwd)
|
|
|
|
if userid:
|
2012-01-27 17:46:30 +00:00
|
|
|
session['userid'] = userid
|
|
|
|
session['login'] = login
|
|
|
|
return redirect(goto)
|
2012-01-27 07:31:32 +00:00
|
|
|
else:
|
2012-01-27 17:46:30 +00:00
|
|
|
flash('Username or password invalid', category='error')
|
|
|
|
return login_form()
|
2012-01-27 07:31:32 +00:00
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/logout')
|
2012-01-27 07:31:32 +00:00
|
|
|
@restrict_to_hs
|
2012-01-27 17:46:30 +00:00
|
|
|
def logout():
|
|
|
|
session.clear()
|
|
|
|
return redirect('/')
|
2012-01-27 07:31:32 +00:00
|
|
|
|
|
|
|
def login_required(f):
|
|
|
|
@wraps(f)
|
2012-01-27 17:46:30 +00:00
|
|
|
def func(*a, **kw):
|
|
|
|
if 'userid' not in session:
|
|
|
|
flash('You must log in to continue', category='error')
|
|
|
|
return redirect('/login?' +
|
|
|
|
urlencode({'goto': request.path}))
|
|
|
|
return f(*a, **kw)
|
2012-01-27 07:31:32 +00:00
|
|
|
return func
|
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/claim', methods=['GET'])
|
2012-01-27 07:31:32 +00:00
|
|
|
@restrict_to_hs
|
|
|
|
@login_required
|
2012-01-27 17:46:30 +00:00
|
|
|
def claim_form():
|
2012-01-27 07:31:32 +00:00
|
|
|
hwaddr, name = updater.get_device(request.remote_addr)
|
2012-01-27 17:46:30 +00:00
|
|
|
return render_template('claim.html', hwaddr=hwaddr, name=name,
|
|
|
|
login=session['login'])
|
2012-01-27 07:31:32 +00:00
|
|
|
|
2012-01-27 17:46:30 +00:00
|
|
|
@app.route('/claim', methods=['POST'])
|
2012-01-27 07:31:32 +00:00
|
|
|
@restrict_to_hs
|
|
|
|
@login_required
|
2012-01-27 17:46:30 +00:00
|
|
|
def claim():
|
2012-01-27 07:31:32 +00:00
|
|
|
hwaddr, lease_name = updater.get_device(request.remote_addr)
|
2012-01-27 17:46:30 +00:00
|
|
|
ctx = None
|
2012-01-27 07:31:32 +00:00
|
|
|
if not hwaddr:
|
2012-01-27 17:46:30 +00:00
|
|
|
ctx = { 'error': 'Invalid device.' }
|
|
|
|
else:
|
|
|
|
userid = session['userid']
|
|
|
|
try:
|
|
|
|
conn.execute('insert into devices (hwaddr, name, owner, ignored)\
|
|
|
|
values (?, ?, ?, ?)', [hwaddr, request.form['name'], userid, False])
|
|
|
|
ctx = {}
|
|
|
|
except sqlite3.Error as e:
|
|
|
|
ctx = { 'error': 'Could not add device! Perhaps someone claimed it?' }
|
|
|
|
return render_template('post_claim.html', **ctx)
|
2012-01-26 18:58:09 +00:00
|
|
|
|
|
|
|
port = 8080
|
|
|
|
if __name__ == '__main__':
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
logger.addHandler(logging.StreamHandler())
|
|
|
|
conn = setup_db()
|
2012-01-27 07:31:32 +00:00
|
|
|
updater = DnsmasqUpdater(config.lease_file, config.lease_offset, config.timeout)
|
2012-01-26 18:58:09 +00:00
|
|
|
updater.start()
|
2012-01-28 20:50:07 +00:00
|
|
|
app.run('0.0.0.0', config.port)
|