Add initial out-of-Matrix login system
This commit is contained in:
@@ -14,6 +14,18 @@ appservice:
|
|||||||
hostname: localhost
|
hostname: localhost
|
||||||
port: 8080
|
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.
|
# Whether or not to enable debug messages in the console.
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
# Partly based on github.com/Cadair/python-appservice-framework (MIT license)
|
# Partly based on github.com/Cadair/python-appservice-framework (MIT license)
|
||||||
from functools import partial
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from .db import init as init_db
|
|||||||
from .user import init as init_user, User
|
from .user import init as init_user, User
|
||||||
from .portal import init as init_portal
|
from .portal import init as init_portal
|
||||||
from .puppet import init as init_puppet
|
from .puppet import init as init_puppet
|
||||||
|
from .public import PublicBridgeWebsite
|
||||||
|
|
||||||
log = logging.getLogger("mau")
|
log = logging.getLogger("mau")
|
||||||
time_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s@%(name)s] %(message)s")
|
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()
|
Base.metadata.create_all()
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
appserv = AppService(config["homeserver.address"], config["homeserver.domain"],
|
az = AppService(config["homeserver.address"], config["homeserver.domain"],
|
||||||
config["appservice.as_token"], config["appservice.hs_token"],
|
config["appservice.as_token"], config["appservice.hs_token"],
|
||||||
config["appservice.bot_username"], log="mau.as", loop=loop)
|
config["appservice.bot_username"], log="mau.as", loop=loop)
|
||||||
context = (appserv, db_session, config, 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)
|
MatrixHandler(context)
|
||||||
init_db(db_session)
|
init_db(db_session)
|
||||||
init_portal(context)
|
init_portal(context)
|
||||||
|
|||||||
@@ -44,12 +44,26 @@ async def login(evt):
|
|||||||
elif len(evt.args) == 0:
|
elif len(evt.args) == 0:
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp login <phone number>`")
|
return await evt.reply("**Usage:** `$cmdprefix+sp login <phone number>`")
|
||||||
phone_number = evt.args[0]
|
phone_number = evt.args[0]
|
||||||
await evt.sender.client.sign_in(phone_number)
|
try:
|
||||||
evt.sender.command_status = {
|
await evt.sender.client.sign_in(phone_number)
|
||||||
"next": enter_code,
|
evt.sender.command_status = {
|
||||||
"action": "Login",
|
"next": enter_code,
|
||||||
}
|
"action": "Login",
|
||||||
return await evt.reply(f"Login code sent to {phone_number}. Please send the code here.")
|
}
|
||||||
|
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)
|
@command_handler(needs_auth=False)
|
||||||
@@ -63,32 +77,23 @@ async def enter_code(evt):
|
|||||||
evt.sender.command_status = None
|
evt.sender.command_status = None
|
||||||
return await evt.reply(f"Successfully logged in as @{user.username}")
|
return await evt.reply(f"Successfully logged in as @{user.username}")
|
||||||
except PhoneNumberUnoccupiedError:
|
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>`.")
|
"Please register with `$cmdprefix+sp register <phone>`.")
|
||||||
except PhoneCodeExpiredError:
|
except PhoneCodeExpiredError:
|
||||||
return await evt.reply(
|
return await evt.reply(
|
||||||
"Phone code expired. Try again with `$cmdprefix+sp login <phone>`.")
|
"Phone code expired. Try again with `$cmdprefix+sp login <phone>`.")
|
||||||
except PhoneCodeInvalidError:
|
except PhoneCodeInvalidError:
|
||||||
return await evt.reply("Invalid phone code.")
|
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:
|
except SessionPasswordNeededError:
|
||||||
evt.sender.command_status = {
|
evt.sender.command_status = {
|
||||||
"next": enter_password,
|
"next": enter_password,
|
||||||
"action": "Login (password entry)",
|
"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.")
|
"Please send your password here.")
|
||||||
except Exception:
|
except Exception:
|
||||||
evt.log.exception("Error sending phone code")
|
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.")
|
"Check console for more details.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import logging
|
|||||||
|
|
||||||
from telethon.errors import FloodWaitError
|
from telethon.errors import FloodWaitError
|
||||||
|
|
||||||
|
from .util import format_duration
|
||||||
|
|
||||||
command_handlers = {}
|
command_handlers = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -65,24 +67,6 @@ class CommandEvent:
|
|||||||
return self.az.intent.send_notice(self.room_id, message, html=html)
|
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:
|
class CommandHandler:
|
||||||
log = logging.getLogger("mau.commands")
|
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>
|
||||||
@@ -44,4 +44,6 @@ setuptools.setup(
|
|||||||
[console_scripts]
|
[console_scripts]
|
||||||
mautrix-telegram=mautrix_telegram.__main__:main
|
mautrix-telegram=mautrix_telegram.__main__:main
|
||||||
""",
|
""",
|
||||||
|
package_data={"mautrix_telegram": ["public/*.html", "public/*.png", "public/*.css", "public/*.js"]},
|
||||||
|
data_files=[(".", "example-config.yaml")],
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user