Skip to main content

futu_rest/server/
startup.rs

1use std::sync::Arc;
2
3use futu_auth::{KeyStore, RuntimeCounters};
4use futu_server::router::RequestRouter;
5use tokio::sync::watch;
6
7use crate::ws::WsBroadcaster;
8
9use super::{LEGACY_WS_WARN_MESSAGE, RestAdminHooks, build_router_with_auth_full_admin};
10
11/// 启动 REST API 服务,挂载 KeyStore 做 Bearer Token 鉴权 +
12/// RuntimeCounters 做限额。
13pub async fn start_with_auth(
14    listen_addr: &str,
15    router: Arc<RequestRouter>,
16    ws_broadcaster: Arc<WsBroadcaster>,
17    key_store: Arc<KeyStore>,
18    counters: Arc<RuntimeCounters>,
19) -> std::io::Result<()> {
20    start_with_auth_and_admin(
21        listen_addr,
22        router,
23        ws_broadcaster,
24        key_store,
25        counters,
26        None,
27    )
28    .await
29}
30
31/// v1.4.32+ 同 `start_with_auth`,但额外接 admin_status_provider。
32/// 让 `/api/admin/status` 能返回实时健康快照。
33/// `push_health_snapshot_provider` 仍需走 `start_with_auth_full_admin` 注入;
34/// 未注入时 `/api/push-subscriber-info` loud-fail 503。
35pub async fn start_with_auth_and_admin(
36    listen_addr: &str,
37    router: Arc<RequestRouter>,
38    ws_broadcaster: Arc<WsBroadcaster>,
39    key_store: Arc<KeyStore>,
40    counters: Arc<RuntimeCounters>,
41    admin_status_provider: Option<crate::adapter::AdminStatusProvider>,
42) -> std::io::Result<()> {
43    start_with_auth_full_admin(
44        listen_addr,
45        router,
46        ws_broadcaster,
47        key_store,
48        counters,
49        RestAdminHooks {
50            admin_status_provider,
51            ..RestAdminHooks::default()
52        },
53    )
54    .await
55}
56
57/// v1.4.32+ 完整 admin 入口:同时接 status provider + reload handler。
58///
59/// v1.4.83 §9 Phase 2 F5: 加 `push_health_snapshot_provider` 参数支持
60/// `/api/push-subscriber-info` 返真实 push 通道健康 state。
61pub async fn start_with_auth_full_admin(
62    listen_addr: &str,
63    router: Arc<RequestRouter>,
64    ws_broadcaster: Arc<WsBroadcaster>,
65    key_store: Arc<KeyStore>,
66    counters: Arc<RuntimeCounters>,
67    hooks: RestAdminHooks,
68) -> std::io::Result<()> {
69    let scope_mode = key_store.is_configured();
70    let app = build_router_with_auth_full_admin(router, ws_broadcaster, key_store, counters, hooks);
71    let listener = tokio::net::TcpListener::bind(listen_addr)
72        .await
73        .map_err(|error| {
74            futu_server::bind_hint::io_bind_error("REST", "--rest-port", listen_addr, error)
75        })?;
76    tracing::info!(
77        addr = %listen_addr,
78        scope_mode,
79        "REST API 服务已启动 (WebSocket: /ws)"
80    );
81    if !scope_mode {
82        warn_legacy_mode(listen_addr);
83    }
84    axum::serve(
85        listener,
86        app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
87    )
88    .await
89}
90
91/// 同 [`start_with_auth_full_admin`],但支持 daemon 统一 shutdown 信号。
92pub async fn start_with_auth_full_admin_until_shutdown(
93    listen_addr: &str,
94    router: Arc<RequestRouter>,
95    ws_broadcaster: Arc<WsBroadcaster>,
96    key_store: Arc<KeyStore>,
97    counters: Arc<RuntimeCounters>,
98    hooks: RestAdminHooks,
99    shutdown_rx: watch::Receiver<bool>,
100) -> std::io::Result<()> {
101    let scope_mode = key_store.is_configured();
102    let app = build_router_with_auth_full_admin(router, ws_broadcaster, key_store, counters, hooks);
103    let listener = tokio::net::TcpListener::bind(listen_addr)
104        .await
105        .map_err(|error| {
106            futu_server::bind_hint::io_bind_error("REST", "--rest-port", listen_addr, error)
107        })?;
108    tracing::info!(
109        addr = %listen_addr,
110        scope_mode,
111        "REST API 服务已启动 (WebSocket: /ws)"
112    );
113    if !scope_mode {
114        warn_legacy_mode(listen_addr);
115    }
116    axum::serve(
117        listener,
118        app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
119    )
120    .with_graceful_shutdown(rest_shutdown_requested(shutdown_rx))
121    .await
122}
123
124fn warn_legacy_mode(listen_addr: &str) {
125    tracing::warn!("REST API in legacy mode (no keys.json); mutating endpoints are blocked");
126    // v1.4.93 P0-5 (NEW-C-02): WS 也对齐 mutating-blocked policy 的"loud
127    // unauth"信号 — REST `/ws` route 在 legacy 模式接受 unauthenticated
128    // handshake (no-token / wrong-bearer / bogus-query 都 HTTP 101),未授权
129    // 客户端可接收 live push。本版不 reject(保持向后兼容,未来 major 版
130    // 默认 reject),但补 startup loud WARN + CHANGELOG 公告。
131    tracing::warn!("{}", LEGACY_WS_WARN_MESSAGE);
132    // v1.4.86 SEC-003 Q4 真 fix: legacy mode 下 mutating endpoint 硬门禁
133    // (/api/order / modify-order / cancel-order / cancel-all-order / unlock-trade /
134    // reconfirm-order / admin/*). 只读 endpoint 继续 legacy 允许.
135    tracing::warn!(
136        listen_addr = %listen_addr,
137        readonly_endpoints = "qot/account/order-read",
138        blocked_mutating_endpoints = "/api/order,/api/modify-order,/api/cancel-order,/api/cancel-all-order,/api/unlock-trade,/api/reconfirm-order,/api/admin/*",
139        ws_legacy_unauthenticated = true,
140        migration = "futucli gen-key --id my-key --scopes qot:read,acc:read,trade:real; restart with --rest-keys-file /path/to/keys.json",
141        "REST API legacy mode: no keys.json configured; read endpoints remain unauthenticated, mutating/admin endpoints return 401, /ws still accepts unauthenticated connections for compatibility and v2 will default-reject"
142    );
143}
144
145async fn rest_shutdown_requested(mut shutdown_rx: watch::Receiver<bool>) {
146    loop {
147        if *shutdown_rx.borrow() {
148            tracing::info!("REST API server stopped by shutdown signal");
149            return;
150        }
151        if shutdown_rx.changed().await.is_err() {
152            tracing::info!("REST API server stopped after shutdown sender dropped");
153            return;
154        }
155    }
156}