Beispiel #1
0
    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)
Beispiel #2
0
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)
Beispiel #3
0
    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
Beispiel #7
0
 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
Beispiel #8
0
 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!')
Beispiel #9
0
 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)
Beispiel #10
0
 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}
Beispiel #11
0
 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'
Beispiel #13
0
 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]))
Beispiel #14
0
    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)
Beispiel #15
0
    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
Beispiel #16
0
    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')])
Beispiel #17
0
 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
Beispiel #19
0
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
Beispiel #20
0
    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
Beispiel #21
0
 def _upgrade_duplex(self, *_):
     try:
         upgrade_duplex(self.own, self.get('conn'), self.id)
     except RuntimeError as e:
         raise InternalException(msg=str(e))
Beispiel #22
0
 def _api_no_implement(self, cmd: str, _):
     """NotImplemented"""
     raise InternalException(msg='Not implemented yet - {}'.format(cmd))
Beispiel #23
0
 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
Beispiel #24
0
    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