def _api_rpc_call(self, cmd: str, data: str): if not self.cfg.gt('smarthome', 'unsafe_rpc'): raise InternalException(msg='[smarthome] unsafe_rpc = off') path, args, kwargs = _rpc_data_extractor(data) for key in path: if key.startswith('_'): raise InternalException( code=4, msg='Private path \'{}\' - ignore'.format(key)) if cmd == 'call.plugin': try: entry = self.own.get_plugin(path[0]) except RuntimeError as e: raise InternalException(code=3, msg=str(e)) walked = ['plugins', path[0]] path = path[1:] elif cmd == 'call.global': try: entry = globals()[path[0]] except Exception as e: raise InternalException( code=3, msg='globals \'{}\' not found: {}'.format(path[0], e)) walked = ['globals', path[0]] path = path[1:] else: entry = self.own walked = ['owner'] return _rpc_caller(entry, path, walked, args, kwargs)
def _rpc_caller(obj, path: list, walked: list, args: list, kwargs: dict): for target in path: obj = getattr(obj, target, None) if obj is None: msg = 'method \'{}\' not found in \'{}\''.format( target, '.'.join(walked)) raise InternalException(code=4, msg=msg) walked.append(target) try: result = obj(*args, **kwargs) except Exception as e: raise InternalException(code=5, msg='Call \'{}\' failed: {}'.format( '.'.join(walked), e)) if result is None or isinstance(result, (int, float, str, bool)): return result if isinstance(result, set): result = list(result) # Проверка на сериализуемость и repr try: json.dumps(result, ensure_ascii=False) return result except (TypeError, json.JSONDecoder, ValueError): pass try: return repr(result) except Exception as e: return 'result serialization error: {}'.format(e)
def _api_recv_model(self, _, pmdl_name: str): """ Отправка модели на сервер. Все данные пакуются в json: filename: валидное имя файла модели, обязательно. data: файл модели завернутый в base64, обязательно если code 0 phrase: ключевая фраза модели, если есть. username: пользователь модели, если есть. """ if not self.cfg.detector.is_model_name(pmdl_name): raise InternalException( msg='Wrong model name: {}'.format(pmdl_name)) pmdl_path = os.path.join(self.cfg.path['models'], pmdl_name) if not os.path.isfile(pmdl_path): raise InternalException(2, 'File {} not found'.format(pmdl_name)) try: result = {'filename': pmdl_name, 'data': file_to_base64(pmdl_path)} except IOError as e: raise InternalException(3, 'IOError: {}'.format(e)) phrase = self.cfg.gt('models', pmdl_name) username = self.cfg.gt('persons', pmdl_name) if phrase: result['phrase'] = phrase if username: result['username'] = username return result
def _api_authorization_totp(self, cmd, data): """ Перед хешированием токена добавляет к нему "соль" - Unix time поделенный на 2 и округленный до целого. Хеш будет постоянно меняться, но требует чтобы время на терминале и подключаемом устройстве точно совпадало. Также можно передать timestamp, он не участвует в хешировании но позволит узнать временную разницу: {"method":"authorization.totp","params":{"hash":"3a2af9d519e51c5bff2e283f2a3d384c6ey0721cb1d715ef356508c57bf1544c498328c59f5670e4aeb6bda135497f4e310960a77f88a046d2bb4185498d941f","timestamp":1582885520.931},"id":"8a5559310b336bad7e139550b7f648ad"} """ time_ = time.time() dict_key_checker(data, ('hash', )) remote_hash = data['hash'] if not isinstance(remote_hash, str): raise InternalException( 2, 'hash must be str, not {}'.format(type(remote_hash))) if 'timestamp' in data: timestamp = data['timestamp'] if not isinstance(timestamp, float): raise InternalException( 2, 'timestamp must be float, not {}'.format(type(timestamp))) else: timestamp = None time_diff = '; diff: {}'.format( pretty_time(time_ - timestamp)) if timestamp else '' return self._base_authorization( cmd, lambda token: check_token_with_totp(token, remote_hash, time_), time_diff)
def _api_authorization_self(self, cmd, data: dict): """ Альтернативный способ авторизации, для внутренних нужд: {"method": "authorization.self", "params": {"token": "token", "owner": "owner"}} """ keys = ('token', 'owner') dict_key_checker(data, keys) for key in keys: if not isinstance(data[key], str): raise InternalException( msg='Value in {} must be str, not {}.'.format( key, type(data[key]))) if not data[key]: raise InternalException(2, 'Empty key - {}.'.format(key)) if not self.get('auth'): fun = self.get('owner_callback', data['owner']) if not fun: raise InternalException( 3, 'Unknown owner - {}'.format(data['owner'])) if not fun(data['token'], self.get('ip'), self.get('port')): raise InternalException(4, 'forbidden: rejected') self.set('auth', True) msg = 'authorized' self.log('API.{} {} from {}'.format(cmd, msg, repr(data['owner'])), logger.INFO) return msg return 'already'
def _handle_exception(self, e: InternalException, method='method', code=0, log_lvl=logger.WARN) -> dict or None: e.method = method e.cmd_code(code or self.api.API_CODE.get(method, 1000)) self.log('API.{}'.format(e), log_lvl) return e.data if e.id is not None else None
def prepare(self, line: str, is_json=None) -> str or dict or list: self.is_jsonrpc = is_json if is_json is not None else ( line.startswith('{') or line.startswith('[')) if self.is_jsonrpc: try: line = json.loads(line) if not isinstance(line, (dict, list)): raise InternalException(code=-32700, msg='must be a dict or list type', id_=Null) except (json.decoder.JSONDecodeError, TypeError) as e: raise InternalException(code=-32700, msg=str(e), id_=Null) return line
def _api_notifications_modify(self, cmd: str, events: str): try: events = json.loads(events) if not isinstance(events, list): events = None except (json.decoder.JSONDecodeError, TypeError): if events: events = events.split(',') if not events: raise InternalException(msg='empty events list') if cmd.endswith('.remove'): return self.own.remove_notifications(events) elif cmd.endswith('.add'): return self.own.add_notifications(events) raise InternalException(code=2, msg='BOOM!')
def _check_auth(self, method, id_): if not self.get('auth') and method not in self.NON_AUTH: raise InternalException( code=self.API_CODE.get('authorization', 1000), msg='forbidden: authorization is necessary', id_=id_, method=method)
def _api_backup_restore(self, _, data): if not data: raise InternalException(msg='Empty filename') if not is_valid_base_filename(data): raise InternalException(2, 'Wrong filename') files = self.own.backup_list() if not files: raise InternalException(3, 'No backups') if data == 'last': filename, timestamp = files[0] else: filename, timestamp = next( (item for item in files if item[0] == data), (None, None)) if not filename: raise InternalException(4, 'File no found: {}'.format(data)) self.own.backup_restore(filename) return {'filename': filename, 'timestamp': timestamp}
def _api_test_recoder(self, _, data): """file: str, limit: [int, float]""" dict_key_checker(data, ('file', )) file, limit = data['file'], data.get('limit', 8) if not isinstance(limit, (int, float)): raise InternalException( msg='limit must be int or float, not {}'.format(type(limit))) if not 3 <= limit <= 3600: raise InternalException( code=2, msg='3 <= limit <= 3600, get {}'.format(limit)) if not isinstance(file, str): raise InternalException(code=3, msg='file must be str, not {}'.format( type(file))) if not file: raise InternalException(code=4, msg='file empty') self.own.terminal_call('test.record', (file, limit))
def _base_authorization(self, cmd, equal, sub_msg='') -> str: # compare(token) -> bool if not self.get('auth'): token = self.cfg.gt('smarthome', 'token') if token: if not equal(token): raise InternalException( msg='forbidden: wrong hash{}'.format(sub_msg)) self.set('auth', True) msg = 'authorized' self.log('API.{} {}{}'.format(cmd, msg, sub_msg), logger.INFO) return msg return 'already'
def _api_rec(self, _, cmd: str): param = cmd.split( '_') # должно быть вида rec_1_1, play_2_1, compile_5_1 if len([1 for x in param if len(x)]) != 3: raise InternalException( msg='Error parsing parameters for \'rec\': {}'.format( repr(param)[:1500])) # a = param[0] # rec, play или compile # b = param[1] # 1-6 # c = param[2] # 1-3 if param[0] in ('play', 'rec', 'compile', 'del'): self.own.terminal_call(param[0], param[1:]) elif param[0] == 'save': self.own.die_in(3, True) elif param[0] == 'update': self.own.update() elif param[0] == 'rollback': self.own.manual_rollback() else: raise InternalException( 2, 'Unknown command for \'rec\': {}'.format(repr(param[0])[:100]))
def _api_send_model(self, _, data: str): """ Получение модели от сервера. Нужно ли отправить на сервер результат? Пока не будем. Т.к. перезапись существующей модели может уронить сноубоя отпарсим данные и передадим их терминалу. Все данные распаковываются из json, где: filename: валидное имя файла модели, обязательно. data: файл модели завернутый в base64, обязательно. phrase: ключевая фраза модели. username: пользователь модели. """ data = json_parser(data, keys=('filename', 'data')) # Недопустимое имя модели? if not self.cfg.detector.is_model_name(data['filename']): raise InternalException( 6, 'Wrong model name: {}'.format(data['filename'])) # И значения на корректность for key in ('username', 'phrase'): if key in data and not isinstance(data[key], str): raise InternalException( 3, 'Wrong value type in {}: {}'.format( repr(key), repr(type(data[key])))) # Переводим файл в байты, будем считать что файл не может быть меньше 3 кбайт try: data['data'] = base64.b64decode(data['data']) except (ValueError, TypeError) as e: raise InternalException(7, 'Invalid file data: {}'.format(e)) if len(data['data']) < 1024 * 3: raise InternalException( 8, 'File too small: {}'.format(len(data['data']))) data = [ data.get(key, '') for key in ('filename', 'data', 'username', 'phrase') ] self.own.terminal_call('send_model', data, save_time=False)
def call_api(self, msg: dict) -> dict or None: if msg[self.METHOD] not in self.API: cmd = repr(msg[self.METHOD])[1:-1] raise InternalException(code=-32601, msg='Unknown command: \'{}\''.format( cmd[:100]), id_=msg['id']) self.id = msg['id'] result = self.API[msg[self.METHOD]](msg[self.METHOD], msg['params']) return { 'result': result, 'id': msg['id'] } if msg['id'] is not None else None
def _api_sre(self, _, data): """ Обработает текст так, как если бы он был успешно распознан. JSON-RPC позволяет опционально задать RMS и имя модели: {"method": "sre", "params": {"text": "выключи свет", "rms": [1, 2, 3], "model": "model1.pmdl"}} """ data = data if isinstance(data, dict) else { 'text': data[0] if isinstance(data, list) and data else data } if 'text' not in data or not data['text'] or not isinstance( data['text'], str): raise InternalException( msg='\'text\' must be contained non-empty string') self.own.terminal_call('sre', [data.get(x) for x in ('text', 'rms', 'model')])
def _extract_str(self, line: str) -> dict: line = line.split(':', 1) if len(line) != 2: line.append('') # id = cmd method = line[0] if method in self.PURE_JSON: InternalException(code=-32700, msg='Allow only in JSON-RPC', id_=method, method=method) data = [line[1]] if method in self.TRUE_JSON else line[1] return { 'type': self.METHOD, self.METHOD: method, 'params': data, 'id': method }
def _parse(self, data: str): if not data: return self._handle_exception( InternalException(code=-32600, msg='no data')) else: self.log('Received data: {}'.format(repr(data)[:1500])) try: data = self.api.prepare(data) except InternalException as e: return self._handle_exception(e, e.method) if not (self.api.is_jsonrpc and isinstance(data, list)): return self.__processing(data) # JSON-RPC Batch return [ x for x in [self.__processing(cmd) for cmd in data] if x is not None ] or None
def _rpc_data_extractor(data: str) -> tuple: try: data = json.loads(data) if not isinstance(data, dict): raise TypeError('must be a dict type') path = data['path'] if not isinstance(path, str): raise TypeError('Wring path type: {}'.format(type(path))) if not path: raise ValueError('Empty path') path = path.split('.') args = data.get('args', []) if not isinstance(args, list): raise TypeError('args must be a list type, get {}'.format( type(args))) kwargs = data.get('kwargs', {}) if not isinstance(kwargs, dict): raise TypeError('kwargs must be a dict type, get {}'.format( type(kwargs))) except (json.decoder.JSONDecodeError, TypeError, KeyError, ValueError) as e: raise InternalException(code=2, msg='Wrong request: {}'.format(e)) return path, args, kwargs
def _api_info(self, _, cmd: str) -> dict: """ Возвращает справку по команде из __doc__ или список доступных команд если команда не задана. Учитывает только команды представленные в API, подписчики не проверяются. """ def allow(cmd_): return self.get('auth') or cmd_ in self.NON_AUTH def flags2(cmd_): return [cmd_ in x for x in (self.TRUE_JSON, self.PURE_JSON)] result = {'cmd': cmd, 'msg': ''} if not cmd: result.update(cmd=[x for x in self.API if allow(x)], msg='Available commands') elif cmd == '*': result.update(cmd={x: flags2(x) for x in self.API if allow(x)}, msg='Flags: TRUE_JSON, PURE_JSON') elif not (cmd in self.API and allow(cmd)): raise InternalException(msg='Unknown command: {}'.format(cmd)) else: if self.API[cmd].__doc__: result['msg'] = self.API[cmd].__doc__.strip('\n').rstrip() result['msg'] = result['msg'] or 'Undocumented' flags = [ k for k, s in ( ('TRUE_JSON', self.TRUE_JSON), ('TRUE_LEGACY', self.TRUE_LEGACY), ('PURE_JSON', self.PURE_JSON), ('ALLOW_RESPONSE', self.ALLOW_RESPONSE), ('NON_AUTH', self.NON_AUTH), ) if cmd in s ] if flags: result['flags'] = flags return result
def _upgrade_duplex(self, *_): try: upgrade_duplex(self.own, self.get('conn'), self.id) except RuntimeError as e: raise InternalException(msg=str(e))
def _api_no_implement(self, cmd: str, _): """NotImplemented""" raise InternalException(msg='Not implemented yet - {}'.format(cmd))
def get_value(key): if key not in cmd_map: raise InternalException( msg='Unknown command: {}'.format(repr(key))) value = cmd_map[key]() return value if value is not None else -1
def _extract_json(self, line: dict) -> dict: def get_id(): return None if id_ is Null else id_ if not isinstance(line, dict): raise InternalException(code=-32600, msg='must be a dict type', id_=Null) # Хак для ошибок парсинга, null != None id_ = line['id'] if line.get('id') is not None else Null found = [key for key in self.ALL_TYPES if key in line] if len(found) != 1: msg = 'Only one key of {} may present, found: {}'.format( self.ALL_TYPES, tuple(found) if found else '') raise InternalException(code=-32600, msg=msg, id_=id_) found = found[0] if found == self.METHOD: # Запрос. method = line[self.METHOD] if not isinstance(method, str): raise InternalException(code=-32600, msg='{} must be a str'.format( self.METHOD), id_=id_) params = line.get('params') if method in self.PURE_JSON: if params is not None and not isinstance(params, (dict, list)): raise InternalException( code=-32600, msg='params must be a dict, list or null', id_=id_, method=method) elif method in self.TRUE_JSON and isinstance(params, (dict, list)): pass elif params: # FIXME: legacy if isinstance(params, list) and len(params) == 1 and isinstance( params[0], str): params = params[0] elif isinstance(params, (dict, list)): # Обратно в строку - костыль. params = json.dumps(params) else: raise InternalException( code=-32602, msg='legacy, params must be a list[str]', id_=id_, method=method) else: params = '' return { 'type': self.METHOD, self.METHOD: method, 'params': params, 'id': get_id() } elif found == self.ERROR: # Получили ответ с ошибкой. if isinstance(line[self.ERROR], dict): return { 'type': self.ERROR, 'code': line[self.ERROR].get('code'), 'message': line[self.ERROR].get('message'), 'id': get_id() } raise InternalException(code=-32600, msg='{} myst be a dict'.format(self.ERROR)) elif found == self.RESULT: # Получили ответ с результатом. return { 'type': self.RESULT, self.RESULT: line[self.RESULT], 'id': get_id() } raise RuntimeError