parent
ae406076f7
commit
dbe9db0e4a
182
at.py
182
at.py
|
@ -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()
|
||||||
|
|
|
@ -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',
|
||||||
|
]
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue