def _init_(self, **kwargs): self.custom_agent = Agent(reactor, connectTimeout=20) self.contentType = self._Configs.get('yomboapi', 'contenttype', 'application/json', False) # TODO: Msgpack later self.base_url = self._Configs.get('yomboapi', 'baseurl', "https://api.yombo.net/api", False) self.allow_system_session = self._Configs.get('yomboapi', 'allow_system_session', True) self.init_defer = None self.api_key = self._Configs.get('yomboapi', 'api_key', 'aBMKp5QcQoW43ipauw88R0PT2AohcE', False) self.valid_system_session = None self.valid_login_key = None self.session_validation_cache = ExpiringDict() try: self.system_session = self._Configs.get( 'yomboapi', 'auth_session') # to be encrypted with gpg later self.system_login_key = self._Configs.get( 'yomboapi', 'login_key') # to be encrypted with gpg later except KeyError: self.system_session = None self.system_login_key = None if self._Loader.operating_mode == 'run': self.init_defer = Deferred() self.validate_system_login() return self.init_defer
def _init_(self, **kwargs): self.enabled = self._Configs.get('webinterface', 'enabled', True) if not self.enabled: return self.gateway_id = self._Configs.get2('core', 'gwid', 'local', False) # self._LocalDB = self._Loader.loadedLibraries['localdb'] self._current_dir = self._Atoms.get('yombo.path') + "/yombo" self._dir = '/lib/webinterface/' self._build_dist() # Make all the JS and CSS files self.secret_pin_totp = self._Configs.get2( 'webinterface', 'auth_pin_totp', yombo.utils.random_string( length=16, letters='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')) self.api = self._Loader.loadedLibraries['yomboapi'] self._VoiceCmds = self._Loader.loadedLibraries['voicecmds'] self.misc_wi_data = {} self.sessions = Sessions(self._Loader) self.wi_port_nonsecure = self._Configs.get2('webinterface', 'nonsecure_port', 8080) self.wi_port_secure = self._Configs.get2('webinterface', 'secure_port', 8443) self.webapp.templates = jinja2.Environment( loader=jinja2.FileSystemLoader(self._current_dir)) self.setup_basic_filters() route_atoms(self.webapp) route_automation(self.webapp) route_api_v1(self.webapp) route_configs(self.webapp) route_devices(self.webapp) route_locations(self.webapp) route_devtools_debug(self.webapp) route_devtools_config(self.webapp) route_gateways(self.webapp) route_home(self.webapp) route_misc(self.webapp) route_modules(self.webapp) route_notices(self.webapp) route_panel(self.webapp) route_setup_wizard(self.webapp) route_statistics(self.webapp) route_states(self.webapp) route_system(self.webapp) route_voicecmds(self.webapp) self.temp_data = ExpiringDict(max_age_seconds=1800) self.web_server_started = False self.web_server_ssl_started = False self.already_start_web_servers = False self.web_factory = None # just here to set a password if it doesn't exist. mqtt_password = self._Configs.get('mqtt_users', 'panel.webinterface', yombo.utils.random_string())
def _init_(self, **kwargs): """ On startup, various libraries will need certs (webinterface, MQTT) for encryption. This module stores certificates in a directory so other programs can use certs as well. It's working data is stored in the database, while a backup is kept in the file system as well and is only used if the data is missing from the database. If a cert isn't avail for the requested sslname, it will receive a self-signed certificate. :return: """ # Since SSL generation can take some time on slower devices, we use a simple queue system. self.generate_csr_queue = self._Queue.new( 'library.sslcerts.generate_csr', self.generate_csr) self.hostname = gethostname() self.gateway_id = self._Configs.get('core', 'gwid', 'local', False) self.fqdn = self._Configs.get2('dns', 'fqdn', None, False) self.received_message_for_unknown = ExpiringDict(100, 600) self.self_signed_cert_file = self._Atoms.get( 'yombo.path') + "/usr/etc/certs/sslcert_selfsigned.cert.pem" self.self_signed_key_file = self._Atoms.get( 'yombo.path') + "/usr/etc/certs/sslcert_selfsigned.key.pem" self.self_signed_expires = self._Configs.get("sslcerts", "self_signed_expires", None, False) self.self_signed_created = self._Configs.get("sslcerts", "self_signed_created", None, False) if os.path.exists(self.self_signed_cert_file) is False or \ self.self_signed_expires is None or \ self.self_signed_expires < int(time() + (60*60*24*60)) or \ self.self_signed_created is None or \ not os.path.exists(self.self_signed_key_file): logger.info( "Generating a self signed cert for SSL. This can take a few moments." ) yield self._create_self_signed_cert() self.self_signed_cert = yield read_file(self.self_signed_cert_file) self.self_signed_key = yield read_file(self.self_signed_key_file) self.managed_certs = yield self._SQLDict.get( self, "managed_certs", serializer=self.sslcert_serializer, unserializer=self.sslcert_unserializer) # for name, data in self.managed_certs.items(): # print("cert name: %s" % name) # print(" cert data: %s" % data.__dict__) self.check_if_certs_need_update_loop = None
def _init_(self): self.custom_agent = Agent(reactor, connectTimeout=20) self.contentType = self._Configs.get('yomboapi', 'contenttype', 'application/json', False) # TODO: Msgpack later self.base_url = self._Configs.get('yomboapi', 'baseurl', "https://api.yombo.net/api", False) self.allow_system_session = self._Configs.get('yomboapi', 'allow_system_session', True) self.init_defer = None self.api_key = self._Configs.get('yomboapi', 'api_key', 'aBMKp5QcQoW43ipauw88R0PT2AohcE', False) self.valid_system_session = None self.session_validation_cache = ExpiringDict() if self.allow_system_session: self.system_session = self._Configs.get('yomboapi', 'auth_session') # to be encrypted with gpg later self.system_login_key = self._Configs.get('yomboapi', 'login_key') # to be encrypted with gpg later else: self.system_session = None self.system_login_key = None
class YomboAPI(YomboLibrary): contentType = None def _init_(self): self.custom_agent = Agent(reactor, connectTimeout=20) self.contentType = self._Configs.get('yomboapi', 'contenttype', 'application/json', False) # TODO: Msgpack later self.base_url = self._Configs.get('yomboapi', 'baseurl', "https://api.yombo.net/api", False) self.allow_system_session = self._Configs.get('yomboapi', 'allow_system_session', True) self.init_defer = None self.api_key = self._Configs.get('yomboapi', 'api_key', 'aBMKp5QcQoW43ipauw88R0PT2AohcE', False) self.valid_system_session = None self.session_validation_cache = ExpiringDict() if self.allow_system_session: self.system_session = self._Configs.get('yomboapi', 'auth_session') # to be encrypted with gpg later self.system_login_key = self._Configs.get('yomboapi', 'login_key') # to be encrypted with gpg later else: self.system_session = None self.system_login_key = None def _load_(self): if self._Atoms['loader.operation_mode'] == 'run': self.init_defer = Deferred() self.validate_system_login() return self.init_defer def _start_(self): # print "system_session status: %s" % self.system_session # print "system_login_key status: %s" % self.system_login_key pass def _stop_(self): pass def _unload_(self): pass @inlineCallbacks def gateway_index(self, session=None): results = yield self.request("GET", "/v1/gateway", None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot get gateways") else: if results['content']['message'] == "Invalid Token.": raise YomboWarningCredentails("URI: '%s' requires credentials." % results['content']['response']['uri']) raise YomboWarning("Unknown error: %s" % results['content']) @inlineCallbacks def gateway_get(self, gateway_id, session=None): results = yield self.request("GET", "/v1/gateway/%s" % gateway_id, None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway_put(self, gateway_id, values, session=None): results = yield self.request("PATCH", "/v1/gateway/%s" % gateway_id, values, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway__module_get(self, gateway_id, session=None): results = yield self.request("GET", "/v1/gateway/%s/modules" % gateway_id, None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway__module_put(self, gateway_id, values, session=None): results = yield self.request("PATCH", "/v1/gateway/%s/modules" % gateway_id, values, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway_config_index(self, gateway_id, session=None): results = yield self.request("GET", "/v1/gateway/%s/config" % gateway_id, None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot get gateways") else: raise YomboWarning("Unknown error: %s" % results['content']['message']) # Below are the core help functions def save_system_session(self, session): print "api save_system_session0: %s" % session self.system_session = session print "api save_system_session1: %s" % session self._Configs.set('yomboapi', 'auth_session', session) # to be encrypted with gpg later print "api save_system_session2: %s" % session def save_system_login_key(self, login_key): print "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@api save_system_login_key: %s" % login_key self.system_login_key = login_key print "api save_system_login_key1: %s" % login_key self._Configs.set('yomboapi', 'login_key', login_key) # to be encrypted with gpg later print "api save_system_login_key2: %s" % login_key def select_session(self, session_id=None, session_key=None): if session_id is None or session_key is None: if self.allow_system_session: return self.system_session, self.system_login_key logger.info("select_session: Yombo API has no session data for 'selection_session'") return None, None def clear_session_cache(self, session=None): if (session is None): self.session_validation_cache.clear() else: hashed = sha1(session) if hashed in self.session_validation_cache: del self.session_validation_cache[hashed] # None works too... @inlineCallbacks def validate_system_login(self): """ Validates a system session if it exists. If not, it tries the login_key and creates a new session. :return: """ if self.allow_system_session is False: self._States.set('yomboapi.valid_system_session', False) self.init_defer.callback(10) returnValue(False) if self.system_session is None and self.system_login_key is None: print "validate_system_login: self.system_session: %s" % self.system_session print "validate_system_login: self.system_login_key: %s" % self.system_login_key logger.warn("No saved system session information and no login_key. Disabling automated system changes.") self._States.set('yomboapi.valid_system_session', False) self.valid_system_session = False if self.init_defer is not None: self.init_defer.callback(10) returnValue(None) self.clear_session_cache() if self.system_session is not None: results = yield self.do_validate_session(self.system_session) if (results is True): print "has a system session!" self._States.set('yomboapi.valid_system_session', True) self.valid_system_session = True self.init_defer.callback(10) returnValue(True) if self.system_login_key is not None: results = yield self.user_login_with_key(self.system_login_key) print "reslts: %s" % results if (results is not False): print "has a system login key!" self._Configs.set('yomboapi', 'auth_session', results['session']) # to be encrypted with gpg later self.system_session = results['session'] self._States.set('yomboapi.valid_system_session', True) self.valid_system_session = True self.init_defer.callback(10) returnValue(True) print "API system has some data, but it's invalid!" self._States.set('yomboapi.valid_system_session', False) self.valid_system_session = False self.init_defer.callback(10) returnValue(False) @inlineCallbacks def validate_session(self, session_id=None, session_key=None, clear_cache=False): session_id, session_key = self.select_session(session_id, session_key) if session_id is None or session_key is None: logger.debug("Yombo API session information is not valid: {id}:{key}", id=session_id, key=session_key) hashed = sha1(session_id + session_key) if hashed in self.session_validation_cache: if clear_cache is True: del self.session_validation_cache[hashed] else: returnValue(self.session_validation_cache[hashed]) results = yield self.do_validate_session(session_id, session_key) self.session_validation_cache[hashed] = results returnValue(results) @inlineCallbacks def do_validate_session(self, session): try: results = yield self.request("GET", "/v1/user/session/validate", None, session) except Exception, e: logger.debug("$$$1 API Errror: {error}", error=e) returnValue(False) logger.debug("$$$a REsults from API: {results}", results=results['content']) # waiting on final API.yombo.com to complete this. If we get something, we are good for now. if (results['content']['code'] != 200): returnValue(False) elif (results['content']['response']['session_status'] == 'valid'): returnValue(True) else: returnValue(False)
def _init_(self): """ Setups up the basic framework. """ # We only cache the last few events, and only for certain time. self.notifications = ExpiringDict(max_len=100, max_age_seconds=600)
class LogEvents(YomboLibrary): """ Manages all notifications. """ def _init_(self): """ Setups up the basic framework. """ # We only cache the last few events, and only for certain time. self.notifications = ExpiringDict(max_len=100, max_age_seconds=600) # return self.init_deferred def _load_(self): self._LocalDB = self._Libraries['localdb'] self._checkExpiredLoop = LoopingCall(self.check_expired) self._checkExpiredLoop.start(self._Configs.get('notifications', 'check_expired', 30, False)) self.load_notifications() def _stop_(self): if self.init_deferred is not None and self.init_deferred.called is False: self.init_deferred.callback(1) # if we don't check for this, we can't stop! def _clear_(self): """ Clear all devices. Should only be called by the loader module during a reconfiguration event. B{Do not call this function!} """ self.notifications.clear() def _reload_(self): self._clear_() self.load_notifications() def check_expired(self): """ Called by looping call to periodically purge expired notifications. :return: """ cur_time = int(time()) for id, notice in self.notifications.iteritems(): print "cur : expired = %s : %s" % (cur_time, notice.expire) if cur_time > notice.expire: print "deleting notice: %s" % notice.title del self.notifications[id] self._LocalDB.delete_expired_notifications() def get(self, notification_requested): """ Performs the actual search. .. note:: Modules shouldn't use this function. Use the built in reference to find notification: `self._Notifications['8w3h4sa']` :raises YomboWarning: Raised when notifcation cannot be found. :param notification_requested: The input type ID or input type label to search for. :type notification_requested: string :return: A dict containing details about the notification :rtype: dict """ if notification_requested in self.notifications: return self.notifications[notification_requested] else: raise YomboWarning('Notification not found: %s' % notification_requested) def delete(self, notification_requested): """ Deletes a provided notification. :param notification_requested: :return: """ try: del self.notifications[notification_requested] except: pass self._LocalDB.delete_notification(notification_requested) @inlineCallbacks def load_notifications(self): """ Load the last few notifications into memory. """ notifications = yield self._LocalDB.get_notifications() for notice in notifications: notice = notice.__dict__ if notice['expire'] < int(time()): continue notice['meta'] = json.loads(notice['meta']) self.add_notice(notice, from_db=True) logger.debug("Done load_notifications: {notifications}", notifications=self.notifications) # self.init_deferred.callback(10) def add_notice(self, notice, from_db=False, persist=True, create_event=False): """ Add a new notice. :param notice: A dictionary containing notification details. :type record: dict :returns: Pointer to new notice. Only used during unittest """ print "adding notice1: %s" % notice if 'id' not in notice: notice['id'] = random_string(length=16) if 'type' not in notice: notice['type'] = 'system' if 'priority' not in notice: notice['priority'] = 'normal' if 'source' not in notice: notice['source'] = '' if 'expire' not in notice: if 'timeout' in notice: notice['expire'] = int(time()) + notice['timeout'] else: notice['expire'] = int(time()) + 3600 else: if notice['expire'] > int(time()): YomboWarning("New notification is set to expire before current time.") if 'created' not in notice: notice['created'] = int(time()) if 'acknowledged' not in notice: notice['acknowledged'] = False else: if notice['acknowledged'] not in (True, False): YomboWarning("New notification 'acknowledged' must be either True or False.") if 'title' not in notice: raise YomboWarning("New notification requires a title.") if 'message' not in notice: raise YomboWarning("New notification requires a message.") if 'meta' not in notice: notice['meta'] = {} logger.debug("notice: {notice}", notice=notice) if from_db is False: self._LocalDB.add_notification(notice) self.notifications.prepend(notice['id'], Notification(notice)) else: self.notifications[notice['id']] = Notification(notice) # self.notifications = OrderedDict(sorted(self.notifications.iteritems(), key=lambda x: x[1]['created'])) pass return notice['id']
class SSLCerts(YomboLibrary): """ Responsible for managing various encryption and TLS (SSL) certificates. """ managed_certs = {} received_message_for_unknown = ExpiringDict(100, 600) def __contains__(self, cert_requested): """ Looks for an sslkey with the given sslname. >>> if "webinterface" in self._SSLCerts["library_webinterface"]: #by uuid :param cert_requested: The ssl cert sslname to search for. :type cert_requested: string :return: Returns true if exists, otherwise false. :rtype: bool """ if cert_requested in self.managed_certs: return True else: return False @inlineCallbacks def _init_(self, **kwargs): """ On startup, various libraries will need certs (webinterface, MQTT) for encryption. This module stores certificates in a directory so other programs can use certs as well. It's working data is stored in the database, while a backup is kept in the file system as well and is only used if the data is missing from the database. If a cert isn't avail for the requested sslname, it will receive a self-signed certificate. :return: """ # Since SSL generation can take some time on slower devices, we use a simple queue system. self.generate_csr_queue = self._Queue.new( "library.sslcerts.generate_csr", self.generate_csr) self.hostname = gethostname() self.local_gateway = self._Gateways.local self.self_signed_cert_file = self._Atoms.get( "working_dir") + "/etc/certs/sslcert_selfsigned.cert.pem" self.self_signed_key_file = self._Atoms.get( "working_dir") + "/etc/certs/sslcert_selfsigned.key.pem" self.self_signed_expires_at = self._Configs.get( "sslcerts", "self_signed_expires_at", None, False) self.self_signed_created_at = self._Configs.get( "sslcerts", "self_signed_created_at", None, False) self.default_key_size = self._Configs.get("sslcerts", "default_key_size", 2048) if os.path.exists(self.self_signed_cert_file) is False or \ self.self_signed_expires_at is None or \ self.self_signed_expires_at < int(time() + (60*60*24*60)) or \ self.self_signed_created_at is None or \ not os.path.exists(self.self_signed_key_file): logger.info( "Generating a self signed cert for SSL. This can take a few moments." ) yield self._create_self_signed_cert() self.self_signed_cert = yield read_file(self.self_signed_cert_file) self.self_signed_key = yield read_file(self.self_signed_key_file) self.managed_certs = yield self._SQLDict.get( self, "managed_certs", serializer=self.sslcert_serializer, unserializer=self.sslcert_unserializer) for key, item in self.managed_certs.items(): print(f"Managed certs: {self.managed_certs}") self.check_if_certs_need_update_loop = None @inlineCallbacks def _load_(self, **kwargs): """ Starts the loop to check if any certs need to be updated. :return: """ self.check_if_certs_need_update_loop = LoopingCall( self.check_if_certs_need_update) self.check_if_certs_need_update_loop.start( self._Configs.get("sqldict", "save_interval", random_int(60 * 60 * 24, .1), False), False) # Check if any libraries or modules need certs. if self.local_gateway.dns_name is None: logger.warn( "Unable to generate sign ssl/tls certs, gateway has no domain name." ) return if self._Loader.operating_mode != "run": return sslcerts = yield global_invoke_all( "_sslcerts_", called_by=self, ) for component_name, ssl_certs in sslcerts.items(): logger.debug( f"Adding new managed certs from hook: {component_name}") if isinstance(ssl_certs, tuple) is False and isinstance( ssl_certs, list) is False: ssl_certs = [ssl_certs] for ssl_item in ssl_certs: yield self.add_sslcert(ssl_item) def _stop_(self, **kwargs): """ Simply stop any loops, tell all the certs to save themselves to disk as a backup. :return: """ if hasattr(self, "check_if_certs_need_update_loop"): if self.check_if_certs_need_update_loop is not None and self.check_if_certs_need_update_loop.running: self.check_if_certs_need_update_loop.stop() if hasattr(self, "managed_certs"): for sslname, cert in self.managed_certs.items(): cert.stop() def sslcert_serializer(self, item): """ Used to hydrate the list of certs. Somethings shouldn't be stored in the SQLDict. :param item: :return: """ return item.asdict() @inlineCallbacks def sslcert_unserializer(self, item): """ Used by SQLDict to hydrate an item stored. :param item: :return: """ # print(f"sslcert unserialze: {item}") results = SSLCert(self, "sqldict", DictObject(item)) yield results.start() return results @inlineCallbacks def check_if_certs_need_update(self): """ Called periodically to see if any certs need to be updated. Once a day is enough, we have 30 days to get this done. """ for sslname, cert in self.managed_certs.items(): yield cert.check_if_rotate_needed() @inlineCallbacks def add_sslcert(self, ssl_data): """ Called when new SSL Certs need to be managed. :param sslcerts: :param bypass_checks: For internal use only. :return: """ logger.debug("add_sslcert: {ssl_data}", ssl_data=ssl_data) if self.local_gateway.dns_name is None: logger.warn( "Unable to generate sign ssl/tls certs, gateway has no domain name." ) return try: ssl_data = self.check_csr_input( ssl_data) # Clean up module developers input. except YomboWarning as e: logger.warn(f"Cannot add cert: {e}") return if ssl_data["sslname"] in self.managed_certs: self.managed_certs[ssl_data["sslname"]].update_attributes(ssl_data) else: yield self._import_cert(ssl_data["sslname"], DictObject(ssl_data)) @inlineCallbacks def _import_cert(self, cert_name, data, source=None): if source is None: source = "sslcerts" self.managed_certs[cert_name] = SSLCert(self, source, DictObject(data)) yield self.managed_certs[cert_name].start() def get(self, sslname_requested): """ Gets a cert for the request name. .. note:: self._SSLCerts("library_webinterface", self.have_updated_ssl_cert) """ # logger.debug("looking for: {sslname_requested}", sslname_requested=sslname_requested) if sslname_requested in self.managed_certs: # logger.debug("found by cert! {sslname_requested}", sslname_requested=sslname_requested) return self.managed_certs[sslname_requested].get() else: if sslname_requested != "selfsigned": logger.info( "Could not find cert for '{sslname}', sending self signed. Library or module should implement _sslcerts_ with a callback method.", sslname=sslname_requested) return self.get_self_signed() def get_self_signed(self): key_crypt = crypto.load_privatekey(crypto.FILETYPE_PEM, self.self_signed_key) if isinstance(key_crypt, tuple): key_crypt = key_crypt[0] cert_crypt = crypto.load_certificate(crypto.FILETYPE_PEM, self.self_signed_cert) if isinstance(cert_crypt, tuple): cert_crypt = cert_crypt[0] return { "key": self.self_signed_key, "cert": self.self_signed_cert, "chain": None, "key_crypt": key_crypt, "cert_crypt": cert_crypt, "chain_crypt": None, "expires_at": self.self_signed_expires_at, "created_at": self.self_signed_created_at, "signed_at": self.self_signed_created_at, "self_signed": True, "cert_file": self.self_signed_cert_file, "key_file": self.self_signed_key_file, "chain_file": None, } def check_csr_input(self, csr_request): results = {} if "sslname" not in csr_request: raise YomboWarning("'sslname' is required.") results["sslname"] = csr_request["sslname"] if self.local_gateway.dns_name is None: raise YomboWarning( "Unable to create SSL Certs, no system domain set.") if "cn" not in csr_request: raise YomboWarning( f"'cn' must be included, and must end with our local FQDN: {self.local_gateway.dns_name}" ) elif csr_request["cn"].endswith(self.local_gateway.dns_name) is False: results[ "cn"] = csr_request["cn"] + "." + self.local_gateway.dns_name else: results["cn"] = csr_request["cn"] if "sans" not in csr_request: results["sans"] = None else: san_list = [] for san in csr_request["sans"]: if san.endswith(self.local_gateway.dns_name) is False: san_list.append( str(san + "." + self.local_gateway.dns_name)) else: san_list.append(str(san)) results["sans"] = san_list # if "key_type" in csr_request: # allow changing default, might change in the future. # if csr_request["key_type"] != "rsa": # raise YomboWarning("key_type must be 'rsa', received: %s" % csr_request["key_type"]) # results["key_type"] = csr_request["key_type"] # else: # if "key_size" in csr_request: if csr_request["key_size"] < 2048: csr_request["key_size"] = 2048 if csr_request["key_size"] > 4096: csr_request["key_size"] = 4096 else: csr_request["key_size"] = self.default_key_size results["key_type"] = "rsa" results["key_size"] = csr_request["key_size"] if "csr_file" not in csr_request: csr_request["csr_file"] = None results["csr_file"] = csr_request["csr_file"] if "key_file" not in csr_request: csr_request["key_file"] = None results["key_file"] = csr_request["key_file"] if "callback" in csr_request: results["update_callback"] = csr_request["callback"] elif "update_callback" in csr_request: results["update_callback"] = csr_request["update_callback"] if "callback_type" in csr_request: results["update_callback_type"] = csr_request["callback_type"] elif "update_callback_type" in csr_request: results["update_callback_type"] = csr_request[ "update_callback_type"] if "callback_component" in csr_request: results["update_callback_component"] = csr_request[ "callback_component"] elif "update_callback_component" in csr_request: results["update_callback_component"] = csr_request[ "update_callback_component"] if "callback_function" in csr_request: results["update_callback_function"] = csr_request[ "callback_function"] elif "update_callback_function" in csr_request: results["update_callback_function"] = csr_request[ "update_callback_function"] return results @inlineCallbacks def generate_csr(self, args): """ This function shouldn't be called directly. Instead, use the queue "self.generate_csr_queue.put(request, callback, callback_args)" or "self._SSLCerts.generate_csr_queue.put()". Requests certs to be made. Will return right away with a request ID. A callback can be set to return the cert once it's complete. :return: """ logger.debug("Generate_CSR called with args: {args}", args=args) kwargs = self.check_csr_input(args) if kwargs["key_type"] == "rsa": kwargs["key_type"] = crypto.TYPE_RSA else: kwargs["key_type"] = crypto.TYPE_DSA req = crypto.X509Req() req.get_subject().CN = kwargs["cn"] req.get_subject().countryName = "US" req.get_subject().stateOrProvinceName = "California" req.get_subject().localityName = "Sacramento" req.get_subject().organizationName = "Yombo" req.get_subject( ).organizationalUnitName = "Gateway " + self.gateway_id[0:15] # Appends SAN to have "DNS:" if kwargs["sans"] is not None: san_string = [] for i in kwargs["sans"]: san_string.append(f"DNS: {i}") san_string = ", ".join(san_string) x509_extensions = [ crypto.X509Extension(b"subjectAltName", False, unicode_to_bytes(san_string)) ] req.add_extensions(x509_extensions) start = time() key = yield threads.deferToThread( self._generate_key, **{ "key_type": kwargs["key_type"], "key_size": kwargs["key_size"] }) duration = round(float(time()) - start, 4) self._Events.new("sslcerts", "generate_new", (args["sslname"], kwargs["cn"], san_string, duration)) req.set_pubkey(key) req.sign(key, "sha256") csr = crypto.dump_certificate_request(crypto.FILETYPE_PEM, req) key_file = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) if kwargs["csr_file"] is not None: yield save_file(kwargs["csr_file"], csr) if kwargs["key_file"] is not None: yield save_file(kwargs["key_file"], key_file) return { "csr": csr, "csr_hash": sha256_compact(unicode_to_bytes(csr)), "key": key_file } @inlineCallbacks def _create_self_signed_cert(self): """ Creates a self signed cert. Shouldn't be called directly except by this library for its own use. """ logger.debug("Creating self signed cert.") req = crypto.X509() req.get_subject().CN = "localhost" req.get_subject().countryName = "US" req.get_subject().stateOrProvinceName = "California" req.get_subject().localityName = "Self Signed" req.get_subject().organizationName = "Yombo" req.get_subject( ).organizationalUnitName = f"Gateway {self.gateway_id[0:15]} Self Signed" req.set_serial_number(int(time())) req.gmtime_adj_notBefore(0) req.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) self.self_signed_expires_at = time() + (10 * 365 * 24 * 60 * 60) self.self_signed_created_at = time() self._Configs.set("sslcerts", "self_signed_expires_at", self.self_signed_expires_at) self._Configs.set("sslcerts", "self_signed_created_at", self.self_signed_created_at) req.set_issuer(req.get_subject()) key = yield threads.deferToThread( self._generate_key, **{ "key_type": crypto.TYPE_RSA, "key_size": self.default_key_size }) req.set_pubkey(key) req.sign(key, "sha256") csr_key = crypto.dump_certificate(crypto.FILETYPE_PEM, req) key_file = crypto.dump_privatekey(crypto.FILETYPE_PEM, key) yield save_file(self.self_signed_cert_file, csr_key) yield save_file(self.self_signed_key_file, key_file) return {"csr_key": csr_key, "key": key_file} def _generate_key(self, **kwargs): """ This is a blocking function and should only be called by the sslcerts library. This is called in a seperate thread. Responsible for generating a key and csr. :return: """ # logger.debug("About to generate key: {kwargs}", kwargs=kwargs) key = crypto.PKey() key.generate_key(kwargs["key_type"], kwargs["key_size"]) return key def send_csr_request(self, csr_text, sslname): """ Submit CSR request to Yombo. The sslname is also sent to be used for tracking. This will be returned directly back to us. This allows us to get out signed cert back if we happen to restart between sending the CSR and getting the signed key back. :param csr_text: CSR request text :param sslname: Name of the ssl for tracking. :return: """ logger.info("Sending CSR request for cert: {sslname}", sslname=sslname) if len(sslname) > 100: raise YomboWarning("'sslname' too long, limit is 100 characters.") body = { "csr_text": csr_text, "sslname": sslname, } request_msg = self._AMQPYombo.generate_message_request( exchange_name="ysrv.e.gw_sslcerts", source="yombo.gateway.lib.amqpyobo", destination="yombo.server.sslcerts", request_type="csr_request", body=body, ) self._AMQPYombo.publish(**request_msg) return request_msg def amqp_incoming_request(self, headers, body, **kwargs): """ Signed SSL certs comes as requests, even though it's really a response. This avoids the requirement that all "responses" have a sent correlation ID. :param headers: :param body: :param kwargs: :return: """ # print(f"sslcerts: amqp_incoming: {body}") # print(f"sslcerts: amqp_incoming: {headers}") request_type = headers["request_type"] kwargs["headers"] = headers kwargs["body"] = body if request_type == "csr_response": self.amqp_incoming_response_to_csr_request(**kwargs) else: logger.warn( "AMQP:Handler:Control - Received unknown request_type: {request_type}", request_type=request_type) def amqp_incoming_response_to_csr_request(self, body=None, properties=None, correlation_info=None, **kwargs): """ Called when we get a signed cert back from a CSR. :param body: :param properties: :param correlation_info: :param kwargs: :return: """ logger.debug("Received CSR response message: {body}", body=body) if "sslname" not in body: logger.warn( "Discarding response, doesn't have an sslname attached." ) # can't raise exception due to AMPQ processing. return logger.info("Received a new signed SSL/TLS certificate for: {sslname}", sslname=body["sslname"]) sslname = bytes_to_unicode(body["sslname"]) if sslname not in self.managed_certs: logger.warn( "It doesn't appear we have a managed cert for the given SSL name. Lets store it for a few minutes: {sslname}", sslname=sslname) if sslname in self.received_message_for_unknown: self.received_message_for_unknown[sslname].append(body) else: self.received_message_for_unknown[sslname] = [body] else: self.managed_certs[sslname].amqp_incoming_response_to_csr_request( properties, body, correlation_info) def validate_csr_private_certs_match(self, csr_text, key_text): csr = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_text) key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_text) return csr.verify(key)
def _init_(self, **kwargs): setup_webinterface_reference( self) # Sets a reference to this library in auth.py self.webapp = Klein() self.webapp.webinterface = self self.api_key = self._Configs.get("frontend.api_key", random_string(length=75)) self.frontend_building: bool = False self.web_interface_fully_started: bool = False self.enabled = self._Configs.get("webinterface.enabled", True) self.fqdn = self._Configs.get("dns.fqdn", None, False, instance=True) self.enabled = self._Configs.get("core.enabled", True) if not self.enabled: return self.file_cache = ExpiringDict( max_len=100, max_age_seconds=120 ) # used to load a few static files into memory that are commonly used. self.translators = {} self.idempotence = self._Cache.ttl(name="lib.webinterface.idempotence", ttl=300) self.wi_dir = "/lib/webinterface" self.misc_wi_data = {} self.wi_port_nonsecure = self._Configs.get( "webinterface.nonsecure_port", 8080, instance=True) self.wi_port_secure = self._Configs.get("webinterface.secure_port", 8443, instance=True) self.webapp.templates = jinja2.Environment( loader=jinja2.FileSystemLoader(f"{self._app_dir}/yombo"), extensions=["jinja2.ext.loopcontrols"]) self.setup_basic_filters() self.web_interface_listener = None self.web_interface_ssl_listener = None self.api_stream_spectators = { } # Tracks all the spectators connected. An alternative to MQTT listening. if self._Configs.get("webinterface.enable_default_routes", default=True, create=False): yield self.webinterface_load_routes() # Loads all the routes. self.npm_build_results = None self.temp_data = ExpiringDict(max_age_seconds=1800) self.web_server_started = False self.web_server_ssl_started = False self.setup_wizard_map_js = None self.web_factory = None self.user_login_tokens = self._Cache.ttl(name="lib.users.cache", ttl=300)
def _init_(self, **kwargs): self.frontend_building = False self.web_interface_fully_started = False self.enabled = self._Configs.get("webinterface", "enabled", True) self.fqdn = self._Configs.get2("dns", "fqdn", None, False) self.enabled = self._Configs.get("core", "enabled", True) if not self.enabled: return self.file_cache = ExpiringDict( max_len=100, max_age_seconds=120 ) # used to load a few static files into memory that are commonly used. self.translators = {} self.idempotence = self._Cache.ttl(name="lib.webinterface.idempotence", ttl=300) self.working_dir = self._Atoms.get("working_dir") self.app_dir = self._Atoms.get("app_dir") self.wi_dir = "/lib/webinterface" self.misc_wi_data = {} self.wi_port_nonsecure = self._Configs.get2("webinterface", "nonsecure_port", 8080) self.wi_port_secure = self._Configs.get2("webinterface", "secure_port", 8443) self.webapp.templates = jinja2.Environment( loader=jinja2.FileSystemLoader(f"{self.app_dir}/yombo"), extensions=["jinja2.ext.loopcontrols"]) self.setup_basic_filters() self.web_interface_listener = None self.web_interface_ssl_listener = None self.api_stream_spectators = {} # Load API routes route_api_v1_atoms(self.webapp) route_api_v1_automation_rules(self.webapp) # route_api_v1_camera(self.webapp) route_api_v1_debug(self.webapp) route_api_v1_device(self.webapp) route_api_v1_device_command(self.webapp) # route_api_v1_events(self.webapp) # route_api_v1_gateway(self.webapp) # route_api_v1_module(self.webapp) route_api_v1_mqtt(self.webapp) # route_api_v1_notification(self.webapp) route_api_v1_scenes(self.webapp) # route_api_v1_server(self.webapp) # route_api_v1_statistics(self.webapp) # route_api_v1_stream(self.webapp, self) route_api_v1_states(self.webapp) route_api_v1_system(self.webapp) # route_api_v1_storage(self.webapp) route_api_v1_user(self.webapp) # route_api_v1_webinterface_logs(self.webapp) # Load web server routes route_home(self.webapp) route_misc(self.webapp) route_system(self.webapp) route_user(self.webapp) if self.operating_mode != "run": from yombo.lib.webinterface.routes.restore import route_restore from yombo.lib.webinterface.routes.setup_wizard import route_setup_wizard route_setup_wizard(self.webapp) route_restore(self.webapp) self.npm_build_results = None self.temp_data = ExpiringDict(max_age_seconds=1800) self.web_server_started = False self.web_server_ssl_started = False self.web_factory = None self.user_login_tokens = self._Cache.ttl(name="lib.users.cache", ttl=300)
class LogEvents(YomboLibrary): """ Manages all notifications. """ def _init_(self): """ Setups up the basic framework. """ # We only cache the last few events, and only for certain time. self.notifications = ExpiringDict(max_len=100, max_age_seconds=600) # return self.init_deferred def _load_(self): self._LocalDB = self._Libraries['localdb'] self._checkExpiredLoop = LoopingCall(self.check_expired) self._checkExpiredLoop.start( self._Configs.get('notifications', 'check_expired', 30, False)) self.load_notifications() def _stop_(self): if self.init_deferred is not None and self.init_deferred.called is False: self.init_deferred.callback( 1) # if we don't check for this, we can't stop! def _clear_(self): """ Clear all devices. Should only be called by the loader module during a reconfiguration event. B{Do not call this function!} """ self.notifications.clear() def _reload_(self): self._clear_() self.load_notifications() def check_expired(self): """ Called by looping call to periodically purge expired notifications. :return: """ cur_time = int(time()) for id, notice in self.notifications.items(): print("cur : expired = %s : %s" % (cur_time, notice.expire)) if cur_time > notice.expire: print("deleting notice: %s" % notice.title) del self.notifications[id] self._LocalDB.delete_expired_notifications() def get(self, notification_requested): """ Performs the actual search. .. note:: Modules shouldn't use this function. Use the built in reference to find notification: `self._Notifications['8w3h4sa']` :raises YomboWarning: Raised when notifcation cannot be found. :param notification_requested: The input type ID or input type label to search for. :type notification_requested: string :return: A dict containing details about the notification :rtype: dict """ if notification_requested in self.notifications: return self.notifications[notification_requested] else: raise YomboWarning('Notification not found: %s' % notification_requested) def delete(self, notification_requested): """ Deletes a provided notification. :param notification_requested: :return: """ try: del self.notifications[notification_requested] except: pass self._LocalDB.delete_notification(notification_requested) @inlineCallbacks def load_notifications(self): """ Load the last few notifications into memory. """ notifications = yield self._LocalDB.get_notifications() for notice in notifications: notice = notice.__dict__ if notice['expire'] < int(time()): continue notice['meta'] = json.loads(notice['meta']) self.add_notice(notice, from_db=True) logger.debug("Done load_notifications: {notifications}", notifications=self.notifications) # self.init_deferred.callback(10) def add_notice(self, notice, from_db=False, persist=True, create_event=False): """ Add a new notice. :param notice: A dictionary containing notification details. :type record: dict :returns: Pointer to new notice. Only used during unittest """ print("adding notice1: %s" % notice) if 'id' not in notice: notice['id'] = random_string(length=16) if 'type' not in notice: notice['type'] = 'system' if 'priority' not in notice: notice['priority'] = 'normal' if 'source' not in notice: notice['source'] = '' if 'expire' not in notice: if 'timeout' in notice: notice['expire'] = int(time()) + notice['timeout'] else: notice['expire'] = int(time()) + 3600 else: if notice['expire'] > int(time()): YomboWarning( "New notification is set to expire before current time.") if 'created_at' not in notice: notice['created_at'] = int(time()) if 'acknowledged' not in notice: notice['acknowledged'] = False else: if notice['acknowledged'] not in (True, False): YomboWarning( "New notification 'acknowledged' must be either True or False." ) if 'title' not in notice: raise YomboWarning("New notification requires a title.") if 'message' not in notice: raise YomboWarning("New notification requires a message.") if 'meta' not in notice: notice['meta'] = {} logger.debug("notice: {notice}", notice=notice) if from_db is False: self._LocalDB.add_notification(notice) self.notifications.prepend(notice['id'], Notification(notice)) else: self.notifications[notice['id']] = Notification(notice) # self.notifications = OrderedDict(sorted(self.notifications.items(), key=lambda x: x[1]['created_at'])) pass return notice['id']
class YomboAPI(YomboLibrary): # contentType = None def _init_(self, **kwargs): self.custom_agent = Agent(reactor, connectTimeout=20) self.contentType = self._Configs.get('yomboapi', 'contenttype', 'application/json', False) # TODO: Msgpack later self.base_url = self._Configs.get('yomboapi', 'baseurl', "https://api.yombo.net/api", False) self.allow_system_session = self._Configs.get('yomboapi', 'allow_system_session', True) self.init_defer = None self.api_key = self._Configs.get('yomboapi', 'api_key', 'aBMKp5QcQoW43ipauw88R0PT2AohcE', False) self.valid_system_session = None self.valid_login_key = None self.session_validation_cache = ExpiringDict() try: self.system_session = self._Configs.get( 'yomboapi', 'auth_session') # to be encrypted with gpg later self.system_login_key = self._Configs.get( 'yomboapi', 'login_key') # to be encrypted with gpg later except KeyError: self.system_session = None self.system_login_key = None if self._Loader.operating_mode == 'run': self.init_defer = Deferred() self.validate_system_login() return self.init_defer @inlineCallbacks def gateway_index(self, session=None): results = yield self.request("GET", "/v1/gateway", None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot get gateways") else: if results['content']['message'] == "Invalid Token.": raise YomboWarningCredentails( "URI: '%s' requires credentials." % results['content']['response']['uri']) raise YomboWarning("Unknown error: %s" % results['content']) @inlineCallbacks def gateway_get(self, gateway_id, session=None): results = yield self.request("GET", "/v1/gateway/%s" % gateway_id, None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway_put(self, gateway_id, values, session=None): results = yield self.request("PATCH", "/v1/gateway/%s" % gateway_id, values, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway__module_get(self, gateway_id, session=None): results = yield self.request("GET", "/v1/gateway/%s/modules" % gateway_id, None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway__module_put(self, gateway_id, values, session=None): results = yield self.request("PUT", "/v1/gateway/%s/modules" % gateway_id, values, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot find requested gateway: %s" % gateway_id) else: raise YomboWarning("Unknown error: %s" % results['content']['message']) @inlineCallbacks def gateway_config_index(self, gateway_id, session=None): results = yield self.request("GET", "/v1/gateway/%s/config" % gateway_id, None, session) if results['code'] == 200: returnValue(results) elif results['code'] == 404: raise YomboWarning("Server cannot get gateways") else: raise YomboWarning("Unknown error: %s" % results['content']['message']) # Below are the core help functions def save_system_session(self, session): self.system_session = session self._Configs.set('yomboapi', 'auth_session', session) # to be encrypted with gpg later def save_system_login_key(self, login_key): self.system_login_key = login_key self._Configs.set('yomboapi', 'login_key', login_key) # to be encrypted with gpg later def select_session(self, session_id=None, session_key=None): if session_id is None or session_key is None: if self.allow_system_session: return self.system_session, self.system_login_key logger.info( "select_session: Yombo API has no session data for 'selection_session'" ) return None, None def clear_session_cache(self, session=None): if (session is None): self.session_validation_cache.clear() else: hashed = sha1(session) if hashed in self.session_validation_cache: del self.session_validation_cache[hashed] # None works too... @inlineCallbacks def validate_system_login(self): """ Validates that the system has a valid user login key and an active system session. If the system session is invalid or expired, it will attempt to automatically createa new session with the systemt he login key. If the system login key is invalid, the system will exit. :return: """ if self.allow_system_session is False: self._States.set('yomboapi.valid_system_session', False) self.valid_system_session = False self._States.set('yomboapi.valid_login_key', False) self.valid_login_key = False if self.init_defer is not None: self.init_defer.callback(10) returnValue(False) if self.system_session is None and self.system_login_key is None: logger.warn( "No saved system session information and no login_key. Disabling automated system changes." ) self._States.set('yomboapi.valid_system_session', False) self.valid_system_session = False self._States.set('yomboapi.valid_login_key', False) self.valid_login_key = False if self.init_defer is not None: self.init_defer.callback(10) returnValue(False) if self.system_login_key is None: logger.warn("System doesn't have a login key!") else: results = yield self.do_validate_login_key(self.system_login_key) if results is True: logger.debug("System has a valid login key.") self._States.set('yomboapi.valid_login_key', True) self.valid_login_key = True else: logger.warn("System has an invalid login key.") self._States.set('yomboapi.valid_login_key', False) self.valid_login_key = False self.clear_session_cache() results = yield self.do_validate_session(self.system_session) if results is True: logger.debug("Yombo API has a system session!") self._States.set('yomboapi.valid_system_session', True) self.valid_system_session = True if self.init_defer is not None: self.init_defer.callback(10) returnValue(True) else: # if invalid, try to get one with the login key! if self.valid_login_key: results = yield self.user_login_with_key(self.system_login_key) if results is not False: self._Configs.set( 'yomboapi', 'auth_session', results['session']) # to be encrypted with gpg later self.system_session = results['session'] self._States.set('yomboapi.valid_system_session', True) self.valid_system_session = True if self.init_defer is not None: self.init_defer.callback(10) returnValue(True) logger.warn("Yombo API does not have a login system session!") self._States.set('yomboapi.valid_system_session', False) self.valid_system_session = False if self.init_defer is not None: self.init_defer.callback(10) returnValue(False) @inlineCallbacks def validate_session(self, session_id=None, session_key=None, clear_cache=False): session_id, session_key = self.select_session(session_id, session_key) if session_id is None or session_key is None: logger.debug( "Yombo API session information is not valid: {id}:{key}", id=session_id, key=session_key) hashed = sha1(session_id + session_key) if hashed in self.session_validation_cache: if clear_cache is True: del self.session_validation_cache[hashed] else: returnValue(self.session_validation_cache[hashed]) results = yield self.do_validate_session(session_id, session_key) self.session_validation_cache[hashed] = results returnValue(results) @inlineCallbacks def do_validate_login_key(self, login_key): try: results = yield self.request( "GET", "/v1/user/login_key/validate/%s" % login_key) except Exception as e: logger.info("do_validate_login_key API Errror: {error}", error=e) returnValue(False) logger.debug("Login key results: REsults from API: {results}", results=results['content']) # waiting on final API.yombo.com to complete this. If we get something, we are good for now. if (results['content']['code'] != 200): returnValue(False) else: returnValue(results['content']['response']['login']) @inlineCallbacks def do_validate_session(self, session): try: results = yield self.request("GET", "/v1/user/session/validate", None, session=session) except Exception as e: logger.debug("$$$1 API Errror: {error}", error=e) returnValue(False) logger.debug("$$$a REsults from API: {results}", results=results['content']) # waiting on final API.yombo.com to complete this. If we get something, we are good for now. if (results['content']['code'] != 200): returnValue(False) else: returnValue(results['content']['response']['login']) @inlineCallbacks def user_login_with_key(self, login_key): results = yield self.request("POST", "/v1/user/login", {'login_key': login_key}, False) try: results = yield self.request("POST", "/v1/user/login", {'login_key': login_key}, False) except Exception as e: logger.debug("$$$2 API Errror: {error}", error=e) returnValue(False) logger.info( "user_login_with_key Results from API for login w key: {results}", results=results['content']) # waiting on final API.yombo.com to complete this. If we get something, we are good for now. if (results['content']['code'] != 200): returnValue(False) elif (results['content']['message'] == 'Logged in'): returnValue(results['content']['response']['login']) else: returnValue(False) @inlineCallbacks def user_login_with_credentials(self, username, password, g_recaptcha_response): # credentials = { 'username':username, 'password':password} results = yield self.request( "POST", "/v1/user/login", { 'username': username, 'password': password, 'g-recaptcha-response': g_recaptcha_response }, False) logger.info("$$$3 REsults from API login creds: {results}", results=results) return results @inlineCallbacks def gateways(self, session_info=None): results = yield self.request("GET", "/v1/gateway") logger.debug("$$$4 REsults from API: {results}", results=results) if results['Code'] == 200: # life is good! returnValue(results['Response']['Gateway']) else: returnValue(False) def make_headers(self, session): headers = { 'Content-Type': self.contentType, 'Authorization': 'Yombo-Gateway-v1', 'x-api-key': self.api_key, 'User-Agent': 'yombo-gateway-v0_12_0', } if session is not None: headers['Authorization'] = 'Bearer %s' % session # for k, v in headers.items(): # headers[k] = v.encode('utf-8') return headers def errorHandler(self, result): raise YomboWarning("Problem with request: %s" % result) @inlineCallbacks def request(self, method, path, data=None, session=None): path = self.base_url + path logger.debug("{method}: {path}", method=method, path=path) # if session is False: # session = None if session is None: if self.system_session is None: if self.valid_system_session is False: raise YomboWarningCredentails( "Yombo request needs an API session.") session = self.system_session if session is False: session = None results = None headers = self.make_headers(session) if data is not None: data = json.dumps(data).encode() logger.debug("yombo api request data: {data}", data=data) if method == 'GET': results = yield self._get(path, headers, data) elif method == 'POST': results = yield self._post(path, headers, data) elif method == 'PATCH': results = yield self._patch(path, headers, data) elif method == 'PUT': results = yield self._put(path, headers, data) elif method == 'DELETE': results = yield self._delete(path, headers, data) else: raise Exception("Bad request type?? %s: %s" % (method, path)) returnValue(results) @inlineCallbacks def _get(self, path, headers, args=None): path = path # response = yield treq.get(path, params=args, agent=self.custom_agent, headers=headers) response = yield treq.get(path, headers=headers, params=args) content = yield treq.content(response) # logger.debug("getting URL: {path} headers: {headers}", path=path, agent=self.custom_agent, headers=headers) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) returnValue(final_response) @inlineCallbacks def _patch(self, path, headers, data): print("yapi patch called. path: %s... headers: %s... data: %s" % (path, headers, data)) response = yield treq.patch(path, data=data, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) returnValue(final_response) @inlineCallbacks def _post(self, path, headers, data): print("yapi post called. path: %s... headers: %s... data: %s" % (path, headers, data)) response = yield treq.post(path, data=data, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) print("dddd: %s" % final_response) returnValue(final_response) @inlineCallbacks def _put(self, path, headers, data): response = yield treq.put(path, data=data, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) returnValue(final_response) @inlineCallbacks def _delete(self, path, headers, args={}): response = yield treq.delete(path, params=args, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) returnValue(final_response) # # def __encode(self, data): # return json.dumps(data) def response_headers(self, response): data = {} raw_headers = bytes_to_unicode(response.headers._rawHeaders) for key, value in raw_headers.items(): data[key.lower()] = value return data def decode_results(self, content, headers, code, phrase): # print("decode_results headers: %s" % headers) content_type = headers['content-type'][0] # print( "###### content: %s" % content) if content_type == 'application/json': try: content = json.loads(content) content_type = "dict" except Exception: raise YomboWarning( "Receive yombo api response reported json, but isn't: %s" % content) elif content_type == 'application/msgpack': try: content = msgpack.loads(content) content_type = "dict" except Exception: raise YomboWarning( "Receive yombo api response reported msgpack, but isn't.") else: try: content = json.loads(content) content_type = "dict" except Exception: try: content = msgpack.loads(content) content_type = "dict" except Exception: content_type = "string" results = { 'content': content, 'content_type': content_type, 'code': code, 'phrase': phrase, 'headers': headers, } if content_type == "string": results['code'] = 500 results['data'] = [] results['content'] = { 'message': 'Unknown api error', 'html_message': 'Unknown api error', } print("Error content: %s" % content) return results else: if 'response' in content: if 'locator' in content['response']: results['data'] = content['response'][content['response'] ['locator']] else: results['data'] = [] return results
class YomboAPI(YomboLibrary): @property def valid_login_key(self): return self._States.get('yomboapi.valid_login_key', False) @valid_login_key.setter def valid_login_key(self, val): return self._States.set('yomboapi.valid_login_key', val) @property def valid_system_session(self): return self._States.get('yomboapi.valid_system_session', False) @valid_system_session.setter def valid_system_session(self, val): return self._States.set('yomboapi.valid_system_session', val) def __str__(self): """ Returns the name of the library. :return: Name of the library :rtype: string """ return "Yombo Yombo API library" def _init_(self, **kwargs): self.custom_agent = Agent(reactor, connectTimeout=20) self.contentType = self._Configs.get('yomboapi', 'contenttype', 'application/json', False) # TODO: Msgpack later self.base_url = self._Configs.get('yomboapi', 'baseurl', "https://api.yombo.net/api", False) self.allow_system_session = self._Configs.get('yomboapi', 'allow_system_session', True) self.init_defer = None self.api_key = self._Configs.get('yomboapi', 'api_key', 'aBMKp5QcQoW43ipauw88R0PT2AohcE', False) self.valid_system_session = None self.valid_login_key = None self.session_validation_cache = ExpiringDict() self.system_login_key = self._Configs.get( 'yomboapi', 'login_key', None) # to be encrypted with gpg later self.system_session = self._Configs.get( 'yomboapi', 'auth_session', None) # to be encrypted with gpg later if self._Loader.operating_mode == 'run': self.init_defer = Deferred() self.validate_system_login() return self.init_defer def save_system_session(self, session): self.system_session = session self._Configs.set('yomboapi', 'auth_session', session) # to be encrypted with gpg later def save_system_login_key(self, login_key): self.system_login_key = login_key self._Configs.set('yomboapi', 'login_key', login_key) # to be encrypted with gpg later def select_session(self, session_id=None, session_key=None): if session_id is None or session_key is None: if self.allow_system_session: return self.system_session, self.system_login_key logger.info( "select_session: Yombo API has no session data for 'selection_session'" ) return None, None def clear_session_cache(self, session=None): if session is None: self.session_validation_cache.clear() else: hashed = sha224(session) if hashed in self.session_validation_cache: del self.session_validation_cache[hashed] # None works too... @inlineCallbacks def validate_system_login(self): """ Validates that the system has a valid user login key and an active system session. If the system session is invalid or expired, it will attempt to automatically createa new session with the systemt he login key. If the system login key is invalid, the system will exit. :return: """ # print("!!!!!!!!!!! starting ytombo api validation") if self.allow_system_session is False: self.valid_system_session = False self.valid_login_key = False if self.init_defer is not None: self.init_defer.callback(10) logger.info( "System session has been disabled. Won't be able to update Yombo cloud settings." ) return False if self.system_session is None and self.system_login_key is None: logger.info( "No saved system session information and no login_key. Won't be able to update Yombo cloud settings." ) self.valid_system_session = False self.valid_login_key = False if self.init_defer is not None: self.init_defer.callback(10) return False if self.system_login_key is None: logger.warn("System doesn't have a login key!") else: results = yield self.do_validate_login_key(self.system_login_key) if results is True: logger.debug("System has a valid login key.") self.valid_login_key = True else: logger.warn("System has an invalid login key.") self.valid_login_key = False if self.valid_login_key is None: logger.warn( "Cannot get system session token, no login token exists.!") else: if self.system_session is not None: results = yield self.do_validate_session(self.system_session) else: results = False if results is True: logger.debug("System has a valid session token.") self.valid_system_session = True # self._Configs.set('yomboapi', 'auth_session', self.system_session) # to be encrypted with gpg later else: if self.valid_login_key is True: new_session = yield self.user_login_with_key( self.system_login_key) if new_session is False: self.valid_system_session = False logger.warn("System has an invalid session token.") else: self._Configs.set('yomboapi', 'auth_session', new_session['session'] ) # to be encrypted with gpg later self.valid_system_session = new_session['session'] self.valid_system_session = True if self.init_defer is not None: self.init_defer.callback(10) @inlineCallbacks def do_validate_login_key(self, login_key): try: results = yield self.request("POST", "/v1/user/login_key/validate", {'login_key': login_key}) except Exception as e: logger.debug("do_validate_login_key API Errror: {error}", error=e) return False # logger.debug("Login key results: REsults from API: {results}", results=results['content']) # waiting on final API.yombo.com to complete this. If we get something, we are good for now. if (results['content']['code'] != 200): return False else: return results['content']['response']['login'] @inlineCallbacks def do_validate_session(self, session): try: results = yield self.request("POST", "/v1/user/session/validate", {'session': session}) # results = yield self.request("GET", "/v1/user/session/validate", None, session=session) except Exception as e: logger.debug("$$$1 API Errror: {error}", error=e) return False # logger.debug("$$$a REsults from API: {results}", results=results['content']) # waiting on final API.yombo.com to complete this. If we get something, we are good for now. if (results['content']['code'] != 200): return False else: return results['content']['response']['login'] @inlineCallbacks def user_login_with_key(self, login_key): try: results = yield self.request("POST", "/v1/user/login", {'login_key': login_key}, False) except Exception as e: logger.debug("$$$2 API Errror: {error}", error=e) return False # logger.info("user_login_with_key Results from API for login w key: {results}", results=results['content']) # waiting on final API.yombo.com to complete this. If we get something, we are good for now. if results['content']['code'] != 200: return False elif results['content']['message'] != 'Logged in': return False else: self._capture_system_login(results['content']['response']['login']) return results['content'] @inlineCallbacks def user_login_with_credentials(self, username, password, g_recaptcha_response): results = yield self.request( "POST", "/v1/user/login", { 'username': username, 'password': password, 'g-recaptcha-response': g_recaptcha_response }, False) # except YomboWarning as e: # results = { # 'status': 'failed', # 'msg': "Couldn't delete command: %s" % e.message, # 'apimsg': "Couldn't delete command: %s" % e.message, # 'apimsghtml': "Couldn't delete command: %s" % e.html_message, # } # return results # logger.info("$$$3 REsults from API login creds: {results}", results=results) # if results['content']['code'] != 200: if results['content']['code'] != 200: return False elif results['content']['message'] != 'Logged in': return False else: self._capture_system_login(results['content']['response']['login']) return results['content'] # return results['content']['response']['login'] def _capture_system_login(self, data): if self.allow_system_session is False: return captured_id = data['user_id'] try: owner_id = self._Configs.get("core", "owner_id") except KeyError as e: pass else: if captured_id == owner_id: self.save_system_login_key(data['login_key']) self.save_system_session(data['session']) self.valid_system_session = True self.valid_login_key = True @inlineCallbacks def gateways(self, session_info=None): results = yield self.request("GET", "/v1/gateway") logger.debug("$$$4 REsults from API: {results}", results=results) if results['Code'] == 200: # life is good! return results['Response']['Gateway'] else: return False def make_headers(self, session): headers = { 'Content-Type': self.contentType, 'Authorization': 'Yombo-Gateway-v1', 'x-api-key': self.api_key, 'User-Agent': 'yombo-gateway-v0_14_0', } if session is not None: headers['Authorization'] = 'Bearer %s' % session return headers def errorHandler(self, result): raise YomboWarning("Problem with request: %s" % result) @inlineCallbacks def request(self, method, path, data=None, session=None): path = self.base_url + path logger.info("{method}: {path}: {data}", method=method, path=path, data=data) # if session is False: # session = None if session is None: if self.system_session is None: if self.valid_system_session is False: raise YomboWarningCredentails( "Yombo request needs an API session.") session = self.system_session if session is False: session = None results = None headers = self.make_headers(session) if data is not None: data = json.dumps(data).encode() logger.debug("yombo api request data: {data}", data=data) if method == 'GET': results = yield self._get(path, headers, data) elif method == 'POST': results = yield self._post(path, headers, data) elif method == 'PATCH': results = yield self._patch(path, headers, data) elif method == 'PUT': results = yield self._put(path, headers, data) elif method == 'DELETE': results = yield self._delete(path, headers, data) else: raise Exception("Bad request type?? %s: %s" % (method, path)) return results @inlineCallbacks def _get(self, path, headers, args=None): path = path # response = yield treq.get(path, params=args, agent=self.custom_agent, headers=headers) response = yield treq.get(path, headers=headers, params=args) content = yield treq.content(response) # logger.debug("getting URL: {path} headers: {headers}", path=path, agent=self.custom_agent, headers=headers) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) return final_response @inlineCallbacks def _patch(self, path, headers, data): # print("yapi patch called. path: %s... headers: %s... data: %s" % (path, headers, data)) response = yield treq.patch(path, data=data, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) return final_response @inlineCallbacks def _post(self, path, headers, data): # print("yapi post called. path: %s... headers: %s... data: %s" % (path, headers, data)) response = yield treq.post(path, data=data, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) # print("dddd: %s" % final_response) return final_response @inlineCallbacks def _put(self, path, headers, data): response = yield treq.put(path, data=data, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) return final_response @inlineCallbacks def _delete(self, path, headers, args={}): response = yield treq.delete(path, params=args, agent=self.custom_agent, headers=headers) content = yield treq.content(response) final_response = self.decode_results(content, self.response_headers(response), response.code, response.phrase) return final_response def response_headers(self, response): data = {} raw_headers = bytes_to_unicode(response.headers._rawHeaders) for key, value in raw_headers.items(): data[key.lower()] = value return data def decode_results(self, content, headers, code, phrase): # print("decode_results headers: %s" % headers) # print(content) content_type = headers['content-type'][0] phrase = bytes_to_unicode(phrase) # print( "###### content: %s" % content) if content_type == 'application/json': try: content = json.loads(content) content_type = "dict" except Exception: raise YomboWarning( "Receive yombo api response reported json, but isn't: %s" % content) elif content_type == 'application/msgpack': try: content = msgpack.loads(content) content_type = "dict" except Exception: raise YomboWarning( "Receive yombo api response reported msgpack, but isn't.") else: try: content = json.loads(content) content_type = "dict" except Exception: try: content = msgpack.loads(content) content_type = "dict" except Exception: content_type = "string" if code < 200: status = 'ok' else: status = 'error' results = { 'status': status, 'content': content, 'content_type': content_type, 'code': code, 'phrase': phrase, 'headers': headers, } if content_type == "string": logger.warn("Error content: {content}", content=content) raise YomboWarning('Unknown api error', 500, html_message='Unknown api error') else: if 'response' in content: if 'locator' in content['response']: results['data'] = content['response'][content['response'] ['locator']] else: results['data'] = [] # Check if there was any errors, if so, raise something. if code >= 300: # print("data: %s" % content) if 'message' in content: message = content['message'] else: message = phrase if 'html_message' in content: html_message = content['html_message'] else: html_message = phrase raise YomboWarning(message, code, html_message=html_message) return results