futu_rest/server/
startup.rs1use 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
11pub 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
31pub 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
57pub 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
91pub 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 tracing::warn!("{}", LEGACY_WS_WARN_MESSAGE);
132 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}