Skip to main content

futu_backend/auth/
http_client.rs

1//! v1.4.110+ Tier 1 split (from `auth/mod.rs`): reqwest HTTP client builder.
2//!
3//! - `build_http_client(client_type)` — 主入口
4//! - `build_http_client_with_resolve(client_type, resolve)` — 测试 / IP override 入口
5//! - `http_header_value` — 私有 helper
6//!
7//! TLS: rustls-tls-webpki-roots (CLAUDE.md 坑 #50, 防 user keychain MITM).
8
9use futu_core::error::{FutuError, Result};
10
11pub fn build_http_client(client_type: u8) -> Result<reqwest::Client> {
12    build_http_client_with_resolve(client_type, None)
13}
14
15pub(crate) fn build_http_client_with_resolve(
16    client_type: u8,
17    resolve: Option<(&str, std::net::SocketAddr)>,
18) -> Result<reqwest::Client> {
19    // v1.4.84 SEC-002 主修复 (eli security report):
20    //
21    // **删除** 之前的 `.danger_accept_invalid_certs(true)` — 这相当于对所有
22    // HTTPS endpoint 完全**禁用** cert 验证, 任何 MITM 都能过. eli 实证:
23    // mitmproxy CA 装入 user keychain 后 Rust daemon 10 个 HTTPS endpoint
24    // 全部 TLS 握手成功, 同条件 C++ OpenD 被 `tlsv1 alert unknown ca` 拒.
25    //
26    // 配合 workspace Cargo.toml `reqwest` dep 改为 `rustls-tls-webpki-roots`:
27    // - 排除 native-tls (OS keychain trust, 会被 user keychain 恶意 CA MITM)
28    // - 使用 Mozilla webpki-roots (curated CA list, 不读 user keychain)
29    //
30    // **防攻击面**:
31    // - Agent skill 装 user keychain MITM CA → 不再被信任, 握手失败
32    // - 企业 MDM 推 CA → 仍会被信任 (MDM 是 system-trusted), 若需阻挡
33    //   此类场景需 v1.4.85+ 加 cert-pinning (SPKI hash) for 敏感 endpoint
34    //
35    // **注**: Futu backend 用公开 CA 签 cert, webpki-roots 内置全球公共 CA,
36    // 正常握手不受影响.
37    let mut default_headers = reqwest::header::HeaderMap::new();
38    default_headers.insert(
39        "X-Futu-Client-Type",
40        http_header_value("X-Futu-Client-Type", client_type)?,
41    );
42    default_headers.insert(
43        "X-Futu-Client-Version",
44        http_header_value(
45            "X-Futu-Client-Version",
46            crate::conn::BackendConn::CLIENT_VER_FTGTW,
47        )?,
48    );
49    default_headers.insert(
50        "X-Futu-Client-Lang",
51        reqwest::header::HeaderValue::from_static("sc"),
52    );
53    default_headers.insert(
54        "Content-Type",
55        reqwest::header::HeaderValue::from_static("application/json"),
56    );
57
58    let mut builder = reqwest::Client::builder()
59        .timeout(std::time::Duration::from_secs(15))
60        .default_headers(default_headers);
61    if let Some((domain, addr)) = resolve {
62        builder = builder.resolve(domain, addr);
63    }
64    builder
65        .build()
66        .map_err(|e| FutuError::Encryption(format!("http client: {e}")))
67}
68
69fn http_header_value(
70    name: &'static str,
71    value: impl std::fmt::Display,
72) -> Result<reqwest::header::HeaderValue> {
73    reqwest::header::HeaderValue::from_str(&value.to_string())
74        .map_err(|e| FutuError::Codec(format!("{name}: invalid header value: {e}")))
75}