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 .