1use anyhow::{Context, Result};
9use std::sync::Arc;
10
11use futu_gateway_core::bridge::GatewayBridge;
12use futu_server::ws_listener::{WsServer, WsServerDeps};
13
14use crate::config::RuntimeConfig;
15use crate::startup::phase1::Phase1Out;
16use crate::startup::phase3::Phase3Out;
17
18mod network_exposure;
19mod shutdown;
20mod summary;
21#[cfg(test)]
22mod tests;
23
24#[cfg(test)]
25use network_exposure::legacy_network_exposure_warnings;
26use network_exposure::warn_legacy_network_exposure;
27use shutdown::{
28 BackgroundJoinHandle, SHUTDOWN_GRACE_PERIOD, SurfaceJoinHandle, await_background_shutdown,
29 await_surface_shutdown, log_surface_task_result, request_surface_shutdown,
30 surface_task_result_or_pending,
31};
32#[cfg(test)]
33use summary::http_host_for_bind;
34use summary::{log_startup_summary, merge_push_health_snapshots_for_rest};
35
36async fn wait_for_sigterm() {
37 #[cfg(unix)]
38 {
39 use tokio::signal::unix::{SignalKind, signal};
40
41 let mut sigterm = match signal(SignalKind::terminate()) {
42 Ok(signal) => signal,
43 Err(error) => {
44 tracing::error!(
45 error = %error,
46 "failed to install SIGTERM handler; graceful SIGTERM shutdown unavailable"
47 );
48 std::future::pending::<()>().await;
49 return;
50 }
51 };
52 let _ = sigterm.recv().await;
53 }
54
55 #[cfg(not(unix))]
56 {
57 std::future::pending::<()>().await;
58 }
59}
60
61pub(super) async fn run_phase4(
62 config: &RuntimeConfig,
63 phase1: Phase1Out,
64 bridge: Arc<GatewayBridge>,
65 phase3: Phase3Out,
66 shutdown_tx: tokio::sync::watch::Sender<bool>,
67 mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
68) -> Result<()> {
69 let Phase1Out {
70 _audit_guard,
71 shared_counters,
72 listen_addr,
73 rest_keys_file,
74 ws_keys_file,
75 grpc_keys_file,
76 allow_tcp_unauthenticated,
77 } = phase1;
78 let Phase3Out {
79 server,
80 server_config,
81 ws_broadcaster,
82 grpc_broadcaster,
83 } = phase3;
84
85 let mut ws_key_store_holder: Option<std::sync::Arc<futu_auth::KeyStore>> = None;
89 let mut rest_key_store_holder: Option<std::sync::Arc<futu_auth::KeyStore>> = None;
90 let mut grpc_key_store_holder: Option<std::sync::Arc<futu_auth::KeyStore>> = None;
91 let any_keys_configured =
92 rest_keys_file.is_some() || grpc_keys_file.is_some() || ws_keys_file.is_some();
93 warn_legacy_network_exposure(config, any_keys_configured);
94
95 let mut ws_handle = if let Some(ws_port) = config.websocket_port {
97 let ws_addr = format!("{}:{}", config.ip, ws_port);
98 let ws_key_store = match &ws_keys_file {
106 Some(path) => match futu_auth::KeyStore::load(path) {
107 Ok(ks) => {
108 tracing::info!(
109 path = %path.display(),
110 keys_loaded = ks.len(),
111 "WS keys file loaded (Bearer/?token auth enabled)"
112 );
113 Some(std::sync::Arc::new(ks))
114 }
115 Err(e) => {
116 tracing::error!(
118 error = %e,
119 path = %path.display(),
120 "failed to load WS keys file (--ws-keys-file 明确指定 → fail-closed). \
121 daemon abort. fix the keys file then restart. \
122 不再 fallback to legacy unauth (v1.4.102 BUG-007 fix)."
123 );
124 return Err(anyhow::anyhow!(
125 "failed to load WS keys file {}: {e}. \
126 --ws-keys-file 明确指定 → fail-closed (BUG-007).",
127 path.display()
128 ));
129 }
130 },
131 None => None,
132 };
133 let ws_counters = std::sync::Arc::clone(&shared_counters);
134 ws_key_store_holder = ws_key_store.as_ref().map(std::sync::Arc::clone);
136 let ws_server = WsServer::with_auth(
137 ws_addr.clone(),
138 server_config.clone(),
139 WsServerDeps::new(
140 std::sync::Arc::clone(server.connections()),
141 std::sync::Arc::clone(server.router()),
142 Some(bridge.subscription_runtime().manager()),
143 ),
144 ws_key_store,
145 Some(ws_counters),
146 )
147 .with_server_time_offset_secs(bridge.server_clock().offset_secs());
148 tracing::info!(addr = %ws_addr, "starting WebSocket server");
149 let ws_shutdown_rx = shutdown_rx.clone();
150 Some(tokio::spawn(async move {
151 ws_server
152 .run_until_shutdown(ws_shutdown_rx)
153 .await
154 .context("WebSocket server error")
155 }))
156 } else {
157 None
158 };
159
160 let mut rest_handle = if let Some(rest_port) = config.rest_port {
162 let rest_addr = format!("{}:{}", config.ip, rest_port);
163 let router = std::sync::Arc::clone(server.router());
164 let broadcaster = std::sync::Arc::clone(&ws_broadcaster);
165 let rest_key_store = match &rest_keys_file {
167 Some(path) => match futu_auth::KeyStore::load(path) {
168 Ok(ks) => {
169 tracing::info!(
170 path = %path.display(),
171 keys_loaded = ks.len(),
172 "REST keys file loaded (Bearer auth enabled)"
173 );
174 std::sync::Arc::new(ks)
175 }
176 Err(e) => {
177 tracing::error!(
178 error = %e,
179 path = %path.display(),
180 "failed to load REST keys file (--rest-keys-file 明确指定 → fail-closed). \
181 daemon abort. fix the keys file then restart. \
182 不再 fallback to legacy unauth (v1.4.102 BUG-007 fix)."
183 );
184 return Err(anyhow::anyhow!(
185 "failed to load REST keys file {}: {e}. \
186 --rest-keys-file 明确指定 → fail-closed (BUG-007).",
187 path.display()
188 ));
189 }
190 },
191 None => std::sync::Arc::new(futu_auth::KeyStore::empty()),
192 };
193 tracing::info!(addr = %rest_addr, "starting REST API server (WebSocket: /ws)");
194
195 if rest_key_store.is_configured() {
199 rest_key_store_holder = Some(std::sync::Arc::clone(&rest_key_store));
200 }
201
202 let rest_counters = std::sync::Arc::clone(&shared_counters);
209 let bridge_for_status = std::sync::Arc::clone(&bridge);
213 let admin_status_provider: futu_rest::adapter::AdminStatusProvider =
214 std::sync::Arc::new(move || {
215 serde_json::to_value(bridge_for_status.snapshot_status())
216 .unwrap_or_else(|_| serde_json::json!({"error": "snapshot serialize failed"}))
217 });
218 let rest_shutdown_tx = shutdown_tx.clone();
219 let admin_shutdown_handler: futu_rest::adapter::AdminShutdownHandler =
220 std::sync::Arc::new(move || {
221 rest_shutdown_tx
222 .send(true)
223 .map_err(|e| format!("shutdown receiver dropped: {e}"))
224 });
225 let bridge_for_reload = std::sync::Arc::clone(&bridge);
233 let admin_reload_handler: futu_rest::adapter::AdminReloadHandler =
234 std::sync::Arc::new(move || {
235 let bridge = std::sync::Arc::clone(&bridge_for_reload);
236 Box::pin(async move {
237 serde_json::to_value(bridge.reload())
238 .unwrap_or_else(|_| serde_json::json!({"error": "reload serialize failed"}))
239 })
240 });
241 let bridge_for_push_health = std::sync::Arc::clone(&bridge);
248 let push_health_snapshot_provider: futu_rest::adapter::PushHealthSnapshotProvider =
249 std::sync::Arc::new(move || {
250 let push = serde_json::to_value(
251 bridge_for_push_health
252 .push_runtime()
253 .push_health()
254 .snapshot(),
255 )
256 .unwrap_or_else(
257 |_| serde_json::json!({"error": "push_health snapshot serialize failed"}),
258 );
259 let qot_login = serde_json::to_value(
260 bridge_for_push_health
261 .push_runtime()
262 .qot_login_health()
263 .snapshot(),
264 )
265 .unwrap_or_else(
266 |_| serde_json::json!({"error": "qot_login_health snapshot serialize failed"}),
267 );
268 let backend_connected =
269 bridge_for_push_health.broker_runtime().platform_connected();
270 merge_push_health_snapshots_for_rest(push, qot_login, backend_connected)
271 });
272 let bridge_for_card_num = std::sync::Arc::clone(&bridge);
278 let card_num_resolver: futu_rest::adapter::CardNumResolver =
279 std::sync::Arc::new(move |cn: &str| {
280 bridge_for_card_num
281 .caches()
282 .trd_cache
283 .find_acc_ids_by_card_num(cn)
284 });
285 let rest_shutdown_rx = shutdown_rx.clone();
286 Some(tokio::spawn(async move {
287 futu_rest::server::start_with_auth_full_admin_until_shutdown(
288 &rest_addr,
289 router,
290 broadcaster,
291 rest_key_store,
292 rest_counters,
293 futu_rest::server::RestAdminHooks {
294 admin_status_provider: Some(admin_status_provider),
295 admin_shutdown_handler: Some(admin_shutdown_handler),
296 admin_reload_handler: Some(admin_reload_handler),
297 push_health_snapshot_provider: Some(push_health_snapshot_provider),
298 card_num_resolver: Some(card_num_resolver),
299 },
300 rest_shutdown_rx,
301 )
302 .await
303 .context("REST API server error")
304 }))
305 } else {
306 None
307 };
308
309 let mut grpc_handle = if let Some(grpc_port) = config.grpc_port {
311 let grpc_addr = format!("{}:{}", config.ip, grpc_port);
312 let router = std::sync::Arc::clone(server.router());
313 let broadcaster = std::sync::Arc::clone(&grpc_broadcaster);
314 let grpc_key_store = match &grpc_keys_file {
316 Some(path) => match futu_auth::KeyStore::load(path) {
317 Ok(ks) => {
318 tracing::info!(
319 path = %path.display(),
320 keys_loaded = ks.len(),
321 "gRPC keys file loaded (Bearer auth enabled)"
322 );
323 std::sync::Arc::new(ks)
324 }
325 Err(e) => {
326 tracing::error!(
327 error = %e,
328 path = %path.display(),
329 "failed to load gRPC keys file (--grpc-keys-file 明确指定 → fail-closed). \
330 daemon abort. fix the keys file then restart. \
331 不再 fallback to legacy unauth (v1.4.102 BUG-007 fix)."
332 );
333 return Err(anyhow::anyhow!(
334 "failed to load gRPC keys file {}: {e}. \
335 --grpc-keys-file 明确指定 → fail-closed (BUG-007).",
336 path.display()
337 ));
338 }
339 },
340 None => std::sync::Arc::new(futu_auth::KeyStore::empty()),
341 };
342 tracing::info!(addr = %grpc_addr, "starting gRPC server (SubscribePush: streaming)");
343
344 if grpc_key_store.is_configured() {
346 grpc_key_store_holder = Some(std::sync::Arc::clone(&grpc_key_store));
347 }
348
349 let grpc_counters = std::sync::Arc::clone(&shared_counters);
354 let grpc_shutdown_rx = shutdown_rx.clone();
355 Some(tokio::spawn(async move {
356 futu_grpc::server::start_with_auth_until_shutdown(
357 &grpc_addr,
358 router,
359 broadcaster,
360 grpc_key_store,
361 grpc_counters,
362 grpc_shutdown_rx,
363 )
364 .await
365 .map_err(|e| anyhow::anyhow!("gRPC server error: {e}"))
366 }))
367 } else {
368 None
369 };
370
371 let mut telnet_handle = if let Some(telnet_port) = config.telnet_port {
373 let telnet_addr = format!("{}:{}", config.telnet_ip, telnet_port);
374 let bridge_for_relogin = std::sync::Arc::clone(&bridge);
378 let relogin_fn: futu_server::telnet::ReloginFn = std::sync::Arc::new(move || {
379 tracing::warn!(
380 "v1.4.97 P1-D-F: telnet relogin clearing login_cache; \
381 next P1-D tick will trigger AuthRefresher relogin"
382 );
383 bridge_for_relogin.caches().login_cache.clear();
384 });
385 let telnet_server = futu_server::telnet::TelnetServer::new(
386 telnet_addr.clone(),
387 std::sync::Arc::clone(server.connections()),
388 Some(bridge.subscription_runtime().manager()),
389 Some(std::sync::Arc::clone(server.metrics())),
390 shutdown_tx.clone(),
391 )
392 .with_relogin_fn(relogin_fn);
393 tracing::info!(addr = %telnet_addr, "starting Telnet server");
394 let telnet_shutdown_rx = shutdown_rx.clone();
395 Some(tokio::spawn(async move {
396 telnet_server
397 .run_until_shutdown(telnet_shutdown_rx)
398 .await
399 .context("Telnet server error")
400 }))
401 } else {
402 None
403 };
404
405 log_startup_summary(config, &listen_addr, &bridge);
406 tracing::info!("gateway ready, accepting connections on {listen_addr}");
407 tracing::info!("press Ctrl+C to exit");
408
409 let card_num_reload_and_expand_fn: std::sync::Arc<dyn Fn(bool) + Send + Sync> = {
427 let bridge_for_expand = std::sync::Arc::clone(&bridge);
428 let ws_ks = ws_key_store_holder.clone();
429 let rest_ks = rest_key_store_holder.clone();
430 let grpc_ks = grpc_key_store_holder.clone();
431 std::sync::Arc::new(move |do_reload: bool| {
432 if do_reload {
434 for (ks_name, ks_opt) in [("ws", &ws_ks), ("rest", &rest_ks), ("grpc", &grpc_ks)] {
435 let Some(ks) = ks_opt.as_ref() else { continue };
436 match ks.reload() {
437 Ok(()) => tracing::warn!(
438 ks = ks_name,
439 keys_loaded = ks.len(),
440 "v1.4.103 F3.1: keys reloaded on SIGHUP (before card_num expand)"
441 ),
442 Err(e) => tracing::error!(
443 ks = ks_name,
444 error = %e,
445 "v1.4.103 F3.1: keys reload failed (skipping expand for this store)"
446 ),
447 }
448 }
449 }
450 let trd_cache = std::sync::Arc::clone(&bridge_for_expand.caches().trd_cache);
452 let resolver = {
453 let cache_clone = std::sync::Arc::clone(&trd_cache);
454 move |cn: &str| cache_clone.find_acc_ids_by_card_num(cn)
455 };
456 for (ks_name, ks_opt) in [("ws", &ws_ks), ("rest", &rest_ks), ("grpc", &grpc_ks)] {
457 let Some(ks) = ks_opt.as_ref() else { continue };
458 let (resolved, unresolved, ambiguous) = ks.expand_allowed_card_nums(
459 &resolver,
460 |key_id, cn| {
461 tracing::warn!(
462 key_id = %key_id,
463 card_num = %cn,
464 "v1.4.103 B10/F1 fail-closed: card_num not found in trd_cache; \
465 writing sentinel acc_id=0 to enforce restrictive denylist \
466 (limits.contains check 永远 false → reject 真账户)"
467 );
468 },
469 |key_id, cn, candidates| {
470 tracing::warn!(
471 key_id = %key_id,
472 card_num = %cn,
473 candidates = ?candidates,
474 "v1.4.103 B10/F1 fail-closed: ambiguous card_num suffix \
475 matched multiple accounts (skipped, write 完整 16 位 / specific 4 位)"
476 );
477 },
478 );
479 tracing::info!(
480 ks = ks_name,
481 resolved,
482 unresolved,
483 ambiguous,
484 "v1.4.103 B10: expanded allowed_card_nums into allowed_acc_ids"
485 );
486 }
487 })
488 };
489 let card_num_expand_fn: std::sync::Arc<dyn Fn() + Send + Sync> = {
491 let inner = std::sync::Arc::clone(&card_num_reload_and_expand_fn);
492 std::sync::Arc::new(move || (inner)(false))
493 };
494
495 (card_num_expand_fn)();
497
498 let card_num_retry_handle: BackgroundJoinHandle = {
500 let card_num_expand_fn_loop = std::sync::Arc::clone(&card_num_expand_fn);
501 let bridge_for_check = std::sync::Arc::clone(&bridge);
502 let mut card_num_retry_shutdown_rx = shutdown_rx.clone();
503 tokio::spawn(async move {
504 let trd_cache = std::sync::Arc::clone(&bridge_for_check.caches().trd_cache);
505 let mut attempts = 0u32;
506 let max_attempts = 6u32; loop {
508 tokio::select! {
509 changed = card_num_retry_shutdown_rx.changed() => {
510 if changed.is_err() || *card_num_retry_shutdown_rx.borrow() {
511 tracing::debug!(
512 "v1.4.111: card_num retry loop received shutdown signal"
513 );
514 return;
515 }
516 }
517 _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {}
518 }
519 attempts += 1;
520 let accounts = trd_cache.get_accounts();
521 if accounts.is_empty() {
522 if attempts >= max_attempts {
523 tracing::warn!(
524 "v1.4.103 B10: trd_cache 仍空 (after {max_attempts} × 10s); \
525 受限 key 仍走 fail-closed sentinel reject 直到 SIGHUP / cache 加载."
526 );
527 return;
528 }
529 continue;
530 }
531 (card_num_expand_fn_loop)();
532 return;
533 }
534 })
535 };
536
537 #[cfg(unix)]
546 let sighup_handle: Option<BackgroundJoinHandle> = {
547 let unified_sighup_fn = std::sync::Arc::clone(&card_num_reload_and_expand_fn);
548 let mut sighup_shutdown_rx = shutdown_rx.clone();
549 Some(tokio::spawn(async move {
550 use tokio::signal::unix::{SignalKind, signal};
551 let mut sig = match signal(SignalKind::hangup()) {
552 Ok(s) => s,
553 Err(e) => {
554 tracing::error!(error = %e, "SIGHUP install failed (unified reload+expand)");
555 return;
556 }
557 };
558 tracing::info!(
559 "v1.4.103 F3.1: unified SIGHUP handler installed (reload all keys + expand card_num)"
560 );
561 loop {
562 tokio::select! {
563 signal = sig.recv() => {
564 if signal.is_none() {
565 return;
566 }
567 tracing::info!(
568 "v1.4.103 F3.1: SIGHUP received — running reload_all_stores + \
569 expand_allowed_card_nums (single ordered op, no race)"
570 );
571 (unified_sighup_fn)(true); }
573 changed = sighup_shutdown_rx.changed() => {
574 if changed.is_err() || *sighup_shutdown_rx.borrow() {
575 tracing::debug!(
576 "v1.4.111: unified SIGHUP handler received shutdown signal"
577 );
578 return;
579 }
580 }
581 }
582 }
583 }))
584 };
585 #[cfg(not(unix))]
586 let sighup_handle: Option<BackgroundJoinHandle> = None;
587
588 let tcp_disabled = any_keys_configured && !allow_tcp_unauthenticated;
599
600 if tcp_disabled {
601 tracing::warn!(
602 listen_addr = %listen_addr,
603 "v1.4.104 external report S-001 (P0) fix: TCP listener (port {}) NOT started — \
604 keys file configured but --allow-tcp-unauthenticated not set. \
605 native TCP FTAPI protocol has no Bearer field, cannot enforce \
606 caller-specific scope check; defaulting to fail-closed (skip TCP). \
607 Use REST/gRPC/WS endpoints for authenticated access. \
608 To restore TCP (legacy Python SDK clients) add --allow-tcp-unauthenticated, \
609 but be aware that port {} will accept ANY local connection without \
610 scope check (跨账户 leak risk).",
611 config.port,
612 config.port,
613 );
614 eprintln!(
615 "⚠️ TCP listener (port {}) DISABLED (v1.4.104 external report S-001 fix): \
616 keys file configured + no --allow-tcp-unauthenticated. \
617 Pass --allow-tcp-unauthenticated to restore (with security warning).",
618 config.port,
619 );
620 } else if any_keys_configured && allow_tcp_unauthenticated {
621 tracing::warn!(
622 listen_addr = %listen_addr,
623 "⚠️ v1.4.104: TCP listener running WITHOUT scope check despite keys configured \
624 (--allow-tcp-unauthenticated set). Port {} accepts ANY local connection — \
625 跨账户 leak risk. Use REST/gRPC/WS for authenticated clients; reserve \
626 TCP only for legacy Python SDK / C++ OpenD where Bearer not feasible.",
627 config.port,
628 );
629 eprintln!(
630 "⚠️ TCP port {} ACCEPTS UNAUTHENTICATED connections (--allow-tcp-unauthenticated). \
631 受限 keys 不在该 surface 强制. 推荐改用 REST/gRPC/WS.",
632 config.port,
633 );
634 }
635
636 let tcp_shutdown_rx = shutdown_rx.clone();
637 let mut tcp_handle: Option<SurfaceJoinHandle> = if tcp_disabled {
638 None
639 } else {
640 Some(tokio::spawn(async move {
641 server
642 .run_until_shutdown(tcp_shutdown_rx)
643 .await
644 .context("API server error")
645 }))
646 };
647
648 let phase4_result: anyhow::Result<()> = tokio::select! {
650 result = surface_task_result_or_pending(&mut tcp_handle) => {
651 tcp_handle = None;
652 log_surface_task_result("API server", result)
653 }
654 result = surface_task_result_or_pending(&mut ws_handle) => {
655 ws_handle = None;
656 log_surface_task_result("WebSocket", result)
657 }
658 result = surface_task_result_or_pending(&mut rest_handle) => {
659 rest_handle = None;
660 log_surface_task_result("REST API", result)
661 }
662 result = surface_task_result_or_pending(&mut grpc_handle) => {
663 grpc_handle = None;
664 log_surface_task_result("gRPC", result)
665 }
666 result = surface_task_result_or_pending(&mut telnet_handle) => {
667 telnet_handle = None;
668 log_surface_task_result("Telnet", result)
669 }
670 _ = tokio::signal::ctrl_c() => {
671 tracing::info!("received Ctrl+C, shutting down gracefully...");
672 Ok(())
673 }
674 _ = wait_for_sigterm() => {
675 tracing::info!("received SIGTERM, shutting down gracefully...");
676 Ok(())
677 }
678 _ = async {
679 while shutdown_rx.changed().await.is_ok() {
680 if *shutdown_rx.borrow() {
681 break;
682 }
683 }
684 } => {
685 tracing::info!("shutdown requested via telnet");
686 Ok(())
687 }
688 };
689
690 request_surface_shutdown(&shutdown_tx, "phase4 exit");
692 await_background_shutdown(
693 "card_num retry",
694 card_num_retry_handle,
695 SHUTDOWN_GRACE_PERIOD,
696 )
697 .await;
698 if let Some(handle) = sighup_handle {
699 await_background_shutdown("SIGHUP reload", handle, SHUTDOWN_GRACE_PERIOD).await;
700 }
701 if let Some(handle) = tcp_handle {
702 await_surface_shutdown("API server", handle, SHUTDOWN_GRACE_PERIOD).await;
703 }
704 if let Some(handle) = ws_handle {
705 await_surface_shutdown("WebSocket", handle, SHUTDOWN_GRACE_PERIOD).await;
706 }
707 if let Some(handle) = rest_handle {
708 await_surface_shutdown("REST API", handle, SHUTDOWN_GRACE_PERIOD).await;
709 }
710 if let Some(handle) = grpc_handle {
711 await_surface_shutdown("gRPC", handle, SHUTDOWN_GRACE_PERIOD).await;
712 }
713 if let Some(handle) = telnet_handle {
714 await_surface_shutdown("Telnet", handle, SHUTDOWN_GRACE_PERIOD).await;
715 }
716
717 phase4_result?;
718 tracing::info!("gateway stopped");
719 Ok(())
720}