Skip to main content

futu_backend/auth/webtcp/
connect.rs

1use crate::conn::{BackendConn, BackendProtocolIdentity, PushCallback};
2use futu_core::error::{FutuError, Result};
3use futu_core::log_redact::endpoint_log_fingerprint;
4use rustls_pki_types::ServerName;
5use std::net::{IpAddr, SocketAddr};
6use std::sync::Arc;
7use std::sync::atomic::Ordering;
8use std::time::Duration;
9use tokio::net::TcpSocket;
10use tokio_rustls::TlsConnector;
11use tokio_rustls::rustls::{ClientConfig, RootCertStore};
12
13/// Ref: `FTLogin/Src/ftlogin/objc/F3CDefine.h:921-922`
14/// (`F3CLIHttpConfig` connect/total timeout defaults are 10s).
15const WEBTCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
16/// Ref: `FTLogin/Src/ftlogin/objc/F3CDefine.h:921-922`.
17/// Rust splits TCP connect and TLS handshake but keeps C++'s 10s WebTCP
18/// connection-phase bound for both steps.
19const WEBTCP_TLS_TIMEOUT: Duration = Duration::from_secs(10);
20/// Ref: `FTBasis/Src/ftbasis/protocol/tcp/impl/tcp_client_basic.cpp:330-342`.
21const WEBTCP_SEND_BUFFER_SIZE: u32 = 64 * 1024;
22/// Ref: `FTBasis/Src/ftbasis/protocol/tcp/impl/tcp_client_basic.cpp:330-342`.
23const WEBTCP_RECV_BUFFER_SIZE: u32 = 512 * 1024;
24
25/// C++ stores a `uint64_t` channel UID in the 32-bit NN protocol header with
26/// `static_cast<uint32_t>`, so values intentionally wrap modulo `2^32`.
27///
28/// Ref: `f3c/FTNet/Src/ftnet/channel/impl/protocol_header.cpp:45-69`.
29pub(super) const fn webtcp_header_user_id_like_cpp(user_id: u64) -> u32 {
30    user_id as u32
31}
32
33fn initialize_webtcp_header_user_id(conn: BackendConn, header_user_id: u64) -> BackendConn {
34    conn.user_id.store(
35        webtcp_header_user_id_like_cpp(header_user_id),
36        Ordering::Relaxed,
37    );
38    conn
39}
40
41/// rustls 0.23 requires an explicit process-level crypto provider when more
42/// than one provider feature is present. WebTCP uses rustls directly, so do
43/// this before the first `ClientConfig::builder()` call.
44pub fn install_default_rustls_crypto_provider() {
45    futu_core::http_client::install_default_rustls_crypto_provider();
46}
47
48/// C++ uses wildcard certificate domains; rustls needs a concrete certificate
49/// verification name, but WebTCP's C++ TLS path does not set SNI.
50pub(in crate::auth) fn tls_server_name_for_web_identity(identity: u32) -> &'static str {
51    match identity {
52        crate::auth::commconfig::CONN_WEB_CN | crate::auth::commconfig::CONN_WEB_HK => {
53            "www.futunn.com"
54        }
55        _ => "www.moomoo.com",
56    }
57}
58
59fn tls_connector() -> TlsConnector {
60    install_default_rustls_crypto_provider();
61
62    let roots = RootCertStore {
63        roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
64    };
65    let mut config = ClientConfig::builder()
66        .with_root_certificates(roots)
67        .with_no_client_auth();
68    // Ref: `FTBasis/Src/ftbasis/protocol/tcp/impl/tcp_client_basic.cpp:58-64`.
69    // C++ sets RFC2818 certificate verification on `ssl_certificate_domain`
70    // but has no `SSL_set_tlsext_host_name` call on the WebTCP TLS path.
71    config.enable_sni = false;
72    TlsConnector::from(Arc::new(config))
73}
74
75pub(in crate::auth) async fn connect_webtcp(
76    ip: &str,
77    port: u16,
78    tls_server_name: &'static str,
79    protocol_identity: BackendProtocolIdentity,
80    header_user_id: u64,
81) -> Result<BackendConn> {
82    let endpoint_fingerprint = endpoint_log_fingerprint(&format!("{ip}:{port}"));
83    let ip_addr = ip.parse::<IpAddr>().map_err(|_error| {
84        FutuError::Codec(format!(
85            "webtcp_invalid_ip endpoint_fingerprint={endpoint_fingerprint}"
86        ))
87    })?;
88    let addr = SocketAddr::new(ip_addr, port);
89    let socket = match addr {
90        SocketAddr::V4(_) => TcpSocket::new_v4(),
91        SocketAddr::V6(_) => TcpSocket::new_v6(),
92    }
93    .map_err(|error| {
94        FutuError::Network(std::io::Error::new(
95            error.kind(),
96            format!("webtcp_socket_create_failed endpoint_fingerprint={endpoint_fingerprint}"),
97        ))
98    })?;
99    // Ref: `FTBasis/Src/ftbasis/protocol/tcp/impl/tcp_client_basic.cpp:330-342`.
100    // C++ enables TCP_NODELAY and fixed socket buffers before connecting
101    // WebTCP sockets. Tokio exposes these portable socket options; TCP_MAXSEG,
102    // proxy, and NAT64 require platform/config plumbing and are intentionally
103    // not guessed here.
104    socket.set_nodelay(true).map_err(|error| {
105        FutuError::Network(std::io::Error::new(
106            error.kind(),
107            format!("webtcp_socket_config_failed endpoint_fingerprint={endpoint_fingerprint}"),
108        ))
109    })?;
110    socket
111        .set_send_buffer_size(WEBTCP_SEND_BUFFER_SIZE)
112        .map_err(|error| {
113            FutuError::Network(std::io::Error::new(
114                error.kind(),
115                format!("webtcp_socket_config_failed endpoint_fingerprint={endpoint_fingerprint}"),
116            ))
117        })?;
118    socket
119        .set_recv_buffer_size(WEBTCP_RECV_BUFFER_SIZE)
120        .map_err(|error| {
121            FutuError::Network(std::io::Error::new(
122                error.kind(),
123                format!("webtcp_socket_config_failed endpoint_fingerprint={endpoint_fingerprint}"),
124            ))
125        })?;
126
127    let stream = tokio::time::timeout(WEBTCP_CONNECT_TIMEOUT, socket.connect(addr))
128        .await
129        .map_err(|_elapsed| {
130            FutuError::Network(std::io::Error::new(
131                std::io::ErrorKind::TimedOut,
132                format!("webtcp_connect_timeout endpoint_fingerprint={endpoint_fingerprint}"),
133            ))
134        })?
135        .map_err(|error| {
136            FutuError::Network(std::io::Error::new(
137                error.kind(),
138                format!("webtcp_connect_failed endpoint_fingerprint={endpoint_fingerprint}"),
139            ))
140        })?;
141
142    let server_name = ServerName::try_from(tls_server_name.to_string()).map_err(|e| {
143        FutuError::Codec(format!(
144            "webtcp invalid tls server name {tls_server_name}: {e}"
145        ))
146    })?;
147    let tls = tokio::time::timeout(
148        WEBTCP_TLS_TIMEOUT,
149        tls_connector().connect(server_name, stream),
150    )
151    .await
152    .map_err(|_elapsed| {
153        FutuError::Network(std::io::Error::new(
154            std::io::ErrorKind::TimedOut,
155            format!("webtcp_tls_timeout endpoint_fingerprint={endpoint_fingerprint}"),
156        ))
157    })?
158    .map_err(|_error| {
159        FutuError::Codec(format!(
160            "webtcp_tls_failed endpoint_fingerprint={endpoint_fingerprint}"
161        ))
162    })?;
163
164    let noop: PushCallback = Arc::new(|_, _, _, _| {});
165    let endpoint = addr.to_string();
166    Ok(initialize_webtcp_header_user_id(
167        BackendConn::from_stream_with_endpoint(tls, noop, &endpoint, protocol_identity),
168        header_user_id,
169    ))
170}
171
172#[cfg(test)]
173pub(in crate::auth) fn backend_conn_from_duplex_for_test(
174    stream: tokio::io::DuplexStream,
175    protocol_identity: BackendProtocolIdentity,
176    header_user_id: u64,
177) -> BackendConn {
178    let noop: PushCallback = Arc::new(|_, _, _, _| {});
179    initialize_webtcp_header_user_id(
180        BackendConn::from_stream_with_endpoint(stream, noop, "webtcp-test-peer", protocol_identity),
181        header_user_id,
182    )
183}