From ca85dc22659910dc44441ba590b6bf09216caa48 Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Sun, 14 Jun 2026 00:32:27 +0200 Subject: [PATCH] refactor: use shared ServiceLink blueprint - Replace the local /rpc Quart route with the shared ServiceLink blueprint helper. - Move mesh scope enforcement into bearer_verifier via require_scope. - Update ServiceLink tests to call the public handle_envelope and verify exports. - Advance the servicelink submodule to include the shared CLI and RPC discovery work. --- routes/api/link.py | 48 +++++++--------------------------- servicelink | 2 +- tests/test_servicelink_link.py | 6 ++--- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/routes/api/link.py b/routes/api/link.py index 4505cb6..a73a2f4 100644 --- a/routes/api/link.py +++ b/routes/api/link.py @@ -1,26 +1,23 @@ '''ServiceLink mesh endpoint for the picoshare/NanoShare node. Lets other nodes (browser-cli, website) push files in and read file metadata -over the shared servicelink envelope at POST /rpc. Sits alongside the existing -web UI and /api routes. +over the shared servicelink envelope at POST /rpc, alongside the existing web +UI and /api routes. -Security: every call needs a bearer token that decodes to the `mesh` scope -(or `all`); the endpoint is rate limited and body-size capped. Intended to be -reachable only from the internal node network (lock it down at the reverse -proxy), with the token as defence-in-depth. +Every call needs a bearer token carrying the `mesh` scope; the endpoint is rate +limited and body-size capped. Keep /rpc on the internal node network. ''' from __future__ import annotations import base64 -from quart import Blueprint, Response, current_app, request +from quart import current_app from my_modules.app.setup import LIMITER from my_modules.expiry import ensure_utc, parse_expires from my_modules.file_meta import format_size, iso_stamp_filename -from servicelink import Forbidden, InvalidParams, NotFound, Router, Unauthorized, bearer_verifier, handle_envelope +from servicelink import InvalidParams, NotFound, Router, Unauthorized, bearer_verifier, create_link_blueprint -# Cap the /rpc body. base64 inflates ~33%, so this bounds upload size too. MAX_RPC_BODY = 16 * 1024 * 1024 MESH_SCOPE = 'mesh' @@ -36,9 +33,6 @@ async def files_upload(params, ctx): user_id = _user_id(ctx) text = params.get('text') content_b64 = params.get('content_b64') - note = params.get('note', '') - expires_at = ensure_utc(parse_expires(params.get('expires', ''))) - if content_b64: data = base64.b64decode(content_b64) content_type = params.get('content_type') or 'application/octet-stream' @@ -55,9 +49,9 @@ async def files_upload(params, ctx): await current_app.convex.add_file( file_name=file_name, file_size=format_size(len(data)), - note=note, + note=params.get('note', ''), content_type=content_type, - expires_at=expires_at, + expires_at=ensure_utc(parse_expires(params.get('expires', ''))), storage_id=storage_id, user_id=user_id, ) @@ -113,27 +107,5 @@ async def _decode_access_token(token): raise ValueError((payload or {}).get('error', 'invalid token')) return payload -_base_verify = bearer_verifier(_decode_access_token) - -async def _verify(authorization, req): - principal = await _base_verify(authorization, req) - if principal is None or not principal.has_scope(MESH_SCOPE): - raise Forbidden(f'token lacks the {MESH_SCOPE!r} scope') - return principal - -link_bp = Blueprint('servicelink_picoshare', __name__) - -@link_bp.post('/rpc') -@LIMITER.limit('30 per minute') -async def rpc_endpoint(): - if (request.content_length or 0) > MAX_RPC_BODY: - return Response('{"error":"payload too large"}', status=413, content_type='application/json') - raw = await request.get_data() - status, body, content_type = await handle_envelope( - router, - raw, - authorization=request.headers.get('Authorization'), - verify=_verify, - content_type=request.headers.get('Content-Type', 'application/json'), - ) - return Response(body, status=status, content_type=content_type) +verify = bearer_verifier(_decode_access_token, require_scope=MESH_SCOPE) +link_bp = create_link_blueprint(router, verify=verify, limiter=LIMITER.limit('30 per minute'), max_body=MAX_RPC_BODY) diff --git a/servicelink b/servicelink index 946d79c..094bdc8 160000 --- a/servicelink +++ b/servicelink @@ -1 +1 @@ -Subproject commit 946d79c95da8762cfab8f2dbcaf10391e0cfbc05 +Subproject commit 094bdc8c569d2980d00a42b55039abd62254898f diff --git a/tests/test_servicelink_link.py b/tests/test_servicelink_link.py index e534c2d..9923adf 100644 --- a/tests/test_servicelink_link.py +++ b/tests/test_servicelink_link.py @@ -9,7 +9,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from quart import Quart -from servicelink import Request +from servicelink import Request, handle_envelope # Load routes/api/link.py in isolation. Importing it as `routes.api.link` would # run routes/__init__.py -> my_modules.app.setup -> constens (asyncio.run at @@ -63,11 +63,11 @@ def _call(envelope, token=None): app = Quart(__name__) app.convex = FakeConvex() async with app.test_request_context('/rpc', method='POST'): - status, body, _ = await link.handle_envelope( + status, body, _ = await handle_envelope( link.router, json.dumps(envelope).encode('utf-8'), authorization=f'Bearer {token}' if token else None, - verify=link._verify, + verify=link.verify, content_type='application/json', ) return status, json.loads(body)