Add verification endpoint for client.

Also fix http code for missing auth.
master
blallo 2019-08-17 22:23:32 -03:00 committed by blallo
parent 7d8a6c4075
commit 0fd0cde11c
Signed by: blallo
GPG Key ID: 0CBE577C9B72DC3F
2 changed files with 41 additions and 6 deletions

View File

@ -5,6 +5,8 @@ The REST endpoints.
""" """
from aiohttp import web from aiohttp import web
import asyncio
from concurrent.futures import ProcessPoolExecutor
import datetime import datetime
import logging import logging
import os import os
@ -12,8 +14,9 @@ import pkg_resources
import typing as T import typing as T
from aiohttp_session import get_session, Session 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.async_bot import login, logout, checkin, checkout, status
from api import BASE_URI, DEBUG from api import BASE_URI, DEBUG
@ -22,6 +25,28 @@ alog = logging.getLogger("api")
routes = web.RouteTableDef() routes = web.RouteTableDef()
OPERATORS = {} # type: T.Dict[T.Text, AsyncOperator] OPERATORS = {} # type: T.Dict[T.Text, AsyncOperator]
BASE_PATH = pkg_resources.resource_filename(__name__, "assets") 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( 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}) 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") @routes.post("/api/login")
async def login_handler(request: web.Request) -> web.Response: async def login_handler(request: web.Request) -> web.Response:
data = await request.json() data = await request.json()
@ -82,7 +115,7 @@ async def login_handler(request: web.Request) -> web.Response:
password = data.get("password") password = data.get("password")
if not user or not password: if not user or not password:
alog.debug("login - missing username or password: %s", data) 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) op, session = await get_set_operator(request, user, password)
alog.debug("login - user: %s, password: %s", user, password) alog.debug("login - user: %s, password: %s", user, password)
res = await login(op, 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) op = OPERATORS.get(op_key)
if not op: if not op:
return web.json_response( return web.json_response(
{"error": "No session", "logged_in": False}, status=403 {"error": "No session", "logged_in": False}, status=401
) )
res = await logout(op) res = await logout(op)
alog.debug("logout result: %s", res) 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")) op = OPERATORS.get(session.get("async_operator"))
if not op: if not op:
return web.json_response( return web.json_response(
{"error": "No session", "logged_in": False}, status=403 {"error": "No session", "logged_in": False}, status=401
) )
res = await checkin(op) res = await checkin(op)
alog.debug("checkin result: %s", res) 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")) op = OPERATORS.get(session.get("async_operator"))
if not op: if not op:
return web.json_response( return web.json_response(
{"error": "No session", "logged_in": False}, status=403 {"error": "No session", "logged_in": False}, status=401
) )
res = await checkout(op) res = await checkout(op)
alog.debug("checkout result: %s", res) 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")) op = OPERATORS.get(session.get("async_operator"))
if not op: if not op:
return web.json_response( return web.json_response(
{"error": "No session", "logged_in": False}, status=403 {"error": "No session", "logged_in": False}, status=401
) )
res = await status(op) res = await status(op)
alog.debug("movements result: %s", res) alog.debug("movements result: %s", res)

View File

@ -53,6 +53,8 @@ requirements = [
"aiohttp-session==2.7.0", "aiohttp-session==2.7.0",
"cryptography==2.7", "cryptography==2.7",
"PyYAML==5.1.2", "PyYAML==5.1.2",
"passlib>=1.7.1, <2.0.0",
"bcrypt==0.1.1",
] ]
setup_requirements = [] # type: T.List[str] setup_requirements = [] # type: T.List[str]