From b89ecf4c038fd548d0e7d30256928d00e382029c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Aug 2019 02:35:58 +0300 Subject: [PATCH] Add unix socket manhole to access bridge internals at runtime --- Dockerfile | 3 +- example-config.yaml | 5 ++ mautrix_telegram/__main__.py | 2 +- mautrix_telegram/commands/__init__.py | 2 +- mautrix_telegram/commands/handler.py | 2 + mautrix_telegram/commands/manhole.py | 89 +++++++++++++++++++++++++++ mautrix_telegram/config.py | 3 + mautrix_telegram/context.py | 6 +- mautrix_telegram/user.py | 3 +- 9 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 mautrix_telegram/commands/manhole.py diff --git a/Dockerfile b/Dockerfile index 6fa38a13..5df52959 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,8 @@ RUN apk add --no-cache \ ca-certificates \ su-exec \ && pip3 install .[fast_crypto,hq_thumbnails,metrics] \ - && pip3 install --upgrade 'https://github.com/LonamiWebs/Telethon/tarball/master#egg=telethon' + && pip3 install --upgrade 'https://github.com/LonamiWebs/Telethon/tarball/master#egg=telethon' \ + 'https://github.com/tulir/mautrix-python/tarball/master#egg=telethon' VOLUME /data diff --git a/example-config.yaml b/example-config.yaml index 4a19329f..5586aa8e 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -73,6 +73,11 @@ metrics: enabled: false listen_port: 8000 +# Manhole config. +manhole: + enabled: false + path: /var/tmp/mautrix-telegram.manhole + # Bridge config bridge: # Localpart template of MXIDs for Telegram users. diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 35714151..8ddb27b8 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -84,7 +84,7 @@ class TelegramBridge(Bridge): def prepare_bridge(self) -> None: self.bot = init_bot(self.config) - context = Context(self.az, self.config, self.loop, self.session_container, self.bot) + context = Context(self.az, self.config, self.loop, self.session_container, self, self.bot) self._prepare_website(context) self.matrix = context.mx = MatrixHandler(context) diff --git a/mautrix_telegram/commands/__init__.py b/mautrix_telegram/commands/__init__.py index b33fe63b..cbdd778c 100644 --- a/mautrix_telegram/commands/__init__.py +++ b/mautrix_telegram/commands/__init__.py @@ -1,7 +1,7 @@ from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent, SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT, SECTION_MISC, SECTION_ADMIN) -from . import portal, telegram, clean_rooms, matrix_auth +from . import portal, telegram, clean_rooms, matrix_auth, manhole __all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent", "SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS", diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 63a6f17b..f50e59e5 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -46,6 +46,7 @@ class CommandEvent(BaseCommandEvent): is_portal: bool) -> None: super().__init__(processor, room_id, event_id, sender, command, args, is_management, is_portal) + self.bridge = processor.bridge self.tgbot = processor.tgbot self.config = processor.config self.public_website = processor.public_website @@ -113,6 +114,7 @@ class CommandProcessor(BaseCommandProcessor): super().__init__(az=context.az, config=context.config, event_class=CommandEvent, loop=context.loop) self.tgbot = context.bot + self.bridge = context.bridge self.az, self.config, self.loop, self.tgbot = context.core self.public_website = context.public_website self.command_prefix = self.config["bridge.command_prefix"] diff --git a/mautrix_telegram/commands/manhole.py b/mautrix_telegram/commands/manhole.py new file mode 100644 index 00000000..df7e7ab9 --- /dev/null +++ b/mautrix_telegram/commands/manhole.py @@ -0,0 +1,89 @@ +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2019 Tulir Asokan +# +# 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 . +from typing import Optional, Callable +import asyncio +import sys +import os + +from telethon import __version__ as __telethon_version__ + +from mautrix import __version__ as __mautrix_version__ +from mautrix.types import UserID +from mautrix.util.manhole import start_manhole + +from .. import __version__ +from . import command_handler, CommandEvent, SECTION_ADMIN + + +class State: + manhole: Optional[asyncio.AbstractServer] = None + opened_by: Optional[UserID] = None + close: Optional[Callable[[], None]] = None + + +@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN, + help_text="Open a manhole into the bridge.") +async def manhole(evt: CommandEvent) -> None: + if not evt.config["manhole.enabled"]: + await evt.reply("The manhole has been disabled in the config.") + return + + if State.manhole: + await evt.reply(f"There's an existing manhole opened by {State.opened_by}") + return + + from ..portal import Portal + from ..puppet import Puppet + from ..user import User + namespace = { + "bridge": evt.bridge, + "User": User, + "Portal": Portal, + "Puppet": Puppet, + } + banner = (f"Python {sys.version} on {sys.platform}\n" + f"mautrix-telegram {__version__} with mautrix-python {__mautrix_version__} " + f"and Telethon {__telethon_version__}\n\nManhole opened by {evt.sender.mxid}\n") + path = evt.config["manhole.path"] + + evt.log.info(f"{evt.sender.mxid} opened a manhole.") + State.manhole, State.close = await start_manhole(path=path, banner=banner, namespace=namespace, + loop=evt.loop) + State.opened_by = evt.sender.mxid + await evt.reply(f"Opened manhole at unix://{path}") + await State.manhole.wait_closed() + try: + os.unlink(path) + except FileNotFoundError: + pass + evt.log.info(f"{evt.sender.mxid}'s manhole was closed.") + await evt.reply("Your manhole was closed.") + + +@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN, + help_text="Close an open manhole.") +async def close_manhole(evt: CommandEvent) -> None: + if not State.manhole: + await evt.reply("There is no open manhole.") + return + + opened_by = State.opened_by + State.close() + State.manhole = None + State.close = None + State.opened_by = None + if opened_by != evt.sender: + await evt.reply(f"Closed manhole opened by {opened_by}") diff --git a/mautrix_telegram/config.py b/mautrix_telegram/config.py index 65240713..c59a6e1c 100644 --- a/mautrix_telegram/config.py +++ b/mautrix_telegram/config.py @@ -74,6 +74,9 @@ class Config(BaseBridgeConfig): copy("metrics.enabled") copy("metrics.listen_port") + copy("manhole.enabled") + copy("manhole.path") + copy("bridge.username_template") copy("bridge.alias_template") copy("bridge.displayname_template") diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py index 4566de3f..3ed15e4f 100644 --- a/mautrix_telegram/context.py +++ b/mautrix_telegram/context.py @@ -25,12 +25,14 @@ if TYPE_CHECKING: from .config import Config from .bot import Bot from .matrix import MatrixHandler + from .__main__ import TelegramBridge class Context: az: AppService config: 'Config' loop: asyncio.AbstractEventLoop + bridge: 'TelegramBridge' bot: Optional['Bot'] mx: Optional['MatrixHandler'] session_container: AlchemySessionContainer @@ -38,10 +40,12 @@ class Context: provisioning_api: Optional['ProvisioningAPI'] def __init__(self, az: AppService, config: 'Config', loop: asyncio.AbstractEventLoop, - session_container: AlchemySessionContainer, bot: Optional['Bot']) -> None: + session_container: AlchemySessionContainer, bridge: 'TelegramBridge', + bot: Optional['Bot']) -> None: self.az = az self.config = config self.loop = loop + self.bridge = bridge self.bot = bot self.mx = None self.session_container = session_container diff --git a/mautrix_telegram/user.py b/mautrix_telegram/user.py index c181d9bf..3d3ab7fa 100644 --- a/mautrix_telegram/user.py +++ b/mautrix_telegram/user.py @@ -27,6 +27,7 @@ from telethon.tl.functions.account import UpdateStatusRequest from mautrix.client import Client from mautrix.errors import MatrixRequestError from mautrix.types import UserID +from mautrix.bridge import BaseUser from .types import TelegramID from .db import User as DBUser @@ -42,7 +43,7 @@ config: Optional['Config'] = None SearchResult = NewType('SearchResult', Tuple['pu.Puppet', int]) -class User(AbstractUser): +class User(AbstractUser, BaseUser): log: logging.Logger = logging.getLogger("mau.user") by_mxid: Dict[str, 'User'] = {} by_tgid: Dict[int, 'User'] = {}