Example #1
0
def main():
    """Entry point for bitsd."""
    enable_pretty_logging()

    try:
        parse_config_file('/etc/bitsd.conf')
    except IOError:
        LOG.warning('Config file not found, using defaults and command line.')

    try:
        parse_command_line()
    except tornado.options.Error as error:
        sys.stderr.write('{}\n'.format(error))
        sys.exit(0)

    persistence.start()
    server.start()
    listener.start()

    # Add signal handlers...
    signal.signal(signal.SIGTERM, sig_handler)
    signal.signal(signal.SIGINT, sig_handler)

    if not options.log_requests:
        logging.getLogger("tornado.access").setLevel(logging.WARNING)

    tornado.ioloop.IOLoop.instance().start()
def start():
    """Setup HTTP/WS server. **MUST** be called prior to any operation."""
    auth.ReCaptcha.init()
    application = tornado.web.Application([
            # FIXME daltonism workaround, should be implemented client-side
            (r'/(?:|blind)', handlers.HomePageHandler),
            (r'/log', handlers.LogPageHandler),
            (r'/status', handlers.StatusPageHandler),
            (r'/data', handlers.DataPageHandler),
            (r'/presence', handlers.PresenceForecastHandler),
            (r'/(info)', handlers.MarkdownPageHandler),
            (r'/ws', handlers.StatusHandler),
            (r'/login', handlers.LoginPageHandler),
            (r'/logout', handlers.LogoutPageHandler),
            (r'/admin', handlers.AdminPageHandler),
            (r'/message', handlers.MessagePageHandler),
            (r'/data.php', handlers.RTCHandler),
            (r'/macupdate', handlers.MACUpdateHandler),
        ],
        ui_modules=uimodules,
        gzip=True,
        debug=options.developer_mode,
        static_path=options.assets_path,
        xsrf_cookies=True,
        cookie_secret=options.cookie_secret
    )
    server = tornado.httpserver.HTTPServer(application, xheaders=options.reverse_proxied)
    LOG.info('Starting HTTP/WS server...')
    bind(server, options.web_port, options.web_usocket)
Example #3
0
def main():
    """Entry point for bitsd."""
    enable_pretty_logging()

    try:
        parse_config_file('/etc/bitsd.conf')
    except IOError:
        LOG.warning('Config file not found, using defaults and command line.')

    try:
        parse_command_line()
    except tornado.options.Error as error:
        sys.stderr.write('{}\n'.format(error))
        sys.exit(0)

    persistence.start()
    server.start()
    listener.start()

    # Add signal handlers...
    signal.signal(signal.SIGTERM, sig_handler)
    signal.signal(signal.SIGINT, sig_handler)

    if not options.log_requests:
        logging.getLogger("tornado.access").setLevel(logging.WARNING)

    tornado.ioloop.IOLoop.instance().start()
Example #4
0
    def change_status(self):
        """Manually change the status of the BITS system"""

        with session_scope() as session:
            curstatus = query.get_current_status(session)

            if curstatus is None:
                textstatus = Status.CLOSED
            else:
                textstatus = Status.OPEN if curstatus.value == Status.CLOSED else Status.CLOSED

            LOG.info('Change of BITS to status={}'.format(textstatus) +
                     ' from web interface.')
            message = ''
            try:
                status = query.log_status(session, textstatus, 'web')
                broadcast(status.jsondict())
                notifier.send_status(textstatus)
                message = "Ora la sede è {}.".format(textstatus)
            except IntegrityError:
                LOG.error("Status changed too quickly, not logged.")
                message = "Errore: modifica troppo veloce!"
                raise
            finally:
                self.render('templates/admin.html', page_message=message)
 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 change_status(self):
        """Manually change the status of the BITS system"""

        with session_scope() as session:
            curstatus = query.get_current_status(session)

            if curstatus is None:
                textstatus = Status.CLOSED
            else:
                textstatus = Status.OPEN if curstatus.value == Status.CLOSED else Status.CLOSED

            LOG.info('Change of BITS to status=%r from web interface.', textstatus)
            message = ''
            try:
                status = query.log_status(session, textstatus, 'web')
                broadcast(status.jsondict())
                notifier.send_status(textstatus)
                message = "Ora la sede è {}.".format(textstatus)
            except IntegrityError:
                LOG.error("Status changed too quickly, not logged.")
                message = "Errore: modifica troppo veloce!"
                raise
            finally:
                self.render(
                    'templates/admin.html',
                    page_message=message,
                    roster=MACUpdateHandler.ROSTER
                )
Example #7
0
def start():
    """Setup HTTP/WS server. **MUST** be called prior to any operation."""
    StatusRouter = SockJSRouter(handlers.StatusConnection, "/data")

    application = tornado.web.Application(
        [
            # FIXME daltonism workaround, should be implemented client-side
            (r'/(?:|blind)', handlers.HomePageHandler),
            (r'/log', handlers.LogPageHandler),
            (r'/status', handlers.StatusPageHandler),
            (r'/presence', handlers.PresenceForecastHandler),
            (r'/(info)', handlers.MarkdownPageHandler),
            (r'/login', handlers.LoginPageHandler),
            (r'/logout', handlers.LogoutPageHandler),
            (r'/admin', handlers.AdminPageHandler),
            (r'/message', handlers.MessagePageHandler),
            (r'/data.php', handlers.RTCHandler)
        ] + StatusRouter.urls,
        ui_modules=uimodules,
        gzip=True,
        debug=options.developer_mode,
        static_path=options.assets_path,
        xsrf_cookies=True,
        cookie_secret=options.cookie_secret)
    server = tornado.httpserver.HTTPServer(application)  #TODO other options
    LOG.info('Starting HTTP/WS server...')
    bind(server, options.web_port, options.web_usocket)
Example #8
0
 def on_open(self, info):
     """Register new handler with MessageNotifier."""
     StatusConnection.CLIENTS.register(self)
     with session_scope() as session:
         latest = query.get_latest_data(session)
     self.send(latest)
     LOG.debug('Registered client')
Example #9
0
def start():
    """Setup HTTP/WS server. **MUST** be called prior to any operation."""
    StatusRouter = SockJSRouter(handlers.StatusConnection, "/data")

    application = tornado.web.Application([
            # FIXME daltonism workaround, should be implemented client-side
            (r'/(?:|blind)', handlers.HomePageHandler),
            (r'/log', handlers.LogPageHandler),
            (r'/status', handlers.StatusPageHandler),
            (r'/presence', handlers.PresenceForecastHandler),
            (r'/(info)', handlers.MarkdownPageHandler),
            (r'/login', handlers.LoginPageHandler),
            (r'/logout', handlers.LogoutPageHandler),
            (r'/admin', handlers.AdminPageHandler),
            (r'/message', handlers.MessagePageHandler),
            (r'/data.php', handlers.RTCHandler)
        ] + StatusRouter.urls,
        ui_modules=uimodules,
        gzip=True,
        debug=options.developer_mode,
        static_path=options.assets_path,
        xsrf_cookies=True,
        cookie_secret=options.cookie_secret
    )
    server = tornado.httpserver.HTTPServer(application) #TODO other options
    LOG.info('Starting HTTP/WS server...')
    bind(server, options.web_port, options.web_usocket)
Example #10
0
 def on_open(self, info):
     """Register new handler with MessageNotifier."""
     StatusConnection.CLIENTS.register(self)
     with session_scope() as session:
         latest = query.get_latest_data(session)
     self.send(latest)
     LOG.debug('Registered client')
Example #11
0
def sig_handler(sig, frame):
    """Catch signal and init callback.

    Reference:
    http://codemehanika.org/blog/2011-10-28-graceful-stop-tornado.html
    """
    LOG.warning('Caught signal: %s', sig)
    tornado.ioloop.IOLoop.instance().add_callback(shutdown)
Example #12
0
def sig_handler(sig, frame):
    """Catch signal and init callback.

    Reference:
    http://codemehanika.org/blog/2011-10-28-graceful-stop-tornado.html
    """
    LOG.warning('Caught signal: %s', sig)
    tornado.ioloop.IOLoop.instance().add_callback(shutdown)
def send(string):
    if RemoteListener.STREAM is None:
        LOG.error("No Fonera connected! Not sending %r", string)
        return
    try:
        RemoteListener.STREAM.write(string)
    except StreamClosedError as error:
        LOG.error('Could not push message to Fonera! %s', error)
Example #14
0
def handle_sound_command(soundid):
    """Handles requests to play a sound."""
    LOG.info('Received sound command: id=%r', soundid)
    try:
        soundid = int(soundid)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command!')
        return
    else:
        notifier.send_sound(soundid)
Example #15
0
def handle_sound_command(soundid):
    """Handles requests to play a sound."""
    LOG.info('Received sound command: id={}'.format(soundid))
    try:
        soundid = int(soundid)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command!')
        return
    else:
        notifier.send_sound(soundid)
Example #16
0
def persist(session, data, flush=True):
    """Persist data to configured DB and return persisted object
    in a consistent state.

    **Note:** will log what's being persisted, so don't put clear text password
    into `__str__()`."""
    LOG.debug('Persisting data {}'.format(data))
    session.add(data)
    if flush:
        session.flush()
    return data
Example #17
0
def start():
    """Connect and bind listeners. **MUST** be called at startup."""
    __inject_broadcast()

    fonera = RemoteListener()
    LOG.info('Starting remote control...')
    LOG.info('My IP address is {}, remote IP address is {}'.format(
        options.control_local_address, options.control_remote_address))
    bind(fonera,
         options.control_local_port,
         options.control_local_usocket,
         address=options.control_local_address)
Example #18
0
def handle_temperature_command(sensorid, value):
    """Receives and log data received from remote sensor."""
    LOG.info('Received temperature: sensorid=%r, value=%r', sensorid, value)
    try:
        sensorid = int(sensorid)
        value = float(value)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command!')
        return

    with session_scope() as session:
        temp = query.log_temperature(session, value, sensorid, 'BITS')
        broadcast(temp.jsondict())
Example #19
0
def handle_temperature_command(sensorid, value):
    """Receives and log data received from remote sensor."""
    LOG.info('Received temperature: sensorid={}, value={}'.format(
        sensorid, value))
    try:
        sensorid = int(sensorid)
        value = float(value)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command!')
        return

    with session_scope() as session:
        temp = query.log_temperature(session, value, sensorid, 'BITS')
        broadcast(temp.jsondict())
Example #20
0
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: {}".format(e))
        session.rollback()
    except:
        LOG.error("Error in DB, rolling back.")
        session.rollback()
        raise
    finally:
        session.close()
def start():
    """Connect and bind listeners. **MUST** be called at startup."""
    __inject_broadcast()

    fonera = RemoteListener()
    LOG.info('Starting remote control...')
    LOG.info(
        'My IP address is {}, remote IP address is {}'.format(
            options.control_local_address,
            options.control_remote_address
        )
    )
    bind(
        fonera,
        options.control_local_port,
        options.control_local_usocket,
        address=options.control_local_address
    )
Example #22
0
    def post(self):
        username = self.get_argument("username", None)
        password = self.get_argument("password", None)
        next = self.get_argument("next", "/")

        with session_scope() as session:
            authenticated = verify(session, username, password)

        if authenticated:
            self.set_secure_cookie(self.USER_COOKIE_NAME,
                                   username,
                                   expires_days=options.cookie_max_age_days)
            LOG.info("Authenticating user `{}`".format(username))
            self.redirect(next)
        else:
            LOG.warning("Wrong authentication for user `{}`".format(username))
            self.render('templates/login.html',
                        next=next,
                        message="Password/username sbagliati!")
    def post(self):
        now = datetime.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)
            else:
                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")
                else:
                    last.timestamp = now
                    persist(session, last)

        try:
            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")

        LOG.info("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.\
                query(distinct(User.name)).\
                filter(User.userid == MACToUser.userid).\
                filter(MACToUser.mac_hash .in_ (macs)).\
                all()

        MACUpdateHandler.ROSTER = [n[0] for n in names]
        LOG.debug("Updated list of checked in users: %s", MACUpdateHandler.ROSTER)
    def handle_command(self, command):
        """Reacts to received commands (callback).
        Will separate args and call appropriate handlers."""

        # Meanwhile, go on with commands...
        RemoteListener.STREAM.read_until(b'\n', self.handle_command)

        command = command.strip('\n')

        if command:
            args = command.split(b' ')
            action = args[0]
            try:
                handler = RemoteListener.ACTIONS[action]
            except KeyError:
                LOG.warning('Remote received unknown command `%s`', args)
            else:
                # Execute handler (index 0) with args (index 1->end)
                try:
                    handler(*args[1:])
                except TypeError:
                    LOG.error(
                        'Command `%s` called with wrong number of args', action
                    )
        else:
            LOG.warning('Remote received empty command.')
Example #25
0
    def post(self):
        username = self.get_argument("username", None)
        password = self.get_argument("password", None)
        next = self.get_argument("next", "/")

        with session_scope() as session:
            authenticated = verify(session, username, password)

        if authenticated:
            self.set_secure_cookie(
                self.USER_COOKIE_NAME,
                username,
                expires_days=options.cookie_max_age_days
            )
            LOG.info("Authenticating user `{}`".format(username))
            self.redirect(next)
        else:
            LOG.warning("Wrong authentication for user `{}`".format(username))
            self.render(
                'templates/login.html',
                next=next,
                message="Password/username sbagliati!"
            )
Example #26
0
def handle_leave_command(userid):
    """Handles signal triggered when a known user leaves."""
    LOG.info('Received leave command: id=%r', userid)
    try:
        userid = int(userid)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command!')
        return

    LOG.error('handle_leave_command not implemented.')
Example #27
0
def handle_leave_command(userid):
    """Handles signal triggered when a known user leaves."""
    LOG.info('Received leave command: id={}'.format(userid))
    try:
        userid = int(userid)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command!')
        return

    LOG.error('handle_leave_command not implemented.')
Example #28
0
def handle_enter_command(userid):
    """Handles signal triggered when a new user enters."""
    LOG.info('Received enter command: id={}'.format(userid))
    try:
        userid = int(userid)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command!')
        return

    LOG.error('handle_enter_command not implemented.')
Example #29
0
def start():
    """Will setup connection and ensure that all tables exist.
    MUST be called prior to any operation."""
    LOG.info('Connecting to DB...')
    engine.connect()

    # Create tables if they don't exist.
    LOG.info('Checking tables in the DB...')
    models.check()

    LOG.info('Done')
def start():
    """Will setup connection and ensure that all tables exist.
    MUST be called prior to any operation."""
    LOG.info('Connecting to DB...')
    engine.connect()

    # Create tables if they don't exist.
    LOG.info('Checking tables in the DB...')
    models.check()

    LOG.info('Done')
 def handle_stream(self, stream, address):
     """Handles inbound TCP connections asynchronously."""
     LOG.info("New connection from Fonera.")
     if address[0] != options.control_remote_address:
         LOG.error(
             "Connection from `%s`, expected from `%s`. Ignoring.",
                 address,
                 options.control_remote_address
         )
         return
     if RemoteListener.STREAM is not None:
         LOG.warning("Another connection was open, closing the previous one.")
         RemoteListener.STREAM.close()
     RemoteListener.STREAM = stream
     RemoteListener.STREAM.read_until(b'\n', self.handle_command)
Example #32
0
def handle_status_command(status):
    """Update status.
    Will reject two identical and consecutive updates
    (prevents opening when already open and vice-versa)."""
    LOG.info('Received status: {}'.format(status))
    try:
        status = int(status)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command')
        return
    if status not in (0, 1):
        LOG.error('Non existent status {}, ignoring.'.format(status))
        return

    textstatus = Status.OPEN if status == 1 else Status.CLOSED
    with session_scope() as session:
        curstatus = query.get_current_status(session)
        if curstatus is None or curstatus.value != textstatus:
            status = query.log_status(session, textstatus, 'BITS')
            broadcast(status.jsondict())
            notifier.send_status(textstatus)
        else:
            LOG.error('BITS already open/closed! Ignoring.')
Example #33
0
def handle_status_command(status):
    """Update status.
    Will reject two identical and consecutive updates
    (prevents opening when already open and vice-versa)."""
    LOG.info('Received status: %r', status)
    try:
        status = int(status)
    except ValueError:
        LOG.error('Wrong type for parameters in temperature command')
        return
    if status not in (0, 1):
        LOG.error('Non existent status %r, ignoring.', status)
        return

    textstatus = Status.OPEN if status == 1 else Status.CLOSED
    with session_scope() as session:
        curstatus = query.get_current_status(session)
        if curstatus is None or curstatus.value != textstatus:
            status = query.log_status(session, textstatus, 'BITS')
            broadcast(status.jsondict())
            notifier.send_status(textstatus)
        else:
            LOG.error('BITS already open/closed! Ignoring.')
    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:
                LOG.warning("DoS protection: %s", error)
                self.log_offender_details()
                self.render(
                    'templates/login.html',
                    next=next,
                    message="Tentativi dal tuo IP over 9000...",
                    show_recaptcha=True,
                    previous_attempt_incorrect=has_recaptcha
                )
                return

        if verified:
            self.set_secure_cookie(
                self.USER_COOKIE_NAME,
                username,
                expires_days=options.cookie_max_age_days
            )
            LOG.info("Authenticating user %r", username)
            self.redirect(next)
        else:
            LOG.warning("Failed authentication for user %r", username)
            self.log_offender_details()
            self.render(
                'templates/login.html',
                next=next,
                message="Password/username sbagliati!",
                show_recaptcha=has_recaptcha,
                # If we have a captcha at this point, it means we already failed once
                previous_attempt_incorrect=True
            )
Example #35
0
def handle_message_command(message):
    """Handles message broadcast requests."""
    LOG.info('Received message command: message=%r', message)
    try:
        decodedmex = base64.b64decode(message)
    except TypeError:
        LOG.error('Received message is not valid base64: %r', message)
    else:
        text = decodedmex.decode('utf8')
        #FIXME maybe get author ID from message?
        user = "******"
        with session_scope() as session:
            user = query.get_user(session, user)
            if not user:
                LOG.error("Non-existent user %r, not logging message.", user)
                return
            message = query.log_message(session, user, text)
            broadcast(message.jsondict())
        notifier.send_message(text)
Example #36
0
    def post(self):
        text = self.get_argument('msgtext')
        username = self.get_current_user()

        text = xhtml_escape(text)

        LOG.info("{} sent message {!r} from web".format(username, text))

        with session_scope() as session:
            user = query.get_user(session, username)
            message = query.log_message(session, user, text)
            LOG.info("Broadcasting to clients")
            broadcast(message.jsondict())
            LOG.info("Notifying Fonera")
            notifier.send_message(text)

        self.render('templates/message.html',
                    message='Messaggio inviato correttamente!',
                    text=text)
    def post(self):
        text = self.get_argument('msgtext')
        username = self.get_current_user()

        LOG.info("%r sent message %r from web", username, text)

        with session_scope() as session:
            user = query.get_user(session, username)
            message = query.log_message(session, user, text)
            LOG.info("Broadcasting to clients")
            broadcast(message.jsondict())
            LOG.info("Notifying Fonera")
            notifier.send_message(text)

        self.render(
            'templates/message.html',
            message='Messaggio inviato correttamente!',
            text=text
        )
Example #38
0
def handle_message_command(message):
    """Handles message broadcast requests."""
    LOG.info('Received message command: message={!r}'.format(message))
    try:
        decodedmex = base64.b64decode(message)
    except TypeError:
        LOG.error('Received message is not valid base64: {!r}'.format(message))
    else:
        text = decodedmex.decode('utf8')
        #FIXME maybe get author ID from message?
        user = "******"
        with session_scope() as session:
            user = query.get_user(session, user)
            if not user:
                LOG.error(
                    "Non-existent user {}, not logging message.".format(user))
                return
            message = query.log_message(session, user, text)
            broadcast(message.jsondict())
        notifier.send_message(text)
Example #39
0
 def register(self, client):
     """Add a new handler to the clients list."""
     LOG.debug('Adding client {} to {}'.format(client, self.name))
     self.clients.append(client)
 def log_offender_details(self):
     userAgent = self.request.headers.get("User-Agent", '<unknown>')
     remoteIp = self.request.remote_ip
     LOG.warning("Request came from %s, user agent is '%s'", remoteIp, userAgent)
Example #41
0
 def unregister(self, client):
     """Remove the handler from the clients list."""
     LOG.debug('Removing client {} from {}'.format(client, self.name))
     self.clients.remove(client)
Example #42
0
def delete(session, data):
    """Delete data from DB."""
    LOG.debug('Deleting {}'.format(data))
    session.delete(data)
Example #43
0
 def on_message(self, message):
     """Disconnect clients sending data (they should not)."""
     LOG.warning('Client sent a message: disconnected.')
Example #44
0
 def on_close(self):
     """Unregister this handler when the connection is closed."""
     StatusConnection.CLIENTS.unregister(self)
     LOG.debug('Unregistered client.')
Example #45
0
def shutdown():
    """Stop server and add callback to stop i/o loop."""
    io_loop = tornado.ioloop.IOLoop.instance()
    LOG.info('Shutting down in 2 seconds')
    io_loop.add_timeout(time.time() + 2, io_loop.stop)
Example #46
0
def shutdown():
    """Stop server and add callback to stop i/o loop."""
    io_loop = tornado.ioloop.IOLoop.instance()
    LOG.info('Shutting down in 2 seconds')
    io_loop.add_timeout(time.time() + 2, io_loop.stop)
Example #47
0
 def unregister(self, client):
     """Remove the handler from the clients list."""
     LOG.debug('Removing client {} from {}'.format(client, self.name))
     self.clients.remove(client)
Example #48
0
 def register(self, client):
     """Add a new handler to the clients list."""
     LOG.debug('Adding client {} to {}'.format(client, self.name))
     self.clients.append(client)
Example #49
0
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