Add basic websocket login API

This commit is contained in:
Tulir Asokan 2020-12-29 16:33:33 +02:00
parent e563ae6edf
commit 684e5bcf38
3 changed files with 51 additions and 39 deletions

View File

@ -54,7 +54,6 @@ class User(DBUser, BaseUser):
self._metric_value = defaultdict(lambda: False) self._metric_value = defaultdict(lambda: False)
self._connection_check_task = None self._connection_check_task = None
self.client = None self.client = None
self.username = None
self.intent = None self.intent = None
@classmethod @classmethod

View File

@ -13,9 +13,9 @@
# #
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Awaitable, Dict from typing import Awaitable, Dict, Optional
import logging import logging
import json import asyncio
from aiohttp import web from aiohttp import web
@ -33,16 +33,14 @@ class ProvisioningAPI:
self.app = web.Application() self.app = web.Application()
self.shared_secret = shared_secret self.shared_secret = shared_secret
self.app.router.add_get("/api/whoami", self.status) self.app.router.add_get("/api/whoami", self.status)
self.app.router.add_options("/api/login", self.login_options) self.app.router.add_get("/api/login", self.login)
self.app.router.add_post("/api/login", self.login)
self.app.router.add_post("/api/logout", self.logout)
@property @property
def _acao_headers(self) -> Dict[str, str]: def _acao_headers(self) -> Dict[str, str]:
return { return {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Authorization, Content-Type", "Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Methods": "GET",
} }
@property @property
@ -55,22 +53,39 @@ class ProvisioningAPI:
async def login_options(self, _: web.Request) -> web.Response: async def login_options(self, _: web.Request) -> web.Response:
return web.Response(status=200, headers=self._headers) return web.Response(status=200, headers=self._headers)
@staticmethod
def _get_ws_token(request: web.Request) -> Optional[str]:
if not request.path.endswith("/login"):
return None
try:
auth_parts = request.headers["Sec-WebSocket-Protocol"].split(",")
except KeyError:
return None
for part in auth_parts:
part = part.strip()
if part.startswith("net.maunium.amp.auth-"):
return part[len("net.maunium.amp.auth-"):]
return None
def check_token(self, request: web.Request) -> Awaitable['u.User']: def check_token(self, request: web.Request) -> Awaitable['u.User']:
try: try:
token = request.headers["Authorization"] token = request.headers["Authorization"]
token = token[len("Bearer "):] token = token[len("Bearer "):]
except KeyError: except KeyError:
raise web.HTTPBadRequest(body='{"error": "Missing Authorization header"}', token = self._get_ws_token(request)
if not token:
raise web.HTTPBadRequest(text='{"error": "Missing Authorization header"}',
headers=self._headers) headers=self._headers)
except IndexError: except IndexError:
raise web.HTTPBadRequest(body='{"error": "Malformed Authorization header"}', raise web.HTTPBadRequest(text='{"error": "Malformed Authorization header"}',
headers=self._headers) headers=self._headers)
if token != self.shared_secret: if token != self.shared_secret:
raise web.HTTPForbidden(body='{"error": "Invalid token"}', headers=self._headers) raise web.HTTPForbidden(text='{"error": "Invalid token"}', headers=self._headers)
try: try:
user_id = request.query["user_id"] user_id = request.query["user_id"]
except KeyError: except KeyError:
raise web.HTTPBadRequest(body='{"error": "Missing user_id query param"}', raise web.HTTPBadRequest(text='{"error": "Missing user_id query param"}',
headers=self._headers) headers=self._headers)
return u.User.get_by_mxid(UserID(user_id)) return u.User.get_by_mxid(UserID(user_id))
@ -78,37 +93,31 @@ class ProvisioningAPI:
async def status(self, request: web.Request) -> web.Response: async def status(self, request: web.Request) -> web.Response:
user = await self.check_token(request) user = await self.check_token(request)
data = { data = {
"permissions": user.permission_level,
"mxid": user.mxid, "mxid": user.mxid,
"twitter": None, "amp": {
"connected": True,
} if await user.is_logged_in() else None,
} }
if await user.is_logged_in():
data["twitter"] = (await user.get_info()).serialize()
return web.json_response(data, headers=self._acao_headers) return web.json_response(data, headers=self._acao_headers)
async def login(self, request: web.Request) -> web.Response: async def login(self, request: web.Request) -> web.WebSocketResponse:
user = await self.check_token(request) user = await self.check_token(request)
try: status = await user.client.start()
data = await request.json() if status.is_logged_in:
except json.JSONDecodeError: raise web.HTTPConflict(text='{"error": "Already logged in"}', headers=self._headers)
raise web.HTTPBadRequest(body='{"error": "Malformed JSON"}', headers=self._headers)
ws = web.WebSocketResponse(protocols=["net.maunium.amp.login"])
await ws.prepare(request)
try: try:
auth_token = data["auth_token"] async for url in user.client.login():
csrf_token = data["csrf_token"] self.log.debug("Sending QR URL %s to websocket", url)
except KeyError: await ws.send_json({"url": url})
raise web.HTTPBadRequest(body='{"error": "Missing keys"}', headers=self._headers)
try:
await user.connect(auth_token=auth_token, csrf_token=csrf_token)
except Exception: except Exception:
self.log.debug("Failed to log in", exc_info=True) await ws.send_json({"success": False})
raise web.HTTPUnauthorized(body='{"error": "Twitter authorization failed"}', self.log.exception("Error logging in")
headers=self._headers) else:
return web.Response(body='{}', status=200, headers=self._headers) await ws.send_json({"success": True})
asyncio.create_task(user.sync())
async def logout(self, request: web.Request) -> web.Response: await ws.close()
user = await self.check_token(request) return ws
await user.logout()
return web.json_response({}, headers=self._acao_headers)

View File

@ -115,8 +115,12 @@ export default class MessagesPuppeteer {
return return
} }
const qrSelector = "mw-authentication-container mw-qr-code" const qrSelector = "mw-authentication-container mw-qr-code"
if (!await this.page.$("mat-slide-toggle.mat-checked")) {
this.log("Clicking Remember Me button") this.log("Clicking Remember Me button")
await this.page.click("mat-slide-toggle:not(.mat-checked) > label") await this.page.click("mat-slide-toggle:not(.mat-checked) > label")
} else {
this.log("Remember Me button already clicked")
}
this.log("Fetching current QR code") this.log("Fetching current QR code")
const currentQR = await this.page.$eval(qrSelector, const currentQR = await this.page.$eval(qrSelector,
element => element.getAttribute("data-qr-code")) element => element.getAttribute("data-qr-code"))