Implement Matrix login with web interface
This commit is contained in:
@@ -23,6 +23,8 @@ from telethon.errors import *
|
||||
|
||||
from ...commands.auth import enter_password
|
||||
from ...util import format_duration
|
||||
from ...puppet import Puppet
|
||||
from ...user import User
|
||||
|
||||
|
||||
class AuthAPI(abc.ABC):
|
||||
@@ -36,6 +38,32 @@ class AuthAPI(abc.ABC):
|
||||
errcode=""):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def get_mx_login_response(self, status=200, state="", username="", mxid="", message="",
|
||||
error="", errcode=""):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def post_matrix_token(self, user: User, token):
|
||||
puppet = Puppet.get(user.tgid)
|
||||
if puppet.is_real_user:
|
||||
return self.get_mx_login_response(state="already-logged-in", status=409,
|
||||
error="You have already logged in with your Matrix "
|
||||
"account.", errcode="already-logged-in")
|
||||
|
||||
resp = await puppet.switch_mxid(token, user.mxid)
|
||||
if resp == 2:
|
||||
return self.get_mx_login_response(status=403, errcode="only-login-self",
|
||||
error="You can only log in as your own Matrix user.")
|
||||
elif resp == 1:
|
||||
return self.get_mx_login_response(status=401, errcode="invalid-access-token",
|
||||
error="Failed to verify access token.")
|
||||
|
||||
return self.get_mx_login_response(mxid=user.mxid, status=200, state="logged-in")
|
||||
|
||||
async def post_matrix_password(self, user, password):
|
||||
return self.get_mx_login_response(mxid=user.mxid, status=501, error="Not yet implemented",
|
||||
errcode="not-yet-implemented")
|
||||
|
||||
async def post_login_phone(self, user, phone):
|
||||
try:
|
||||
await user.client.sign_in(phone or "+123")
|
||||
|
||||
@@ -297,6 +297,10 @@ class ProvisioningAPI(AuthAPI):
|
||||
"errcode": errcode,
|
||||
}, status=status)
|
||||
|
||||
def get_mx_login_response(self, status=200, state="", username="", mxid="", message="",
|
||||
error="", errcode=""):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
|
||||
errcode="") -> web.Response:
|
||||
if username:
|
||||
|
||||
@@ -24,6 +24,7 @@ import time
|
||||
|
||||
from ...util import sign_token, verify_token
|
||||
from ...user import User
|
||||
from ...puppet import Puppet
|
||||
from ..common import AuthAPI
|
||||
|
||||
|
||||
@@ -38,28 +39,35 @@ class PublicBridgeWebsite(AuthAPI):
|
||||
self.login = Template(
|
||||
pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako"))
|
||||
|
||||
self.mx_login = Template(
|
||||
pkg_resources.resource_string("mautrix_telegram", "web/public/matrix-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_route("GET", "/matrix-login", self.get_matrix_login)
|
||||
self.app.router.add_route("POST", "/matrix-login", self.post_matrix_login)
|
||||
self.app.router.add_static("/", pkg_resources.resource_filename("mautrix_telegram",
|
||||
"web/public/"))
|
||||
|
||||
def make_token(self, mxid, expires_in=900):
|
||||
def make_token(self, mxid, endpoint="/login", expires_in=900):
|
||||
return sign_token(self.secret_key, {
|
||||
"mxid": mxid,
|
||||
"endpoint": endpoint,
|
||||
"expiry": int(time.time()) + expires_in,
|
||||
})
|
||||
|
||||
def verify_token(self, token):
|
||||
def verify_token(self, token, endpoint="/login"):
|
||||
token = verify_token(self.secret_key, token)
|
||||
if token and token.get("expiry", 0) > int(time.time()):
|
||||
if token and (token.get("expiry", 0) > int(time.time()) and
|
||||
token.get("endpoint", None) == endpoint):
|
||||
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))
|
||||
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/login")
|
||||
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
|
||||
@@ -75,14 +83,65 @@ class PublicBridgeWebsite(AuthAPI):
|
||||
|
||||
return self.get_login_response(mxid=user.mxid, username=user.username)
|
||||
|
||||
async def get_matrix_login(self, request):
|
||||
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/matrix-login")
|
||||
if not mxid:
|
||||
return self.get_mx_login_response(status=401, state="invalid-token")
|
||||
user = User.get_by_mxid(mxid, create=False) if mxid else None
|
||||
|
||||
if not user:
|
||||
return self.get_mx_login_response(mxid=mxid)
|
||||
elif not user.puppet_whitelisted:
|
||||
return self.get_mx_login_response(mxid=user.mxid, error="You are not whitelisted.",
|
||||
status=403)
|
||||
await user.ensure_started()
|
||||
if not await user.is_logged_in():
|
||||
return self.get_mx_login_response(mxid=user.mxid, status=403,
|
||||
error="You are not logged in to Telegram.")
|
||||
|
||||
puppet = Puppet.get(user.tgid)
|
||||
if puppet.is_real_user:
|
||||
return self.get_mx_login_response(state="already-logged-in", status=409)
|
||||
|
||||
return self.get_mx_login_response(mxid=user.mxid)
|
||||
|
||||
def get_login_response(self, status=200, state="", username="", mxid="", message="", error="",
|
||||
errcode=""):
|
||||
return web.Response(status=status, content_type="text/html",
|
||||
text=self.login.render(username=username, state=state, error=error,
|
||||
message=message, mxid=mxid))
|
||||
|
||||
def get_mx_login_response(self, status=200, state="", username="", mxid="", message="",
|
||||
error="", errcode=""):
|
||||
return web.Response(status=status, content_type="text/html",
|
||||
text=self.mx_login.render(username=username, state=state, error=error,
|
||||
message=message, mxid=mxid))
|
||||
|
||||
async def post_matrix_login(self, request):
|
||||
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/matrix-login")
|
||||
if not mxid:
|
||||
return self.get_mx_login_response(status=401, state="invalid-token")
|
||||
|
||||
data = await request.post()
|
||||
|
||||
user = await User.get_by_mxid(mxid).ensure_started()
|
||||
if not user.puppet_whitelisted:
|
||||
return self.get_mx_login_response(mxid=user.mxid, error="You are not whitelisted.",
|
||||
status=403)
|
||||
elif not await user.is_logged_in():
|
||||
return self.get_mx_login_response(mxid=user.mxid, status=403,
|
||||
error="You are not logged in to Telegram.")
|
||||
mode = data.get("mode", "access_token")
|
||||
if mode == "password":
|
||||
return await self.post_matrix_password(user, data["value"])
|
||||
elif mode == "access_token":
|
||||
return await self.post_matrix_token(user, data["value"])
|
||||
return self.get_mx_login_response(mxid=user.mxid, status=400,
|
||||
error="You must provide an access token or "
|
||||
"password.")
|
||||
|
||||
async def post_login(self, request):
|
||||
mxid = self.verify_token(request.rel_url.query.get("token", None))
|
||||
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/login")
|
||||
if not mxid:
|
||||
return self.get_login_response(status=401, state="invalid-token")
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ form > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
form[data-status="request"] > div.status-request,
|
||||
form[data-status="code"] > div.status-code,
|
||||
form[data-status="request"] > div.status-request,
|
||||
form[data-status="code"] > div.status-code,
|
||||
form[data-status="password"] > div.status-password {
|
||||
display: initial;
|
||||
}
|
||||
@@ -48,3 +48,52 @@ form[data-status="password"] > div.status-password {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
[type="checkbox"], [type="radio"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[type="checkbox"] + label, [type="radio"] + label {
|
||||
position: relative;
|
||||
padding-left: 2.5rem;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
[type="checkbox"] + label:before, [type="radio"] + label:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.4rem;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border: 0.1rem solid #d1d1d1;
|
||||
}
|
||||
|
||||
[type="radio"] + label:before, [type="radio"] + label:after {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
[type="checkbox"]:checked + label:after,
|
||||
[type="radio"]:checked + label:after {
|
||||
content: '';
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
background: #9b4dca;
|
||||
position: absolute;
|
||||
top: 0.9rem;
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
[type="radio"]:disabled + label:before, [type="checkbox"]:disabled + label:before {
|
||||
background-color: #d1d1d1;
|
||||
}
|
||||
|
||||
[type="radio"]:disabled + label, [type="checkbox"]:disabled + label {
|
||||
color: #d1d1d1;
|
||||
}
|
||||
|
||||
[type="radio"]:disabled:checked + label:after, [type="checkbox"]:disabled:checked + label:after {
|
||||
background: #606c76;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Mautrix-Telegram bridge</title>
|
||||
<title>Login - Mautrix-Telegram bridge</title>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<meta property="og:title" content="Mautrix-Telegram bridge">
|
||||
<meta property="og:title" content="Login - 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">
|
||||
@@ -40,10 +40,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
function goBack() {
|
||||
let params = new URLSearchParams(location.search.slice(1))
|
||||
const mxid = params.get("mxid")
|
||||
const token = params.get("token")
|
||||
params = new URLSearchParams()
|
||||
if (mxid) {
|
||||
params.set("mxid", mxid)
|
||||
if (token) {
|
||||
params.set("token", token)
|
||||
}
|
||||
location.replace(location.href.split("?")[0] + "?" + params.toString())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Matrix login - Mautrix-Telegram bridge</title>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<meta property="og:title" content="Matrix login - 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="//fonts.googleapis.com/css?family=Roboto:300,700">
|
||||
<link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">
|
||||
<link rel="stylesheet"
|
||||
href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
|
||||
<link rel="stylesheet" href="login.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
% if state == "logged-in":
|
||||
<h1>Logged in successfully!</h1>
|
||||
<p>
|
||||
Logged in as ${mxid}.
|
||||
You can now close this page.
|
||||
</p>
|
||||
% elif state == "already-logged-in":
|
||||
<h1>You're already logged in!</h1>
|
||||
<p>
|
||||
If you want to log in with another account, log out using the
|
||||
<code>logout-matrix</code> management command first.
|
||||
</p>
|
||||
% 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 Matrix</h1>
|
||||
% if error:
|
||||
<div class="error">${error}</div>
|
||||
% endif
|
||||
% if message:
|
||||
<div class="message">${message}</div>
|
||||
% endif
|
||||
<form method="post">
|
||||
<fieldset>
|
||||
<label for="mxid">Matrix ID</label>
|
||||
<input type="text" id="mxid" name="mxid" disabled value="${mxid}"/>
|
||||
|
||||
<input id="access_token" type="radio" name="mode" value="access_token" checked>
|
||||
<label for="access_token">Access token</label><br>
|
||||
<input id="password" type="radio" name="mode" value="password">
|
||||
<label for="password">Password</label><br>
|
||||
|
||||
<label for="value">Value</label>
|
||||
<input type="text" id="value" name="value"
|
||||
placeholder="Enter Matrix access token or password"/>
|
||||
|
||||
<button type="submit">Sign in</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
% endif
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user