Add initial out-of-Matrix login system
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -44,12 +44,26 @@ async def login(evt):
|
||||
elif len(evt.args) == 0:
|
||||
return await evt.reply("**Usage:** `$cmdprefix+sp login <phone number>`")
|
||||
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 <phone>`.")
|
||||
except PhoneCodeExpiredError:
|
||||
return await evt.reply(
|
||||
"Phone code expired. Try again with `$cmdprefix+sp login <phone>`.")
|
||||
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.")
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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)
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
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.")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Mautrix-Telegram bridge</title>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<meta property="og:title" content="Mautrix-Telegram bridge">
|
||||
<meta property="og:description" content="A hybrid puppeting/relaybot Matrix-Telegram bridge">
|
||||
<meta property="og:image" content="favicon.png">
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="login.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
% if state == "logged-in":
|
||||
<h1>Logged in successfully!</h1>
|
||||
<p>Logged in as @${username}</p>
|
||||
% else:
|
||||
<h1>Log in to Telegram</h1>
|
||||
% if error:
|
||||
<div class="error">${error}</div>
|
||||
% endif
|
||||
% if message:
|
||||
<div class="message">${message}</div>
|
||||
% endif
|
||||
<form method="post">
|
||||
<input type="text" name="mxid" placeholder="Enter Matrix ID" value="${mxid}"/>
|
||||
% if state == "request":
|
||||
<input type="text" name="phone" placeholder="Enter phone number"/>
|
||||
<button type="submit">Request code</button>
|
||||
% elif state == "code":
|
||||
<input type="number" name="code" placeholder="Enter phone code"/>
|
||||
<button type="submit">Sign in</button>
|
||||
% elif state == "password":
|
||||
<input type="password" name="password" placeholder="Enter password"/>
|
||||
<button type="submit">Sign in</button>
|
||||
% endif
|
||||
</form>
|
||||
% endif
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user