Skip to main content

futu_server/
ws_listener.rs

1// WebSocket 监听器:接受 WebSocket 连接,复用 TCP 的请求路由和连接池
2//
3// 每个 WebSocket 二进制消息 = 一个完整的 FutuAPI 帧(44 字节帧头 + body)。
4// 与 TCP 共享同一个 connections DashMap、RequestRouter、SubscriptionManager。
5//
6// ## v1.0 鉴权
7//
8// 握手阶段(accept_hdr_async)校验 HTTP `Authorization: Bearer <token>` 或
9// `?token=<plaintext>` query —— 通过 `KeyStore::verify` 得到 `KeyRecord`,
10// 把 scope 集合和 key_id 存到 `ClientConn`。每条消息进 `ws_process_requests`
11// 时按 `futu_auth::scope_for_proto_id(proto_id)` 查所需 scope,不匹配 → 不 dispatch、
12// 记 audit reject。`trade:real` 额外跑 `check_and_commit` 过一道 rate + hours
13// 全局闸门。未注入 KeyStore(TCP listener 或 legacy 模式)→ scopes 空集被
14// 解释为"全放行",保持向后兼容。
15//
16// ## v1.4.104 阶段 3
17//
18// 把 inline scope gate / rate gate / body-aware acc_id check 替换为
19// `futu_auth_pipeline::authenticate_request` 单一调用. 流程:
20//   1. AES decrypt (handshake protocol INIT_CONNECT 跳过)
21//   2. (非 INIT_CONNECT + 非 1xxx 系统协议) → 调 pipeline
22//      Credential::PreVerified(rec_from_get_by_id) 拿 SIGHUP-aware 最新 rec.
23//      Reject → drop. Allow → 取 allowed_acc_ids 给 response filter.
24//   3. dispatch (router / handle_init_connect / handle_keepalive)
25//   4. response filter (proto 2001 TRD_GET_ACC_LIST 等)
26//
27// 删除: 旧 `ws_body_aware_check` (功能进 pipeline body_aware::build_check_ctxs).
28// 删除: 旧 inline scope gate + rate gate + audit allow/reject. 全 pipeline 一处.
29
30use std::collections::HashSet;
31use std::sync::Arc;
32use std::time::Instant;
33
34use dashmap::DashMap;
35use tokio::net::TcpListener;
36use tokio::sync::{mpsc, watch};
37
38mod connection;
39mod handshake;
40
41use connection::prepare_ws_connection;
42use handshake::AuthResult;
43
44use futu_auth::{KeyStore, RuntimeCounters};
45use futu_auth_pipeline::{
46    AuthDecision, AuthEnvelope, Credential, Endpoint, FilterRegistry, RejectKind, SurfaceId,
47    authenticate_request,
48};
49
50/// v1.4.106 D1 5c: WS surface adapter — `AuthDecision::Reject` 翻成 silent drop.
51///
52/// **历史**: WS 在 v1.4.103 / v1.4.104 阶段 3 都按 silent drop 处理 reject
53/// (与 v1.4.103 行为一致, 防 timing 探测 — 给客户端任何 wire response 都让
54/// 它知道帧被读了 vs 没读). v1.4.106 D1 把这层"翻译为 unit"也走 trait, 让 4
55/// surface SurfaceAdapter 一致.
56///
57/// **WireResponse = ()**: WS 不发任何东西回 client; 调用方拿 `Option<()>`
58/// 知道是 reject (Some) 还是 allow (None) 即可继续 dispatch / 丢弃.
59///
60/// **不变量**: `translate_reject` 不能写日志 (pipeline 已 audit::reject 一次,
61/// 不要重复). 只 drain reason / kind 让它进 `_`.
62pub struct WsAdapter;
63
64impl futu_auth_pipeline::SurfaceAdapter for WsAdapter {
65    type WireResponse = ();
66
67    fn surface_id() -> SurfaceId {
68        SurfaceId::Ws
69    }
70
71    fn translate_reject(_kind: RejectKind, _reason: String) -> Self::WireResponse {
72        // 与 v1.4.103/104 行为一致: silent drop. pipeline 已 audit reject,
73        // 不再写 log; reject fields intentionally unused to avoid leaking daemon state.
74    }
75}
76use futu_codec::header::ProtoFmtType;
77use futu_core::proto_id;
78use futu_core::server_time::ServerTimeAnchorStore;
79
80use crate::conn::{ClientConn, ConnState, DisconnectNotify, IncomingRequest};
81use crate::listener::{
82    MAX_CONNECTIONS, PER_CONNECTION_REQUEST_QUEUE_CAPACITY, ServerConfig,
83    default_server_time_store, server_now_ts,
84};
85use crate::listener_status::{
86    ListenerBindEventSender, ListenerSurface, notify_listener_failed, notify_listener_opened,
87};
88use crate::router::RequestRouter;
89
90/// WebSocket 服务端
91pub struct WsServer {
92    listen_addr: String,
93    config: ServerConfig,
94    connections: Arc<DashMap<u64, ClientConn>>,
95    router: Arc<RequestRouter>,
96    subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
97    /// v1.0:握手时做 Bearer token 鉴权。None 或 `!is_configured()` → legacy 模式放行
98    key_store: Option<Arc<KeyStore>>,
99    /// v1.0:跨 REST / gRPC / WS 共享的限额 counters
100    counters: Option<Arc<RuntimeCounters>>,
101    /// v1.4.104 阶段 3: 跨 surface 共享的 response filter registry (proto 2001
102    /// TRD_GET_ACC_LIST 默认装入). None 时 fallback 到内置 with_defaults.
103    filter_registry: Option<Arc<FilterRegistry>>,
104    server_time_store: Arc<ServerTimeAnchorStore>,
105}
106
107/// Shared dependencies for the WebSocket server.
108///
109/// These are the same runtime objects the raw TCP server owns. Keeping them in a
110/// bundle avoids every constructor carrying a growing positional list as auth /
111/// counters / filters evolve.
112pub struct WsServerDeps {
113    connections: Arc<DashMap<u64, ClientConn>>,
114    router: Arc<RequestRouter>,
115    subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
116}
117
118impl WsServerDeps {
119    pub fn new(
120        connections: Arc<DashMap<u64, ClientConn>>,
121        router: Arc<RequestRouter>,
122        subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
123    ) -> Self {
124        Self {
125            connections,
126            router,
127            subscriptions,
128        }
129    }
130}
131
132impl WsServer {
133    /// 创建 WsServer,共享 TCP 的连接池、路由器、订阅管理器(无鉴权,向后兼容)
134    pub fn new(
135        listen_addr: String,
136        config: ServerConfig,
137        connections: Arc<DashMap<u64, ClientConn>>,
138        router: Arc<RequestRouter>,
139        subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
140    ) -> Self {
141        Self::with_auth(
142            listen_addr,
143            config,
144            WsServerDeps::new(connections, router, subscriptions),
145            None,
146            None,
147        )
148    }
149
150    /// v1.0 入口:同时接入 KeyStore + 共享 RuntimeCounters 做握手鉴权和 per-message
151    /// scope / 限额检查。`key_store = None` 或未配置时保持 legacy(全放行)。
152    pub fn with_auth(
153        listen_addr: String,
154        config: ServerConfig,
155        deps: WsServerDeps,
156        key_store: Option<Arc<KeyStore>>,
157        counters: Option<Arc<RuntimeCounters>>,
158    ) -> Self {
159        Self {
160            listen_addr,
161            config,
162            connections: deps.connections,
163            router: deps.router,
164            subscriptions: deps.subscriptions,
165            key_store,
166            counters,
167            filter_registry: None,
168            server_time_store: default_server_time_store(),
169        }
170    }
171
172    /// Inject the shared backend server-time anchor for SDK-facing WS fields.
173    pub fn with_server_time_store(mut self, store: Arc<ServerTimeAnchorStore>) -> Self {
174        self.server_time_store = store;
175        self
176    }
177
178    /// v1.4.104 阶段 3: 注入显式 FilterRegistry (跨 surface 共享同一份).
179    /// 不调用此 setter 则 run() 时 fallback 到 `FilterRegistry::with_defaults()`.
180    pub fn with_filter_registry(mut self, registry: Arc<FilterRegistry>) -> Self {
181        self.filter_registry = Some(registry);
182        self
183    }
184
185    /// 启动 WebSocket 服务端监听
186    pub async fn run(&self) -> anyhow::Result<()> {
187        let (_shutdown_tx, shutdown_rx) = watch::channel(false);
188        self.run_until_shutdown(shutdown_rx).await
189    }
190
191    /// 启动 WebSocket 服务端监听,并在 shutdown 信号到来时停止接受新连接。
192    pub async fn run_until_shutdown(
193        &self,
194        shutdown_rx: watch::Receiver<bool>,
195    ) -> anyhow::Result<()> {
196        self.run_until_shutdown_with_listener_events(shutdown_rx, None)
197            .await
198    }
199
200    /// Run until shutdown and report the exact socket bind result to startup.
201    pub async fn run_until_shutdown_with_listener_events(
202        &self,
203        mut shutdown_rx: watch::Receiver<bool>,
204        listener_events: Option<ListenerBindEventSender>,
205    ) -> anyhow::Result<()> {
206        let listener = TcpListener::bind(&self.listen_addr)
207            .await
208            .map_err(|error| {
209                notify_listener_failed(&listener_events, ListenerSurface::WebSocket);
210                anyhow::Error::new(crate::bind_hint::io_bind_error(
211                    "WebSocket",
212                    "--websocket-port",
213                    &self.listen_addr,
214                    error,
215                ))
216            })?;
217        tracing::info!(addr = %self.listen_addr, "WebSocket server listening");
218
219        // The disconnect cleanup signal is intentionally unbounded: each item
220        // is a tiny `conn_id`, and blocking this path behind request-queue
221        // backpressure would risk leaking connection/subscription state.
222        let (disconnect_tx, mut disconnect_rx) = mpsc::unbounded_channel::<DisconnectNotify>();
223
224        // v1.4.104 阶段 3: KeyStore + RuntimeCounters 总是材料化 (legacy mode 用
225        // empty / new()), 让 ws_process_requests 拿 non-Option Arc 直接调 pipeline.
226        // 行为与 v1.4.103 等价: KeyStore::empty().is_configured() = false, pipeline
227        // 走 legacy short-circuit (Allow{rec:None} 不 audit, body-aware 不 enforce).
228        let key_store_for_process = self
229            .key_store
230            .clone()
231            .unwrap_or_else(|| Arc::new(KeyStore::empty()));
232        let counters_for_process = self
233            .counters
234            .clone()
235            .unwrap_or_else(|| Arc::new(RuntimeCounters::new()));
236        let filter_registry_for_process = self
237            .filter_registry
238            .clone()
239            .unwrap_or_else(|| Arc::new(FilterRegistry::with_defaults()));
240        // 启动连接清理任务
241        let cleanup_connections = Arc::clone(&self.connections);
242        let cleanup_subs = self.subscriptions.clone();
243        tokio::spawn(async move {
244            while let Some(notify) = disconnect_rx.recv().await {
245                let removed = cleanup_connections.remove(&notify.conn_id);
246                if removed.is_some() {
247                    if let Some(ref subs) = cleanup_subs {
248                        subs.on_disconnect(notify.conn_id);
249                    }
250                    tracing::info!(
251                        conn_id = notify.conn_id,
252                        remaining = cleanup_connections.len(),
253                        "ws connection removed from pool"
254                    );
255                }
256            }
257        });
258
259        // 接受连接循环
260        let connections = Arc::clone(&self.connections);
261        let key_store_accept = self.key_store.clone();
262        // v1.4.104 阶段 3: scope_mode 局部计算 (KeyStore configured = 启用 auth).
263        let scope_mode = self.key_store.as_ref().is_some_and(|ks| ks.is_configured());
264        if !scope_mode {
265            // v1.4.93 P0-5 (NEW-C-02): 加强 legacy mode loud WARN —
266            // 对齐 REST mutating-blocked policy 的 startup signal。任何
267            // 未授权客户端都可 handshake + 接收 push(HTTP 101 OK)。本版
268            // **不 reject**(保持向后兼容),未来 v2 默认 reject。
269            tracing::warn!("{}", legacy_mode_warn_tracing_message());
270            eprintln!("{}", legacy_mode_warn_stderr_message());
271        }
272        let _serving =
273            notify_listener_opened(&listener_events, ListenerSurface::WebSocket, &shutdown_rx)
274                .await?;
275        drop(listener_events);
276
277        loop {
278            let (stream, peer_addr) = tokio::select! {
279                _ = crate::listener::shutdown_requested(&mut shutdown_rx) => {
280                    tracing::info!("WebSocket server accept loop stopped by shutdown signal");
281                    break;
282                }
283                accepted = listener.accept() => accepted?,
284            };
285
286            if connections.len() >= MAX_CONNECTIONS {
287                tracing::warn!(
288                    peer = %peer_addr,
289                    "max connections reached ({}), rejecting ws client",
290                    MAX_CONNECTIONS,
291                );
292                drop(stream);
293                continue;
294            }
295
296            let conn_id = ClientConn::generate_conn_id();
297            let session_generation = ClientConn::generate_session_generation();
298            let aes_key = ClientConn::generate_aes_key();
299            crate::listener::set_nodelay_with_log(&stream, peer_addr, "ws");
300
301            tracing::info!(
302                conn_id = conn_id,
303                peer = %peer_addr,
304                total = connections.len() + 1,
305                "ws client connected"
306            );
307
308            let (req_tx, req_rx) =
309                mpsc::channel::<IncomingRequest>(PER_CONNECTION_REQUEST_QUEUE_CAPACITY);
310            let (tx, authed, close_control, io_start) = prepare_ws_connection(
311                stream,
312                peer_addr,
313                conn_id,
314                aes_key,
315                req_tx,
316                disconnect_tx.clone(),
317                shutdown_rx.clone(),
318                key_store_accept.clone(),
319            )
320            .await;
321
322            // 握手鉴权失败 → run_ws_connection 已经 drop 连接;这里什么都不做
323            let Some(authed) = authed else {
324                continue;
325            };
326            let (key_id, scopes, allowed_markets, allowed_acc_ids) = match authed {
327                AuthResult::Authenticated(rec) => (
328                    Some(rec.id.clone()),
329                    rec.scopes.clone(),
330                    // v1.4.105 D3 (Phase 4) T-B2: 拷贝 caller key 的 allowed_markets
331                    // 到 ClientConn 让 PushDispatcher::push_trd_acc Layer 3 用.
332                    rec.allowed_markets
333                        .as_ref()
334                        .map(|s| std::sync::Arc::new(s.clone())),
335                    // codex round 1 F4 (P2) v1.4.105: 拷贝 caller key 的
336                    // allowed_acc_ids 到 ClientConn 让 PushDispatcher::push_trd_acc
337                    // Layer 1 push-time 硬过滤. 防 stale subscription /
338                    // KeyRecord reload 后 acc 范围窄化 时 push leak.
339                    rec.allowed_acc_ids
340                        .as_ref()
341                        .map(|s| std::sync::Arc::new(s.clone())),
342                ),
343                AuthResult::Legacy => (None, HashSet::new(), None, None),
344            };
345
346            let conn = ClientConn {
347                conn_id,
348                session_generation,
349                state: ConnState::Connected,
350                aes_key,
351                aes_encrypt_enabled: false,
352                proto_fmt_type: ProtoFmtType::Protobuf,
353                last_keepalive: Instant::now(),
354                recv_notify: false,
355                ai_type: 0,
356                keepalive_count: std::sync::atomic::AtomicU32::new(0),
357                tx,
358                key_id,
359                scopes,
360                allowed_markets,
361                allowed_acc_ids,
362            };
363
364            connections.insert(conn_id, conn);
365            if let Some(ref subscriptions) = self.subscriptions {
366                subscriptions.on_connect(conn_id, session_generation);
367                subscriptions.register_client_close_control(conn_id, close_control);
368            }
369            tokio::spawn(ws_process_requests(
370                req_rx,
371                Arc::clone(&connections),
372                Arc::clone(&self.router),
373                self.subscriptions.clone(),
374                self.config.clone(),
375                Arc::clone(&counters_for_process),
376                Arc::clone(&key_store_for_process),
377                Arc::clone(&filter_registry_for_process),
378                Arc::clone(&self.server_time_store),
379                peer_addr.ip().is_loopback(),
380            ));
381            io_start.start();
382        }
383
384        Ok(())
385    }
386}
387
388/// v1.4.93 P0-5 (NEW-C-02): legacy mode 的 `tracing::warn!` 内容。
389///
390/// 抽出 const fn 以便单测验证 warn 消息携带 "v2"/"reject" 等关键提示词,
391/// 防止后续被误删(同模式 v1.4.86 SEC-003 Q4 已沉淀)。
392pub(crate) const fn legacy_mode_warn_tracing_message() -> &'static str {
393    "WS server running WITHOUT API key auth (legacy mode); \
394     all WS clients accept unauthenticated handshake (no-token / \
395     wrong-bearer / bogus-query all return success). \
396     Pass KeyStore via with_auth() to enable. \
397     v2 will default-reject; migrate to --rest-keys-file / --ws-keys-file for production."
398}
399
400/// v1.4.93 P0-5 (NEW-C-02): legacy mode 的 stderr 用户可见消息。
401///
402/// 比 tracing::warn 更短,方便 systemd / docker logs 一行抓住。
403pub(crate) const fn legacy_mode_warn_stderr_message() -> &'static str {
404    "⚠️  WS server (legacy mode, no --ws-keys-file): \
405     unauthenticated handshakes accepted. v2 will default-reject. \
406     Migrate to --ws-keys-file for production."
407}
408
409/// 处理 WebSocket 连接的请求(逻辑与 TCP 的 process_requests 相同,额外做 scope / 限额)
410///
411/// v1.4.104 阶段 3 重构: inline scope gate + trade:real rate gate +
412/// `ws_body_aware_check` 三段折叠为单一 `authenticate_request` pipeline 调用.
413///
414/// **流程**:
415/// 1. AES decrypt (handshake INIT_CONNECT 跳过 — body 是明文 RSA 加密 InitReq)
416/// 2. 非 INIT_CONNECT + scope_for_proto_id != None → 调 pipeline:
417///    - Credential::PreVerified(`KeyStore::get_by_id(key_id)`) 拿 SIGHUP-aware
418///      最新 rec, 复用 handshake 已 verify 的身份 (skip re-verify).
419///    - Endpoint::Proto(proto_id), commit_rate=true (per-msg rate 闸门).
420///    - Reject → drop request (audit 已 emit). Allow → 拿 allowed_acc_ids.
421/// 3. dispatch (router / handle_init_connect / handle_keepalive)
422/// 4. response filter (proto 2001 等, 通过 FilterRegistry::apply).
423///
424/// **legacy mode 行为**: KeyStore::empty().is_configured() = false, pipeline
425/// 走 legacy short-circuit (Allow{rec:None}, 不 audit, body-aware 不 enforce).
426/// 1xxx 系统协议 (INIT_CONNECT / KEEP_ALIVE / GET_GLOBAL_STATE) 与 v1.4.103
427/// 一致跳过 pipeline 直接 dispatch (handshake / heartbeat / 公开协议无 scope).
428async fn ws_process_requests(
429    mut req_rx: mpsc::Receiver<IncomingRequest>,
430    connections: Arc<DashMap<u64, ClientConn>>,
431    router: Arc<RequestRouter>,
432    subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
433    config: ServerConfig,
434    counters: Arc<RuntimeCounters>,
435    key_store: Arc<KeyStore>,
436    filter_registry: Arc<FilterRegistry>,
437    server_time_store: Arc<ServerTimeAnchorStore>,
438    peer_is_loopback: bool,
439) {
440    use crate::listener::ApiServer;
441
442    let mut pending_init_connect: Option<tokio::task::JoinHandle<()>> = None;
443    while let Some(mut req) = req_rx.recv().await {
444        req.caller_is_loopback = peer_is_loopback;
445        req.caller_legacy_local_mode = !key_store.is_configured();
446        let Some(session_generation) = connections
447            .get(&req.conn_id)
448            .map(|conn| conn.session_generation)
449        else {
450            continue;
451        };
452        req.session_generation = session_generation;
453        req.caller_has_auth_setup_scope = connections
454            .get(&req.conn_id)
455            .is_some_and(|conn| conn.scopes.contains(&futu_auth::Scope::AuthSetup));
456        let conn_id = req.conn_id;
457        let proto_id_val = req.proto_id;
458        let serial_no = req.serial_no;
459
460        // 更新 last_keepalive(任何包都算活跃)
461        if let Some(mut conn) = connections.get_mut(&conn_id) {
462            conn.last_keepalive = Instant::now();
463        }
464
465        // ── Step 0: v1.4.106 codex 0532 F3 (P2): daemon-internal proto_id
466        // (高位 0x8000_0000 bit) 绝不应从 raw WS 公开 surface 进入 — 仅 REST
467        // handler 内部合成给 router. 在 AES decrypt 前显式 reject + log,
468        // 防探测 daemon 内部 routing.
469        if futu_auth::is_internal_proto_id(proto_id_val) {
470            tracing::warn!(
471                conn_id,
472                proto_id = proto_id_val,
473                "rejecting daemon-internal proto_id at raw WS public surface (audit 0532 F3)"
474            );
475            continue;
476        }
477
478        if proto_id_val != proto_id::INIT_CONNECT
479            && connections
480                .get(&conn_id)
481                .is_some_and(|conn| conn.state == crate::conn::ConnState::Connected)
482            && !crate::identity::StartupReadiness::is_prelogin_proto(proto_id_val)
483        {
484            if router.startup_readiness().snapshot().state != crate::identity::StartupState::Ready {
485                if let Some(body) = router.dispatch(conn_id, &req).await
486                    && ApiServer::send_response(
487                        connections.as_ref(),
488                        conn_id,
489                        proto_id_val,
490                        serial_no,
491                        body,
492                    )
493                    .await
494                {
495                    req.mark_response_committed();
496                }
497                continue;
498            }
499            let Some(pending) = pending_init_connect.take() else {
500                tracing::warn!(
501                    conn_id,
502                    proto_id = proto_id_val,
503                    "dropping WS request from Ready connection with no completed InitConnect"
504                );
505                continue;
506            };
507            if let Err(error) = pending.await {
508                tracing::warn!(conn_id, error = %error, "pending WS InitConnect task failed");
509                continue;
510            }
511            if connections
512                .get(&conn_id)
513                .is_none_or(|conn| conn.state == crate::conn::ConnState::Connected)
514            {
515                continue;
516            }
517        }
518
519        // ── Step 1: AES decrypt (always for non-INIT_CONNECT) ─────────────────
520        // pipeline + body-aware 都需 plaintext body. INIT_CONNECT body 是 RSA
521        // 加密的 ConnInitReq, 由 handle_init_connect 自行 RSA decrypt.
522        if proto_id_val != proto_id::INIT_CONNECT
523            && let Some(conn) = connections.get(&conn_id)
524            && conn.aes_encrypt_enabled
525        {
526            match conn.decrypt_body(&req.body) {
527                Ok(decrypted) => {
528                    req.body = bytes::Bytes::from(decrypted);
529                }
530                Err(e) => {
531                    tracing::warn!(
532                        conn_id = conn_id,
533                        proto_id = proto_id_val,
534                        error = %e,
535                        "ws AES decrypt request failed, dropping"
536                    );
537                    continue;
538                }
539            }
540        }
541
542        // ── Step 2: Pipeline auth ─────────────────────────────────────────────
543        // INIT_CONNECT (handshake) + 1xxx 系统协议 (scope_for_proto_id == None)
544        // 跳 pipeline. 与 v1.4.103 行为一致: 系统协议不审 / 不限频 / 直 dispatch.
545        let needed_scope = futu_auth_pipeline::capability::scope_for_proto_id(proto_id_val);
546        // codex 0522 F1 v1.4.106: 提前抓 conn.key_id 快照让 dispatch IncomingRequest
547        // 也能填 caller_key_id (per-call snapshot, 与 caller_allowed_acc_ids 同源).
548        // 即使 INIT_CONNECT / 1xxx 系统协议 (跳 pipeline) 也带上 — handler 可基于
549        // key_id 做 per-key 订阅配额 / cleanup / 审计.
550        let dispatch_caller_key_id: Option<String> =
551            connections.get(&conn_id).and_then(|c| c.key_id.clone());
552        let allowed_acc_ids_for_resp_filter: Option<HashSet<u64>> =
553            if proto_id_val == proto_id::INIT_CONNECT || needed_scope.is_none() {
554                None
555            } else {
556                // 从 conn 取 key_id 快照, 然后 KeyStore::get_by_id 拿 SIGHUP-aware
557                // 最新 rec (limits / scopes / allowed_acc_ids 都跟最新). 找不到 ↦
558                // Credential::None (legacy mode 放行 / scope mode reject Unauth).
559                let key_id_snap = dispatch_caller_key_id.clone();
560                let rec_opt = key_id_snap.as_ref().and_then(|id| key_store.get_by_id(id));
561                let credential = match rec_opt {
562                    Some(rec) => Credential::PreVerified(rec),
563                    None => Credential::None,
564                };
565
566                let env = AuthEnvelope {
567                    surface: SurfaceId::Ws,
568                    endpoint: Endpoint::Proto(proto_id_val),
569                    needed_scope,
570                    credential,
571                    proto_id: Some(proto_id_val),
572                    body: &req.body,
573                    explicit_acc_id: None,
574                    explicit_ctx: None,
575                    commit_rate: true, // WS per-msg 是 trade:real 唯一 rate gate
576                    audit_emit: true,
577                };
578                let session_id = conn_id.to_string();
579                let audit_ctx =
580                    futu_auth::audit::AuditContext::new(None::<&str>, Some(session_id.as_str()));
581
582                // v1.4.106 D1 5c: 走 SurfaceAdapter trait
583                // (`WsAdapter::translate_decision`), 与 4 surface 一致.
584                // Allow → Some(allowed_acc_ids), Reject → None + silent drop.
585                use futu_auth_pipeline::SurfaceAdapter;
586                match futu_auth::audit::with_context(audit_ctx.clone(), || {
587                    authenticate_request(&key_store, &counters, env)
588                }) {
589                    AuthDecision::Allow {
590                        allowed_acc_ids, ..
591                    } => allowed_acc_ids,
592                    decision @ AuthDecision::Reject { .. } => {
593                        // WsAdapter::translate_decision 返 Some(()) 表 reject
594                        // (silent drop). pipeline 已 audit reject, 这里不打 log.
595                        let silent_drop = WsAdapter::translate_decision(decision);
596                        debug_assert!(silent_drop.is_some());
597                        continue;
598                    }
599                }
600            };
601
602        // ── Step 3: Dispatch ──────────────────────────────────────────────────
603        let response_body = match proto_id_val {
604            proto_id::INIT_CONNECT => match crate::conn::start_init_connect_for_startup(
605                Arc::clone(&connections),
606                router.startup_readiness().clone(),
607                conn_id,
608                &req.body,
609                serial_no,
610                config.server_ver,
611                config.keepalive_interval,
612                config.rsa_private_key.clone(),
613            ) {
614                Ok(crate::conn::InitConnectStart::Immediate(prepared)) => {
615                    if let Err(error) = crate::conn::complete_prepared_init_connect(
616                        connections.as_ref(),
617                        conn_id,
618                        serial_no,
619                        prepared,
620                    )
621                    .await
622                    {
623                        tracing::warn!(conn_id, error = %error, "ws InitConnect response failed");
624                    }
625                    None
626                }
627                Ok(crate::conn::InitConnectStart::Deferred(task)) => {
628                    if let Some(previous) = pending_init_connect.replace(task) {
629                        previous.abort();
630                    }
631                    None
632                }
633                Err(error) => {
634                    tracing::warn!(
635                        conn_id,
636                        proto_id = proto_id_val,
637                        error = %error,
638                        "ws InitConnect handling failed"
639                    );
640                    None
641                }
642            },
643            proto_id::KEEP_ALIVE => match connections.get(&conn_id) {
644                Some(conn) => {
645                    match conn.handle_keepalive_at(&req.body, server_now_ts(&server_time_store)) {
646                        Ok(body) => Some(body),
647                        Err(error) => {
648                            tracing::warn!(
649                                conn_id,
650                                proto_id = proto_id_val,
651                                error = %error,
652                                "ws KeepAlive handling failed"
653                            );
654                            None
655                        }
656                    }
657                }
658                None => {
659                    tracing::warn!(
660                        conn_id,
661                        proto_id = proto_id_val,
662                        "ws KeepAlive request received for missing connection"
663                    );
664                    None
665                }
666            },
667            _ => {
668                // v1.4.105 D2 T-A1 fix: caller_allowed_acc_ids 从 pipeline allow
669                // decision 真填进 IncomingRequest, 让 dispatch handler (e.g.
670                // SubAccPushHandler) 端 enforce per-acc whitelist defense-in-depth.
671                // codex 0522 F1 v1.4.106: 同步填 caller_key_id (per-call snapshot
672                // 来自 conn.key_id), 让 cross-surface handler 都能识别 caller.
673                let dispatch_req = IncomingRequest::builder(
674                    req.conn_id,
675                    req.proto_id,
676                    req.serial_no,
677                    req.proto_fmt_type,
678                    req.body.clone(),
679                )
680                .with_response_commit_from(&req)
681                .with_transport(req.transport)
682                .with_session_generation(req.session_generation)
683                .with_idempotency_key(req.idempotency_key.clone())
684                .with_caller_scope(
685                    allowed_acc_ids_for_resp_filter
686                        .as_ref()
687                        .map(|s| std::sync::Arc::new(s.clone())),
688                    dispatch_caller_key_id.clone(),
689                )
690                .build();
691                router.dispatch(conn_id, &dispatch_req).await
692            }
693        };
694
695        crate::listener::reconcile_post_dispatch_connection_state(
696            &connections,
697            subscriptions.as_deref(),
698            conn_id,
699        );
700
701        // ── Step 4: Response filter (TRD_GET_ACC_LIST 等) ────────────────────
702        if let Some(body) = response_body {
703            // FilterRegistry::apply(): proto_id 未注册 → 原 body 不动 (no-op).
704            // 注册了 (e.g. proto 2001) → filter by allowed_acc_ids.
705            let filtered =
706                filter_registry.apply(proto_id_val, body, allowed_acc_ids_for_resp_filter.as_ref());
707            if ApiServer::send_response(&connections, conn_id, proto_id_val, serial_no, filtered)
708                .await
709            {
710                req.mark_response_committed();
711            }
712        }
713    }
714    if let Some(pending) = pending_init_connect {
715        pending.abort();
716    }
717}
718
719#[cfg(test)]
720mod tests;