dnsmasq.leases based updater

Also a self-claim module
master
Tomek Dubrownik 2012-01-27 08:31:32 +01:00
parent ae406076f7
commit dbe9db0e4a
7 changed files with 233 additions and 19 deletions

182
at.py
View File

@ -7,15 +7,22 @@ import traceback
from datetime import datetime from datetime import datetime
from wsgiref import simple_server from wsgiref import simple_server
from pesto import Response, dispatcher_app from pesto import Response, dispatcher_app
from pesto.session import session_middleware
from pesto.session.memorysessionmanager import MemorySessionManager
from time import sleep, time from time import sleep, time
from collections import namedtuple from collections import namedtuple
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from urllib import urlencode
from hashlib import sha256
import config import config
dispatcher = dispatcher_app() dispatcher = dispatcher_app()
app = session_middleware(MemorySessionManager())(dispatcher)
logger = logging.getLogger() logger = logging.getLogger()
env = Environment(loader=FileSystemLoader('templates')) env = Environment(loader=FileSystemLoader('templates'),
autoescape='html',
extensions=['jinja2.ext.autoescape'])
conn = None conn = None
updater = None updater = None
@ -30,6 +37,17 @@ def render(filepath):
return func return func
return decorator 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'): def strfts(ts, format='%d/%m/%Y %H:%M'):
return datetime.fromtimestamp(ts).strftime(format) return datetime.fromtimestamp(ts).strftime(format)
env.filters['strfts'] = strfts env.filters['strfts'] = strfts
@ -37,6 +55,7 @@ env.filters['strfts'] = strfts
def setup_db(): def setup_db():
conn = sqlite3.connect(config.db) conn = sqlite3.connect(config.db)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.isolation_level = None # for autocommit mode
return conn return conn
DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'owner', 'ignored']) DeviceInfo = namedtuple('DeviceInfo', ['hwaddr', 'owner', 'ignored'])
@ -55,15 +74,14 @@ def get_device_infos(conn, hwaddrs):
yield DeviceInfo(row['hwaddr'], owner, ignored) yield DeviceInfo(row['hwaddr'], owner, ignored)
class Updater(threading.Thread): class Updater(threading.Thread):
def __init__(self, cap_file, timeout, *a, **kw): def __init__(self, timeout, *a, **kw):
self.cap_file = cap_file
self.timeout = timeout self.timeout = timeout
self.lock = threading.Lock() self.lock = threading.Lock()
self.active = {} self.active = {}
threading.Thread.__init__(self, *a, **kw) threading.Thread.__init__(self, *a, **kw)
def purge_stale(self): def purge_stale(self):
now = time() now = time()
for addr, atime 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):
@ -72,38 +90,166 @@ class Updater(threading.Thread):
r = dict(self.active) r = dict(self.active)
self.lock.release() self.lock.release()
return r 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.lock.acquire()
self.active[hwaddr] = time() self.active[hwaddr] = (atime, ip, name)
self.lock.release() 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): def run(self):
while True: while True:
try: try:
f = open(self.cap_file, 'r', buffering=0) with open(self.cap_file, 'r', buffering=0) as f:
logger.info('Updater ready on cap file %s', self.cap_file) logger.info('Updater ready on cap file %s', self.cap_file)
while True: while True:
hwaddr = f.readline().strip() hwaddr = f.readline().strip()
if not hwaddr: if not hwaddr:
break break
self.update(hwaddr) self.update(hwaddr)
logger.info('logged dhcp request from %s', hwaddr)
logging.warning('Cap file %s closed, reopening', self.cap_file) logging.warning('Cap file %s closed, reopening', self.cap_file)
except Exception as e: except Exception as e:
logging.error('Updater got an exception:\n' + \ logging.error('Updater got an exception:\n' + \
traceback.format_exc(e)) traceback.format_exc(e))
sleep(10.0) 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') @dispatcher.match('/', 'GET')
@render('main.html') @render('main.html')
def now_at(request): def now_at(request):
devices = updater.get_active_devices() devices = updater.get_active_devices()
device_infos = list(get_device_infos(conn, devices.keys())) device_infos = list(get_device_infos(conn, 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]) 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()) if info.owner and not info.ignored).iteritems())
users.sort(key=lambda (u, a): a, reverse=True) 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) 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 port = 8080
if __name__ == '__main__': if __name__ == '__main__':
@ -111,7 +257,7 @@ if __name__ == '__main__':
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler()) logger.addHandler(logging.StreamHandler())
conn = setup_db() conn = setup_db()
updater = Updater(config.cap_file, config.timeout) updater = DnsmasqUpdater(config.lease_file, config.lease_offset, config.timeout)
updater.start() updater.start()
server = simple_server.make_server('', port, dispatcher) server = simple_server.make_server('', port, app)
server.serve_forever() server.serve_forever()

View File

@ -1,4 +1,12 @@
db = './at.db' db = './at.db'
cap_file = './dhcp-cap' cap_file = './dhcp-cap'
lease_file = './leases'
lease_offset = 60 * 20
timeout = 300 timeout = 300
wiki_url = 'http://hackerspace.pl/wiki/doku.php?id=people:%(login)s:start'
claimable_prefix = ''
claimable_exclude = [
# '127.0.0.1',
]

14
templates/claim.html Normal file
View File

@ -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>

13
templates/login.html Normal file
View File

@ -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>

View File

@ -1,4 +1,12 @@
<html> <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>: Recently at <a href="http://www.hackerspace.pl">hackerspace</a>:
<ul> <ul>
{% for user, timestamp in users %} {% for user, timestamp in users %}
@ -10,5 +18,6 @@ Recently at <a href="http://www.hackerspace.pl">hackerspace</a>:
{% endfor %} {% endfor %}
</ul> </ul>
<hr> <hr>
There are {{ unknown|length }} unknown devices operating. There are {{ unknown|length }} unknown devices operating. <a href="claim">Claim this device!</a>
</body>
</html> </html>

10
templates/post_claim.html Normal file
View File

@ -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>

14
templates/register.html Normal file
View File

@ -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>