1use std::sync::Arc;
4use std::time::Instant;
5
6use dashmap::DashMap;
7use tokio::net::TcpListener;
8use tokio::sync::{mpsc, watch};
9
10use futu_codec::header::ProtoFmtType;
11use futu_core::proto_id;
12use futu_core::server_time::ServerTimeAnchorStore;
13
14use crate::conn::{ClientConn, ConnState, DisconnectNotify, IncomingRequest};
15use crate::listener_status::{
16 ListenerBindEventSender, ListenerSurface, notify_listener_failed, notify_listener_opened,
17};
18use crate::metrics::GatewayMetrics;
19use crate::router::RequestRouter;
20
21pub const MAX_CONNECTIONS: usize = 128;
23
24pub(crate) const REQUEST_QUEUE_CAPACITY: usize = 4096;
29
30pub(crate) const PER_CONNECTION_REQUEST_QUEUE_CAPACITY: usize =
35 REQUEST_QUEUE_CAPACITY / MAX_CONNECTIONS;
36const _: () = assert!(REQUEST_QUEUE_CAPACITY.is_multiple_of(MAX_CONNECTIONS));
37const _: () = assert!(PER_CONNECTION_REQUEST_QUEUE_CAPACITY > 0);
38
39#[derive(Debug, Clone)]
41pub struct ServerConfig {
42 pub listen_addr: String,
44 pub server_ver: i32,
46 pub login_user_id: u64,
48 pub keepalive_interval: i32,
50 pub rsa_private_key: Option<String>,
52}
53
54#[must_use]
55pub(crate) fn default_server_time_store() -> Arc<ServerTimeAnchorStore> {
56 Arc::new(ServerTimeAnchorStore::new())
57}
58
59#[must_use]
60pub(crate) fn server_now_ts_at(
61 server_time_store: &ServerTimeAnchorStore,
62 local_now_unix_micros: i64,
63 monotonic_now: Instant,
64) -> i64 {
65 server_time_store
66 .now_unix_micros_at(local_now_unix_micros, monotonic_now)
67 .div_euclid(1_000_000)
68}
69
70#[must_use]
71pub(crate) fn server_now_ts(server_time_store: &ServerTimeAnchorStore) -> i64 {
72 server_now_ts_at(
73 server_time_store,
74 chrono::Utc::now().timestamp_micros(),
75 Instant::now(),
76 )
77}
78
79pub(crate) fn set_nodelay_with_log(
80 stream: &tokio::net::TcpStream,
81 peer_addr: std::net::SocketAddr,
82 surface: &'static str,
83) {
84 if let Err(error) = stream.set_nodelay(true) {
85 tracing::debug!(
86 peer = %peer_addr,
87 surface,
88 error = %error,
89 "tcp nodelay setup failed"
90 );
91 }
92}
93
94pub(crate) async fn shutdown_requested(shutdown_rx: &mut watch::Receiver<bool>) {
95 loop {
96 if *shutdown_rx.borrow() {
97 return;
98 }
99 if shutdown_rx.changed().await.is_err() {
100 return;
101 }
102 }
103}
104
105pub struct ApiServer {
107 config: ServerConfig,
108 connections: Arc<DashMap<u64, ClientConn>>,
109 router: Arc<RequestRouter>,
110 subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
111 metrics: Arc<GatewayMetrics>,
112 server_time_store: Arc<ServerTimeAnchorStore>,
113}
114
115impl ApiServer {
116 pub fn new(config: ServerConfig) -> Self {
118 let startup_readiness =
119 crate::identity::StartupReadiness::ready(config.login_user_id, None);
120 Self {
121 config,
122 connections: Arc::new(DashMap::new()),
123 router: Arc::new(RequestRouter::with_startup_readiness(startup_readiness)),
124 subscriptions: None,
125 metrics: Arc::new(GatewayMetrics::new()),
126 server_time_store: default_server_time_store(),
127 }
128 }
129
130 pub fn set_subscriptions(&mut self, subs: Arc<crate::subscription::SubscriptionManager>) {
132 self.subscriptions = Some(subs);
133 }
134
135 pub fn router(&self) -> &Arc<RequestRouter> {
137 &self.router
138 }
139
140 pub fn connections(&self) -> &Arc<DashMap<u64, ClientConn>> {
142 &self.connections
143 }
144
145 pub fn set_metrics(&mut self, metrics: Arc<GatewayMetrics>) {
147 self.metrics = metrics;
148 }
149
150 pub fn set_server_time_store(&mut self, store: Arc<ServerTimeAnchorStore>) {
152 self.server_time_store = store;
153 }
154
155 pub fn metrics(&self) -> &Arc<GatewayMetrics> {
157 &self.metrics
158 }
159
160 pub async fn run(&self) -> anyhow::Result<()> {
162 let (_shutdown_tx, shutdown_rx) = watch::channel(false);
163 self.run_until_shutdown(shutdown_rx).await
164 }
165
166 pub async fn run_until_shutdown(
168 &self,
169 shutdown_rx: watch::Receiver<bool>,
170 ) -> anyhow::Result<()> {
171 self.run_until_shutdown_with_listener_events(shutdown_rx, None)
172 .await
173 }
174
175 pub async fn run_until_shutdown_with_listener_events(
177 &self,
178 mut shutdown_rx: watch::Receiver<bool>,
179 listener_events: Option<ListenerBindEventSender>,
180 ) -> anyhow::Result<()> {
181 let listener = TcpListener::bind(&self.config.listen_addr)
182 .await
183 .map_err(|error| {
184 notify_listener_failed(&listener_events, ListenerSurface::Ftapi);
185 anyhow::Error::new(crate::bind_hint::io_bind_error(
186 "FTAPI TCP",
187 "--port",
188 &self.config.listen_addr,
189 error,
190 ))
191 })?;
192 tracing::info!(addr = %self.config.listen_addr, "API server listening");
193
194 let (disconnect_tx, mut disconnect_rx) = mpsc::unbounded_channel::<DisconnectNotify>();
198
199 let cleanup_connections = Arc::clone(&self.connections);
201 let cleanup_subs = self.subscriptions.clone();
202 let cleanup_metrics = Arc::clone(&self.metrics);
203 tokio::spawn(async move {
204 while let Some(notify) = disconnect_rx.recv().await {
205 let removed = cleanup_connections.remove(¬ify.conn_id);
206 if removed.is_some() {
207 cleanup_metrics
208 .total_disconnections
209 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
210 if let Some(ref subs) = cleanup_subs {
212 subs.on_disconnect(notify.conn_id);
213 }
214 tracing::info!(
215 conn_id = notify.conn_id,
216 remaining = cleanup_connections.len(),
217 "connection removed from pool"
218 );
219 }
220 }
221 });
222
223 let ka_connections = Arc::clone(&self.connections);
225 let ka_subs = self.subscriptions.clone();
226 let ka_metrics = Arc::clone(&self.metrics);
227 let mut ka_shutdown_rx = shutdown_rx.clone();
228 tokio::spawn(async move {
229 const CHECK_INTERVAL_SECS: u64 = 15;
230 const TIMEOUT_SECS: u64 = 66;
231 let mut interval =
232 tokio::time::interval(std::time::Duration::from_secs(CHECK_INTERVAL_SECS));
233 interval.tick().await; loop {
235 tokio::select! {
236 _ = shutdown_requested(&mut ka_shutdown_rx) => {
237 tracing::info!("API server keepalive task stopped by shutdown signal");
238 break;
239 }
240 _ = interval.tick() => {}
241 }
242 let now = Instant::now();
243 let mut timed_out = Vec::new();
244 for entry in ka_connections.iter() {
245 let conn = entry.value();
246 if now.duration_since(conn.last_keepalive).as_secs() >= TIMEOUT_SECS {
247 timed_out.push(conn.conn_id);
248 }
249 }
250 for conn_id in timed_out {
251 if ka_connections.remove(&conn_id).is_some() {
252 ka_metrics
253 .keepalive_timeouts
254 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
255 ka_metrics
256 .total_disconnections
257 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
258 if let Some(ref subs) = ka_subs {
259 subs.on_disconnect(conn_id);
260 }
261 tracing::info!(
262 conn_id = conn_id,
263 remaining = ka_connections.len(),
264 "keepalive timeout, connection removed"
265 );
266 }
267 }
268 }
269 });
270
271 let connections = Arc::clone(&self.connections);
273 let accept_metrics = Arc::clone(&self.metrics);
274 let _serving =
275 notify_listener_opened(&listener_events, ListenerSurface::Ftapi, &shutdown_rx).await?;
276 drop(listener_events);
277
278 loop {
279 let (stream, peer_addr) = tokio::select! {
280 _ = shutdown_requested(&mut shutdown_rx) => {
281 tracing::info!("API server accept loop stopped by shutdown signal");
282 break;
283 }
284 accepted = listener.accept() => accepted?,
285 };
286
287 if connections.len() >= MAX_CONNECTIONS {
288 accept_metrics
289 .rejected_connections
290 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
291 tracing::warn!(
292 peer = %peer_addr,
293 "max connections reached ({}), rejecting",
294 MAX_CONNECTIONS
295 );
296 drop(stream);
297 continue;
298 }
299
300 accept_metrics
301 .total_connections
302 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
303
304 let conn_id = crate::conn::ClientConn::generate_conn_id();
305 let session_generation = crate::conn::ClientConn::generate_session_generation();
306 let aes_key = crate::conn::ClientConn::generate_aes_key();
307 set_nodelay_with_log(&stream, peer_addr, "tcp");
308
309 tracing::info!(
310 conn_id = conn_id,
311 peer = %peer_addr,
312 total = connections.len() + 1,
313 "client connected"
314 );
315
316 let (req_tx, req_rx) =
317 mpsc::channel::<IncomingRequest>(PER_CONNECTION_REQUEST_QUEUE_CAPACITY);
318 let (tx, close_control, io_start) = crate::conn::prepare_connection_with_close_control(
319 stream,
320 conn_id,
321 aes_key,
322 req_tx,
323 disconnect_tx.clone(),
324 shutdown_rx.clone(),
325 )
326 .await;
327
328 let conn = ClientConn {
329 conn_id,
330 session_generation,
331 state: ConnState::Connected,
332 aes_key,
333 aes_encrypt_enabled: false,
334 proto_fmt_type: ProtoFmtType::Protobuf,
335 last_keepalive: Instant::now(),
336 recv_notify: false,
337 ai_type: 0,
338 keepalive_count: std::sync::atomic::AtomicU32::new(0),
339 tx,
340 key_id: None,
343 scopes: std::collections::HashSet::new(),
344 allowed_markets: None,
347 allowed_acc_ids: None,
349 };
350
351 connections.insert(conn_id, conn);
352 if let Some(ref subscriptions) = self.subscriptions {
353 subscriptions.on_connect(conn_id, session_generation);
354 subscriptions.register_client_close_control(conn_id, close_control);
355 }
356 tokio::spawn(process_requests(
357 req_rx,
358 Arc::clone(&connections),
359 Arc::clone(&self.router),
360 self.subscriptions.clone(),
361 self.config.clone(),
362 Arc::clone(&self.metrics),
363 Arc::clone(&self.server_time_store),
364 peer_addr.ip().is_loopback(),
365 ));
366 io_start.start();
367 }
368
369 Ok(())
370 }
371
372 pub async fn send_response(
374 connections: &DashMap<u64, ClientConn>,
375 conn_id: u64,
376 proto_id: u32,
377 serial_no: u32,
378 body: Vec<u8>,
379 ) -> bool {
380 if let Some(conn) = connections.get(&conn_id) {
381 let frame = conn.make_frame(proto_id, serial_no, bytes::Bytes::from(body));
382 if conn.tx.send(frame).await.is_err() {
383 tracing::warn!(
384 conn_id = conn_id,
385 "failed to send response, connection closed"
386 );
387 return false;
388 }
389 return true;
390 }
391 false
392 }
393}
394
395pub(crate) fn reconcile_post_dispatch_connection_state(
396 connections: &DashMap<u64, ClientConn>,
397 subscriptions: Option<&crate::subscription::SubscriptionManager>,
398 conn_id: u64,
399) {
400 if connections.contains_key(&conn_id) {
401 return;
402 }
403
404 if let Some(subscriptions) = subscriptions {
405 let became_empty = subscriptions.on_disconnect(conn_id);
406 tracing::debug!(
407 conn_id,
408 became_empty_count = became_empty.len(),
409 "reconciled subscription state committed after connection disconnect"
410 );
411 }
412}
413
414async fn process_requests(
416 mut req_rx: mpsc::Receiver<IncomingRequest>,
417 connections: Arc<DashMap<u64, ClientConn>>,
418 router: Arc<RequestRouter>,
419 subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
420 config: ServerConfig,
421 metrics: Arc<GatewayMetrics>,
422 server_time_store: Arc<ServerTimeAnchorStore>,
423 peer_is_loopback: bool,
424) {
425 let mut pending_init_connect: Option<tokio::task::JoinHandle<()>> = None;
426 while let Some(mut req) = req_rx.recv().await {
427 req.caller_is_loopback = peer_is_loopback;
428 req.caller_legacy_local_mode = true;
429 let conn_id = req.conn_id;
430 let Some(session_generation) = connections
431 .get(&conn_id)
432 .map(|conn| conn.session_generation)
433 else {
434 continue;
435 };
436 req.session_generation = session_generation;
437 let proto_id_val = req.proto_id;
438 let serial_no = req.serial_no;
439 let req_start = Instant::now();
440
441 metrics
442 .total_requests
443 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
444
445 if let Some(mut conn) = connections.get_mut(&conn_id) {
447 conn.last_keepalive = Instant::now();
448 }
449
450 if futu_auth::is_internal_proto_id(proto_id_val) {
455 metrics
456 .total_request_errors
457 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
458 tracing::warn!(
459 conn_id,
460 proto_id = proto_id_val,
461 "rejecting daemon-internal proto_id at raw TCP public surface (audit 0532 F3)"
462 );
463 continue;
464 }
465
466 if proto_id_val != proto_id::INIT_CONNECT
467 && connections
468 .get(&conn_id)
469 .is_some_and(|conn| conn.state == crate::conn::ConnState::Connected)
470 && !crate::identity::StartupReadiness::is_prelogin_proto(proto_id_val)
471 {
472 if router.startup_readiness().snapshot().state != crate::identity::StartupState::Ready {
473 if let Some(body) = router.dispatch(conn_id, &req).await {
474 metrics
475 .total_response_bytes
476 .fetch_add(body.len() as u64, std::sync::atomic::Ordering::Relaxed);
477 if ApiServer::send_response(
478 connections.as_ref(),
479 conn_id,
480 proto_id_val,
481 serial_no,
482 body,
483 )
484 .await
485 {
486 req.mark_response_committed();
487 }
488 }
489 continue;
490 }
491 let Some(pending) = pending_init_connect.take() else {
492 tracing::warn!(
493 conn_id,
494 proto_id = proto_id_val,
495 "dropping request from Ready connection with no completed InitConnect"
496 );
497 continue;
498 };
499 if let Err(error) = pending.await {
500 tracing::warn!(conn_id, error = %error, "pending InitConnect task failed");
501 continue;
502 }
503 if connections
504 .get(&conn_id)
505 .is_none_or(|conn| conn.state == crate::conn::ConnState::Connected)
506 {
507 continue;
508 }
509 }
510
511 if proto_id_val != proto_id::INIT_CONNECT
513 && let Some(conn) = connections.get(&conn_id)
514 && conn.aes_encrypt_enabled
515 {
516 match conn.decrypt_body(&req.body) {
517 Ok(decrypted) => {
518 req.body = bytes::Bytes::from(decrypted);
519 }
520 Err(e) => {
521 metrics
522 .total_request_errors
523 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
524 tracing::warn!(
525 conn_id = conn_id,
526 proto_id = proto_id_val,
527 error = %e,
528 "AES decrypt request failed, dropping"
529 );
530 continue;
531 }
532 }
533 }
534
535 let response_body =
537 futu_core::delay_stats::with_api_request(conn_id, serial_no, proto_id_val, || async {
538 match proto_id_val {
539 proto_id::INIT_CONNECT => match crate::conn::start_init_connect_for_startup(
540 Arc::clone(&connections),
541 router.startup_readiness().clone(),
542 conn_id,
543 &req.body,
544 serial_no,
545 config.server_ver,
546 config.keepalive_interval,
547 config.rsa_private_key.clone(),
548 ) {
549 Ok(crate::conn::InitConnectStart::Immediate(prepared)) => {
550 if let Err(error) = crate::conn::complete_prepared_init_connect(
551 connections.as_ref(),
552 conn_id,
553 serial_no,
554 prepared,
555 )
556 .await
557 {
558 metrics
559 .total_request_errors
560 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
561 tracing::warn!(conn_id, error = %error, "InitConnect response failed");
562 }
563 None
564 }
565 Ok(crate::conn::InitConnectStart::Deferred(task)) => {
566 if let Some(previous) = pending_init_connect.replace(task) {
567 previous.abort();
568 }
569 None
570 }
571 Err(error) => {
572 metrics
573 .total_request_errors
574 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
575 tracing::warn!(
576 conn_id,
577 proto_id = proto_id_val,
578 error = %error,
579 "InitConnect handling failed"
580 );
581 None
582 }
583 },
584 proto_id::KEEP_ALIVE => match connections.get(&conn_id) {
585 Some(conn) => match conn
586 .handle_keepalive_at(&req.body, server_now_ts(&server_time_store))
587 {
588 Ok(body) => Some(body),
589 Err(error) => {
590 metrics
591 .total_request_errors
592 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
593 tracing::warn!(
594 conn_id,
595 proto_id = proto_id_val,
596 error = %error,
597 "KeepAlive handling failed"
598 );
599 None
600 }
601 },
602 None => {
603 metrics
604 .total_request_errors
605 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
606 tracing::warn!(
607 conn_id,
608 proto_id = proto_id_val,
609 "KeepAlive request received for missing connection"
610 );
611 None
612 }
613 },
614 _ => {
615 router.dispatch(conn_id, &req).await
617 }
618 }
619 })
620 .await;
621
622 metrics.record_latency_ns(req_start.elapsed().as_nanos() as u64);
624
625 reconcile_post_dispatch_connection_state(&connections, subscriptions.as_deref(), conn_id);
626
627 if let Some(body) = response_body {
629 metrics
630 .total_response_bytes
631 .fetch_add(body.len() as u64, std::sync::atomic::Ordering::Relaxed);
632 if ApiServer::send_response(&connections, conn_id, proto_id_val, serial_no, body).await
633 {
634 req.mark_response_committed();
635 }
636 } else if proto_id_val != proto_id::INIT_CONNECT && proto_id_val != proto_id::KEEP_ALIVE {
637 metrics
638 .total_request_errors
639 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
640 }
641 }
642 if let Some(pending) = pending_init_connect {
643 pending.abort();
644 }
645}
646
647#[cfg(test)]
648mod tests;