Commit c5fefeb0 authored by Daniele Iamartino's avatar Daniele Iamartino

Merge pull request #22 from BitsDevelopmentTeam/macroster

Presence list (roster)
Good job!
parents c418ef91 d74e2f27
......@@ -83,3 +83,9 @@ This is also a valid Python file, but don't abuse that.
## ReCaptcha private key
#recaptcha_privkey = "privkey"
## Authentication token for live presence updates
#mac_update_password = "default"
## Minimum number of seconds between two successive MAC updates.
mac_update_interval = 0
......@@ -7,6 +7,7 @@
"""Common elements needed by all modules."""
from itertools import izip_longest
from tornado.netutil import bind_sockets, bind_unix_socket
from tornado.options import options
......@@ -19,6 +20,14 @@ import os
LOG = logging.getLogger('tornado.general')
def secure_compare(a, b):
"""Reasonably safe way to securely compare two strings.
Works in constant time to prevent timing attacks.
return sum(1 for x, y in izip_longest(a, b) if x != y) == 0
def bind(server, port, usocket, address=None):
"""Make server listen on port (inet socket).
If given, prefer `usocket`, path to a unix socket.
......@@ -15,7 +15,7 @@ import re
from datetime import datetime
from sqlalchemy import Column, ForeignKey, UniqueConstraint, Index
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, backref
from sqlalchemy.types import Integer, Float, DateTime, Enum, Text, BigInteger, String, UnicodeText
from sqlalchemy.ext.declarative import declarative_base
......@@ -110,7 +110,7 @@ class Message(Base):
timestamp = Column(DateTime, primary_key=True,
message = Column(Text, nullable=False)
author = relationship("User")
author = relationship("User", backref=backref("messages", order_by=timestamp))
def __init__(self, userid, message):
self.userid = userid
......@@ -200,3 +200,20 @@ class LoginAttempt(Base):
def __str__(self):
return 'Failed attempt for `{self.username}` from {self.ipaddress} at {self.timestamp}'.format(self=self)
class MACToUser(Base):
__tablename__ = 'MACToUser'
__table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'}
userid = Column(Integer, ForeignKey("User.userid"))
mac_hash = Column(String(length=64), primary_key=True)
user = relationship("User", backref=backref("macs", order_by=userid))
def __init__(self, userid, mac_hash):
self.userid = userid
self.mac_hash = mac_hash
def __str__(self):
return 'User with ID {self.userid} has MAC with hash {self.mac_hash}'.format(self=self)
......@@ -147,3 +147,15 @@ define("usocket_mode",
help="Permissions for chmod on the unix sockets",
help="Authentication token for live presence updates",
help="Minimum number of seconds between two successive MAC updates.",
......@@ -38,7 +38,8 @@ def start():
(r'/logout', handlers.LogoutPageHandler),
(r'/admin', handlers.AdminPageHandler),
(r'/message', handlers.MessagePageHandler),
(r'/data.php', handlers.RTCHandler)
(r'/data.php', handlers.RTCHandler),
(r'/macupdate', handlers.MACUpdateHandler),
......@@ -215,3 +215,12 @@ nav#paginator ul li {
width: 300px;
margin: 0 auto;
#roster {
width: 200px;
margin: 0 auto;
#roster li {
text-align: left;
......@@ -10,20 +10,22 @@
HTTP requests handlers.
import json
import markdown
import datetime
from datetime import datetime, timedelta
from sqlalchemy import distinct
from sqlalchemy.exc import IntegrityError
import tornado.web
from tornado.web import MissingArgumentError, HTTPError, RequestHandler
import tornado.websocket
import tornado.auth
from tornado.options import options
import bitsd.listener.notifier as notifier
from bitsd.persistence.engine import session_scope
from bitsd.persistence.models import Status
from bitsd.persistence.engine import session_scope, persist
from bitsd.persistence.models import Status, User, MACToUser, LoginAttempt
from .auth import verify, DoSError
from .presence import PresenceForecaster
......@@ -31,7 +33,7 @@ from .notifier import MessageNotifier
import bitsd.persistence.query as query
from bitsd.common import LOG
from bitsd.common import LOG, secure_compare
def cache(seconds):
......@@ -51,8 +53,8 @@ def cache(seconds):
def set_cacheable(get_function):
def wrapper(self, *args, **kwargs):
self.set_header("Expires", datetime.datetime.utcnow() +
self.set_header("Expires", datetime.utcnow() +
self.set_header("Cache-Control", "max-age=" + str(seconds))
return get_function(self, *args, **kwargs)
return wrapper
......@@ -66,7 +68,7 @@ def broadcast(message):
class BaseHandler(tornado.web.RequestHandler):
class BaseHandler(RequestHandler):
"""Base requests handler"""
USER_COOKIE_NAME = "usertoken"
......@@ -275,8 +277,11 @@ class AdminPageHandler(BaseHandler):
def get(self):
"""Display the admin page."""
page_message='Very secret information here')
page_message='Very secret information here',
def post(self):
......@@ -307,7 +312,11 @@ class AdminPageHandler(BaseHandler):
message = "Errore: modifica troppo veloce!"
self.render('templates/admin.html', page_message=message)
class PresenceForecastHandler(BaseHandler):
......@@ -352,7 +361,56 @@ class MessagePageHandler(BaseHandler):
class RTCHandler(BaseHandler):
def get(self):
now =
now =
self.write(now.strftime("%Y-%m-%d %H:%M:%S"))
class MACUpdateHandler(BaseHandler):
def post(self):
now =
remote_ip = self.request.remote_ip
with session_scope() as session:
last = query.get_last_login_attempt(session, remote_ip)
if last is None:
last = LoginAttempt(None, remote_ip)
persist(session, last)
if (now - last.timestamp) < timedelta(seconds=options.mac_update_interval):
LOG.warning("Too frequent attempts to update, remote IP address is %s", remote_ip)
raise HTTPError(403, "Too frequent")
last.timestamp = now
persist(session, last)
password = self.get_argument("password")
macs = self.get_argument("macs")
except MissingArgumentError:
LOG.warning("MAC update received malformed parameters: %s", self.request.arguments)
raise HTTPError(400, "Bad parameters list")
if not secure_compare(password, options.mac_update_password):
LOG.warning("Client provided wrong password for MAC update!")
raise HTTPError(403, "Wrong password")"Authorized request to update list of checked-in users from IP address %s", remote_ip)
macs = json.loads(macs)
with session_scope() as session:
names = session.\
filter(User.userid == MACToUser.userid).\
filter(MACToUser.mac_hash .in_ (macs)).\
MACUpdateHandler.ROSTER = [n[0] for n in names]
LOG.debug("Updated list of checked in users: %s", MACUpdateHandler.ROSTER)
def check_xsrf_cookie(self):
# Since this is an API call, we need to disable anti-XSRF protection
\ No newline at end of file
......@@ -21,6 +21,18 @@
<input type="submit" value="Cambia stato sede">
<div id="roster">
<h2>Utenti in sede</h2>
{% if roster %}
{% for name in roster %}
{% end %}
{% else %}
{% end %}
<ul class="link">
<li><a id="logout" href="/logout">Logout</a></li>
<li><a id="messages" href="/message">Invia messaggio</a></li>
......@@ -26,7 +26,7 @@ class DebugMode(tornado.web.UIModule):
class BasePage(tornado.web.UIModule):
"""Module providing base css, ico files for all pages and encoding tag."""
def css_files(self):
css = ['/static/default.css?v=2',]
css = ['/static/default.css?v=3',]
# FIXME daltonism workaround, should be implemented client-side
if 'blind' in self.request.path:
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment