From 0fd0cde11ceb8a7a3dbd66974043b6e8e6bf8df3 Mon Sep 17 00:00:00 2001 From: Blallo Date: Sat, 17 Aug 2019 22:23:32 -0300 Subject: [PATCH] Add verification endpoint for client. Also fix http code for missing auth. --- api/rest.py | 45 +++++++++++++++++++++++++++++++++++++++------ setup.py | 2 ++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/api/rest.py b/api/rest.py index d1fef98..06b6375 100644 --- a/api/rest.py +++ b/api/rest.py @@ -5,6 +5,8 @@ The REST endpoints. """ from aiohttp import web +import asyncio +from concurrent.futures import ProcessPoolExecutor import datetime import logging import os @@ -12,8 +14,9 @@ import pkg_resources import typing as T from aiohttp_session import get_session, Session +from passlib.hash import bcrypt -from bot_z.async_operator import AsyncOperator +from bot_z.async_operator import AsyncOperator, push_to_loop from api.async_bot import login, logout, checkin, checkout, status from api import BASE_URI, DEBUG @@ -22,6 +25,28 @@ alog = logging.getLogger("api") routes = web.RouteTableDef() OPERATORS = {} # type: T.Dict[T.Text, AsyncOperator] BASE_PATH = pkg_resources.resource_filename(__name__, "assets") +EXECUTOR = ProcessPoolExecutor() +# WARN: the default il 12 rounds; both the server and the client shall compute +# this hash. The target client is a smartphone with poor performance on +# crypto computations, so we should keep this low, as the verification step +# is optionally enforced by the client. +ROUNDS = 6 + + +def _reckon_token_response(base_uri: T.Text) -> T.Text: + return bcrypt.using(rounds=ROUNDS, truncate_error=True).hash(base_uri) + + +async def reckon_token_response( + base_uri: T.Text, loop: asyncio.AbstractEventLoop +) -> T.Text: + """ + A client and the server should agree if the pairing is adequate. + This could be accomplished calculating on both sides an cryptographic + secret. The current implementation uses bcrypt to compute the hash on + server side and the client should verify the secret on its side. + """ + return await push_to_loop(loop, EXECUTOR, _reckon_token_response, base_uri) async def get_set_operator( @@ -75,6 +100,14 @@ async def routing_handler(request: web.Request) -> web.Response: return web.json_response({"logged_in": _logged_in}) +@routes.get("/api/ping") +async def ping_handler(request: web.Request) -> web.Response: + alog.debug("pinged on %s", request.path) + resp_data = await reckon_token_response(request.app["base_uri"], request.app.loop) + alog.debug("ping response: %s", resp_data) + return web.json_response({"hash": resp_data}) + + @routes.post("/api/login") async def login_handler(request: web.Request) -> web.Response: data = await request.json() @@ -82,7 +115,7 @@ async def login_handler(request: web.Request) -> web.Response: password = data.get("password") if not user or not password: alog.debug("login - missing username or password: %s", data) - return web.json_response({"error": "Missing username or password"}, status=403) + return web.json_response({"error": "Missing username or password"}, status=401) op, session = await get_set_operator(request, user, password) alog.debug("login - user: %s, password: %s", user, password) res = await login(op, user, password) @@ -101,7 +134,7 @@ async def logout_handler(request: web.Request) -> web.Response: op = OPERATORS.get(op_key) if not op: return web.json_response( - {"error": "No session", "logged_in": False}, status=403 + {"error": "No session", "logged_in": False}, status=401 ) res = await logout(op) alog.debug("logout result: %s", res) @@ -117,7 +150,7 @@ async def checkin_handler(request: web.Request) -> web.Response: op = OPERATORS.get(session.get("async_operator")) if not op: return web.json_response( - {"error": "No session", "logged_in": False}, status=403 + {"error": "No session", "logged_in": False}, status=401 ) res = await checkin(op) alog.debug("checkin result: %s", res) @@ -131,7 +164,7 @@ async def checkout_handler(request: web.Request) -> web.Response: op = OPERATORS.get(session.get("async_operator")) if not op: return web.json_response( - {"error": "No session", "logged_in": False}, status=403 + {"error": "No session", "logged_in": False}, status=401 ) res = await checkout(op) alog.debug("checkout result: %s", res) @@ -145,7 +178,7 @@ async def movements_handle(request: web.Request) -> web.Response: op = OPERATORS.get(session.get("async_operator")) if not op: return web.json_response( - {"error": "No session", "logged_in": False}, status=403 + {"error": "No session", "logged_in": False}, status=401 ) res = await status(op) alog.debug("movements result: %s", res) diff --git a/setup.py b/setup.py index 3592514..700736b 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,8 @@ requirements = [ "aiohttp-session==2.7.0", "cryptography==2.7", "PyYAML==5.1.2", + "passlib>=1.7.1, <2.0.0", + "bcrypt==0.1.1", ] setup_requirements = [] # type: T.List[str]