summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomek Dubrownik <t.dubrownik@gmail.com>2012-01-27 08:31:32 +0100
committerTomek Dubrownik <t.dubrownik@gmail.com>2012-01-27 08:31:32 +0100
commitdbe9db0e4ace032734256f90773c039f590dfcba (patch)
tree5ebb903db9775ea73b11dcf8092a5e05df85e95a
parentae406076f7a5691a9922a9b66d29c29f03fb2892 (diff)
downloadcheckinator-dbe9db0e4ace032734256f90773c039f590dfcba.tar.gz
checkinator-dbe9db0e4ace032734256f90773c039f590dfcba.tar.bz2
checkinator-dbe9db0e4ace032734256f90773c039f590dfcba.zip
dnsmasq.leases based updater
Also a self-claim module
-rw-r--r--at.py182
-rw-r--r--config.py8
-rw-r--r--templates/claim.html14
-rw-r--r--templates/login.html13
-rw-r--r--templates/main.html11
-rw-r--r--templates/post_claim.html10
-rw-r--r--templates/register.html14
7 files changed, 233 insertions, 19 deletions
diff --git a/at.py b/at.py
index e7e58fe..4a3b09c 100644
--- a/at.py
+++ b/at.py
@@ -7,15 +7,22 @@ import traceback
from datetime import datetime
from wsgiref import simple_server
from pesto import Response, dispatcher_app
+from pesto.session import session_middleware
+from pesto.session.memorysessionmanager import MemorySessionManager
from time import sleep, time
from collections import namedtuple
from jinja2 import Environment, FileSystemLoader
+from urllib import urlencode
+from hashlib import sha256
import config
dispatcher = dispatcher_app()
+app = session_middleware(MemorySessionManager())(dispatcher)
logger = logging.getLogger()
-env = Environment(loader=FileSystemLoader('templates'))
+env = Environment(loader=FileSystemLoader('templates'),
+ autoescape='html',
+ extensions=['jinja2.ext.autoescape'])
conn = None
updater = None
@@ -30,6 +37,17 @@ def render(filepath):
return func
return decorator
+def restrict_ip(prefix='', exclude=[], fail_response=Response(status=403)):
+ def decorator(f):
+ @wraps(f)
+ def func(request, *a, **kw):
+ r_addr = request.remote_addr
+ if not r_addr.startswith(prefix) or r_addr in exclude:
+ return fail_response
+ return f(request, *a, **kw)
+ return func
+ return decorator
+
def strfts(ts, format='%d/%m/%Y %H:%M'):
return datetime.fromtimestamp(ts).strftime(format)
env.filters['strfts'] = strfts
@@ -37,6 +55,7 @@ env.filters['strfts'] = strfts
def setup_db():
conn = sqlite3.connect(config.db)
conn.row_factory = sqlite3.Row
+ conn.isolation_level = None # for autocommit mode
return conn
DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'owner', 'ignored'])
@@ -55,15 +74,14 @@ def get_device_infos(conn, hwaddrs):
yield DeviceInfo(row['hwaddr'], owner, ignored)
class Updater(threading.Thread):
- def __init__(self, cap_file, timeout, *a, **kw):
- self.cap_file = cap_file
+ def __init__(self, timeout, *a, **kw):
self.timeout = timeout
self.lock = threading.Lock()
self.active = {}
threading.Thread.__init__(self, *a, **kw)
def purge_stale(self):
now = time()
- for addr, atime in self.active.items():
+ for addr, (atime, ip, name) in self.active.items():
if now - atime > self.timeout:
del self.active[addr]
def get_active_devices(self):
@@ -72,38 +90,166 @@ class Updater(threading.Thread):
r = dict(self.active)
self.lock.release()
return r
- def update(self, hwaddr):
+ def get_device(self, ip):
+ for hwaddr, (atime, dip, name) in \
+ self.get_active_devices().iteritems():
+ if ip == dip:
+ return hwaddr, name
+ def update(self, hwaddr, atime = None, ip = None, name = None):
+ atime = atime or time()
self.lock.acquire()
- self.active[hwaddr] = time()
+ self.active[hwaddr] = (atime, ip, name)
self.lock.release()
+ 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:
- f = open(self.cap_file, 'r', buffering=0)
- logger.info('Updater ready on cap file %s', self.cap_file)
- while True:
- hwaddr = f.readline().strip()
- if not hwaddr:
- break
- self.update(hwaddr)
- logger.info('logged dhcp request from %s', hwaddr)
+ 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)
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)
+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)
+
+
@dispatcher.match('/', 'GET')
@render('main.html')
def now_at(request):
devices = updater.get_active_devices()
device_infos = list(get_device_infos(conn, devices.keys()))
device_infos.sort(key=lambda di: devices.__getitem__)
- users = list(dict((info.owner, devices[info.hwaddr]) for info in device_infos
+ 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)
- return dict(users=users, unknown=unknown)
+ return dict(users=users, unknown=unknown, login=request.session.get('login'))
+
+restrict_to_hs = restrict_ip(prefix=config.claimable_prefix,
+ exclude=config.claimable_exclude)
+
+@dispatcher.match('/register', 'GET')
+@restrict_to_hs
+@render('register.html')
+def register_form(request):
+ return request.form
+
+@dispatcher.match('/register', 'POST')
+@restrict_to_hs
+def register(request):
+ login = request['login']
+ url = request['url']
+ if 'wiki' in request.form:
+ url = config.wiki_url % { 'login': login }
+ try:
+ conn.execute('insert into users (login, url, pass) values (?, ?, ?)',
+ [login, url, sha256(request['password']).hexdigest()])
+ return Response.redirect('/')
+ except sqlite3.Error as e:
+ request.form['error'] = 'Cannot add user - username taken?'
+ return register_form(request)
+
+@dispatcher.match('/login', 'GET')
+@restrict_to_hs
+@render('login.html')
+def login_form(request):
+ return request.form
+
+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']
+
+@dispatcher.match('/login', 'POST')
+@restrict_to_hs
+def login(request):
+ login = request.get('login')
+ pwd = request.get('password')
+ goto = request.get('goto') or '/'
+ userid = get_credentials(login, pwd)
+ if userid:
+ request.session['userid'] = userid
+ request.session['login'] = login
+ return Response.redirect(goto)
+ else:
+ request.form['error'] = 'Username or password invalid'
+ return login_form(request)
+
+@dispatcher.match('/logout', 'GET')
+@restrict_to_hs
+def logout(request):
+ request.session.clear()
+ return Response.redirect('/')
+
+def login_required(f):
+ @wraps(f)
+ def func(request, *a, **kw):
+ if 'userid' not in request.session:
+ return Response.redirect('/login?' +
+ urlencode({'goto': request.path_info,
+ 'error': 'You must log in to continue'}))
+ return f(request, *a, **kw)
+ return func
+
+@dispatcher.match('/claim', 'GET')
+@restrict_to_hs
+@login_required
+@render('claim.html')
+def claim_form(request):
+ hwaddr, name = updater.get_device(request.remote_addr)
+ return { 'hwaddr': hwaddr, 'name': name,
+ 'login': request.session['login'] }
+
+@dispatcher.match('/claim', 'POST')
+@restrict_to_hs
+@login_required
+@render('post_claim.html')
+def claim(request):
+ hwaddr, lease_name = updater.get_device(request.remote_addr)
+ if not hwaddr:
+ return { 'error': 'Invalid device.' }
+ userid = request.session['userid']
+ try:
+ conn.execute('insert into devices (hwaddr, name, owner, ignored)\
+ values (?, ?, ?, ?)', [hwaddr, request['name'], userid, False])
+ return {}
+ except sqlite3.Error as e:
+ return { 'error': 'Could not add device! Perhaps someone claimed it?' }
port = 8080
if __name__ == '__main__':
@@ -111,7 +257,7 @@ if __name__ == '__main__':
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())
conn = setup_db()
- updater = Updater(config.cap_file, config.timeout)
+ updater = DnsmasqUpdater(config.lease_file, config.lease_offset, config.timeout)
updater.start()
- server = simple_server.make_server('', port, dispatcher)
+ server = simple_server.make_server('', port, app)
server.serve_forever()
diff --git a/config.py b/config.py
index f3d3033..b08cf21 100644
--- a/config.py
+++ b/config.py
@@ -1,4 +1,12 @@
db = './at.db'
cap_file = './dhcp-cap'
+lease_file = './leases'
+lease_offset = 60 * 20
timeout = 300
+wiki_url = 'http://hackerspace.pl/wiki/doku.php?id=people:%(login)s:start'
+
+claimable_prefix = ''
+claimable_exclude = [
+# '127.0.0.1',
+]
diff --git a/templates/claim.html b/templates/claim.html
new file mode 100644
index 0000000..ab4243b
--- /dev/null
+++ b/templates/claim.html
@@ -0,0 +1,14 @@
+<html>
+<body>
+{% if not hwaddr %}
+<p class="error">Unknown MAC. Are you sure you're in the hackerspace?</p>
+{% else %}
+You are about to claim {{ hwaddr }} as {{ login }}. Do you wish to continue?
+<form action="" method="post">
+<label>Device name (optional):<input type="text" name="name" value="{{ name }}"></label>
+<input type="submit" value="yes">
+</form>
+<a href="/"><button>no</button></button>
+{% endif %}
+</body>
+</html>
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..2a3d65a
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,13 @@
+<html>
+<body>
+<p class="error">{{ error }}</p>
+<form action="" method="POST">
+<label>login<input type="text" name="login" value="{{ login }}"></label>
+<label>password<input type="password" name="password"></label>
+{% if goto %}
+<input type="hidden" name="goto" value="{{ goto }}">
+{% endif %}
+<input type="submit" value="login"></input>
+</form>
+</body>
+</html>
diff --git a/templates/main.html b/templates/main.html
index 9b0769c..d647833 100644
--- a/templates/main.html
+++ b/templates/main.html
@@ -1,4 +1,12 @@
<html>
+<body>
+<div class="login">
+{% if login %}
+logged in as {{ login }} | <a href="logout">log out</a>
+{% else %}
+<a href="login">login</a> | <a href="register">register</a>
+{% endif %}
+</div>
Recently at <a href="http://www.hackerspace.pl">hackerspace</a>:
<ul>
{% for user, timestamp in users %}
@@ -10,5 +18,6 @@ Recently at <a href="http://www.hackerspace.pl">hackerspace</a>:
{% endfor %}
</ul>
<hr>
-There are {{ unknown|length }} unknown devices operating.
+There are {{ unknown|length }} unknown devices operating. <a href="claim">Claim this device!</a>
+</body>
</html>
diff --git a/templates/post_claim.html b/templates/post_claim.html
new file mode 100644
index 0000000..cd24469
--- /dev/null
+++ b/templates/post_claim.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+{% if error %}
+<p class="error">{{ error }}</p>
+{% else %}
+Congratulations, you just claimed this device!
+<a href="/">go back</a>
+{% endif %}
+</body>
+</html>
diff --git a/templates/register.html b/templates/register.html
new file mode 100644
index 0000000..bbe9626
--- /dev/null
+++ b/templates/register.html
@@ -0,0 +1,14 @@
+<html>
+<body>
+<p class="error">{{ error }}</p>
+<form action="" method="POST">
+<label>login<input type="text" name="login" value="{{ login }}"></label>
+<label>password<input type="password" name="password"></label>
+<label>confirm password<input type="password" name="password2"></label>
+<label>homepage url<input type="text" name="url" value="{{ url }}"></label>
+<label>use wiki page as url<input type="checkbox" name="wiki"
+ value="yes" {% if wiki == 'yes' %}checked="yes"{% endif %}></label>
+<input type="submit" value="register"></input>
+</form>
+</body>
+</html>