From ad11abb56e7eb6a0ae5c72ac439e41ba6464b92b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 18 Feb 2018 19:44:32 +0200 Subject: [PATCH] Add initial out-of-Matrix login system --- example-config.yaml | 12 +++ mautrix_appservice/appservice.py | 1 - mautrix_telegram/__main__.py | 15 ++- mautrix_telegram/commands/auth.py | 41 +++---- mautrix_telegram/commands/handler.py | 20 +--- mautrix_telegram/commands/util.py | 34 ++++++ mautrix_telegram/public/__init__.py | 137 ++++++++++++++++++++++++ mautrix_telegram/public/favicon.png | Bin 0 -> 43371 bytes mautrix_telegram/public/login.css | 9 ++ mautrix_telegram/public/login.html.mako | 41 +++++++ setup.py | 2 + 11 files changed, 270 insertions(+), 42 deletions(-) create mode 100644 mautrix_telegram/commands/util.py create mode 100644 mautrix_telegram/public/__init__.py create mode 100644 mautrix_telegram/public/favicon.png create mode 100644 mautrix_telegram/public/login.css create mode 100644 mautrix_telegram/public/login.html.mako diff --git a/example-config.yaml b/example-config.yaml index 1aa2a508..32a178e1 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -14,6 +14,18 @@ appservice: hostname: localhost port: 8080 + # Public part of web server for out-of-Matrix interaction with the bridge. + # Used for things like login if the user wants to make sure the 2FA password isn't stored in + # the HS database. + public: + # Whether or not the public-facing endpoints should be enabled. + enabled: true + # The prefix to use in the public-facing endpoints. + prefix: /public + # The base URL where the public-facing endpoints are available. The prefix is not added + # implicitly. + external: https://example.com/public + # Whether or not to enable debug messages in the console. debug: false diff --git a/mautrix_appservice/appservice.py b/mautrix_appservice/appservice.py index 37ea3ea4..bc237007 100644 --- a/mautrix_appservice/appservice.py +++ b/mautrix_appservice/appservice.py @@ -16,7 +16,6 @@ # along with this program. If not, see . # # Partly based on github.com/Cadair/python-appservice-framework (MIT license) -from functools import partial from contextlib import contextmanager from aiohttp import web import aiohttp diff --git a/mautrix_telegram/__main__.py b/mautrix_telegram/__main__.py index 9af163e8..27115a88 100644 --- a/mautrix_telegram/__main__.py +++ b/mautrix_telegram/__main__.py @@ -32,6 +32,7 @@ from .db import init as init_db from .user import init as init_user, User from .portal import init as init_portal from .puppet import init as init_puppet +from .public import PublicBridgeWebsite log = logging.getLogger("mau") time_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s@%(name)s] %(message)s") @@ -73,12 +74,16 @@ Base.metadata.bind = db_engine Base.metadata.create_all() loop = asyncio.get_event_loop() -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) -context = (appserv, db_session, config, loop) +az = AppService(config["homeserver.address"], config["homeserver.domain"], + config["appservice.as_token"], config["appservice.hs_token"], + config["appservice.bot_username"], log="mau.as", loop=loop) +context = (az, db_session, config, loop) -with appserv.run(config["appservice.hostname"], config["appservice.port"]) as start: +if config["appservice.public.enabled"]: + public = PublicBridgeWebsite(loop) + az.app.add_subapp(config.get("appservice.public.prefix", "/public"), public.app) + +with az.run(config["appservice.hostname"], config["appservice.port"]) as start: MatrixHandler(context) init_db(db_session) init_portal(context) diff --git a/mautrix_telegram/commands/auth.py b/mautrix_telegram/commands/auth.py index 6c114785..3797f651 100644 --- a/mautrix_telegram/commands/auth.py +++ b/mautrix_telegram/commands/auth.py @@ -44,12 +44,26 @@ async def login(evt): elif len(evt.args) == 0: return await evt.reply("**Usage:** `$cmdprefix+sp login `") phone_number = evt.args[0] - await evt.sender.client.sign_in(phone_number) - evt.sender.command_status = { - "next": enter_code, - "action": "Login", - } - return await evt.reply(f"Login code sent to {phone_number}. Please send the code here.") + try: + await evt.sender.client.sign_in(phone_number) + evt.sender.command_status = { + "next": enter_code, + "action": "Login", + } + return await evt.reply(f"Login code sent to {phone_number}. Please send the code here.") + except PhoneNumberAppSignupForbiddenError: + return await evt.reply( + "Your phone number does not allow 3rd party apps to sign in.") + except PhoneNumberFloodError: + return await evt.reply( + "Your phone number has been temporarily blocked for flooding. " + "The ban is usually applied for around a day.") + except PhoneNumberBannedError: + return await evt.reply("Your phone number has been banned from Telegram.") + except Exception: + evt.log.exception("Error requesting phone code") + return await evt.reply("Unhandled exception while requesting code. " + "Check console for more details.") @command_handler(needs_auth=False) @@ -63,32 +77,23 @@ async def enter_code(evt): evt.sender.command_status = None return await evt.reply(f"Successfully logged in as @{user.username}") except PhoneNumberUnoccupiedError: - return await evt.reply("That phone number has not been registered." + return await evt.reply("That phone number has not been registered. " "Please register with `$cmdprefix+sp register `.") except PhoneCodeExpiredError: return await evt.reply( "Phone code expired. Try again with `$cmdprefix+sp login `.") except PhoneCodeInvalidError: return await evt.reply("Invalid phone code.") - except PhoneNumberAppSignupForbiddenError: - return await evt.reply( - "Your phone number does not allow 3rd party apps to sign in.") - except PhoneNumberFloodError: - return await evt.reply( - "Your phone number has been temporarily blocked for flooding. " - "The block is usually applied for around a day.") - except PhoneNumberBannedError: - return await evt.reply("Your phone number has been banned from Telegram.") except SessionPasswordNeededError: evt.sender.command_status = { "next": enter_password, "action": "Login (password entry)", } - return await evt.reply("Your account has two-factor authentication." + return await evt.reply("Your account has two-factor authentication. " "Please send your password here.") except Exception: evt.log.exception("Error sending phone code") - return await evt.reply("Unhandled exception while sending code." + return await evt.reply("Unhandled exception while sending code. " "Check console for more details.") diff --git a/mautrix_telegram/commands/handler.py b/mautrix_telegram/commands/handler.py index 0e06e3a4..a297ea8a 100644 --- a/mautrix_telegram/commands/handler.py +++ b/mautrix_telegram/commands/handler.py @@ -19,6 +19,8 @@ import logging from telethon.errors import FloodWaitError +from .util import format_duration + command_handlers = {} @@ -65,24 +67,6 @@ class CommandEvent: return self.az.intent.send_notice(self.room_id, message, html=html) -def format_duration(seconds): - def pluralize(count, singular): return singular if count == 1 else singular + "s" - - def include(count, word): return f"{count} {pluralize(count, word)}" if count > 0 else "" - - minutes, seconds = divmod(seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - parts = [a for a in [ - include(days, "day"), - include(hours, "hour"), - include(minutes, "minute"), - include(seconds, "second")] if a] - if len(parts) > 2: - return "{} and {}".format(", ".join(parts[:-1]), parts[-1]) - return " and ".join(parts) - - class CommandHandler: log = logging.getLogger("mau.commands") diff --git a/mautrix_telegram/commands/util.py b/mautrix_telegram/commands/util.py new file mode 100644 index 00000000..ffbac714 --- /dev/null +++ b/mautrix_telegram/commands/util.py @@ -0,0 +1,34 @@ +# -*- 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 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +def format_duration(seconds): + def pluralize(count, singular): return singular if count == 1 else singular + "s" + + def include(count, word): return f"{count} {pluralize(count, word)}" if count > 0 else "" + + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + parts = [a for a in [ + include(days, "day"), + include(hours, "hour"), + include(minutes, "minute"), + include(seconds, "second")] if a] + if len(parts) > 2: + return "{} and {}".format(", ".join(parts[:-1]), parts[-1]) + return " and ".join(parts) diff --git a/mautrix_telegram/public/__init__.py b/mautrix_telegram/public/__init__.py new file mode 100644 index 00000000..890d24cb --- /dev/null +++ b/mautrix_telegram/public/__init__.py @@ -0,0 +1,137 @@ +# -*- 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 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from aiohttp import web +from mako.template import Template +import asyncio +import pkg_resources +import logging + +from telethon.errors import * + +from ..user import User +from ..commands.auth import enter_password + + +class PublicBridgeWebsite: + log = logging.getLogger("mau.public") + + def __init__(self, loop): + self.loop = loop + + self.login = Template( + pkg_resources.resource_string("mautrix_telegram", "public/login.html.mako")) + + self.app = web.Application(loop=loop) + self.app.router.add_route("GET", "/login", self.get_login) + self.app.router.add_route("POST", "/login", self.post_login) + self.app.router.add_static("/", + pkg_resources.resource_filename("mautrix_telegram", "public/")) + + async def get_login(self, request): + return self.render_login( + request.rel_url.query["mxid"] if "mxid" in request.rel_url.query else "") + + def render_login(self, mxid, state="request", phone="", code="", password="", + error="", message="", username="", status=200): + return web.Response(status=status, + content_type="text/html", + text=self.login.render(mxid=mxid, state=state, phone=phone, code=code, + message=message, username=username, error=error, + password=password)) + + async def post_login(self, request): + self.log.debug(request) + data = await request.post() + if "mxid" not in data: + return self.render_login(error="Please enter your Matrix ID.", status=400) + + user = User.get_by_mxid(data["mxid"]) + if not user.whitelisted: + return self.render_login(mxid=user.mxid, error="You are not whitelisted.", status=403) + + if "phone" in data: + try: + await user.client.sign_in(data["phone"] or "+123") + return self.render_login(mxid=user.mxid, state="code", status=200, + message="Code requested successfully.") + except PhoneNumberInvalidError: + return self.render_login(mxid=user.mxid, state="request", status=400, + error="Invalid phone number.") + except PhoneNumberUnoccupiedError: + return self.render_login(mxid=user.mxid, state="request", status=404, + error="That phone number has not been registered.") + except PhoneNumberFloodError: + return self.render_login( + mxid=user.mxid, state="request", status=429, + error="Your phone number has been temporarily banned for flooding. " + "The ban is usually applied for around a day.") + except PhoneNumberBannedError: + return self.render_login(mxid=user.mxid, state="request", status=401, + error="Your phone number is banned from Telegram.") + except PhoneNumberAppSignupForbiddenError: + return self.render_login(mxid=user.mxid, state="request", status=401, + error="You have disabled 3rd party apps on your account.") + except Exception: + self.log.exception("Error requesting phone code") + return self.render_login(mxid=user.mxid, state="request", status=500, + error="Internal server error while requesting code.") + elif "code" in data: + try: + user_info = await user.client.sign_in(code=data["code"]) + asyncio.ensure_future(user.post_login(user_info), loop=self.loop) + if user.command_status.action == "Login": + user.command_status = None + return self.render_login(mxid=user.mxid, state="logged-in", status=200, + username=user_info.username) + except PhoneCodeInvalidError: + return self.render_login(mxid=user.mxid, state="code", status=403, + error="Incorrect phone code.") + except PhoneCodeExpiredError: + return self.render_login(mxid=user.mxid, state="code", status=403, + error="Phone code expired.") + except SessionPasswordNeededError: + if "password" not in data: + if user.command_status.action == "Login": + user.command_status = { + "next": enter_password, + "action": "Login (password entry)", + } + return self.render_login( + mxid=user.mxid, state="password", status=200, + error="Code accepted, but you have 2-factor authentication is enabled.") + except Exception: + self.log.exception("Error sending phone code") + return self.render_login(mxid=user.mxid, state="code", status=500, + error="Internal server error while sending code.") + elif "password" not in data: + return self.render_login(error="No data given.", status=400) + + if "password" in data: + try: + user_info = await user.client.sign_in(password=data["password"]) + asyncio.ensure_future(user.post_login(user_info), loop=self.loop) + if user.command_status.action == "Login (password entry)": + user.command_status = None + return self.render_login(mxid=user.mxid, state="logged-in", status=200, + username=user_info.username) + except (PasswordHashInvalidError, PasswordEmptyError): + return self.render_login(mxid=user.mxid, state="password", status=400, + error="Incorrect password.") + except Exception: + self.log.exception("Error sending password") + return self.render_login(mxid=user.mxid, state="password", status=500, + error="Internal server error while sending password.") diff --git a/mautrix_telegram/public/favicon.png b/mautrix_telegram/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b5fae7269f6fef0c4f2b755dd7837bd0bbc139 GIT binary patch literal 43371 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 KhUqKp5B?YM@XmMu literal 0 HcmV?d00001 diff --git a/mautrix_telegram/public/login.css b/mautrix_telegram/public/login.css new file mode 100644 index 00000000..461ec7e0 --- /dev/null +++ b/mautrix_telegram/public/login.css @@ -0,0 +1,9 @@ +form > div { + display: none; +} + +form[data-status="request"] > div.status-request, +form[data-status="code"] > div.status-code, +form[data-status="password"] > div.status-password { + display: initial; +} diff --git a/mautrix_telegram/public/login.html.mako b/mautrix_telegram/public/login.html.mako new file mode 100644 index 00000000..c2aefbf0 --- /dev/null +++ b/mautrix_telegram/public/login.html.mako @@ -0,0 +1,41 @@ + + + + Mautrix-Telegram bridge + + + + + + + + +
+ % if state == "logged-in": +

Logged in successfully!

+

Logged in as @${username}

+ % else: +

Log in to Telegram

+ % if error: +
${error}
+ % endif + % if message: +
${message}
+ % endif +
+ + % if state == "request": + + + % elif state == "code": + + + % elif state == "password": + + + % endif +
+ % endif +
+ + diff --git a/setup.py b/setup.py index 6f02919e..c7874097 100644 --- a/setup.py +++ b/setup.py @@ -44,4 +44,6 @@ setuptools.setup( [console_scripts] mautrix-telegram=mautrix_telegram.__main__:main """, + package_data={"mautrix_telegram": ["public/*.html", "public/*.png", "public/*.css", "public/*.js"]}, + data_files=[(".", "example-config.yaml")], )