From 8acd4578e29e494bc7724417a1f2683e803a09f7 Mon Sep 17 00:00:00 2001 From: MingxuanGame Date: Mon, 11 Aug 2025 14:41:07 +0000 Subject: [PATCH] feat(private): initialize private API --- .env.example | 2 ++ README.md | 1 + app/config.py | 1 + app/router/__init__.py | 9 +++++++- app/router/private/__init__.py | 7 ++++++ app/router/private/router.py | 39 ++++++++++++++++++++++++++++++++++ main.py | 7 ++++++ 7 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 app/router/private/__init__.py create mode 100644 app/router/private/router.py diff --git a/.env.example b/.env.example index a713dbb..cec6fbd 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,8 @@ HOST="0.0.0.0" PORT=8000 # 调试模式,生产环境请设置为 false DEBUG=false +# 私有 API 密钥,用于前后端 API 调用,使用 openssl rand -hex 32 生成 +PRIVATE_API_SECRET="your_private_api_secret_here" # osu! 登录设置 OSU_CLIENT_ID=5 # lazer client ID diff --git a/README.md b/README.md index 4bf5445..927767a 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ docker-compose -f docker-compose-osurx.yml up -d | `HOST` | 服务器监听地址 | `0.0.0.0` | | `PORT` | 服务器监听端口 | `8000` | | `DEBUG` | 调试模式 | `false` | +| `PRIVATE_API_SECRET` | 私有 API 密钥,用于前后端 API 调用 | `your_private_api_secret_here` | ### OAuth 设置 | 变量名 | 描述 | 默认值 | diff --git a/app/config.py b/app/config.py index 004971d..37cef2d 100644 --- a/app/config.py +++ b/app/config.py @@ -37,6 +37,7 @@ class Settings(BaseSettings): host: str = "0.0.0.0" port: int = 8000 debug: bool = False + private_api_secret: str = "your_private_api_secret_here" # SignalR 设置 signalr_negotiate_timeout: int = 30 diff --git a/app/router/__init__.py b/app/router/__init__.py index 4a4fb9b..b8d722b 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -4,6 +4,13 @@ from app.signalr import signalr_router as signalr_router from .auth import router as auth_router from .fetcher import fetcher_router as fetcher_router +from .private import private_router as private_router from .v2 import api_v2_router as api_v2_router -__all__ = ["api_v2_router", "auth_router", "fetcher_router", "signalr_router"] +__all__ = [ + "api_v2_router", + "auth_router", + "fetcher_router", + "private_router", + "signalr_router", +] diff --git a/app/router/private/__init__.py b/app/router/private/__init__.py new file mode 100644 index 0000000..4e7fb7a --- /dev/null +++ b/app/router/private/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .router import router as private_router + +__all__ = [ + "private_router", +] diff --git a/app/router/private/router.py b/app/router/private/router.py new file mode 100644 index 0000000..1d3a6e4 --- /dev/null +++ b/app/router/private/router.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import hashlib +import hmac +import time + +from app.config import settings + +from fastapi import APIRouter, Depends, Header, HTTPException, Request + + +async def verify_signature( + request: Request, + ts: int = Header(..., alias="X-Timestamp"), + nonce: str = Header(..., alias="X-Nonce"), + signature: str = Header(..., alias="X-Signature"), +): + path = request.url.path + data = await request.body() + body = data.decode("utf-8") + + py_ts = ts // 1000 + if abs(time.time() - py_ts) > 30: + raise HTTPException(status_code=403, detail="Invalid timestamp") + + payload = f"{path}|{body}|{ts}|{nonce}" + expected_sig = hmac.new( + settings.private_api_secret.encode(), payload.encode(), hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(expected_sig, signature): + raise HTTPException(status_code=403, detail="Invalid signature") + + +router = APIRouter( + prefix="/api/private", + dependencies=[Depends(verify_signature)], + include_in_schema=False, +) diff --git a/main.py b/main.py index 774a7b9..5c56eb0 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ from app.router import ( api_v2_router, auth_router, fetcher_router, + private_router, signalr_router, ) from app.service.daily_challenge import daily_challenge_job @@ -39,6 +40,7 @@ app.include_router(api_v2_router) app.include_router(signalr_router) app.include_router(fetcher_router) app.include_router(auth_router) +app.include_router(private_router) @app.get("/") @@ -63,6 +65,11 @@ if settings.osu_web_client_secret == "your_osu_web_client_secret_here": "osu_web_client_secret is unset. Your server is unsafe. " "Use this command to generate: openssl rand -hex 40" ) +if settings.private_api_secret == "your_private_api_secret_here": + logger.warning( + "private_api_secret is unset. Your server is unsafe. " + "Use this command to generate: openssl rand -hex 32" + ) if __name__ == "__main__": import uvicorn