예제 #1
0
    def decrypt_sync_body(self, body, ignore_failures=True):
        # type: (Dict[Any, Any], bool) -> Dict[Any, Any]
        """Go through a json sync response and decrypt megolm encrypted events.

        Args:
            body (Dict[Any, Any]): The dictionary of a Sync response.

        Returns the json response with decrypted events.
        """
        logger.info("Decrypting sync")

        self.handle_to_device_from_sync_body(body)

        for room_id, room_dict in body["rooms"]["join"].items():
            try:
                if not self.rooms[room_id].encrypted:
                    logger.info("Room {} is not encrypted skipping...".format(
                        self.rooms[room_id].display_name))
                    continue
            except KeyError:
                # We don't know if the room is encrypted or not, probably
                # because the client sync stream got to join the room before the
                # pan sync stream did. Let's assume that the room is encrypted.
                pass

            for event in room_dict["timeline"]["events"]:
                if "type" not in event:
                    continue

                if event["type"] != "m.room.encrypted":
                    continue

                self.pan_decrypt_event(event, room_id, ignore_failures)

        return body
예제 #2
0
    async def loop(self):
        """Start a loop that runs forever and keeps on syncing with the server.

        The loop can be stopped with the stop_loop() method.
        """
        self.loop_running = True
        self.loop_stopped.clear()

        logger.info(f"Starting sync loop for {self.user_id}")

        while self.loop_running:
            if not self.logged_in:
                # TODO login
                pass

            # TODO use user lazy loading here
            response = await self.sync(30000)

            if self.should_upload_keys:
                await self.keys_upload()

            if self.should_query_keys:
                key_query_response = await self.keys_query()
                if isinstance(key_query_response, KeysQueryResponse):
                    self.verify_devices(key_query_response.changed)

            if not isinstance(response, SyncResponse):
                # TODO error handling
                pass

            self.synced.set()
            self.synced.clear()

        logger.info("Stopping the sync loop")
        self.loop_stopped.set()
예제 #3
0
    def start_loop(self, loop_sleep_time=100):
        """Start a loop that runs forever and keeps on syncing with the server.

        The loop can be stopped with the stop_loop() method.
        """
        assert not self.task

        logger.info(f"Starting sync loop for {self.user_id}")

        loop = asyncio.get_event_loop()

        if INDEXING_ENABLED:
            self.history_fetcher_task = loop.create_task(self.fetcher_loop())

        timeout = 30000
        sync_filter = {"room": {"state": {"lazy_load_members": True}}}
        next_batch = self.pan_store.load_token(self.server_name, self.user_id)
        self.last_sync_token = next_batch

        # We don't store any room state so initial sync needs to be with the
        # full_state parameter. Subsequent ones are normal.
        task = loop.create_task(
            self.sync_forever(
                timeout,
                sync_filter,
                full_state=True,
                since=next_batch,
                loop_sleep_time=loop_sleep_time,
            )
        )
        self.task = task

        return task
예제 #4
0
    async def sync_tasks(self, response):
        if self.index:
            await self.index.commit_events()

        if self.last_sync_token == self.next_batch:
            return

        self.last_sync_token = self.next_batch

        self.pan_store.save_token(self.server_name, self.user_id, self.next_batch)

        for room_id, room_info in response.rooms.join.items():
            if room_info.timeline.limited:
                room = self.rooms[room_id]

                if not room.encrypted and self.pan_conf.index_encrypted_only:
                    continue

                logger.info(
                    "Room {} had a limited timeline, queueing "
                    "room for history fetching.".format(room.display_name)
                )
                task = FetchTask(room_id, room_info.timeline.prev_batch)
                self.pan_store.save_fetcher_task(self.server_name, self.user_id, task)

                await self.history_fetch_queue.put(task)
                self.new_fetch_task.set()
                self.new_fetch_task.clear()
예제 #5
0
    def pan_decrypt_event(self, event_dict, room_id=None, ignore_failures=True):
        # type: (Dict[Any, Any], Optional[str], bool) -> (bool)
        event = Event.parse_encrypted_event(event_dict)

        if not isinstance(event, MegolmEvent):
            logger.warn(
                "Encrypted event is not a megolm event:"
                "\n{}".format(pformat(event_dict))
            )
            return False

        if not event.room_id:
            event.room_id = room_id

        try:
            decrypted_event = self.decrypt_event(event)
            logger.info("Decrypted event: {}".format(decrypted_event))

            event_dict.update(decrypted_event.source)
            event_dict["decrypted"] = True
            event_dict["verified"] = decrypted_event.verified

            return True

        except EncryptionError as error:
            logger.warn(error)

            if ignore_failures:
                event_dict.update(self.unable_to_decrypt)
            else:
                raise

            return False
예제 #6
0
    async def loop_stop(self):
        """Stop the client loop."""
        logger.info("Stopping the sync loop")

        if self.task and not self.task.done():
            self.task.cancel()

            try:
                await self.task
            except KeyboardInterrupt:
                pass

            self.task = None

        if self.history_fetcher_task and not self.history_fetcher_task.done():
            self.history_fetcher_task.cancel()

            try:
                await self.history_fetcher_task
            except KeyboardInterrupt:
                pass

            self.history_fetcher_task = None

        if isinstance(self.store, SqliteQueueDatabase):
            self.store.close()

        self.history_fetch_queue = asyncio.Queue()
예제 #7
0
    def decrypt_messages_body(self, body, ignore_failures=True):
        # type: (Dict[Any, Any], bool) -> Dict[Any, Any]
        """Go through a messages response and decrypt megolm encrypted events.

        Args:
            body (Dict[Any, Any]): The dictionary of a Sync response.

        Returns the json response with decrypted events.
        """
        if "chunk" not in body:
            return body

        logger.info("Decrypting room messages")

        for event in body["chunk"]:
            if "type" not in event:
                continue

            if event["type"] != "m.room.encrypted":
                logger.debug("Event is not encrypted: " "\n{}".format(pformat(event)))
                continue

            self.pan_decrypt_event(event, ignore_failures=ignore_failures)

        return body
예제 #8
0
    async def key_verification_cb(self, event):
        logger.info("Received key verification event: {}".format(event))
        if isinstance(event, KeyVerificationStart):
            logger.info(f"{event.sender} via {event.from_device} has started "
                        f"a key verification process.")

            message = InviteSasSignal(self.user_id, event.sender,
                                      event.from_device, event.transaction_id)

            await self.send_message(message)

        elif isinstance(event, KeyVerificationKey):
            sas = self.key_verifications.get(event.transaction_id, None)
            if not sas:
                return

            device = sas.other_olm_device
            emoji = sas.get_emoji()

            message = ShowSasSignal(self.user_id, device.user_id, device.id,
                                    sas.transaction_id, emoji)

            await self.send_message(message)

        elif isinstance(event, KeyVerificationMac):
            sas = self.key_verifications.get(event.transaction_id, None)
            if not sas:
                return
            device = sas.other_olm_device

            if sas.verified:
                await self.send_message(
                    SasDoneSignal(self.user_id, device.user_id, device.id,
                                  sas.transaction_id))
                await self.send_update_device(device)
예제 #9
0
    async def start_panta_client(self, access_token, user, user_id, password):
        client = Client(user_id, access_token)
        self.client_info[access_token] = client

        if user_id in self.panta_clients:
            logger.info(f"Background sync client already exists for {user_id},"
                        f" not starting new one")
            return

        panta_client = PantaClient(
            self.homeserver,
            user,
            store_path=self.data_dir,
            ssl=self.ssl,
            proxy=self.proxy
        )
        response = await panta_client.login(password, "pantalaimon")

        if not isinstance(response, LoginResponse):
            await panta_client.close()
            return

        logger.info(f"Succesfully started new background sync client for "
                    f"{user_id}")

        self.panta_clients[user_id] = panta_client

        loop = asyncio.get_event_loop()
        loop.create_task(panta_client.loop())
예제 #10
0
    async def download(self, request):
        server_name = request.match_info["server_name"]
        media_id = request.match_info["media_id"]
        file_name = request.match_info.get("file_name")

        try:
            media_info = self.media_info[(server_name, media_id)]
        except KeyError:
            media_info = self.store.load_media(self.name, server_name,
                                               media_id)

            if not media_info:
                logger.info(
                    f"No media info found for {server_name}/{media_id}")
                return await self.forward_to_web(request)

            self.media_info[(server_name, media_id)] = media_info

        try:
            key = media_info.key["k"]
            hash = media_info.hashes["sha256"]
        except KeyError:
            logger.warn(
                f"Media info for {server_name}/{media_id} doesn't contain a key or hash."
            )
            return await self.forward_to_web(request)

        if not self.pan_clients:
            return await self.forward_to_web(request)

        client = next(iter(self.pan_clients.values()))

        try:
            response = await client.download(server_name, media_id, file_name)
        except ClientConnectionError as e:
            return web.Response(status=500, text=str(e))

        if not isinstance(response, DownloadResponse):
            return web.Response(
                status=response.transport_response.status,
                content_type=response.transport_response.content_type,
                headers=CORS_HEADERS,
                body=await response.transport_response.read(),
            )

        logger.info(f"Decrypting media {server_name}/{media_id}")

        loop = asyncio.get_running_loop()
        with concurrent.futures.ProcessPoolExecutor() as pool:
            decrypted_file = await loop.run_in_executor(
                pool, decrypt_attachment, response.body, key, hash,
                media_info.iv)

        return web.Response(
            status=response.transport_response.status,
            content_type=response.transport_response.content_type,
            headers=CORS_HEADERS,
            body=decrypted_file,
        )
예제 #11
0
    def __attrs_post_init__(self):
        loop = asyncio.get_event_loop()

        self.homeserver_url = self.homeserver.geturl()
        self.hostname = self.homeserver.hostname
        self.store = PanStore(self.data_dir)
        accounts = self.store.load_users(self.name)
        self.media_info = self.store.load_media(self.name)

        for user_id, device_id in accounts:
            if self.conf.keyring:
                try:
                    token = keyring.get_password(
                        "pantalaimon", f"{user_id}-{device_id}-token"
                    )
                except RuntimeError as e:
                    logger.error(e)
            else:
                token = self.store.load_access_token(user_id, device_id)

            if not token:
                logger.warn(
                    f"Not restoring client for {user_id} {device_id}, "
                    f"missing access token."
                )
                continue

            logger.info(f"Restoring client for {user_id} {device_id}")

            pan_client = PanClient(
                self.name,
                self.store,
                self.conf,
                self.homeserver_url,
                self.send_queue,
                user_id,
                device_id,
                store_path=self.data_dir,
                ssl=self.ssl,
                proxy=self.proxy,
                store_class=self.client_store_class,
                media_info=self.media_info,
            )
            pan_client.user_id = user_id
            pan_client.access_token = token
            pan_client.load_store()
            self.pan_clients[user_id] = pan_client

            loop.create_task(
                self.send_ui_message(
                    UpdateUsersMessage(self.name, user_id, pan_client.device_id)
                )
            )

            loop.create_task(pan_client.send_update_devices(pan_client.device_store))

            pan_client.start_loop()
예제 #12
0
    def verify_devices(self, changed_devices):
        # Verify new devices automatically for now.
        for user_id, device_dict in changed_devices.items():
            for device in device_dict.values():
                if device.deleted:
                    continue

                logger.info("Automatically verifying device {} of "
                            "user {}".format(device.id, user_id))
                self.verify_device(device)
예제 #13
0
    async def start_pan_client(self, access_token, user, user_id, password):
        client = ClientInfo(user_id, access_token)
        self.client_info[access_token] = client
        self.store.save_server_user(self.name, user_id)

        if user_id in self.pan_clients:
            logger.info(
                f"Background sync client already exists for {user_id},"
                f" not starting new one"
            )
            return

        pan_client = PanClient(
            self.name,
            self.store,
            self.conf,
            self.homeserver_url,
            self.send_queue,
            user_id,
            store_path=self.data_dir,
            ssl=self.ssl,
            proxy=self.proxy,
            store_class=self.client_store_class,
            media_info=self.media_info,
        )
        response = await pan_client.login(password, "pantalaimon")

        if not isinstance(response, LoginResponse):
            await pan_client.close()
            return

        logger.info(f"Succesfully started new background sync client for " f"{user_id}")

        await self.send_ui_message(
            UpdateUsersMessage(self.name, user_id, pan_client.device_id)
        )

        self.pan_clients[user_id] = pan_client

        if self.conf.keyring:
            try:
                keyring.set_password(
                    "pantalaimon",
                    f"{user_id}-{pan_client.device_id}-token",
                    pan_client.access_token,
                )
            except RuntimeError as e:
                logger.error(e)
        else:
            self.store.save_access_token(
                user_id, pan_client.device_id, pan_client.access_token
            )

        pan_client.start_loop()
예제 #14
0
    async def undecrypted_event_cb(self, room, event):
        logger.info("Unable to decrypt event from {} via {}.".format(
            event.sender, event.device_id))

        if event.session_id not in self.outgoing_key_requests:
            logger.info("Requesting room key for undecrypted event.")

            # TODO we may want to retry this
            try:
                await self.request_room_key(event)
            except ClientConnectionError:
                pass
예제 #15
0
    async def _unverify_device(self, message_id, client, device):
        ret = client.unverify_device(device)

        if ret:
            msg = (f"Device {device.id} of user "
                   f"{device.user_id} succesfully unverified.")
            await client.send_update_device(device)
        else:
            msg = f"Device {device.id} of user " f"{device.user_id} already unverified."

        logger.info(msg)
        await self.send_response(message_id, client.user_id, "m.ok", msg)
예제 #16
0
    async def upload(self, request):
        file_name = request.query.get("filename", "")
        content_type = request.headers.get("Content-Type",
                                           "application/octet-stream")
        client = next(iter(self.pan_clients.values()))

        body = await request.read()
        try:
            response, maybe_keys = await client.upload(
                data_provider=BufferedReader(BytesIO(body)),
                content_type=content_type,
                filename=file_name,
                encrypt=True,
                filesize=len(body),
            )

            if not isinstance(response, UploadResponse):
                return web.Response(
                    status=response.transport_response.status,
                    content_type=response.transport_response.content_type,
                    headers=CORS_HEADERS,
                    body=await response.transport_response.read(),
                )

            self.store.save_upload(self.name, response.content_uri, file_name,
                                   content_type)

            mxc = urlparse(response.content_uri)
            mxc_server = mxc.netloc.strip("/")
            mxc_path = mxc.path.strip("/")

            logger.info(
                f"Adding media info for {mxc_server}/{mxc_path} to the store")
            media_info = MediaInfo(
                mxc_server,
                mxc_path,
                maybe_keys["key"],
                maybe_keys["iv"],
                maybe_keys["hashes"],
            )
            self.store.save_media(self.name, media_info)

            return web.Response(
                status=response.transport_response.status,
                content_type=response.transport_response.content_type,
                headers=CORS_HEADERS,
                body=await response.transport_response.read(),
            )

        except ClientConnectionError as e:
            return web.Response(status=500, text=str(e))
        except SendRetryError as e:
            return web.Response(status=503, text=str(e))
예제 #17
0
    async def login(self, request):
        try:
            body = await request.json()
        except (JSONDecodeError, ContentTypeError):
            # After a long debugging session the culprit ended up being aiohttp
            # and a similar bug to
            # https://github.com/aio-libs/aiohttp/issues/2277 but in the server
            # part of aiohttp. The bug is fixed in the latest master of
            # aiohttp.
            # Return 500 here for now since quaternion doesn't work otherwise.
            # After aiohttp 4.0 gets replace this with a 400 M_NOT_JSON
            # response.
            return web.json_response(
                {
                    "errcode": "M_NOT_JSON",
                    "error": "Request did not contain valid JSON.",
                },
                status=500,
            )

        user = self._get_login_user(body)
        password = body.get("password", "")

        logger.info(f"New user logging in: {user}")

        try:
            response = await self.forward_request(request)
        except ClientConnectionError as e:
            return web.Response(status=500, text=str(e))

        try:
            json_response = await response.json()
        except (JSONDecodeError, ContentTypeError):
            json_response = None

        if response.status == 200 and json_response:
            user_id = json_response.get("user_id", None)
            access_token = json_response.get("access_token", None)
            device_id = json_response.get("device_id", None)

            if user_id and access_token:
                logger.info(f"User: {user} succesfully logged in, starting "
                            f"a background sync client.")
                await self.start_pan_client(access_token, user, user_id,
                                            password, device_id)

        return web.Response(
            status=response.status,
            content_type=response.content_type,
            headers=CORS_HEADERS,
            body=await response.read(),
        )
예제 #18
0
    def decrypt_sync_body(self, body):
        # type: (Dict[Any, Any]) -> Dict[Any, Any]
        """Go through a json sync response and decrypt megolm encrypted events.

        Args:
            body (Dict[Any, Any]): The dictionary of a Sync response.

            Returns the json response with decrypted events.
        """
        for room_id, room_dict in body["rooms"]["join"].items():
            try:
                if not self.rooms[room_id].encrypted:
                    logger.info("Room {} is not encrypted skipping...".format(
                        self.rooms[room_id].display_name))
                    continue
            except KeyError:
                logger.info("Unknown room {} skipping...".format(room_id))
                continue

            for event in room_dict["timeline"]["events"]:
                if event["type"] != "m.room.encrypted":
                    logger.info("Event is not encrypted: "
                                "\n{}".format(pformat(event)))
                    continue

                parsed_event = RoomEncryptedEvent.parse_event(event)
                parsed_event.room_id = room_id

                if not isinstance(parsed_event, MegolmEvent):
                    logger.warn("Encrypted event is not a megolm event:"
                                "\n{}".format(pformat(event)))
                    continue

                try:
                    decrypted_event = self.decrypt_event(parsed_event)
                    logger.info("Decrypted event: {}".format(decrypted_event))
                    event["type"] = "m.room.message"

                    # TODO support other event types
                    # This should be best done in nio, modify events so they
                    # keep the dictionary from which they are built in a source
                    # attribute.
                    event["content"] = {
                        "msgtype": "m.text",
                        "body": decrypted_event.body
                    }

                    if decrypted_event.formatted_body:
                        event["content"]["formatted_body"] = (
                            decrypted_event.formatted_body)
                        event["content"]["format"] = decrypted_event.format

                    event["decrypted"] = True
                    event["verified"] = decrypted_event.verified

                except EncryptionError as error:
                    logger.warn(error)
                    continue

        return body
예제 #19
0
    def pan_decrypt_event(self,
                          event_dict,
                          room_id=None,
                          ignore_failures=True):
        # type: (Dict[Any, Any], Optional[str], bool) -> (bool)
        event = Event.parse_encrypted_event(event_dict)

        if not isinstance(event, MegolmEvent):
            logger.warn("Encrypted event is not a megolm event:"
                        "\n{}".format(pformat(event_dict)))
            return False

        if not event.room_id:
            event.room_id = room_id

        try:
            decrypted_event = self.decrypt_event(event)
            logger.debug("Decrypted event: {}".format(decrypted_event))
            logger.info("Decrypted event from {} in {}, event id: {}".format(
                decrypted_event.sender,
                decrypted_event.room_id,
                decrypted_event.event_id,
            ))

            if isinstance(decrypted_event, RoomEncryptedMedia):
                self.store_event_media(decrypted_event)

                decrypted_event.source["content"]["url"] = decrypted_event.url

                if decrypted_event.thumbnail_url:
                    decrypted_event.source["content"]["info"][
                        "thumbnail_url"] = decrypted_event.thumbnail_url

            event_dict.update(decrypted_event.source)
            event_dict["decrypted"] = True
            event_dict["verified"] = decrypted_event.verified

            return True

        except EncryptionError as error:
            logger.warn(error)

            if ignore_failures:
                event_dict.update(self.unable_to_decrypt)
            else:
                raise

            return False
예제 #20
0
    async def key_request_cb(self, event):
        if isinstance(event, RoomKeyRequest):
            logger.info(f"{event.sender} via {event.requesting_device_id} has "
                        f" requested room keys from  us.")

            message = KeyRequestMessage(self.user_id, event)
            await self.send_message(message)

        elif isinstance(event, RoomKeyRequestCancellation):
            logger.info(f"{event.sender} via {event.requesting_device_id} has "
                        f" canceled its key request.")

            message = KeyRequestMessage(self.user_id, event)
            await self.send_message(message)

        else:
            assert False
예제 #21
0
    async def _find_client(self, access_token):
        client_info = self.client_info.get(access_token, None)

        if not client_info:
            async with aiohttp.ClientSession() as session:
                try:
                    method, path = Api.whoami(access_token)
                    resp = await session.request(
                        method,
                        self.homeserver_url + path,
                        proxy=self.proxy,
                        ssl=self.ssl,
                    )
                except ClientConnectionError:
                    return None

                if resp.status != 200:
                    return None

                try:
                    body = await resp.json()
                except (JSONDecodeError, ContentTypeError):
                    return None

                try:
                    user_id = body["user_id"]
                except KeyError:
                    return None

                if user_id not in self.pan_clients:
                    logger.warn(
                        f"User {user_id} doesn't have a matching pan " f"client."
                    )
                    return None

                logger.info(
                    f"Homeserver confirmed valid access token "
                    f"for user {user_id}, caching info."
                )

                client_info = ClientInfo(user_id, access_token)
                self.client_info[access_token] = client_info

        client = self.pan_clients.get(client_info.user_id, None)

        return client
예제 #22
0
    async def _load_decrypted_file(self, server_name, media_id, file_name):
        try:
            media_info = self.media_info[(server_name, media_id)]
        except KeyError:
            media_info = self.store.load_media(self.name, server_name,
                                               media_id)

            if not media_info:
                logger.info(
                    f"No media info found for {server_name}/{media_id}")
                return None, None

            self.media_info[(server_name, media_id)] = media_info

        try:
            key = media_info.key["k"]
            hash = media_info.hashes["sha256"]
        except KeyError as e:
            logger.warn(
                f"Media info for {server_name}/{media_id} doesn't contain a key or hash."
            )
            raise e
        if not self.pan_clients:
            return None, None

        client = next(iter(self.pan_clients.values()))

        try:
            response = await client.download(server_name, media_id, file_name)
        except ClientConnectionError as e:
            raise e

        if not isinstance(response, DownloadResponse):
            return response, None

        logger.info(f"Decrypting media {server_name}/{media_id}")

        loop = asyncio.get_running_loop()
        with concurrent.futures.ProcessPoolExecutor() as pool:
            decrypted_file = await loop.run_in_executor(
                pool, decrypt_attachment, response.body, key, hash,
                media_info.iv)

        return response, decrypted_file
예제 #23
0
    def store_media_cb(self, room, event):
        try:
            mxc = urlparse(event.url)
        except ValueError:
            return

        if mxc is None:
            return

        mxc_server = mxc.netloc.strip("/")
        mxc_path = mxc.path.strip("/")

        logger.info(
            f"Adding media info for {mxc_server}/{mxc_path} to the store")

        media = MediaInfo(mxc_server, mxc_path, event.key, event.iv,
                          event.hashes)
        self.media_info[(mxc_server, mxc_path)] = media
        self.pan_store.save_media(self.server_name, media)
예제 #24
0
    def decrypt_sync_body(self, body, ignore_failures=True):
        # type: (Dict[Any, Any], bool) -> Dict[Any, Any]
        """Go through a json sync response and decrypt megolm encrypted events.

        Args:
            body (Dict[Any, Any]): The dictionary of a Sync response.

        Returns the json response with decrypted events.
        """
        logger.info("Decrypting sync")
        for room_id, room_dict in body["rooms"]["join"].items():
            try:
                if not self.rooms[room_id].encrypted:
                    logger.info(
                        "Room {} is not encrypted skipping...".format(
                            self.rooms[room_id].display_name
                        )
                    )
                    continue
            except KeyError:
                logger.info("Unknown room {} skipping...".format(room_id))
                continue

            for event in room_dict["timeline"]["events"]:
                if "type" not in event:
                    continue

                if event["type"] != "m.room.encrypted":
                    continue

                self.pan_decrypt_event(event, room_id, ignore_failures)

        return body
예제 #25
0
    async def decrypt_body(self, client, body, sync=True):
        """Try to decrypt the a sync or messages body."""
        decryption_method = (client.decrypt_sync_body
                             if sync else client.decrypt_messages_body)

        async def decrypt_loop(client, body):
            while True:
                try:
                    logger.info("Trying to decrypt sync")
                    return decryption_method(body, ignore_failures=False)
                except EncryptionError:
                    logger.info("Error decrypting sync, waiting for next pan "
                                "sync")
                    await client.synced.wait(),
                    logger.info("Pan synced, retrying decryption.")

        try:
            return await asyncio.wait_for(decrypt_loop(client, body),
                                          timeout=self.decryption_timeout)
        except asyncio.TimeoutError:
            logger.info("Decryption attempt timed out, decrypting with "
                        "failures")
            return decryption_method(body, ignore_failures=True)
예제 #26
0
 async def decrypt_loop(client, body):
     while True:
         try:
             logger.info("Trying to decrypt sync")
             return decryption_method(body, ignore_failures=False)
         except EncryptionError:
             logger.info("Error decrypting sync, waiting for next pan " "sync")
             await client.synced.wait(),
             logger.info("Pan synced, retrying decryption.")
예제 #27
0
    async def send_message(self, request):
        access_token = self.get_access_token(request)

        if not access_token:
            return self._missing_token

        client = await self._find_client(access_token)
        if not client:
            return self._unknown_token

        room_id = request.match_info["room_id"]

        # The room is not in the joined rooms list, just forward it.
        try:
            room = client.rooms[room_id]
            encrypt = room.encrypted
        except KeyError:
            return await self.forward_to_web(request,
                                             token=client.access_token)

        # Don't encrypt reactions for now - they are weird and clients
        # need to support them like this.
        # TODO: Fix when MSC1849 is fully supported by clients.
        if request.match_info["event_type"] == "m.reaction":
            encrypt = False

        # The room isn't encrypted just forward the message.
        if not encrypt:
            return await self.forward_to_web(request,
                                             token=client.access_token)

        msgtype = request.match_info["event_type"]
        txnid = request.match_info.get("txnid", uuid4())

        try:
            content = await request.json()
        except (JSONDecodeError, ContentTypeError):
            return self._not_json

        async def _send(ignore_unverified=False):
            try:
                response = await client.room_send(room_id, msgtype, content,
                                                  txnid, ignore_unverified)

                return web.Response(
                    status=response.transport_response.status,
                    content_type=response.transport_response.content_type,
                    headers=CORS_HEADERS,
                    body=await response.transport_response.read(),
                )
            except ClientConnectionError as e:
                return web.Response(status=500, text=str(e))
            except SendRetryError as e:
                return web.Response(status=503, text=str(e))

        # Aquire a semaphore here so we only send out one
        # UnverifiedDevicesSignal
        sem = client.send_semaphores[room_id]

        async with sem:
            # Even though we request the full state we don't receive room
            # members since we're using lazy loading. The summary is for some
            # reason empty so nio can't know if room members are missing from
            # our state. Fetch the room members here instead.
            if not client.room_members_fetched[room_id]:
                try:
                    await client.joined_members(room_id)
                    client.room_members_fetched[room_id] = True
                except ClientConnectionError as e:
                    return web.Response(status=500, text=str(e))

            try:
                return await _send(self.conf.ignore_verification)
            except OlmTrustError as e:
                # There are unverified/unblocked devices in the room, notify
                # the UI thread about this and wait for a response.
                queue = asyncio.Queue()
                client.send_decision_queues[room_id] = queue

                message = UnverifiedDevicesSignal(client.user_id, room_id,
                                                  room.display_name)

                await self.send_ui_message(message)

                try:
                    response = await asyncio.wait_for(
                        queue.get(), self.unverified_send_timeout)

                    if isinstance(response, CancelSendingMessage):
                        # The send was canceled notify the client that sent the
                        # request about this.
                        info_msg = (f"Canceled message sending for room "
                                    f"{room.display_name} ({room_id}).")
                        logger.info(info_msg)
                        await self.send_response(response.message_id,
                                                 client.user_id, "m.ok",
                                                 info_msg)

                        return web.Response(status=503, text=str(e))

                    elif isinstance(response, SendAnywaysMessage):
                        # We are sending and ignoring devices along the way.
                        info_msg = (f"Ignoring unverified devices and sending "
                                    f"message to room "
                                    f"{room.display_name} ({room_id}).")
                        logger.info(info_msg)
                        await self.send_response(response.message_id,
                                                 client.user_id, "m.ok",
                                                 info_msg)

                        ret = await _send(True)
                        await client.send_update_devices(
                            client.room_devices(room_id))
                        return ret

                except asyncio.TimeoutError:
                    # We didn't get a response to our signal, send out an error
                    # response.

                    return web.Response(
                        status=503,
                        text=(f"Room contains unverified devices and no "
                              f"action was taken for "
                              f"{self.unverified_send_timeout} seconds, "
                              f"request timed out"),
                    )

                finally:
                    client.send_decision_queues.pop(room_id)
예제 #28
0
    async def start_pan_client(self,
                               access_token,
                               user,
                               user_id,
                               password,
                               device_id=None):
        client = ClientInfo(user_id, access_token)
        self.client_info[access_token] = client
        self.store.save_server_user(self.name, user_id)

        if user_id in self.pan_clients:
            logger.info(f"Background sync client already exists for {user_id},"
                        f" not starting new one")
            return

        pan_client = PanClient(
            self.name,
            self.store,
            self.conf,
            self.homeserver_url,
            self.send_queue,
            user_id,
            store_path=self.data_dir,
            ssl=self.ssl,
            proxy=self.proxy,
            store_class=self.client_store_class,
            media_info=self.media_info,
        )

        if password == "":
            if device_id is None:
                logger.warn(
                    "Empty password provided and device_id was also None, not "
                    "starting background sync client ")
                return
            # If password is blank, we cannot login normally and must
            # fall back to using the provided device_id.
            pan_client.restore_login(user_id, device_id, access_token)
        else:
            response = await pan_client.login(password, "pantalaimon")

            if not isinstance(response, LoginResponse):
                await pan_client.close()
                return

        logger.info(f"Succesfully started new background sync client for "
                    f"{user_id}")

        await self.send_ui_message(
            UpdateUsersMessage(self.name, user_id, pan_client.device_id))

        self.pan_clients[user_id] = pan_client

        if self.conf.keyring:
            try:
                keyring.set_password(
                    "pantalaimon",
                    f"{user_id}-{pan_client.device_id}-token",
                    pan_client.access_token,
                )
            except RuntimeError as e:
                logger.error(e)
        else:
            self.store.save_access_token(user_id, pan_client.device_id,
                                         pan_client.access_token)

        pan_client.start_loop()
예제 #29
0
    async def receive_message(self, message):
        client = self.pan_clients.get(message.pan_user)

        if isinstance(
                message,
            (
                DeviceVerifyMessage,
                DeviceUnverifyMessage,
                StartSasMessage,
                DeviceBlacklistMessage,
                DeviceUnblacklistMessage,
            ),
        ):

            device = client.device_store[message.user_id].get(
                message.device_id, None)

            if not device:
                msg = (f"No device found for {message.user_id} and "
                       f"{message.device_id}")
                await self.send_response(message.message_id, message.pan_user,
                                         "m.unknown_device", msg)
                logger.info(msg)
                return

            if isinstance(message, DeviceVerifyMessage):
                await self._verify_device(message.message_id, client, device)
            elif isinstance(message, DeviceUnverifyMessage):
                await self._unverify_device(message.message_id, client, device)
            elif isinstance(message, DeviceBlacklistMessage):
                await self._blacklist_device(message.message_id, client,
                                             device)
            elif isinstance(message, DeviceUnblacklistMessage):
                await self._unblacklist_device(message.message_id, client,
                                               device)
            elif isinstance(message, StartSasMessage):
                await client.start_sas(message, device)

        elif isinstance(message, SasMessage):
            if isinstance(message, AcceptSasMessage):
                await client.accept_sas(message)
            elif isinstance(message, ConfirmSasMessage):
                await client.confirm_sas(message)
            elif isinstance(message, CancelSasMessage):
                await client.cancel_sas(message)

        elif isinstance(message, ExportKeysMessage):
            path = os.path.abspath(os.path.expanduser(message.file_path))
            logger.info(f"Exporting keys to {path}")

            try:
                await client.export_keys(path, message.passphrase)
            except OSError as e:
                info_msg = (f"Error exporting keys for {client.user_id} to"
                            f" {path} {e}")
                logger.info(info_msg)
                await self.send_response(message.message_id, client.user_id,
                                         "m.os_error", str(e))

            else:
                info_msg = (f"Succesfully exported keys for {client.user_id} "
                            f"to {path}")
                logger.info(info_msg)
                await self.send_response(message.message_id, client.user_id,
                                         "m.ok", info_msg)

        elif isinstance(message, ImportKeysMessage):
            path = os.path.abspath(os.path.expanduser(message.file_path))
            logger.info(f"Importing keys from {path}")

            try:
                await client.import_keys(path, message.passphrase)
            except (OSError, EncryptionError) as e:
                info_msg = (f"Error importing keys for {client.user_id} "
                            f"from {path} {e}")
                logger.info(info_msg)
                await self.send_response(message.message_id, client.user_id,
                                         "m.os_error", str(e))
            else:
                info_msg = (f"Succesfully imported keys for {client.user_id} "
                            f"from {path}")
                logger.info(info_msg)
                await self.send_response(message.message_id, client.user_id,
                                         "m.ok", info_msg)

        elif isinstance(message, UnverifiedResponse):
            client = self.pan_clients[message.pan_user]

            if message.room_id not in client.send_decision_queues:
                msg = (f"No send request found for user {message.pan_user} "
                       f"and room {message.room_id}.")
                await self.send_response(message.message_id, message.pan_user,
                                         "m.unknown_request", msg)
                return

            queue = client.send_decision_queues[message.room_id]
            await queue.put(message)

        elif isinstance(message, (ContinueKeyShare, CancelKeyShare)):
            client = self.pan_clients[message.pan_user]
            await client.handle_key_request_message(message)
예제 #30
0
    def __init__(
        self,
        server_name,
        pan_store,
        pan_conf,
        homeserver,
        queue=None,
        user_id="",
        device_id="",
        store_path="",
        config=None,
        ssl=None,
        proxy=None,
        store_class=None,
    ):
        config = config or AsyncClientConfig(
            store=store_class or SqliteStore, store_name="pan.db"
        )
        super().__init__(homeserver, user_id, device_id, store_path, config, ssl, proxy)

        index_dir = os.path.join(store_path, server_name, user_id)

        try:
            os.makedirs(index_dir)
        except OSError:
            pass

        self.server_name = server_name
        self.pan_store = pan_store
        self.pan_conf = pan_conf

        if INDEXING_ENABLED:
            logger.info("Indexing enabled.")
            from pantalaimon.index import IndexStore

            self.index = IndexStore(self.user_id, index_dir)
        else:
            logger.info("Indexing disabled.")
            self.index = None

        self.task = None
        self.queue = queue

        # Those two events are mainly used for testing.
        self.new_fetch_task = asyncio.Event()
        self.fetch_loop_event = asyncio.Event()

        self.room_members_fetched = defaultdict(bool)

        self.send_semaphores = defaultdict(asyncio.Semaphore)
        self.send_decision_queues = dict()  # type: asyncio.Queue
        self.last_sync_token = None

        self.history_fetcher_task = None
        self.history_fetch_queue = asyncio.Queue()

        self.add_to_device_callback(self.key_verification_cb, KeyVerificationEvent)
        self.add_to_device_callback(
            self.key_request_cb, (RoomKeyRequest, RoomKeyRequestCancellation)
        )
        self.add_event_callback(self.undecrypted_event_cb, MegolmEvent)

        if INDEXING_ENABLED:
            self.add_event_callback(
                self.store_message_cb,
                (
                    RoomMessageText,
                    RoomMessageMedia,
                    RoomEncryptedMedia,
                    RoomTopicEvent,
                    RoomNameEvent,
                ),
            )

        self.add_response_callback(self.keys_query_cb, KeysQueryResponse)
        self.add_response_callback(self.sync_tasks, SyncResponse)