@contextmanager
def session_scope():
    """Provide a transactional scope around a series of operations."""
    session = Session()
    try:
        yield session
        session.commit()
    except IntegrityError as e:
        LOG.error("Integrity error in DB, rolling back: %s", e)
        session.rollback()
    except:
        LOG.error("Error in DB, rolling back.")
        session.rollback()
        raise
    finally:
        session.close() def persist(session, data, flush=True):
    """Persist data to configured DB and return persisted object
    in a consistent state.

    **Note:** might log what's being persisted, so don't put clear text password
    into `__str__()`."""
    LOG.debug('Persisting data %r', data)
    session.add(data)
    if flush:
        session.flush()
    return data class Status(Base):
    """Representation of a logged status change."""
    __tablename__ = 'Status'
    __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'}

    OPEN = 'open'
    CLOSED = 'closed'
    AWAY = 'away'

    timestamp = Column(DateTime, primary_key=True, default=datetime.now)
    value = Column(
        Enum(OPEN, CLOSED, AWAY),
        nullable=False
    )
    modified_by = Column(Enum('BITS', 'web'), nullable=False)

    def __init__(self, value, modified_by):
        self.value = value
        self.modified_by = modified_by

    def __str__(self):
        return 'Status {.value}'.format(self)

    def jsondict(self, wrap=True):
        """Return a JSON-serializable dictionary representing the object"""
        data = {
            "timestamp": self.timestamp.isoformat(' '),
            "modifiedby": self.modified_by,
            "value": self.value
        }
        if wrap:
            return {'status': data}
        else:
            return data class User(Base):
    """User name/password hash entity."""
    __tablename__ = 'User'
    __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'}

    userid = Column(Integer, primary_key=True)
    name = Column(String(length=256), unique=True, nullable=False)
    password = Column(String(length=512), nullable=False)

    def __init__(self, name, pwhash):
        self.name = name
        self.password = pwhash

    def __str__(self):
        return '{self.name}: {self.password}'.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) class LoginAttempt(Base):
    """Holds IP address, user and timestamp for failed login attempts.

     Failed attempts are kept in order to prevent DoS and bruteforcing attacks."""
    __tablename__ = 'LoginAttempt'
    __table_args__ = {'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8mb4'}

    id = Column(Integer, primary_key=True)
    username = Column(String(length=256), nullable=True)
    ipaddress = Column(String(length=64), nullable=False)
    timestamp = Column(DateTime, nullable=False, default=datetime.now)

    UniqueConstraint('username', 'ipaddress', name='user_ip_key')

    def __init__(self, username, ipaddress):
        self.username = username
        self.ipaddress = ipaddress

    def __str__(self):
        return 'Failed attempt for `{self.username}` from {self.ipaddress} at {self.timestamp}'.format(self=self) def verify(session, username, supplied_password, ip_address, has_captcha, recaptcha_challenge, recaptcha_response):
    """Verify user credentials.

    If the username exists, then the supplied password is hashed and
    compared with the stored hash. Otherwise, an hash is calculated and discarded.

    An hash is calculated regardless of the existence of the username, so that
    the response time is approximately the same whether the username exists or not,
    mitigating a timing attack to reveal valid usernames.

    In order to mitigate DoS/bruteforce attacks, two temporal limitations are enforced:

    1. Max 1 failed login attempt per IP address each second (regardless of username)
    2. Max 1 failed login attempt per each username per IP address each
       `min_log_retry` seconds (see bitsd.properties), whether the username
       exists or not.

    A DoS protection is necessary because password hashing is an expensive operation.
    """
    if has_captcha:
        solved_captcha = ReCaptcha.is_solution_correct(recaptcha_response, recaptcha_challenge, ip_address)
        # Exit immediately if wrong answer
        if not solved_captcha:
            return False
    else:
        solved_captcha = False

    # Save "now" so that the two timestamp checks are referred to the same instant
    now = datetime.now()

    def detect_dos(attempt, timeout):
        if solved_captcha:
            return False

        # Otherwise, check timing
        if attempt is not None:
            too_quick = (now - attempt.timestamp) < timeout
            if too_quick:
                log_last_login_attempt(session, ip_address, username)
                return True
            else:
                # Clean up if no more relevant
                session.delete(attempt)
        return False

    last_attempt_for_ip = get_last_login_attempt(session, ip_address)
    last_attempt_for_ip_and_username = get_last_login_attempt(session, ip_address, username)

    if detect_dos(last_attempt_for_ip, timedelta(seconds=1)):
        raise DoSError("Too frequent requests from {}".format(ip_address))

    if detect_dos(last_attempt_for_ip_and_username, timedelta(seconds=options.min_login_retry)):
        raise DoSError("Too frequent attempts from {} for username {}".format(ip_address, username))

    user = get_user(session, username)

    if user is None:
        LOG.warn("Failed attempt for non existent user %r", username)
        # Calculate hash anyway (see docs for the explanation)
        Hasher.encrypt(supplied_password)
        log_last_login_attempt(session, ip_address, username)
        return False
    else:
        valid = Hasher.verify(supplied_password, user.password)
        if not valid:
            log_last_login_attempt(session, ip_address, username)
        return valid class DoSError(Exception):
    """DoS error: raise by verify() if the user is trying
    to authenticate too fast"""
    pass class PresenceForecaster:
    class InvalidResolutionError(Exception):
        def __init__(self):
            self.message = "Resolution must be a submultiple of 60 minutes!"

    def __init__(self, resolution=30, samples_cont=5000):
        if self.resolution_is_invalid(resolution):
            raise self.InvalidResolutionError()

        self.samples_count = samples_cont
        self.ticks_per_hour = 60 / resolution
        self.minutes_per_tick = resolution

    def forecast(self):
        #TODO caching
        return self.calculate_frequencies()

    def calculate_frequencies(self):
        samples = self.get_samples()
        buckets = self.count_presence_per_slot(samples)
        return self.normalize(buckets)

    def count_presence_per_slot(self, samples):
        buckets = self.init_buckets()

        # TODO algorithm here

        return buckets

    def init_buckets(self):
        return [[0] * (24 * self.ticks_per_hour) for i in range(7)]

    def get_samples(self):
        with session_scope() as session:
            samples = session \
                .query(Status.timestamp, Status.value) \
                .filter((Status.value == Status.OPEN) | (Status.value == Status.CLOSED)) \
                .order_by(asc(Status.timestamp)) \
                .limit(self.samples_count)

        offset = self.first_open_offset(samples)
        return samples[offset:]

    def first_open_offset(self, samples):
        offset = 0
        while samples[offset].value != Status.OPEN:
            offset += 1
        return offset

    @staticmethod
    def resolution_is_invalid(resolution):
        return (60 % resolution) != 0

    def calculate_coordinates(self, sample):
        timestamp = sample.timestamp
        weekday = timestamp.weekday()
        timeslot = (self.ticks_per_hour * timestamp.hour) + int(1. * timestamp.minute / self.minutes_per_tick)
        return weekday, timeslot

    def normalize(self, buckets):
        for day in buckets:
            for i, slot in enumerate(day):
                day[i] = 1. * slot / self.samples_count
        return buckets class MessageNotifier(object):
    """Keeps a list of WebSocket handlers to notify with a message."""
    def __init__(self, name):
        self.clients = []
        self.name = name

    def register(self, client):
        """Add a new handler to the clients list."""
        LOG.debug('Adding client %r to %r', client, self.name)
        self.clients.append(client)

    def unregister(self, client):
        """Remove the handler from the clients list."""
        LOG.debug('Removing client %r from %r', client, self.name)
        self.clients.remove(client)

    def broadcast(self, message):
        """Notify all clients."""
        for client in self.clients:
            client.write_message(message) 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
import json
import markdown
import tornado.websocket
import tornado.auth
import bitsd.listener.notifier as notifier
import bitsd.persistence.query as query
from datetime import datetime, timedelta
from sqlalchemy import distinct
from sqlalchemy.exc import IntegrityError
from tornado.web import MissingArgumentError, HTTPError, RequestHandler
from tornado.options import options
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
from .notifier import MessageNotifier
from bitsd.common import LOG, secure_compare
        wants_json = self.get_argument("format", "html") == "json"
        offset = self.get_integer_or_400("offset", 0)
        limit = self.get_integer_or_400("limit", self.LINES_PER_PAGE)

        with session_scope() as session:
            latest_statuses = query.get_latest_statuses(
                session,
                offset=offset,
                limit=limit
            )

            # Handle limit = 1 case (result is not a list)
            if type(latest_statuses) == Status:
                latest_statuses = [latest_statuses]

            if wants_json:
                self.write(self.jsonize(latest_statuses))
                self.finish()
            else:
                self.render('templates/log.html',
                    latest_statuses=latest_statuses,
                    # Used by the paginator
                    offset=offset,
                    limit=self.LINES_PER_PAGE,
                    count=query.get_number_of_statuses(session),
                )

    @staticmethod
    def jsonize(latest_statuses):
        """Turn an array of Status objects into a JSON-serializable dict"""
        data = [s.jsondict(wrap=False) for s in latest_statuses]
        return {"log": data}

    def get_integer_or_400(self, name, default):
        """Try to get the parameter by name (and default), then convert it to
        integer. In case of failure, raise a HTTP error 400"""
        try:
            return int(self.get_argument(name, default))
        except ValueError:
            raise tornado.web.HTTPError(400)


class StatusPageHandler(BaseHandler):
    """Get a single digit, indicating BITS status (open/closed)"""
    def get(self):
        with session_scope() as session:
            status = query.get_current_status(session)
            answer = '1' if status is not None and status.value == Status.OPEN else '0'
        self.write(answer)
        self.finish()


class MarkdownPageHandler(BaseHandler):
    """Renders page from markdown source."""
    @cache(86400*10)
    def get(self, slug):
        with session_scope() as session:
            page = query.get_page(session, slug)

            if page is None:
                raise tornado.web.HTTPError(404)

            self.render('templates/mdpage.html',
                body=markdown.markdown(
                    page.body,
                    safe_mode='escape' if options.mdescape else False,
                ),
                title=page.title,
            )


class StatusHandler(tornado.websocket.WebSocketHandler):
    """Handler for POuL status via websocket"""

    CLIENTS = MessageNotifier('Status handler queue')

    def open(self):
        """Register new handler with MessageNotifier."""
        StatusHandler.CLIENTS.register(self)
        with session_scope() as session:
            latest = query.get_latest_data(session)
        self.write_message(latest)
        LOG.debug('Registered client')

    def on_close(self):
        """Unregister this handler when the connection is closed."""
        StatusHandler.CLIENTS.unregister(self)
        LOG.debug('Unregistered client.')
        
    def check_origin(self, origin):
        """No Same-Origin Policy"""
        return True


class LoginPageHandler(BaseHandler):
    """Handle login browser requests for reserved area."""
    def get(self):
        next = self.get_argument("next", "/")
        if self.get_current_user():
            self.redirect(next)
        else:
            self.render(
                'templates/login.html',
                next=next,
                message=None,
                show_recaptcha=False
            )

    def post(self):
        username = self.get_argument("username")
        password = self.get_argument("password")
        ip_address = self.request.remote_ip
        next = self.get_argument("next", "/")
        captcha_challenge = self.get_argument("recaptcha_challenge_field", "")
        captcha_response = self.get_argument("recaptcha_response_field", "")
        has_recaptcha = captcha_challenge or captcha_response

        with session_scope() as session:
            try:
                verified = verify(session, username, password, ip_address, has_recaptcha, captcha_challenge, captcha_response)

            except DoSError as error: