def message_auth_token(user_id): try: salt = current_app.config['MESSAGE_TOKEN_SALT'] except RuntimeError: # handle case that we're running outside app (e.g. in a worker process) config = load_server_config() salt = config['MESSAGE_TOKEN_SALT'] # find/create a key for this user if user_id: keys = Key.query.filter(Key.access_as_user_id == user_id, Key.revocation_timestamp.is_(None)) if keys.count(): key_hash = keys[0].key_hash key_id = keys[0].id else: org_users = OrganizationUser.query.filter(OrganizationUser.user_id == user_id) if org_users.count(): organization_id = org_users[0].organization_id # for now we'll just create a key for one organization; revisit this (key, _) = create_key(user_id, organization_id, user_id, None) key_hash = key.key_hash key_id = key.id else: return '' else: # this case is used for inter-server access; we could create special keys for inter-server access (access from another server, not a user) key_hash = '' key_id = 0 # generate the token nonce = generate_access_code(10) timestamp = int(time.time()) hash_message = '%d,%d,%s,%s,%s' % (timestamp, key_id, nonce, salt, key_hash) b64hash = base64.standard_b64encode(hashlib.sha512(hash_message.encode()).digest()).decode() return '0,%d,%d,%s,%s' % (timestamp, key_id, nonce, b64hash)
def inner_password_hash(password): try: salt = current_app.config['SALT'] except RuntimeError: # handle case that we're running outside app (e.g. creating admin from command line) config = load_server_config() salt = config['SALT'] return base64.standard_b64encode(hashlib.sha512((password + salt).encode('utf-8')).digest()).decode()
def handle_send_text_message(controller_id, parameters): # get parameters if 'phoneNumbers' in parameters: phone_numbers = parameters['phoneNumbers'] else: phone_numbers = parameters['phone_numbers'] message = parameters['message'] # check to make sure not sending too many messages if check_and_update_throttle(controller_id, 'send_text_message'): # record message (for use investigating abuse) outgoing_message = OutgoingMessage() outgoing_message.timestamp = datetime.datetime.utcnow() outgoing_message.controller_id = controller_id outgoing_message.recipients = phone_numbers outgoing_message.message = message outgoing_message.attributes = '{}' db.session.add(outgoing_message) db.session.commit() # send the message send_text_message(phone_numbers, message, load_server_config()) else: pass # fix(later): provide an error?
def handle_send_email(controller_id, parameters): # get parameters if 'emailAddresses' in parameters: email_addresses = parameters['emailAddresses'] else: email_addresses = parameters['email_addresses'] subject = parameters['subject'] body = parameters['body'] # check to make sure not sending too many messages if check_and_update_throttle(controller_id, 'send_email'): # record message (for use investigating abuse) outgoing_message = OutgoingMessage() outgoing_message.timestamp = datetime.datetime.utcnow() outgoing_message.controller_id = controller_id outgoing_message.recipients = email_addresses outgoing_message.message = subject outgoing_message.attributes = '{}' db.session.add(outgoing_message) db.session.commit() # send the message # fix(soon): handle multiple recipients (up to 5) send_email(email_addresses, subject, body, load_server_config()) else: pass # fix(later): provide an error?
def message_auth_token(user_id): try: salt = current_app.config['MESSAGE_TOKEN_SALT'] except RuntimeError: # handle case that we're running outside app (e.g. in a worker process) config = load_server_config() salt = config['MESSAGE_TOKEN_SALT'] timestamp = int(time.time()) hash_message = '%d;%d;%s' % (user_id, timestamp, salt) b64hash = base64.standard_b64encode( hashlib.sha512(hash_message.encode()).digest()).decode() return '1;%d;%d;%s' % (user_id, timestamp, b64hash)
def migrate_keys(): keys = Key.query.order_by('id') app_config = load_server_config() aes = AESCipher(app_config['KEY_STORAGE_KEY']) count = 0 for key in keys: if key.key_storage and not key.key_hash: count += 1 nonce_and_key = aes.decrypt(key.key_storage) parts = nonce_and_key.split(';') secret_key = parts[1] key.key_hash = hash_password(secret_key) db.session.commit() print 'migrated keys:', count
def find_key_by_code(auth_code): from main.users.encrypt import AESCipher parts = auth_code.split(';') if len(parts) != 3: return None client_key_part = parts[0] client_nonce = parts[1] client_key_hash = parts[2] keys = Key.query.filter(Key.key_part == client_key_part, Key.revocation_timestamp == None) app_config = load_server_config() aes = AESCipher(app_config['KEY_STORAGE_KEY']) for key in keys: nonce_and_key = aes.decrypt(key.key_storage) parts = nonce_and_key.split(';') secret_key = parts[1] key_hash = base64.b64encode( hashlib.sha512(client_nonce + ';' + secret_key).digest()) if key_hash == client_key_hash: return key return None
def message_monitor(): server_config = load_server_config() if 'MQTT_HOST' not in server_config: worker_log('message_monitor', 'MQTT host not configured') return worker_log('message_monitor', 'starting') mqtt_host = server_config['MQTT_HOST'] mqtt_port = server_config.get('MQTT_PORT', 443) # run this on connect/reconnect def on_connect(client, userdata, flags, rc): # pylint: disable=unused-argument if rc: worker_log( 'message_monitor', 'unable to connect to MQTT broker/server at %s:%d' % (mqtt_host, mqtt_port)) else: worker_log( 'message_monitor', 'connected to MQTT broker/server at %s:%d' % (mqtt_host, mqtt_port)) client.subscribe('#') # subscribe to all messages # run this on message def on_message(client, userdata, msg): # pylint: disable=unused-argument payload = msg.payload.decode() # handle full (JSON) messages if payload.startswith('{'): message_struct = json.loads(payload) for message_type, parameters in message_struct.items(): # update sequence values; doesn't support image sequence; should use REST API for image sequences if message_type == 'update': folder = find_resource( '/' + msg.topic ) # for now we assume these messages are published on controller channels if folder and folder.type in (Resource.BASIC_FOLDER, Resource.ORGANIZATION_FOLDER, Resource.CONTROLLER_FOLDER): timestamp = parameters.get('$t', '') if timestamp: timestamp = parse_json_datetime( timestamp ) # fix(soon): handle conversion errors else: timestamp = datetime.datetime.utcnow() for name, value in parameters.items(): if name != '$t': seq_name = '/' + msg.topic + '/' + name resource = find_resource(seq_name) if resource: # don't emit new message since UI will receive this message update_sequence_value(resource, seq_name, timestamp, value, emit_message=False) db.session.commit() # update controller watchdog status elif message_type == 'watchdog': controller = find_resource( '/' + msg.topic ) # for now we assume these messages are published on controller channels if controller and controller.type == Resource.CONTROLLER_FOLDER: controller_status = ControllerStatus.query.filter( ControllerStatus.id == controller.id).one() controller_status.last_watchdog_timestamp = datetime.datetime.utcnow( ) db.session.commit() # send emails elif message_type == 'send_email': controller = find_resource( '/' + msg.topic ) # for now we assume these messages are published on controller channels if controller and controller.type == Resource.CONTROLLER_FOLDER: print('sending email') handle_send_email(controller.id, parameters) # send SMS messages elif message_type == 'send_sms' or message_type == 'send_text_message': controller = find_resource( '/' + msg.topic ) # for now we assume these messages are published on controller channels if controller and controller.type == Resource.CONTROLLER_FOLDER: handle_send_text_message(controller.id, parameters) # handle short (non-JSON) messages else: # print('MQTT: %s %s' % (msg.topic, payload)) if payload.startswith( 's,' ): # type 's' is "store and display new sequence value" parts = payload.split(',', 3) if len(parts) == 4: seq_name = '/' + msg.topic + '/' + parts[1] timestamp = parse_json_datetime( parts[2]) # fix(soon): handle conversion errors value = parts[3] resource = find_resource(seq_name) if resource and resource.type == Resource.SEQUENCE: # don't emit new message since UI will receive this message update_sequence_value(resource, seq_name, timestamp, value, emit_message=False) db.session.commit() # connect and run mqtt_client = mqtt.Client(transport='websockets') mqtt_client.on_connect = on_connect mqtt_client.on_message = on_message mqtt_client.username_pw_set( 'token', message_auth_token(0) ) # user_id 0 indicates that this is an internal connection from the server mqtt_client.tls_set() # enable SSL mqtt_client.connect(mqtt_host, mqtt_port) mqtt_client.loop_start() while True: gevent.sleep(60)
def message_monitor(): server_config = load_server_config() if 'MQTT_HOST' not in server_config: worker_log('message_monitor', 'MQTT host not configured') return worker_log('message_monitor', 'starting') mqtt_host = server_config['MQTT_HOST'] mqtt_port = server_config.get('MQTT_PORT', 443) # run this on connect/reconnect def on_connect(client, userdata, flags, rc): # pylint: disable=unused-argument if rc: worker_log('message_monitor', 'unable to connect to MQTT broker/server at %s:%d' % (mqtt_host, mqtt_port)) else: worker_log('message_monitor', 'connected to MQTT broker/server at %s:%d' % (mqtt_host, mqtt_port)) client.subscribe('#') # subscribe to all messages # run this on message def on_message(client, userdata, msg): # pylint: disable=unused-argument # print('MQTT: %s %s' % (msg.topic, msg.payload.decode())) message_struct = json.loads(msg.payload.decode()) message_type = message_struct['type'] if message_type == 'update_sequence': controller = find_resource('/' + msg.topic) # for now we assume these messages are published on controller channels if controller and controller.type == Resource.CONTROLLER_FOLDER: parameters = message_struct['parameters'] seq_name = parameters['sequence'] if not seq_name.startswith('/'): # handle relative sequence names resource = Resource.query.filter(Resource.id == controller.id).one() # this is ok for now since .. doesn't have special meaning in resource path (no way to escape controller folder) seq_name = resource.path() + '/' + seq_name timestamp = parameters.get('timestamp', '') # fix(soon): need to convert to datetime if not timestamp: timestamp = datetime.datetime.utcnow() value = parameters['value'] if 'encoded' in parameters: value = base64.b64decode(value) # remove this; require clients to use REST POST for images resource = find_resource(seq_name) if not resource: return system_attributes = json.loads(resource.system_attributes) if resource.system_attributes else None if system_attributes and system_attributes['data_type'] == Resource.IMAGE_SEQUENCE: value = base64.b64decode(value) else: value = str(value) # don't emit message since the message is already in the system update_sequence_value(resource, seq_name, timestamp, value, emit_message=False) db.session.commit() # update controller watchdog status elif message_type == 'watchdog': controller = find_resource('/' + msg.topic) # for now we assume these messages are published on controller channels if controller and controller.type == Resource.CONTROLLER_FOLDER: controller_status = ControllerStatus.query.filter(ControllerStatus.id == controller.id).one() controller_status.last_watchdog_timestamp = datetime.datetime.utcnow() db.session.commit() # send emails elif message_type == 'send_email': controller = find_resource('/' + msg.topic) # for now we assume these messages are published on controller channels if controller and controller.type == Resource.CONTROLLER_FOLDER: print('sending email') handle_send_email(controller.id, message_struct['parameters']) # send SMS messages elif message_type == 'send_sms' or message_type == 'send_text_message': controller = find_resource('/' + msg.topic) # for now we assume these messages are published on controller channels if controller and controller.type == Resource.CONTROLLER_FOLDER: handle_send_text_message(controller.id, message_struct['parameters']) # connect and run mqtt_client = mqtt.Client(transport='websockets') mqtt_client.on_connect = on_connect mqtt_client.on_message = on_message mqtt_client.username_pw_set('token', message_auth_token(0)) # user_id 0 indicates that this is an internal connection from the server mqtt_client.tls_set() # enable SSL mqtt_client.connect(mqtt_host, mqtt_port) mqtt_client.loop_start() while True: gevent.sleep(60)
def controller_watchdog(): server_config = load_server_config() worker_log('controller_watchdog', 'starting') last_log_time = None while True: watchdog_check_count = 0 watchdog_expire_count = 0 try: # get list of controllers controllers = Resource.query.filter( Resource.type == Resource.CONTROLLER_FOLDER, not_(Resource.deleted)) for controller in controllers: system_attributes = json.loads( controller.system_attributes ) if controller.system_attributes else {} # if watchdog notifications are enabled if system_attributes.get( 'watchdog_minutes', 0) > 0 and 'watchdog_recipients' in system_attributes: watchdog_check_count += 1 try: # check controller status; if stale watchdog timestamp, send message (if not done already); # if no watchdog timestamp, don't send message (assume user is just setting on the controller for the first time) controller_status = ControllerStatus.query.filter( ControllerStatus.id == controller.id).one() # fix(soon): safe int convert time_thresh = datetime.datetime.utcnow( ) - datetime.timedelta( minutes=system_attributes['watchdog_minutes']) if controller_status.last_watchdog_timestamp and controller_status.last_watchdog_timestamp < time_thresh: watchdog_expire_count += 1 if controller_status.watchdog_notification_sent is False: # send notifications recipients = system_attributes[ 'watchdog_recipients'] worker_log( 'controller_watchdog', 'sending notification for %s to %s' % (controller.path(), recipients)) recipients = recipients.split(',') message = '%s is offline' % controller.path() if server_config[ 'PRODUCTION']: # only send message in production; this is very important for recipient in recipients: if '@' in recipient: send_email(recipient, message, message, server_config) else: send_text_message( recipient, message, server_config) controller_status.watchdog_notification_sent = True db.session.commit() else: if controller_status.watchdog_notification_sent: controller_status.watchdog_notification_sent = False db.session.commit() db.session.expire(controller_status) except NoResultFound: worker_log( 'controller_watchdog', 'controller status not found (%d)' % controller.id) # handle all exceptions because we don't want an error in this code (e.g. sending email or bad status/controller data) stopping all # notifications # pylint: disable=broad-except except Exception as e: print('controller_watchdog error: %s' % str(e)) worker_log('controller_watchdog', str(e)) # once an hour, log current status if (last_log_time is None) or time.time() - last_log_time > 60 * 60: worker_log( 'controller_watchdog', 'checked %d controllers; %d are currently expired' % (watchdog_check_count, watchdog_expire_count)) last_log_time = time.time() # wait one minute time.sleep(60)