futu_rest/
server.rs

1//! REST API HTTP 服务
2//!
3//! 使用 axum 构建,复用 OpenD 的 RequestRouter 处理请求。
4//! 支持 WebSocket 推送: 客户端连接 /ws 可接收实时行情和交易推送。
5
6use std::sync::Arc;
7
8use axum::response::IntoResponse;
9use axum::routing::{get, post};
10use axum::Router;
11use futu_auth::{KeyStore, RuntimeCounters};
12use tower_http::cors::{Any, CorsLayer};
13
14use futu_server::router::RequestRouter;
15
16use crate::adapter::RestState;
17use crate::auth::{bearer_auth, AuthState};
18use crate::routes::{qot, sys, trd};
19use crate::ws::{self, WsBroadcaster};
20
21/// Prometheus `/metrics` handler —— 放在 bearer_auth middleware 之外
22///
23/// 返回全局 `futu_auth::metrics::Registry` 的文本 exposition;未 install registry
24/// 时返回空 body(404 会让运维以为路由不存在,反而误导)。
25async fn metrics_handler() -> impl IntoResponse {
26    let body = futu_auth::metrics::global()
27        .map(|r| r.render_prometheus())
28        .unwrap_or_default();
29    (
30        axum::http::StatusCode::OK,
31        [(
32            axum::http::header::CONTENT_TYPE,
33            "text/plain; version=0.0.4",
34        )],
35        body,
36    )
37}
38
39/// `/health` liveness probe handler —— 200 OK + body "ok"
40///
41/// 故意做得很轻:只要 axum 还能 schedule 任务返回响应就算"alive"。**不**检查
42/// 网关连接 / DB / 下游依赖(那是 readiness 的活,运维真要可以另写一个
43/// `/ready`)。LB / k8s liveness probe 直接打这个端点,bind_auth 不拦。
44async fn health_handler() -> impl IntoResponse {
45    (axum::http::StatusCode::OK, "ok")
46}
47
48/// 构建 REST API 路由(无鉴权,向后兼容)
49pub fn build_router(router: Arc<RequestRouter>, ws_broadcaster: Arc<WsBroadcaster>) -> Router {
50    build_router_with_auth(
51        router,
52        ws_broadcaster,
53        Arc::new(KeyStore::empty()),
54        Arc::new(RuntimeCounters::new()),
55    )
56}
57
58/// 构建 REST API 路由,携带 KeyStore 做 Bearer Token 鉴权 + RuntimeCounters 做限额
59///
60/// `key_store.is_configured() == false` 时等价于 `build_router`(保持旧行为)。
61/// `counters` 应由 main 全进程共享:REST / gRPC / MCP 共用一个实例才能保证
62/// rate limit / 日累计跨接口一致
63pub fn build_router_with_auth(
64    router: Arc<RequestRouter>,
65    ws_broadcaster: Arc<WsBroadcaster>,
66    key_store: Arc<KeyStore>,
67    counters: Arc<RuntimeCounters>,
68) -> Router {
69    let state = RestState::with_auth(
70        router,
71        ws_broadcaster,
72        Arc::clone(&key_store),
73        Arc::clone(&counters),
74    );
75    let auth_state = AuthState::new(Arc::clone(&key_store), Arc::clone(&counters));
76
77    let cors = CorsLayer::new()
78        .allow_origin(Any)
79        .allow_methods(Any)
80        .allow_headers(Any);
81
82    Router::new()
83        // ── WebSocket 推送 ──
84        .route("/ws", get(ws::ws_handler))
85        // ── 系统 ──
86        .route("/api/global-state", get(sys::get_global_state))
87        .route("/api/user-info", get(sys::get_user_info))
88        .route("/api/delay-statistics", get(sys::get_delay_statistics))
89        // ── 行情 ──
90        .route("/api/subscribe", post(qot::subscribe))
91        .route("/api/sub-info", get(qot::get_sub_info))
92        .route("/api/quote", post(qot::get_basic_qot))
93        .route("/api/kline", post(qot::get_kl))
94        .route("/api/orderbook", post(qot::get_order_book))
95        .route("/api/broker", post(qot::get_broker))
96        .route("/api/ticker", post(qot::get_ticker))
97        .route("/api/rt", post(qot::get_rt))
98        .route("/api/snapshot", post(qot::get_snapshot))
99        .route("/api/static-info", post(qot::get_static_info))
100        .route("/api/plate-set", post(qot::get_plate_set))
101        .route("/api/plate-security", post(qot::get_plate_security))
102        .route("/api/reference", post(qot::get_reference))
103        .route("/api/owner-plate", post(qot::get_owner_plate))
104        .route("/api/option-chain", post(qot::get_option_chain))
105        .route("/api/warrant", post(qot::get_warrant))
106        .route("/api/capital-flow", post(qot::get_capital_flow))
107        .route(
108            "/api/capital-distribution",
109            post(qot::get_capital_distribution),
110        )
111        .route("/api/user-security", post(qot::get_user_security))
112        .route("/api/stock-filter", post(qot::stock_filter))
113        .route("/api/ipo-list", post(qot::get_ipo_list))
114        .route("/api/future-info", post(qot::get_future_info))
115        .route("/api/market-state", post(qot::get_market_state))
116        .route("/api/history-kline", post(qot::request_history_kl))
117        // ── 交易 ──
118        .route("/api/accounts", get(trd::get_acc_list))
119        .route("/api/unlock-trade", post(trd::unlock_trade))
120        .route("/api/sub-acc-push", post(trd::sub_acc_push))
121        .route("/api/funds", post(trd::get_funds))
122        .route("/api/positions", post(trd::get_positions))
123        .route("/api/orders", post(trd::get_orders))
124        .route("/api/order", post(trd::place_order))
125        .route("/api/modify-order", post(trd::modify_order))
126        .route("/api/order-fills", post(trd::get_order_fills))
127        .route("/api/max-trd-qtys", post(trd::get_max_trd_qtys))
128        .route("/api/history-orders", post(trd::get_history_orders))
129        .route(
130            "/api/history-order-fills",
131            post(trd::get_history_order_fills),
132        )
133        .route("/api/margin-ratio", post(trd::get_margin_ratio))
134        .route("/api/order-fee", post(trd::get_order_fee))
135        .layer(axum::middleware::from_fn_with_state(
136            auth_state,
137            bearer_auth,
138        ))
139        // `/metrics` + `/health` 都在 bearer_auth 之外(middleware 只过 /api/*)
140        //   - `/metrics`:Prometheus 抓取(无需 token,运维用 firewall 控访问)
141        //   - `/health`:load balancer / k8s liveness probe(200 OK + body "ok")
142        .route("/metrics", get(metrics_handler))
143        .route("/health", get(health_handler))
144        .layer(cors)
145        .with_state(state)
146}
147
148/// 启动 REST API 服务,返回 WsBroadcaster 供外部推送事件
149pub async fn start(listen_addr: &str, router: Arc<RequestRouter>) -> std::io::Result<()> {
150    let ws_broadcaster = Arc::new(WsBroadcaster::new(1024));
151    let app = build_router(router, ws_broadcaster);
152    let listener = tokio::net::TcpListener::bind(listen_addr).await?;
153    tracing::info!(addr = %listen_addr, "REST API 服务已启动 (WebSocket: /ws)");
154    axum::serve(listener, app).await
155}
156
157/// 启动 REST API 服务并返回 WsBroadcaster(供外部推送系统使用)
158pub async fn start_with_broadcaster(
159    listen_addr: &str,
160    router: Arc<RequestRouter>,
161    ws_broadcaster: Arc<WsBroadcaster>,
162) -> std::io::Result<()> {
163    let app = build_router(router, ws_broadcaster);
164    let listener = tokio::net::TcpListener::bind(listen_addr).await?;
165    tracing::info!(addr = %listen_addr, "REST API 服务已启动 (WebSocket: /ws)");
166    axum::serve(listener, app).await
167}
168
169/// 同 `start_with_broadcaster`,但挂载 KeyStore 做 Bearer Token 鉴权 +
170/// RuntimeCounters 做限额
171pub async fn start_with_auth(
172    listen_addr: &str,
173    router: Arc<RequestRouter>,
174    ws_broadcaster: Arc<WsBroadcaster>,
175    key_store: Arc<KeyStore>,
176    counters: Arc<RuntimeCounters>,
177) -> std::io::Result<()> {
178    let scope_mode = key_store.is_configured();
179    let app = build_router_with_auth(router, ws_broadcaster, key_store, counters);
180    let listener = tokio::net::TcpListener::bind(listen_addr).await?;
181    tracing::info!(
182        addr = %listen_addr,
183        scope_mode,
184        "REST API 服务已启动 (WebSocket: /ws)"
185    );
186    if !scope_mode {
187        tracing::warn!("REST API started WITHOUT auth; all clients can access /api/* freely");
188    }
189    axum::serve(listener, app).await
190}