Skip to main content

futu_backend/auth/commconfig/
accessors.rs

1//! CommConfig snapshot accessors and refresh scheduling policy.
2
3use crate::auth::UserAttribution;
4
5use super::types::CommonConfigSnapshot;
6
7use super::parsers::is_web_identity;
8use super::types::{
9    CONN_WEB_AU, CONN_WEB_CA, CONN_WEB_CN, CONN_WEB_HK, CONN_WEB_JP, CONN_WEB_MY, CONN_WEB_SG,
10    CONN_WEB_US,
11};
12
13pub fn forced_ip_for_attribution(
14    snapshot: &CommonConfigSnapshot,
15    attr: UserAttribution,
16    now_ts: i64,
17) -> Option<(String, u16)> {
18    snapshot.forced_ip.get(&attr).and_then(|entry| {
19        if entry.is_valid(now_ts) {
20            Some((entry.ip.clone(), entry.port))
21        } else {
22            tracing::debug!(
23                ?attr,
24                expire_ts = entry.expire_ts,
25                now_ts,
26                "commconfig: forced_ip expired, ignored"
27            );
28            None
29        }
30    })
31}
32
33/// 从 snapshot 取某 attribution 对应的 IP 池(去拷贝成 `Vec<(String, u16)>`)。
34/// 如果没有则返回空。调用者可以把返回值 merge 到硬编码池最前面。
35pub fn ips_for_attribution(
36    snapshot: &CommonConfigSnapshot,
37    attr: UserAttribution,
38) -> Vec<(String, u16)> {
39    snapshot
40        .guaranteed_ip
41        .get(&attr)
42        .cloned()
43        .unwrap_or_default()
44}
45
46/// 从 snapshot 取 broker_id 对应的 broker 专用 IP 池。
47///
48/// broker 通道 CMD 1001 登录**必须连到正确的 broker IP**,否则服务端按
49/// customer_id 返的是该 IP 对应 broker 的账户列表(实测:broker 1001
50/// 连到 Platform US IP 时返回的是 broker 1007 账户)。
51///
52/// 空返回 → 降级到 Platform addr + 登录 redirect 兜底路径(v1.4.8 legacy
53/// 行为,不总靠得住)。
54pub fn ips_for_broker(snapshot: &CommonConfigSnapshot, broker_id: u32) -> Vec<(String, u16)> {
55    snapshot
56        .guaranteed_ip_broker
57        .get(&broker_id)
58        .cloned()
59        .unwrap_or_default()
60}
61
62/// 从 snapshot 取 WebTCP-short identity 对应的 IP 池。
63///
64/// broker_auth 的 C++ 主路径是 WebTCP-short(CMD 65507),目标 IP 来自
65/// commconfig `guaranteed_ip_for_conn` 的 10100..10107 identity。这个 helper
66/// 只返回服务端下发值;需要完整 fallback 链时用 `webtcp_addrs_for_identity`。
67pub fn ips_for_web_identity(snapshot: &CommonConfigSnapshot, identity: u32) -> Vec<(String, u16)> {
68    snapshot
69        .guaranteed_ip_web
70        .get(&identity)
71        .cloned()
72        .unwrap_or_default()
73}
74
75/// C++ `LoadHardcodeAddress()` 里 WebTCP-short identity 的本地保底池。
76///
77/// Ref:
78/// - `FTLogin/Src/ftlogin/channel/impl/address.cpp:616-653`
79/// - `FTLogin/Src/ftlogin/channel/address.h:12-18,70` (`ChannelAddress`
80///   默认主端口 443,构造参数里的 9595 是 backup port)
81/// - `FTLogin/Src/ftlogin/channel/impl/connector.cpp:228-233`(正常先连
82///   `Port()`,backup 分支才连 `BackupPort()`)
83///
84/// Rust 目前没有完整搬 C++ connector 的 backup-port 状态机。为了避免退化到
85/// 系统 DNS(外网环境容易被 reset),这里在 hardcoded fallback 时按
86/// "443 主端口 → 9595 备用端口" 的顺序展开同一组 IP。commconfig 下发池仍
87/// 原样优先,不追加 hardcoded。
88pub fn webtcp_hardcoded_addrs(identity: u32) -> &'static [(&'static str, u16)] {
89    match identity {
90        CONN_WEB_CN => &[
91            ("119.91.245.213", 443),
92            ("119.91.245.125", 443),
93            ("119.91.245.213", 9595),
94            ("119.91.245.125", 9595),
95        ],
96        CONN_WEB_HK => &[
97            ("101.32.198.103", 443),
98            ("43.135.64.109", 443),
99            ("101.32.198.103", 9595),
100            ("43.135.64.109", 9595),
101        ],
102        CONN_WEB_US => &[
103            ("49.51.78.82", 443),
104            ("170.106.62.85", 443),
105            ("49.51.78.82", 9595),
106            ("170.106.62.85", 9595),
107        ],
108        CONN_WEB_SG => &[
109            ("101.32.173.177", 443),
110            ("101.33.49.67", 443),
111            ("101.32.173.177", 9595),
112            ("101.33.49.67", 9595),
113        ],
114        CONN_WEB_AU => &[
115            ("54.206.243.201", 443),
116            ("3.104.68.90", 443),
117            ("54.206.243.201", 9595),
118            ("3.104.68.90", 9595),
119        ],
120        CONN_WEB_JP => &[
121            ("43.163.254.232", 443),
122            ("43.163.252.131", 443),
123            ("43.163.254.232", 9595),
124            ("43.163.252.131", 9595),
125        ],
126        CONN_WEB_MY => &[
127            ("47.254.245.70", 443),
128            ("47.254.254.140", 443),
129            ("47.254.245.70", 9595),
130            ("47.254.254.140", 9595),
131        ],
132        CONN_WEB_CA => &[
133            ("15.157.179.115", 443),
134            ("15.157.83.146", 443),
135            ("15.157.179.115", 9595),
136            ("15.157.83.146", 9595),
137        ],
138        _ => &[],
139    }
140}
141
142/// broker_auth WebTCP-short 的连接候选池:commconfig 优先,空时落到 C++
143/// hardcoded WebTCP 池。调用方不应自己拼 DNS 域名作为主路径。
144pub fn webtcp_addrs_for_identity(
145    snapshot: &CommonConfigSnapshot,
146    identity: u32,
147) -> Vec<(String, u16)> {
148    let dynamic = ips_for_web_identity(snapshot, identity);
149    if !dynamic.is_empty() {
150        return dynamic;
151    }
152    webtcp_hardcoded_addrs(identity)
153        .iter()
154        .map(|(ip, port)| ((*ip).to_string(), *port))
155        .collect()
156}
157
158/// C++ `WebRequestManager::UpdateCommConfig()` 的 WebTCP identity 兜底规则:
159/// 服务端未下发有效 `web_tcp_config.web_conn_identity` 时,moomoo app type
160/// 默认 `CONN_WEB_US`,其它(Futu OpenD/Futu HK)默认 `CONN_WEB_HK`。
161///
162/// `client_type=60` 是 moomoo OpenD;`client_type=40` 是 Futu OpenD。
163pub fn default_webtcp_identity_for_client_type(client_type: u8) -> u32 {
164    if client_type == 60 {
165        CONN_WEB_US
166    } else {
167        CONN_WEB_HK
168    }
169}
170
171/// broker_auth 的 WebTCP-short identity 归一入口。
172///
173/// 注意:这里故意不接收 `broker_id`。C++ broker auth 的请求域名按
174/// broker 变化,但 WebTCP-short 目标 identity 来自全局 `web_tcp_config`。
175/// 之前按 broker_id 把 1007 强行送到 `CONN_WEB_US`,在中国境内网络会
176/// 走 `www.moomoo.com` SNI/IP 池而失败;这与 FTLogin 不一致。
177pub fn broker_auth_webtcp_identity(snapshot: &CommonConfigSnapshot, client_type: u8) -> u32 {
178    snapshot
179        .web_conn_identity
180        .filter(|identity| is_web_identity(*identity))
181        .unwrap_or_else(|| default_webtcp_identity_for_client_type(client_type))
182}
183
184/// 计算下次刷新 delay 秒。输入 `next_refresh_ts`(0 表示未知),输出 clamp 后
185/// 的 sleep 时长。
186///
187/// - `next_ts > now + 7200` → 7200(最长 2h 刷一次)
188/// - `next_ts <= now` 或 0 → 1800(默认 30 分钟)
189/// - 其它 → 取 `next_ts - now`,最少 300(5 分钟,避免 busy loop)
190///
191/// 提出来方便单测。
192pub fn delay_until_next_refresh(next_refresh_ts: i64, now: i64) -> u64 {
193    const MIN_DELAY: u64 = 300; // 5 分钟最短
194    const MAX_DELAY: u64 = 7200; // 2 小时最长
195    const DEFAULT_DELAY: u64 = 1800; // 30 分钟兜底
196    if next_refresh_ts <= 0 {
197        return DEFAULT_DELAY;
198    }
199    let diff = next_refresh_ts - now;
200    if diff <= 0 {
201        return DEFAULT_DELAY;
202    }
203    (diff as u64).clamp(MIN_DELAY, MAX_DELAY)
204}