Add verification endpoint for client.
Also fix http code for missing auth.
This commit is contained in:
parent
7d8a6c4075
commit
0fd0cde11c
45
api/rest.py
45
api/rest.py
|
@ -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)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user