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)
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 )
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)
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')
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)
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)
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)
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)
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
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)
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())
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())
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 )
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.')
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 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.')
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.')
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.')
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)
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.')
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 )
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)
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 )
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)
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)
def unregister(self, client): """Remove the handler from the clients list.""" LOG.debug('Removing client {} from {}'.format(client, self.name)) self.clients.remove(client)
def delete(session, data): """Delete data from DB.""" LOG.debug('Deleting {}'.format(data)) session.delete(data)
def on_message(self, message): """Disconnect clients sending data (they should not).""" LOG.warning('Client sent a message: disconnected.')
def on_close(self): """Unregister this handler when the connection is closed.""" StatusConnection.CLIENTS.unregister(self) LOG.debug('Unregistered client.')
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)
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