diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index c8865686..1969437b 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -85,18 +85,21 @@ appserv = AppService(config["homeserver.address"], config["homeserver.domain"], config["appservice.as_token"], config["appservice.hs_token"], config["appservice.bot_username"], log="mau.as", loop=loop, verify_ssl=config["homeserver.verify_ssl"]) - -context = Context(appserv, db_session, config, loop, None, None, session_container) +public_website = None +provisioning_api = None if config["appservice.public.enabled"]: - public = PublicBridgeWebsite(loop) - appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public.app) + public_website = PublicBridgeWebsite(loop) + appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public_website.app) if config["appservice.provisioning.enabled"]: provisioning_api = ProvisioningAPI(config, loop) appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning", provisioning_api.app) +context = Context(appserv, db_session, config, loop, None, None, session_container, public_website, + provisioning_api) + with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start: init_db(db_session) init_abstract_user(context) diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py index 8caf21a5..b5eaf1d9 100644 --- a/mautrix_telegram/commands/auth.py +++ b/mautrix_telegram/commands/auth.py @@ -114,7 +114,7 @@ async def login(evt: CommandEvent): if evt.config["appservice.public.enabled"]: prefix = evt.config["appservice.public.external"] - url = f"{prefix}/login?mxid={evt.sender.mxid}" + url = f"{prefix}/login?token={evt.public_website.make_token(evt.sender.mxid)}" if evt.config.get("bridge.allow_matrix_login", True): return await evt.reply( "This bridge instance allows you to log in inside or outside Matrix.\n\n" diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 7bf4323d..53a71a4b 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -22,8 +22,7 @@ import logging from telethon.errors import FloodWaitError from ..util import format_duration -from ..context import Context -from .. import user as u +from .. import user as u, context as c command_handlers = {} # type: Dict[str, CommandHandler] @@ -45,6 +44,7 @@ class CommandEvent: self.loop = processor.loop self.tgbot = processor.tgbot self.config = processor.config + self.public_website = processor.public_website self.command_prefix = processor.command_prefix self.room_id = room self.sender = sender @@ -131,8 +131,9 @@ def command_handler(_func: Optional[Callable[[CommandEvent], None]] = None, *, n class CommandProcessor: log = logging.getLogger("mau.commands") - def __init__(self, context: Context): + def __init__(self, context: c.Context): self.az, self.db, self.config, self.loop, self.tgbot = context + self.public_website = context.public_website self.command_prefix = self.config["bridge.command_prefix"] async def handle(self, room: str, sender: u.User, command: str, args: List[str], diff --git a/mautrix_telegram/context.py b/mautrix_telegram/context.py index dfb32b59..ad48d7e4 100644 --- a/mautrix_telegram/context.py +++ b/mautrix_telegram/context.py @@ -14,17 +14,30 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import asyncio + +from sqlalchemy.orm import scoped_session +from alchemysession import AlchemySessionContainer +from mautrix_appservice import AppService class Context: - def __init__(self, az, db, config, loop, bot, mx, session_container): - self.az = az - self.db = db - self.config = config - self.loop = loop - self.bot = bot - self.mx = mx - self.session_container = session_container + def __init__(self, az, db, config, loop, bot, mx, session_container, public_website, + provisioning_api): + from .web import PublicBridgeWebsite, ProvisioningAPI + from .config import Config + from .bot import Bot + from .matrix import MatrixHandler + + self.az = az # type: AppService + self.db = db # type: scoped_session + self.config = config # type: Config + self.loop = loop # type: asyncio.AbstractEventLoop + self.bot = bot # type: Bot + self.mx = mx # type: MatrixHandler + self.session_container = session_container # type: AlchemySessionContainer + self.public_website = public_website # type: PublicBridgeWebsite + self.provisioning_api = provisioning_api # type: ProvisioningAPI def __iter__(self): yield self.az diff --git a/mautrix_telegram/formatter/__init__.py b/mautrix_telegram/formatter/__init__.py index 7cb102f7..51802ebb 100644 --- a/mautrix_telegram/formatter/__init__.py +++ b/mautrix_telegram/formatter/__init__.py @@ -1,9 +1,9 @@ from .from_matrix import (matrix_reply_to_telegram, matrix_to_telegram, matrix_text_to_telegram, init_mx) from .from_telegram import (telegram_reply_to_matrix, telegram_to_matrix, init_tg) -from ..context import Context +from .. import context as c -def init(context: Context): +def init(context: c.Context): init_mx(context) init_tg(context) diff --git a/mautrix_telegram/formatter/from_matrix.py b/mautrix_telegram/formatter/from_matrix.py index 145177fc..f98d3ad5 100644 --- a/mautrix_telegram/formatter/from_matrix.py +++ b/mautrix_telegram/formatter/from_matrix.py @@ -27,8 +27,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, M MessageEntityItalic, MessageEntityCode, MessageEntityPre, MessageEntityBotCommand, TypeMessageEntity) -from ..context import Context -from .. import user as u, puppet as pu, portal as po +from .. import user as u, puppet as pu, portal as po, context as c from ..db import Message as DBMessage from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html, trim_reply_fallback_text, html_to_unicode) @@ -352,7 +351,7 @@ def plain_mention_to_text() -> Tuple[List[TypeMessageEntity], Callable[[str], st return entities, replacer -def init_mx(context: Context): +def init_mx(context: c.Context): global plain_mention_regex, should_bridge_plaintext_highlights config = context.config dn_template = config.get("bridge.displayname_template", "{displayname} (Telegram)") diff --git a/mautrix_telegram/formatter/from_telegram.py b/mautrix_telegram/formatter/from_telegram.py index 3e9992e4..70a13a55 100644 --- a/mautrix_telegram/formatter/from_telegram.py +++ b/mautrix_telegram/formatter/from_telegram.py @@ -33,8 +33,7 @@ from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, from mautrix_appservice import MatrixRequestError from mautrix_appservice.intent_api import IntentAPI -from .. import user as u, puppet as pu, portal as po -from ..context import Context +from .. import user as u, puppet as pu, portal as po, context as c from ..db import Message as DBMessage from .util import (add_surrogates, remove_surrogates, trim_reply_fallback_html, trim_reply_fallback_text, unicode_to_html) @@ -321,6 +320,6 @@ def _parse_url(html: List[str], entity_text: str, url: str) -> bool: return False -def init_tg(context: Context): +def init_tg(context: c.Context): global should_highlight_edits should_highlight_edits = htmldiff and context.config["bridge.highlight_edits"] diff --git a/mautrix_telegram/util/__init__.py b/mautrix_telegram/util/__init__.py index 7d431396..99cdee2a 100644 --- a/mautrix_telegram/util/__init__.py +++ b/mautrix_telegram/util/__init__.py @@ -1,2 +1,3 @@ from .file_transfer import transfer_file_to_matrix, convert_image from .format_duration import format_duration +from .signed_token import sign_token, verify_token diff --git a/mautrix_telegram/util/signed_token.py b/mautrix_telegram/util/signed_token.py new file mode 100644 index 00000000..13281012 --- /dev/null +++ b/mautrix_telegram/util/signed_token.py @@ -0,0 +1,53 @@ +# -*- coding: future_fstrings -*- +# mautrix-telegram - A Matrix-Telegram puppeting bridge +# Copyright (C) 2018 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 +import json +import base64 +import hashlib + + +def _get_checksum(key: str, payload: bytes) -> str: + hasher = hashlib.sha256() + hasher.update(payload) + hasher.update(key.encode("utf-8")) + checksum = hasher.hexdigest() + return checksum + + +def sign_token(key: str, payload: dict) -> str: + payload = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8")) + checksum = _get_checksum(key, payload) + return f"{checksum}:{payload.decode('utf-8')}" + + +def verify_token(key: str, data: str) -> Optional[dict]: + if not data: + return None + + try: + checksum, payload = data.split(":", 1) + except ValueError: + return None + + if checksum != _get_checksum(key, payload.encode("utf-8")): + return None + + payload = base64.urlsafe_b64decode(payload).decode("utf-8") + try: + return json.loads(payload) + except json.JSONDecodeError: + return None diff --git a/mautrix_telegram/web/public/__init__.py b/mautrix_telegram/web/public/__init__.py index eab1f5fe..fb5f6de7 100644 --- a/mautrix_telegram/web/public/__init__.py +++ b/mautrix_telegram/web/public/__init__.py @@ -18,7 +18,11 @@ from aiohttp import web from mako.template import Template import pkg_resources import logging +import random +import string +import time +from ...util import sign_token, verify_token from ...user import User from ..common import AuthAPI @@ -28,6 +32,8 @@ class PublicBridgeWebsite(AuthAPI): def __init__(self, loop): super().__init__(loop) + self.secret_key = "".join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(64)) self.login = Template( pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako")) @@ -38,10 +44,24 @@ class PublicBridgeWebsite(AuthAPI): self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_telegram", "web/public/")) - async def get_login(self, request): - state = "token" if request.rel_url.query.get("mode", "") == "bot" else "request" + def make_token(self, mxid, expires_in=900): + return sign_token(self.secret_key, { + "mxid": mxid, + "expiry": int(time.time()) + expires_in, + }) - mxid = request.rel_url.query.get("mxid", None) + def verify_token(self, token): + token = verify_token(self.secret_key, token) + if token and token.get("expiry", 0) > int(time.time()): + return token.get("mxid", None) + return None + + async def get_login(self, request): + state = "bot_token" if request.rel_url.query.get("mode", "") == "bot" else "request" + + mxid = self.verify_token(request.rel_url.query.get("token", None)) + if not mxid: + return self.get_login_response(status=401, state="invalid-token") user = User.get_by_mxid(mxid, create=False) if mxid else None if not user: @@ -62,11 +82,13 @@ class PublicBridgeWebsite(AuthAPI): message=message, mxid=mxid)) async def post_login(self, request): - data = await request.post() - if "mxid" not in data: - return self.get_login_response(error="Please enter your Matrix ID.", status=400) + mxid = self.verify_token(request.rel_url.query.get("token", None)) + if not mxid: + return self.get_login_response(status=401, state="invalid-token") - user = await User.get_by_mxid(data["mxid"]).ensure_started(even_if_no_session=True) + data = await request.post() + + user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) if not user.puppet_whitelisted: return self.get_login_response(mxid=user.mxid, error="You are not whitelisted.", status=403) @@ -77,8 +99,8 @@ class PublicBridgeWebsite(AuthAPI): if "phone" in data: return await self.post_login_phone(user, data["phone"]) - elif "token" in data: - return await self.post_login_token(user, data["token"]) + elif "bot_token" in data: + return await self.post_login_token(user, data["bot_token"]) elif "code" in data: resp = await self.post_login_code(user, data["code"], password_in_data="password" in data) diff --git a/mautrix_telegram/web/public/login.html.mako b/mautrix_telegram/web/public/login.html.mako index 8c03cbdc..f00b6a69 100644 --- a/mautrix_telegram/web/public/login.html.mako +++ b/mautrix_telegram/web/public/login.html.mako @@ -76,6 +76,9 @@ along with this program. If not, see . management command first.

% endif + % elif state == "invalid-token": +

Invalid or expired token

+
Please ask the bridge bot for a new login link.
% else:

Log in to Telegram

% if error: @@ -87,8 +90,7 @@ along with this program. If not, see .
- + % if state == "request": @@ -96,9 +98,9 @@ along with this program. If not, see . - % elif state == "token": + % elif state == "bot_token": - + % elif state == "code":