1use std::sync::Arc;
2use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
3use std::time::Duration;
4
5use bytes::Bytes;
6use dashmap::DashMap;
7use tokio::sync::{mpsc, oneshot};
8
9use futu_codec::frame::FutuFrame;
10use futu_core::error::{FutuError, Result};
11use futu_core::proto_id;
12
13use crate::connection::Connection;
14use crate::encrypt;
15use crate::reconnect::ReconnectPolicy;
16
17const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
20const DEFAULT_KEEP_ALIVE_INTERVAL_SECS: u64 = 10;
23const MIN_KEEP_ALIVE_INTERVAL_SECS: u64 = 1;
24const MAX_KEEP_ALIVE_INTERVAL_SECS: u64 = 60;
25const MAX_HEARTBEAT_SEND_FAILURES: u32 = 3;
28pub(crate) const PUSH_CHANNEL_CAPACITY: usize = 4096;
31
32fn sanitize_keep_alive_interval_secs(raw: i32) -> u64 {
33 if raw <= 0 {
34 return DEFAULT_KEEP_ALIVE_INTERVAL_SECS;
35 }
36 (raw as u64).clamp(MIN_KEEP_ALIVE_INTERVAL_SECS, MAX_KEEP_ALIVE_INTERVAL_SECS)
37}
38
39fn heartbeat_send_failure_should_exit(consecutive_failures: &mut u32, result: &Result<()>) -> bool {
40 if result.is_ok() {
41 *consecutive_failures = 0;
42 return false;
43 }
44
45 *consecutive_failures = consecutive_failures.saturating_add(1);
46 *consecutive_failures >= MAX_HEARTBEAT_SEND_FAILURES
47}
48
49fn init_connect_server_err(ret_type: i32, ret_msg: Option<String>) -> FutuError {
50 let msg = match ret_msg {
51 Some(msg) if !msg.is_empty() => msg,
52 _ => "<missing ret_msg>".to_string(),
53 };
54 FutuError::ServerError { ret_type, msg }
55}
56
57#[derive(Debug, Clone)]
59pub struct ClientConfig {
60 pub addr: String,
61 pub client_ver: String,
62 pub client_id: String,
63 pub recv_notify: bool,
64 pub rsa_key: Option<String>,
69}
70
71#[derive(Debug, Clone)]
73pub struct PushMessage {
74 pub proto_id: u32,
75 pub body: Bytes,
76}
77
78type PushSender = mpsc::Sender<PushMessage>;
79pub type PushReceiver = mpsc::Receiver<PushMessage>;
80
81struct PendingRequest {
83 tx: oneshot::Sender<FutuFrame>,
84}
85
86pub struct FutuClient {
95 config: ClientConfig,
96 serial_no: AtomicU32,
97 pending: Arc<DashMap<u32, PendingRequest>>,
98 push_tx: PushSender,
99 cmd_tx: Option<mpsc::Sender<ClientCommand>>,
100 conn_aes_key: parking_lot::Mutex<Option<[u8; 16]>>,
101 conn_id: AtomicU64,
102 init_ai_type: Option<i32>,
103}
104
105enum ClientCommand {
106 Send(FutuFrame, oneshot::Sender<()>),
107}
108
109fn decode_client_inbound_body(frame: &FutuFrame, aes_key: Option<&[u8; 16]>) -> Result<Bytes> {
110 let body = match aes_key {
111 Some(key) if !frame.body.is_empty() => {
112 encrypt::aes_ecb_decrypt(key, &frame.body).map(Bytes::from)
113 }
114 _ => Ok(frame.body.clone()),
115 }?;
116 verify_plaintext_body_sha1(frame, body)
117}
118
119fn verify_plaintext_body_sha1(frame: &FutuFrame, plaintext: Bytes) -> Result<Bytes> {
120 let plaintext_frame = FutuFrame {
121 header: frame.header.clone(),
122 body: plaintext.clone(),
123 };
124 if plaintext_frame.verify_sha1() {
125 Ok(plaintext)
126 } else {
127 Err(FutuError::Sha1Mismatch)
128 }
129}
130
131fn build_init_connect_request(
132 config: &ClientConfig,
133 ai_type: Option<i32>,
134) -> futu_proto::init_connect::Request {
135 futu_proto::init_connect::Request {
136 c2s: futu_proto::init_connect::C2s {
137 client_ver: 100,
138 client_id: config.client_id.clone(),
139 recv_notify: Some(config.recv_notify),
140 packet_enc_algo: Some(0),
141 push_proto_fmt: Some(0),
142 programming_language: Some(String::new()),
143 ai_type,
144 },
145 }
146}
147
148fn parse_conn_aes_key(conn_aes_key: &str) -> Result<[u8; 16]> {
149 let key_bytes = conn_aes_key.as_bytes();
150 match key_bytes.len() {
151 16 => {
152 let mut key = [0u8; 16];
153 key.copy_from_slice(key_bytes);
154 Ok(key)
155 }
156 32 => hex_decode_16(key_bytes)
157 .ok_or_else(|| FutuError::Codec("invalid hex InitConnect AES key".into())),
158 len => Err(FutuError::Codec(format!(
159 "unexpected AES key length: {len}"
160 ))),
161 }
162}
163
164fn release_pending_on_inbound_decode_error(
165 frame: &FutuFrame,
166 pending: &DashMap<u32, PendingRequest>,
167) {
168 if !proto_id::is_push_proto(frame.header.proto_id) {
169 pending.remove(&frame.header.serial_no);
170 }
171}
172
173fn push_channel() -> (PushSender, PushReceiver) {
174 mpsc::channel(PUSH_CHANNEL_CAPACITY)
175}
176
177fn send_push_message(push_tx: &PushSender, message: PushMessage) -> bool {
178 let proto_id = message.proto_id;
179 match push_tx.try_send(message) {
180 Ok(()) => true,
181 Err(mpsc::error::TrySendError::Full(_)) => {
182 tracing::warn!(
183 proto_id,
184 capacity = PUSH_CHANNEL_CAPACITY,
185 "push receiver is slow; dropping push message from bounded client queue"
186 );
187 false
188 }
189 Err(mpsc::error::TrySendError::Closed(_)) => {
190 tracing::debug!(proto_id, "push receiver dropped before delivery");
191 false
192 }
193 }
194}
195
196fn send_response_frame(tx: oneshot::Sender<FutuFrame>, frame: FutuFrame) -> bool {
197 let serial = frame.header.serial_no;
198 let proto_id = frame.header.proto_id;
199 match tx.send(frame) {
200 Ok(()) => true,
201 Err(_) => {
202 tracing::debug!(
203 serial,
204 proto_id,
205 "response receiver dropped before delivery"
206 );
207 false
208 }
209 }
210}
211
212fn send_command_ack(ack: oneshot::Sender<()>) -> bool {
213 match ack.send(()) {
214 Ok(()) => true,
215 Err(_) => {
216 tracing::debug!("command ack receiver dropped before delivery");
217 false
218 }
219 }
220}
221
222impl FutuClient {
223 pub fn new(config: ClientConfig) -> (Self, PushReceiver) {
227 let (push_tx, push_rx) = push_channel();
228
229 let client = Self {
230 config,
231 serial_no: AtomicU32::new(0),
232 pending: Arc::new(DashMap::new()),
233 push_tx,
234 cmd_tx: None,
235 conn_aes_key: parking_lot::Mutex::new(None),
236 conn_id: AtomicU64::new(0),
237 init_ai_type: None,
238 };
239
240 (client, push_rx)
241 }
242
243 pub fn with_init_ai_type(mut self, ai_type: Option<i32>) -> Self {
248 self.init_ai_type = ai_type;
249 self
250 }
251
252 pub async fn connect(&mut self) -> Result<InitConnectInfo> {
254 let mut conn = Connection::connect(&self.config.addr).await?;
255
256 let serial = self.next_serial();
258 let req = build_init_connect_request(&self.config, self.init_ai_type);
259 let raw_body = prost::Message::encode_to_vec(&req);
260
261 let send_body = if let Some(ref rsa_key) = self.config.rsa_key {
263 tracing::debug!("encrypting InitConnect with RSA");
264 encrypt::rsa_public_encrypt(rsa_key, &raw_body)?
265 } else {
266 raw_body.clone()
267 };
268
269 let mut frame = Connection::build_frame(proto_id::INIT_CONNECT, serial, send_body);
271 {
272 use sha1::{Digest, Sha1};
273 let mut hasher = Sha1::new();
274 hasher.update(&raw_body);
275 let hash = hasher.finalize();
276 frame.header.body_sha1.copy_from_slice(&hash);
277 }
278 conn.send(frame).await?;
279
280 let resp_frame = conn.recv().await?.ok_or(FutuError::Codec(
282 "connection closed during InitConnect".into(),
283 ))?;
284
285 let resp_body = if let Some(ref rsa_key) = self.config.rsa_key {
287 tracing::debug!("decrypting InitConnect response with RSA");
288 Bytes::from(encrypt::rsa_private_decrypt(rsa_key, &resp_frame.body)?)
289 } else {
290 resp_frame.body.clone()
291 };
292 let resp_body = verify_plaintext_body_sha1(&resp_frame, resp_body)?;
293
294 let resp: futu_proto::init_connect::Response =
295 prost::Message::decode(resp_body.as_ref()).map_err(FutuError::Proto)?;
296
297 let ret_type = resp.ret_type;
299 if ret_type != 0 {
300 return Err(init_connect_server_err(ret_type, resp.ret_msg));
301 }
302
303 let s2c = resp.s2c.ok_or(FutuError::Codec(
304 "missing s2c in InitConnect response".into(),
305 ))?;
306
307 let info = InitConnectInfo {
308 server_ver: s2c.server_ver,
309 login_user_id: s2c.login_user_id,
310 conn_id: s2c.conn_id,
311 conn_aes_key: s2c.conn_aes_key.clone(),
312 keep_alive_interval: s2c.keep_alive_interval,
313 };
314 self.conn_id.store(info.conn_id, Ordering::Relaxed);
315
316 if !info.conn_aes_key.is_empty() {
318 let key_bytes = info.conn_aes_key.as_bytes();
319 tracing::debug!(key_len = key_bytes.len(), "received AES key");
320 *self.conn_aes_key.lock() = Some(parse_conn_aes_key(&info.conn_aes_key)?);
321 }
322
323 tracing::info!(
324 server_ver = info.server_ver,
325 conn_id = info.conn_id,
326 keep_alive_interval = info.keep_alive_interval,
327 "InitConnect succeeded"
328 );
329
330 let keep_alive_interval_secs = sanitize_keep_alive_interval_secs(info.keep_alive_interval);
335 if info.keep_alive_interval <= 0
336 || keep_alive_interval_secs != info.keep_alive_interval as u64
337 {
338 tracing::warn!(
339 raw_keep_alive_interval = info.keep_alive_interval,
340 sanitized_keep_alive_interval = keep_alive_interval_secs,
341 "sanitized InitConnect keep_alive_interval"
342 );
343 }
344 let keep_alive_interval = Duration::from_secs(keep_alive_interval_secs);
345 self.start_background_tasks(conn, keep_alive_interval);
346
347 Ok(info)
348 }
349
350 pub async fn request(&self, proto_id: u32, body: Vec<u8>) -> Result<FutuFrame> {
352 self.request_with_timeout(proto_id, body, DEFAULT_REQUEST_TIMEOUT)
353 .await
354 }
355
356 pub async fn request_with_timeout(
362 &self,
363 proto_id: u32,
364 body: Vec<u8>,
365 timeout: Duration,
366 ) -> Result<FutuFrame> {
367 let deadline = tokio::time::Instant::now() + timeout;
368 let serial = self.next_serial();
369
370 let (final_body, sha1) = self.prepare_body(&body);
372
373 let frame = FutuFrame::with_sha1(proto_id, serial, Bytes::from(final_body), sha1);
374
375 let (resp_tx, resp_rx) = oneshot::channel();
376 self.pending.insert(serial, PendingRequest { tx: resp_tx });
377
378 if let Some(cmd_tx) = &self.cmd_tx {
380 let (ack_tx, ack_rx) = oneshot::channel();
381 match tokio::time::timeout_at(deadline, cmd_tx.send(ClientCommand::Send(frame, ack_tx)))
382 .await
383 {
384 Ok(Ok(())) => {}
385 Ok(Err(_)) => {
386 self.pending.remove(&serial);
387 return Err(FutuError::NotInitialized);
388 }
389 Err(_) => {
390 self.pending.remove(&serial);
391 return Err(FutuError::Timeout);
392 }
393 }
394
395 match tokio::time::timeout_at(deadline, ack_rx).await {
396 Ok(Ok(())) => {}
397 Ok(Err(_)) => {
398 self.pending.remove(&serial);
399 return Err(FutuError::Codec("send ack failed".into()));
400 }
401 Err(_) => {
402 self.pending.remove(&serial);
403 return Err(FutuError::Timeout);
404 }
405 }
406 } else {
407 self.pending.remove(&serial);
408 return Err(FutuError::NotInitialized);
409 }
410
411 let resp = tokio::time::timeout_at(deadline, resp_rx)
413 .await
414 .map_err(|elapsed| {
415 self.pending.remove(&serial);
416 tracing::warn!(
417 serial,
418 error = %elapsed,
419 "request response wait timed out; pending entry removed"
420 );
421 FutuError::Timeout
422 })?
423 .map_err(|err| {
424 FutuError::Codec(format!(
425 "response channel closed for serial {serial}: {err}"
426 ))
427 })?;
428
429 Ok(resp)
430 }
431
432 pub fn conn_id(&self) -> Option<u64> {
437 let conn_id = self.conn_id.load(Ordering::Relaxed);
438 (conn_id != 0).then_some(conn_id)
439 }
440
441 fn next_serial(&self) -> u32 {
442 self.serial_no.fetch_add(1, Ordering::Relaxed) + 1
443 }
444
445 fn prepare_body(&self, plaintext: &[u8]) -> (Vec<u8>, [u8; 20]) {
446 use sha1::{Digest, Sha1};
447
448 let mut hasher = Sha1::new();
450 hasher.update(plaintext);
451 let sha1_result = hasher.finalize();
452 let mut sha1 = [0u8; 20];
453 sha1.copy_from_slice(&sha1_result);
454
455 let body = if self.config.rsa_key.is_some() {
457 let key = self.conn_aes_key.lock();
458 match key.as_ref() {
459 Some(k) => encrypt::aes_ecb_encrypt(k, plaintext),
460 None => plaintext.to_vec(),
461 }
462 } else {
463 plaintext.to_vec()
464 };
465
466 (body, sha1)
467 }
468
469 fn start_background_tasks(&mut self, conn: Connection, keep_alive_interval: Duration) {
470 let (cmd_tx, cmd_rx) = mpsc::channel(256);
471 self.cmd_tx = Some(cmd_tx.clone());
472
473 let pending = Arc::clone(&self.pending);
474 let push_tx = self.push_tx.clone();
475 let aes_key = if self.config.rsa_key.is_some() {
476 *self.conn_aes_key.lock()
477 } else {
478 None
479 };
480
481 tokio::spawn(async move {
482 run_event_loop(conn, cmd_rx, pending, push_tx, aes_key, keep_alive_interval).await;
483 });
484 }
485}
486
487async fn run_event_loop(
489 mut conn: Connection,
490 mut cmd_rx: mpsc::Receiver<ClientCommand>,
491 pending: Arc<DashMap<u32, PendingRequest>>,
492 push_tx: PushSender,
493 aes_key: Option<[u8; 16]>,
494 keep_alive_interval: Duration,
495) {
496 let mut heartbeat = tokio::time::interval(keep_alive_interval);
497 heartbeat.tick().await; let mut heartbeat_serial: u32 = 10_000_000; let mut heartbeat_send_failures = 0u32;
500
501 loop {
502 tokio::select! {
503 result = conn.recv() => {
505 match result {
506 Ok(Some(frame)) => {
507 let proto_id = frame.header.proto_id;
508 let body = match decode_client_inbound_body(&frame, aes_key.as_ref()) {
509 Ok(body) => body,
510 Err(e) => {
511 tracing::warn!(
512 error = %e,
513 serial = frame.header.serial_no,
514 proto_id,
515 "decrypt failed, dropping inbound frame"
516 );
517 release_pending_on_inbound_decode_error(&frame, &pending);
518 continue;
519 }
520 };
521
522 if proto_id::is_push_proto(proto_id) {
523 send_push_message(&push_tx, PushMessage { proto_id, body });
525 } else {
526 let serial = frame.header.serial_no;
528 match pending.remove(&serial) {
529 Some((_, req)) => {
530 let resp_frame = FutuFrame {
531 header: frame.header,
532 body,
533 };
534 send_response_frame(req.tx, resp_frame);
535 }
536 _ => {
537 tracing::debug!(
538 serial = serial,
539 proto_id = proto_id,
540 "unmatched response"
541 );
542 }
543 }
544 }
545 }
546 Ok(None) => {
547 tracing::warn!("connection closed by server");
548 break;
549 }
550 Err(e) => {
551 tracing::error!(error = %e, "recv error");
552 break;
553 }
554 }
555 }
556
557 cmd = cmd_rx.recv() => {
559 match cmd {
560 Some(ClientCommand::Send(frame, ack)) => {
561 let result = conn.send(frame).await;
562 if let Err(e) = &result {
563 tracing::error!(error = %e, "send failed");
564 }
565 send_command_ack(ack);
566 if result.is_err() {
567 break;
568 }
569 }
570 None => {
571 tracing::info!("shutting down event loop");
572 break;
573 }
574 }
575 }
576
577 _ = heartbeat.tick() => {
579 heartbeat_serial += 1;
580 let req = futu_proto::keep_alive::Request {
581 c2s: futu_proto::keep_alive::C2s {
582 time: chrono::Utc::now().timestamp(),
583 },
584 };
585 let body = prost::Message::encode_to_vec(&req);
586 let frame = Connection::build_frame(
587 proto_id::KEEP_ALIVE,
588 heartbeat_serial,
589 body,
590 );
591 let result = conn.send(frame).await;
592 if let Err(e) = &result {
593 tracing::warn!(
594 error = %e,
595 consecutive_failures = heartbeat_send_failures.saturating_add(1),
596 max_failures = MAX_HEARTBEAT_SEND_FAILURES,
597 "heartbeat send failed"
598 );
599 }
600 if heartbeat_send_failure_should_exit(&mut heartbeat_send_failures, &result) {
601 tracing::error!(
602 consecutive_failures = heartbeat_send_failures,
603 max_failures = MAX_HEARTBEAT_SEND_FAILURES,
604 "heartbeat send failure threshold reached; closing event loop"
605 );
606 break;
607 }
608 if result.is_ok() {
609 tracing::trace!("heartbeat sent");
610 }
611 }
612 }
613 }
614
615 pending.clear();
617 tracing::info!("event loop exited");
618}
619
620#[derive(Debug, Clone)]
622pub struct InitConnectInfo {
623 pub server_ver: i32,
624 pub login_user_id: u64,
625 pub conn_id: u64,
626 pub conn_aes_key: String,
627 pub keep_alive_interval: i32,
628}
629
630pub struct ReconnectingClient {
632 config: ClientConfig,
633 policy: ReconnectPolicy,
634 init_ai_type: Option<i32>,
635}
636
637impl ReconnectingClient {
638 pub fn new(config: ClientConfig) -> Self {
639 Self {
640 config,
641 policy: ReconnectPolicy::default_policy(),
642 init_ai_type: None,
643 }
644 }
645
646 pub fn with_policy(mut self, policy: ReconnectPolicy) -> Self {
647 self.policy = policy;
648 self
649 }
650
651 pub fn with_init_ai_type(mut self, ai_type: Option<i32>) -> Self {
656 self.init_ai_type = ai_type;
657 self
658 }
659
660 pub async fn connect(&mut self) -> Result<(FutuClient, PushReceiver, InitConnectInfo)> {
665 loop {
666 let (mut client, push_rx) = FutuClient::new(self.config.clone());
667 client = client.with_init_ai_type(self.init_ai_type);
668 match client.connect().await {
669 Ok(info) => {
670 self.policy.reset();
671 return Ok((client, push_rx, info));
672 }
673 Err(e) => {
674 tracing::warn!(
675 error = %e,
676 attempt = self.policy.attempts(),
677 "connection failed"
678 );
679 match self.policy.next_delay() {
680 Some(delay) => {
681 tracing::info!(delay_ms = delay.as_millis(), "reconnecting...");
682 tokio::time::sleep(delay).await;
683 }
684 None => {
685 return Err(FutuError::Codec(format!(
686 "max retries reached after {} attempts",
687 self.policy.attempts()
688 )));
689 }
690 }
691 }
692 }
693 }
694 }
695}
696
697#[cfg(test)]
698mod tests;
699
700fn hex_decode_16(hex_bytes: &[u8]) -> Option<[u8; 16]> {
701 if hex_bytes.len() != 32 {
702 return None;
703 }
704 let hex_str = std::str::from_utf8(hex_bytes).ok()?;
705 let mut key = [0u8; 16];
706 for i in 0..16 {
707 key[i] = u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16).ok()?;
708 }
709 Some(key)
710}