Add basic websocket login API
This commit is contained in:
parent
e563ae6edf
commit
684e5bcf38
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
headers=self._headers)
|
if not token:
|
||||||
|
raise web.HTTPBadRequest(text='{"error": "Missing Authorization header"}',
|
||||||
|
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)
|
|
||||||
|
|
|
@ -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"
|
||||||
this.log("Clicking Remember Me button")
|
if (!await this.page.$("mat-slide-toggle.mat-checked")) {
|
||||||
await this.page.click("mat-slide-toggle:not(.mat-checked) > label")
|
this.log("Clicking Remember Me button")
|
||||||
|
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"))
|
||||||
|
|
Loading…
Reference in New Issue