Add unix socket manhole to access bridge internals at runtime

This commit is contained in:
Tulir Asokan
2019-08-11 02:35:58 +03:00
parent 468412100c
commit b89ecf4c03
9 changed files with 110 additions and 5 deletions
+2 -1
View File
@@ -38,7 +38,8 @@ RUN apk add --no-cache \
ca-certificates \ ca-certificates \
su-exec \ su-exec \
&& pip3 install .[fast_crypto,hq_thumbnails,metrics] \ && 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 VOLUME /data
+5
View File
@@ -73,6 +73,11 @@ metrics:
enabled: false enabled: false
listen_port: 8000 listen_port: 8000
# Manhole config.
manhole:
enabled: false
path: /var/tmp/mautrix-telegram.manhole
# Bridge config # Bridge config
bridge: bridge:
# Localpart template of MXIDs for Telegram users. # Localpart template of MXIDs for Telegram users.
+1 -1
View File
@@ -84,7 +84,7 @@ class TelegramBridge(Bridge):
def prepare_bridge(self) -> None: def prepare_bridge(self) -> None:
self.bot = init_bot(self.config) 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._prepare_website(context)
self.matrix = context.mx = MatrixHandler(context) self.matrix = context.mx = MatrixHandler(context)
+1 -1
View File
@@ -1,7 +1,7 @@
from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent, from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent,
SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT, SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT,
SECTION_MISC, SECTION_ADMIN) 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", __all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent",
"SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS", "SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS",
+2
View File
@@ -46,6 +46,7 @@ class CommandEvent(BaseCommandEvent):
is_portal: bool) -> None: is_portal: bool) -> None:
super().__init__(processor, room_id, event_id, sender, command, args, is_management, super().__init__(processor, room_id, event_id, sender, command, args, is_management,
is_portal) is_portal)
self.bridge = processor.bridge
self.tgbot = processor.tgbot self.tgbot = processor.tgbot
self.config = processor.config self.config = processor.config
self.public_website = processor.public_website self.public_website = processor.public_website
@@ -113,6 +114,7 @@ class CommandProcessor(BaseCommandProcessor):
super().__init__(az=context.az, config=context.config, event_class=CommandEvent, super().__init__(az=context.az, config=context.config, event_class=CommandEvent,
loop=context.loop) loop=context.loop)
self.tgbot = context.bot self.tgbot = context.bot
self.bridge = context.bridge
self.az, self.config, self.loop, self.tgbot = context.core self.az, self.config, self.loop, self.tgbot = context.core
self.public_website = context.public_website self.public_website = context.public_website
self.command_prefix = self.config["bridge.command_prefix"] self.command_prefix = self.config["bridge.command_prefix"]
+89
View File
@@ -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 <https://www.gnu.org/licenses/>.
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}")
+3
View File
@@ -74,6 +74,9 @@ class Config(BaseBridgeConfig):
copy("metrics.enabled") copy("metrics.enabled")
copy("metrics.listen_port") copy("metrics.listen_port")
copy("manhole.enabled")
copy("manhole.path")
copy("bridge.username_template") copy("bridge.username_template")
copy("bridge.alias_template") copy("bridge.alias_template")
copy("bridge.displayname_template") copy("bridge.displayname_template")
+5 -1
View File
@@ -25,12 +25,14 @@ if TYPE_CHECKING:
from .config import Config from .config import Config
from .bot import Bot from .bot import Bot
from .matrix import MatrixHandler from .matrix import MatrixHandler
from .__main__ import TelegramBridge
class Context: class Context:
az: AppService az: AppService
config: 'Config' config: 'Config'
loop: asyncio.AbstractEventLoop loop: asyncio.AbstractEventLoop
bridge: 'TelegramBridge'
bot: Optional['Bot'] bot: Optional['Bot']
mx: Optional['MatrixHandler'] mx: Optional['MatrixHandler']
session_container: AlchemySessionContainer session_container: AlchemySessionContainer
@@ -38,10 +40,12 @@ class Context:
provisioning_api: Optional['ProvisioningAPI'] provisioning_api: Optional['ProvisioningAPI']
def __init__(self, az: AppService, config: 'Config', loop: asyncio.AbstractEventLoop, 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.az = az
self.config = config self.config = config
self.loop = loop self.loop = loop
self.bridge = bridge
self.bot = bot self.bot = bot
self.mx = None self.mx = None
self.session_container = session_container self.session_container = session_container
+2 -1
View File
@@ -27,6 +27,7 @@ from telethon.tl.functions.account import UpdateStatusRequest
from mautrix.client import Client from mautrix.client import Client
from mautrix.errors import MatrixRequestError from mautrix.errors import MatrixRequestError
from mautrix.types import UserID from mautrix.types import UserID
from mautrix.bridge import BaseUser
from .types import TelegramID from .types import TelegramID
from .db import User as DBUser from .db import User as DBUser
@@ -42,7 +43,7 @@ config: Optional['Config'] = None
SearchResult = NewType('SearchResult', Tuple['pu.Puppet', int]) SearchResult = NewType('SearchResult', Tuple['pu.Puppet', int])
class User(AbstractUser): class User(AbstractUser, BaseUser):
log: logging.Logger = logging.getLogger("mau.user") log: logging.Logger = logging.getLogger("mau.user")
by_mxid: Dict[str, 'User'] = {} by_mxid: Dict[str, 'User'] = {}
by_tgid: Dict[int, 'User'] = {} by_tgid: Dict[int, 'User'] = {}