view libervia/web/server/restricted_bridge.py @ 1603:e105d7719479

doc (user/calls): Add a section to explain remote control: fix 436
author Goffi <goffi@goffi.org>
date Sat, 11 May 2024 14:02:54 +0200
parents 6feac4a25e60
children 4a9679369856
line wrap: on
line source

#!/usr/bin/env python3

# Libervia Web Frontend
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from libervia.backend.core import exceptions
from libervia.backend.core.log import getLogger
from libervia.backend.tools.common import data_format

from libervia.web.server.constants import Const as C


log = getLogger(__name__)


class RestrictedBridge:
    """bridge with limited access, which can be used in browser

    Only a few method are implemented, with potentially dangerous argument controlled.
    Security limit is used
    """

    def __init__(self, host):
        self.host = host
        self.security_limit = C.SECURITY_LIMIT

    def no_service_profile(self, profile):
        """Raise an error if service profile is used"""
        if profile == C.SERVICE_PROFILE:
            raise exceptions.PermissionError(
                "This action is not allowed for service profile"
            )

    async def action_launch(
        self, callback_id: str, data_s: str, profile: str
    ) -> str:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "action_launch", callback_id, data_s, profile
        )

    async def bookmarks_list(
        self,
        type_: str,
        storage_location: str,
        profile: str
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "bookmarks_list", type_, storage_location, profile
        )

    async def call_start(self, entity: str, call_data_s: str, profile: str) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "call_start", entity, call_data_s, profile
        )

    async def call_answer_sdp(
        self, session_id: str, answer_sdp: str, profile: str
    ) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "call_answer_sdp", session_id, answer_sdp, profile
        )

    async def call_info(
        self, session_id: str, info_type: str, extra_s: str, profile: str
    ) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "call_info", session_id, info_type, extra_s, profile
        )

    async def call_end(self, session_id: str, call_data: str, profile: str) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "call_end", session_id, call_data, profile
        )

    async def contacts_get(self, profile):
        return await self.host.bridge_call("contacts_get", profile)

    async def external_disco_get(self, entity, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "external_disco_get", entity, profile)

    async def file_jingle_send(
        self,
        peer_jid: str,
        filepath: str,
        name: str,
        file_desc: str,
        extra_s: str,
        profile: str
    ) -> str:
        self.no_service_profile(profile)
        if filepath:
            # The file sending must be done P2P from the browser directly (the file is
            # from end-user machine), and its data must be set in "extra".
            # "filepath" must NOT be used in this case, as it would link a local file
            # (i.e. from the backend machine), which is an obvious security issue.
            log.warning(
                f'"filepath" user by {profile!r} in file_jingle_send, this is not '
                "allowed, hack attempt?"
            )
            raise exceptions.PermissionError(
                "Using a filepath is not allowed."
            )
        return await self.host.bridge_call(
            "file_jingle_send", peer_jid, "", name, file_desc, extra_s, profile
        )

    async def history_get(
        self,
        from_jid: str,
        to_jid: str,
        limit: int,
        between: bool,
        filters: dict[str, str],
        profile: str
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "history_get", from_jid, to_jid, limit, between, filters, profile
        )

    async def ice_candidates_add(self, session_id, media_ice_data_s, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "ice_candidates_add", session_id, media_ice_data_s, profile
        )

    async def identity_get(self, entity, metadata_filter, use_cache, profile):
        return await self.host.bridge_call(
            "identity_get", entity, metadata_filter, use_cache, profile)

    async def identities_get(self, entities, metadata_filter, profile):
        return await self.host.bridge_call(
            "identities_get", entities, metadata_filter, profile)

    async def identities_base_get(self, profile):
        return await self.host.bridge_call(
            "identities_base_get", profile)

    async def message_edit(
        self, message_id: str, edit_data_s: str, profile: str
    ) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "message_edit", message_id, edit_data_s, profile
        )

    async def message_reactions_set(
        self, message_id: str, reactions: list[str], update_type: str, profile: str
    ) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "message_reactions_set", message_id, reactions, update_type, profile
        )

    async def message_retract(
        self, message_id: str, profile: str
    ) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "message_retract", message_id, profile
        )

    async def message_send(
        self, to_jid_s, message, subject, mess_type, extra_s,
        profile
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "message_send", to_jid_s, message, subject, mess_type, extra_s, profile
        )

    async def ps_node_delete(self, service_s, node, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "ps_node_delete", service_s, node, profile)

    async def ps_node_affiliations_set(self, service_s, node, affiliations, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "ps_node_affiliations_set", service_s, node, affiliations, profile)

    async def ps_item_retract(self, service_s, node, item_id, notify, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "ps_item_retract", service_s, node, item_id, notify, profile)

    async def mb_preview(self, service_s, node, data, profile):
        return await self.host.bridge_call(
            "mb_preview", service_s, node, data, profile)

    async def list_set(self, service_s, node, values, schema, item_id, extra, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "list_set", service_s, node, values, "", item_id, "", profile)


    async def file_http_upload_get_slot(
        self, filename, size, content_type, upload_jid, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "file_http_upload_get_slot", filename, size, content_type,
            upload_jid, profile)

    async def file_sharing_delete(
        self, service_jid, path, namespace, profile):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "file_sharing_delete", service_jid, path, namespace, profile)

    async def interests_file_sharing_register(
        self, service, repos_type, namespace, path, name, extra_s, profile
    ):
        self.no_service_profile(profile)
        if extra_s:
            # we only allow "thumb_url" here
            extra = data_format.deserialise(extra_s)
            if "thumb_url" in extra:
                extra_s = data_format.serialise({"thumb_url": extra["thumb_url"]})
            else:
                extra_s = ""

        return await self.host.bridge_call(
            "interests_file_sharing_register", service, repos_type, namespace, path, name,
            extra_s, profile
        )

    async def interest_retract(
        self, service_jid, item_id, profile
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "interest_retract", service_jid, item_id, profile)

    async def jingle_terminate(
        self, session_id: str, reason: str, reason_txt: str, profile: str
    ) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "jingle_terminate", session_id, reason, reason_txt, profile
        )

    async def ps_invite(
        self, invitee_jid_s, service_s, node, item_id, name, extra_s, profile
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "ps_invite", invitee_jid_s, service_s, node, item_id, name, extra_s, profile
        )

    async def fis_invite(
        self, invitee_jid_s, service_s, repos_type, namespace, path, name, extra_s,
        profile
    ):
        self.no_service_profile(profile)
        if extra_s:
            # we only allow "thumb_url" here
            extra = data_format.deserialise(extra_s)
            if "thumb_url" in extra:
                extra_s = data_format.serialise({"thumb_url": extra["thumb_url"]})
            else:
                extra_s = ""

        return await self.host.bridge_call(
            "fis_invite", invitee_jid_s, service_s, repos_type, namespace, path, name,
            extra_s, profile
        )

    async def fis_affiliations_set(
        self, service_s, namespace, path, affiliations, profile
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "fis_affiliations_set", service_s, namespace, path, affiliations, profile
        )

    async def invitation_simple_create(
        self, invitee_email, invitee_name, url_template, extra_s, profile
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "invitation_simple_create", invitee_email, invitee_name, url_template, extra_s,
            profile
        )

    async def url_preview_get(
        self, url, options_s, profile
    ):
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "url_preview_get", url, options_s, profile
        )

    async def jid_search(
        self, search_term: str, options_s: str, profile: str
    ) -> str:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "jid_search", search_term, options_s, profile
        )

    async def remote_control_start(
        self, peer_jid_s: str, extra_s: str, profile: str
    ) -> None:
        self.no_service_profile(profile)
        return await self.host.bridge_call(
            "remote_control_start", peer_jid_s, extra_s, profile
        )