1use std::sync::Arc;
4use std::sync::atomic::{AtomicI64, Ordering};
5use std::time::Instant;
6
7use dashmap::DashMap;
8use tokio::net::TcpListener;
9use tokio::sync::{mpsc, watch};
10
11use futu_codec::header::ProtoFmtType;
12use futu_core::proto_id;
13
14use crate::conn::{ClientConn, ConnState, DisconnectNotify, IncomingRequest};
15use crate::metrics::GatewayMetrics;
16use crate::router::RequestRouter;
17
18pub const MAX_CONNECTIONS: usize = 128;
20
21pub(crate) const REQUEST_QUEUE_CAPACITY: usize = 4096;
26
27#[derive(Debug, Clone)]
29pub struct ServerConfig {
30 pub listen_addr: String,
32 pub server_ver: i32,
34 pub login_user_id: u64,
36 pub keepalive_interval: i32,
38 pub rsa_private_key: Option<String>,
40}
41
42#[must_use]
43pub(crate) fn default_server_time_offset_secs() -> Arc<AtomicI64> {
44 Arc::new(AtomicI64::new(0))
45}
46
47#[must_use]
48pub(crate) fn server_now_ts_at(server_time_offset_secs: &AtomicI64, local_now_ts: i64) -> i64 {
49 local_now_ts.saturating_add(server_time_offset_secs.load(Ordering::Relaxed))
50}
51
52#[must_use]
53pub(crate) fn server_now_ts(server_time_offset_secs: &AtomicI64) -> i64 {
54 server_now_ts_at(server_time_offset_secs, chrono::Utc::now().timestamp())
55}
56
57pub(crate) fn set_nodelay_with_log(
58 stream: &tokio::net::TcpStream,
59 peer_addr: std::net::SocketAddr,
60 surface: &'static str,
61) {
62 if let Err(error) = stream.set_nodelay(true) {
63 tracing::debug!(
64 peer = %peer_addr,
65 surface,
66 error = %error,
67 "tcp nodelay setup failed"
68 );
69 }
70}
71
72pub(crate) async fn shutdown_requested(shutdown_rx: &mut watch::Receiver<bool>) {
73 loop {
74 if *shutdown_rx.borrow() {
75 return;
76 }
77 if shutdown_rx.changed().await.is_err() {
78 return;
79 }
80 }
81}
82
83pub struct ApiServer {
85 config: ServerConfig,
86 connections: Arc<DashMap<u64, ClientConn>>,
87 router: Arc<RequestRouter>,
88 subscriptions: Option<Arc<crate::subscription::SubscriptionManager>>,
89 metrics: Arc<GatewayMetrics>,
90 server_time_offset_secs: Arc<AtomicI64>,
91}
92
93impl ApiServer {
94 pub fn new(config: ServerConfig) -> Self {
96 Self {
97 config,
98 connections: Arc::new(DashMap::new()),
99 router: Arc::new(RequestRouter::new()),
100 subscriptions: None,
101 metrics: Arc::new(GatewayMetrics::new()),
102 server_time_offset_secs: default_server_time_offset_secs(),
103 }
104 }
105
106 pub fn set_subscriptions(&mut self, subs: Arc<crate::subscription::SubscriptionManager>) {
108 self.subscriptions = Some(subs);
109 }
110
111 pub fn router(&self) -> &Arc<RequestRouter> {
113 &self.router
114 }
115
116 pub fn connections(&self) -> &Arc<DashMap<u64, ClientConn>> {
118 &self.connections
119 }
120
121 pub fn set_metrics(&mut self, metrics: Arc<GatewayMetrics>) {
123 self.metrics = metrics;
124 }
125
126 pub fn set_server_time_offset_secs(&mut self, offset: Arc<AtomicI64>) {
128 self.server_time_offset_secs = offset;
129 }
130
131 pub fn metrics(&self) -> &Arc<GatewayMetrics> {
133 &self.metrics
134 }
135
136 pub async fn run(&self) -> anyhow::Result<()> {
138 let (_shutdown_tx, shutdown_rx) = watch::channel(false);
139 self.run_until_shutdown(shutdown_rx).await
140 }
141
142 pub async fn run_until_shutdown(
144 &self,
145 mut shutdown_rx: watch::Receiver<bool>,
146 ) -> anyhow::Result<()> {
147 let listener = TcpListener::bind(&self.config.listen_addr)
148 .await
149 .map_err(|error| {
150 anyhow::anyhow!(
151 "{}",
152 crate::bind_hint::bind_error_message(
153 "FTAPI TCP",
154 "--port",
155 &self.config.listen_addr,
156 error
157 )
158 )
159 })?;
160 tracing::info!(addr = %self.config.listen_addr, "API server listening");
161
162 let (req_tx, req_rx) = mpsc::channel::<IncomingRequest>(REQUEST_QUEUE_CAPACITY);
163 let (disconnect_tx, mut disconnect_rx) = mpsc::unbounded_channel::<DisconnectNotify>();
167
168 let connections = Arc::clone(&self.connections);
170 let router = Arc::clone(&self.router);
171 let config = self.config.clone();
172 let metrics = Arc::clone(&self.metrics);
173 let server_time_offset_secs = Arc::clone(&self.server_time_offset_secs);
174 tokio::spawn(async move {
175 process_requests(
176 req_rx,
177 connections,
178 router,
179 config,
180 metrics,
181 server_time_offset_secs,
182 )
183 .await;
184 });
185
186 let cleanup_connections = Arc::clone(&self.connections);
188 let cleanup_subs = self.subscriptions.clone();
189 let cleanup_metrics = Arc::clone(&self.metrics);
190 tokio::spawn(async move {
191 while let Some(notify) = disconnect_rx.recv().await {
192 let removed = cleanup_connections.remove(¬ify.conn_id);
193 if removed.is_some() {
194 cleanup_metrics
195 .total_disconnections
196 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
197 if let Some(ref subs) = cleanup_subs {
199 subs.on_disconnect(notify.conn_id);
200 }
201 tracing::info!(
202 conn_id = notify.conn_id,
203 remaining = cleanup_connections.len(),
204 "connection removed from pool"
205 );
206 }
207 }
208 });
209
210 let ka_connections = Arc::clone(&self.connections);
212 let ka_subs = self.subscriptions.clone();
213 let ka_metrics = Arc::clone(&self.metrics);
214 let mut ka_shutdown_rx = shutdown_rx.clone();
215 tokio::spawn(async move {
216 const CHECK_INTERVAL_SECS: u64 = 15;
217 const TIMEOUT_SECS: u64 = 66;
218 let mut interval =
219 tokio::time::interval(std::time::Duration::from_secs(CHECK_INTERVAL_SECS));
220 interval.tick().await; loop {
222 tokio::select! {
223 _ = shutdown_requested(&mut ka_shutdown_rx) => {
224 tracing::info!("API server keepalive task stopped by shutdown signal");
225 break;
226 }
227 _ = interval.tick() => {}
228 }
229 let now = Instant::now();
230 let mut timed_out = Vec::new();
231 for entry in ka_connections.iter() {
232 let conn = entry.value();
233 if now.duration_since(conn.last_keepalive).as_secs() >= TIMEOUT_SECS {
234 timed_out.push(conn.conn_id);
235 }
236 }
237 for conn_id in timed_out {
238 if ka_connections.remove(&conn_id).is_some() {
239 ka_metrics
240 .keepalive_timeouts
241 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
242 ka_metrics
243 .total_disconnections
244 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
245 if let Some(ref subs) = ka_subs {
246 subs.on_disconnect(conn_id);
247 }
248 tracing::info!(
249 conn_id = conn_id,
250 remaining = ka_connections.len(),
251 "keepalive timeout, connection removed"
252 );
253 }
254 }
255 }
256 });
257
258 let connections = Arc::clone(&self.connections);
260 let accept_metrics = Arc::clone(&self.metrics);
261 loop {
262 let (stream, peer_addr) = tokio::select! {
263 _ = shutdown_requested(&mut shutdown_rx) => {
264 tracing::info!("API server accept loop stopped by shutdown signal");
265 break;
266 }
267 accepted = listener.accept() => accepted?,
268 };
269
270 if connections.len() >= MAX_CONNECTIONS {
271 accept_metrics
272 .rejected_connections
273 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
274 tracing::warn!(
275 peer = %peer_addr,
276 "max connections reached ({}), rejecting",
277 MAX_CONNECTIONS
278 );
279 drop(stream);
280 continue;
281 }
282
283 accept_metrics
284 .total_connections
285 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
286
287 let conn_id = crate::conn::ClientConn::generate_conn_id();
288 let aes_key = crate::conn::ClientConn::generate_aes_key();
289 set_nodelay_with_log(&stream, peer_addr, "tcp");
290
291 tracing::info!(
292 conn_id = conn_id,
293 peer = %peer_addr,
294 total = connections.len() + 1,
295 "client connected"
296 );
297
298 let tx = crate::conn::run_connection(
299 stream,
300 conn_id,
301 aes_key,
302 req_tx.clone(),
303 disconnect_tx.clone(),
304 shutdown_rx.clone(),
305 )
306 .await;
307
308 let conn = ClientConn {
309 conn_id,
310 state: ConnState::Connected,
311 aes_key,
312 aes_encrypt_enabled: false,
313 proto_fmt_type: ProtoFmtType::Protobuf,
314 last_keepalive: Instant::now(),
315 recv_notify: false,
316 ai_type: 0,
317 keepalive_count: std::sync::atomic::AtomicU32::new(0),
318 tx,
319 key_id: None,
322 scopes: std::collections::HashSet::new(),
323 allowed_markets: None,
326 allowed_acc_ids: None,
328 };
329
330 connections.insert(conn_id, conn);
331 }
332
333 Ok(())
334 }
335
336 pub async fn send_response(
338 connections: &DashMap<u64, ClientConn>,
339 conn_id: u64,
340 proto_id: u32,
341 serial_no: u32,
342 body: Vec<u8>,
343 ) {
344 if let Some(conn) = connections.get(&conn_id) {
345 let frame = conn.make_frame(proto_id, serial_no, bytes::Bytes::from(body));
346 if conn.tx.send(frame).await.is_err() {
347 tracing::warn!(
348 conn_id = conn_id,
349 "failed to send response, connection closed"
350 );
351 }
352 }
353 }
354}
355
356async fn process_requests(
358 mut req_rx: mpsc::Receiver<IncomingRequest>,
359 connections: Arc<DashMap<u64, ClientConn>>,
360 router: Arc<RequestRouter>,
361 config: ServerConfig,
362 metrics: Arc<GatewayMetrics>,
363 server_time_offset_secs: Arc<AtomicI64>,
364) {
365 while let Some(mut req) = req_rx.recv().await {
366 let conn_id = req.conn_id;
367 let proto_id_val = req.proto_id;
368 let serial_no = req.serial_no;
369 let req_start = Instant::now();
370
371 metrics
372 .total_requests
373 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
374
375 if let Some(mut conn) = connections.get_mut(&conn_id) {
377 conn.last_keepalive = Instant::now();
378 }
379
380 if futu_auth::is_internal_proto_id(proto_id_val) {
385 metrics
386 .total_request_errors
387 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
388 tracing::warn!(
389 conn_id,
390 proto_id = proto_id_val,
391 "rejecting daemon-internal proto_id at raw TCP public surface (audit 0532 F3)"
392 );
393 continue;
394 }
395
396 if proto_id_val != proto_id::INIT_CONNECT
398 && let Some(conn) = connections.get(&conn_id)
399 && conn.aes_encrypt_enabled
400 {
401 match conn.decrypt_body(&req.body) {
402 Ok(decrypted) => {
403 req.body = bytes::Bytes::from(decrypted);
404 }
405 Err(e) => {
406 metrics
407 .total_request_errors
408 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
409 tracing::warn!(
410 conn_id = conn_id,
411 proto_id = proto_id_val,
412 error = %e,
413 "AES decrypt request failed, dropping"
414 );
415 continue;
416 }
417 }
418 }
419
420 let response_body =
422 futu_core::delay_stats::with_api_request(conn_id, serial_no, proto_id_val, || async {
423 match proto_id_val {
424 proto_id::INIT_CONNECT => match connections.get_mut(&conn_id) {
425 Some(mut conn) => match conn.handle_init_connect(
426 &req.body,
427 config.server_ver,
428 config.login_user_id,
429 config.keepalive_interval,
430 config.rsa_private_key.as_deref(),
431 ) {
432 Ok(body) => Some(body),
433 Err(error) => {
434 metrics
435 .total_request_errors
436 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
437 tracing::warn!(
438 conn_id,
439 proto_id = proto_id_val,
440 error = %error,
441 "InitConnect handling failed"
442 );
443 None
444 }
445 },
446 None => {
447 metrics
448 .total_request_errors
449 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
450 tracing::warn!(
451 conn_id,
452 proto_id = proto_id_val,
453 "InitConnect request received for missing connection"
454 );
455 None
456 }
457 },
458 proto_id::KEEP_ALIVE => match connections.get(&conn_id) {
459 Some(conn) => match conn
460 .handle_keepalive_at(&req.body, server_now_ts(&server_time_offset_secs))
461 {
462 Ok(body) => Some(body),
463 Err(error) => {
464 metrics
465 .total_request_errors
466 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
467 tracing::warn!(
468 conn_id,
469 proto_id = proto_id_val,
470 error = %error,
471 "KeepAlive handling failed"
472 );
473 None
474 }
475 },
476 None => {
477 metrics
478 .total_request_errors
479 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
480 tracing::warn!(
481 conn_id,
482 proto_id = proto_id_val,
483 "KeepAlive request received for missing connection"
484 );
485 None
486 }
487 },
488 _ => {
489 router.dispatch(conn_id, &req).await
491 }
492 }
493 })
494 .await;
495
496 metrics.record_latency_ns(req_start.elapsed().as_nanos() as u64);
498
499 if let Some(body) = response_body {
501 metrics
502 .total_response_bytes
503 .fetch_add(body.len() as u64, std::sync::atomic::Ordering::Relaxed);
504 ApiServer::send_response(&connections, conn_id, proto_id_val, serial_no, body).await;
505 } else if proto_id_val != proto_id::INIT_CONNECT && proto_id_val != proto_id::KEEP_ALIVE {
506 metrics
507 .total_request_errors
508 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
509 }
510 }
511}
512
513#[cfg(test)]
514mod tests;