summaryrefslogtreecommitdiffstats
path: root/at.py
diff options
context:
space:
mode:
Diffstat (limited to 'at.py')
-rw-r--r--at.py422
1 files changed, 0 insertions, 422 deletions
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)