Require authentication for web login. Fixes #163

This commit is contained in:
Tulir Asokan
2018-07-14 16:00:20 +03:00
parent 90e7a09b7e
commit d97281bcdc
11 changed files with 130 additions and 37 deletions
+7 -4
View File
@@ -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)
+1 -1
View File
@@ -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"
+4 -3
View File
@@ -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],
+21 -8
View File
@@ -14,17 +14,30 @@
#
# 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/>.
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
+2 -2
View File
@@ -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)
+2 -3
View File
@@ -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)")
+2 -3
View File
@@ -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"]
+1
View File
@@ -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
+53
View File
@@ -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 <https://www.gnu.org/licenses/>.
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
+31 -9
View File
@@ -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)
+6 -4
View File
@@ -76,6 +76,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
management command first.
</p>
% endif
% elif state == "invalid-token":
<h1>Invalid or expired token</h1>
<div class="error">Please ask the bridge bot for a new login link.</div>
% else:
<h1>Log in to Telegram</h1>
% if error:
@@ -87,8 +90,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<form method="post">
<fieldset>
<label for="mxid">Matrix ID</label>
<input type="text" id="mxid" name="mxid" placeholder="Enter Matrix ID"
value="${mxid}"/>
<input type="text" id="mxid" name="mxid" disabled value="${mxid}"/>
% if state == "request":
<label for="value">Phone number</label>
<input type="tel" id="value" name="phone" placeholder="Enter phone number"/>
@@ -96,9 +98,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<button class="button-clear" type="button" onclick="switchToBotLogin()">
Use bot token
</button>
% elif state == "token":
% elif state == "bot_token":
<label for="value">Bot token</label>
<input type="text" id="value" name="token" placeholder="Enter bot API token"/>
<input type="text" id="value" name="bot_token" placeholder="Enter bot API token"/>
<button type="submit">Sign in</button>
% elif state == "code":
<label for="value">Phone code</label>