Skip to main content

futu_backend/auth/commconfig/
parsers.rs

1//! auth/commconfig/parsers — api helpers + fetch_page + parse_forced_ip + parse_guaranteed_ip +
2//! parse_web_tcp + parse_auth_guaranteed_domain + value_kind + is_*_identity
3//! (v1.4.110 CC Batch M: 拆自 commconfig.rs L245-664)
4
5use std::collections::HashMap;
6
7use crate::auth::UserAttribution;
8
9use super::types::{
10    AuthGuaranteedDomainMap, CONN_WEB_AU, CONN_WEB_CA, CONN_WEB_CN, CONN_WEB_HK, CONN_WEB_JP,
11    CONN_WEB_MY, CONN_WEB_SG, CONN_WEB_US, ForcedIpEntry, ForcedIpMap, GuaranteedBrokerIpMap,
12    GuaranteedIpMap, GuaranteedWebIpMap,
13};
14
15/// `conf_info["forced_ip_for_conn"]` 解析 —— 对齐 C++
16/// `address.cpp:360-400` 的 `ParseForcedIpConfig()`。
17///
18/// 这个字段的值**和 `guaranteed_ip_for_conn` schema 不同**:
19/// - 外层是 object(不是直接 array):`{"forced_ip_for_conn": [...]}`
20/// - entry 字段:`{identity, ip, port, expire}` —— 单 IP + expire 时间戳
21/// - 未过期的 forced_ip **绕过**其他 fallback 直接用(最高优先级)
22///
23/// 和 `parse_guaranteed_ip` 一样支持三态(Null / Array-JSON-string / Object)。
24/// 多了一层 "object → forced_ip_for_conn" 的嵌套解包。
25pub fn parse_forced_ip(value: &serde_json::Value) -> ForcedIpMap {
26    let mut map: ForcedIpMap = HashMap::new();
27    if value.is_null() {
28        tracing::debug!("commconfig: forced_ip_for_conn is null");
29        return map;
30    }
31    // 取出 object 层:直接 object、或字符串装 object 两种都接受
32    let obj_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
33        if s.is_empty() {
34            return map;
35        }
36        match serde_json::from_str::<serde_json::Value>(s) {
37            Ok(v) => std::borrow::Cow::Owned(v),
38            Err(e) => {
39                tracing::warn!(
40                    error = %e,
41                    "commconfig: forced_ip_for_conn string-to-json parse failed"
42                );
43                return map;
44            }
45        }
46    } else {
47        std::borrow::Cow::Borrowed(value)
48    };
49    // 嵌套 unwrap:{ "forced_ip_for_conn": [...] } → array
50    let arr = obj_value
51        .as_object()
52        .and_then(|o| o.get("forced_ip_for_conn"))
53        .and_then(|v| v.as_array());
54    let Some(arr) = arr else {
55        tracing::warn!(
56            kind = value_kind(value),
57            "commconfig: forced_ip_for_conn missing nested `forced_ip_for_conn` array"
58        );
59        return map;
60    };
61
62    for entry in arr {
63        let Some(o) = entry.as_object() else {
64            continue;
65        };
66        let Some(identity) = json_u32_field(o, "identity", 0, "forced_ip_for_conn.identity") else {
67            continue;
68        };
69        let ip = o
70            .get("ip")
71            .and_then(|v| v.as_str())
72            .unwrap_or("")
73            .to_string();
74        let Some(port) = json_u16_field(o, "port", 9595, "forced_ip_for_conn.port") else {
75            continue;
76        };
77        let expire_ts = o.get("expire").and_then(|v| v.as_i64()).unwrap_or(0);
78
79        if ip.is_empty() {
80            continue;
81        }
82        let Some(attr) = UserAttribution::from_u32(identity) else {
83            tracing::debug!(
84                identity,
85                "commconfig: forced_ip skipping non-platform identity"
86            );
87            continue;
88        };
89        tracing::debug!(
90            identity,
91            ip = %ip,
92            port,
93            expire_ts,
94            "commconfig: forced_ip loaded"
95        );
96        map.insert(
97            attr,
98            ForcedIpEntry {
99                ip,
100                port,
101                expire_ts,
102            },
103        );
104    }
105    map
106}
107
108/// 诊断 log 用 —— 返回 `serde_json::Value` 的类型名字(Null/Bool/Number/
109/// String/Array/Object)。WARN 里带这个比 `{:?}` 更可读。
110pub fn value_kind(v: &serde_json::Value) -> &'static str {
111    match v {
112        serde_json::Value::Null => "Null",
113        serde_json::Value::Bool(_) => "Bool",
114        serde_json::Value::Number(_) => "Number",
115        serde_json::Value::String(_) => "String",
116        serde_json::Value::Array(_) => "Array",
117        serde_json::Value::Object(_) => "Object",
118    }
119}
120
121fn json_u32_field(
122    obj: &serde_json::Map<String, serde_json::Value>,
123    field: &'static str,
124    default: u32,
125    context: &'static str,
126) -> Option<u32> {
127    let Some(value) = obj.get(field) else {
128        return Some(default);
129    };
130    if let Some(raw) = value.as_i64()
131        && let Ok(parsed) = u32::try_from(raw)
132    {
133        return Some(parsed);
134    }
135    if let Some(raw) = value.as_u64()
136        && let Ok(parsed) = u32::try_from(raw)
137    {
138        return Some(parsed);
139    }
140    tracing::warn!(
141        context,
142        field,
143        kind = value_kind(value),
144        value = ?value,
145        "commconfig: skipping invalid u32 field"
146    );
147    None
148}
149
150fn json_u16_field(
151    obj: &serde_json::Map<String, serde_json::Value>,
152    field: &'static str,
153    default: u16,
154    context: &'static str,
155) -> Option<u16> {
156    let Some(value) = obj.get(field) else {
157        return Some(default);
158    };
159    if let Some(raw) = value.as_i64()
160        && let Ok(parsed) = u16::try_from(raw)
161    {
162        return Some(parsed);
163    }
164    if let Some(raw) = value.as_u64()
165        && let Ok(parsed) = u16::try_from(raw)
166    {
167        return Some(parsed);
168    }
169    tracing::warn!(
170        context,
171        field,
172        kind = value_kind(value),
173        value = ?value,
174        "commconfig: skipping invalid u16 field"
175    );
176    None
177}
178
179/// `conf_info["guaranteed_ip_for_conn"]` 的值可能是:
180/// 1. **JSON 字符串**(C++ `NNBiz_CommonConfig.cpp:141` + `toStyledString()`
181///    的典型来源,值是 `"[{...},{...}]"` 需要二次 parse)
182/// 2. **直接 array**(服务端没 stringify,直接嵌 JSON object)
183/// 3. `null` / 空串 / 缺字段(某些地区 / 账号状态,服务端没配置 guaranteed
184///    IP;正常,由调用方进入该通道自己的 fallback 链)
185///
186/// v1.4.21 前只支持 1,遇到 2/3 会打 `EOF while parsing a value` WARN 并
187/// 返回空 map。改成**同时支持三种形态**,只在明显的"格式错误"时才 WARN。
188///
189/// 对齐 C++ `ChannelAddressManager::ParseGuaranteedIpConfig()`
190/// (`address.cpp:302-358`) —— C++ 只处理字符串入口,我们更宽松。
191/// 返回 `(platform_map, broker_map, web_map)` —— Platform identity(1-6) 进 platform_map;
192/// C++ `channel.h:61-75` `kAllConnIdentity` 里列出的
193/// `CONN_BROKER_FUTU_*` (1001/1007/1008/1009/1012/1017/1019) 进 broker_map;
194/// `CONN_WEB_*` (10100..10107) 进 web_map;其他未知 identity 跳过。
195pub fn parse_guaranteed_ip(
196    value: &serde_json::Value,
197) -> (GuaranteedIpMap, GuaranteedBrokerIpMap, GuaranteedWebIpMap) {
198    let mut platform: GuaranteedIpMap = HashMap::new();
199    let mut broker: GuaranteedBrokerIpMap = HashMap::new();
200    let mut web: GuaranteedWebIpMap = HashMap::new();
201    // 空 / null → 没配置,安静返回(避免噪音 WARN 污染日志)
202    if value.is_null() {
203        tracing::debug!(
204            "commconfig: guaranteed_ip_for_conn is null (no guaranteed IPs for this account)"
205        );
206        return (platform, broker, web);
207    }
208    // 取出 array:直接是 array、或字符串里装 array 两种都接受
209    let arr_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
210        if s.is_empty() {
211            tracing::debug!("commconfig: guaranteed_ip_for_conn is empty string");
212            return (platform, broker, web);
213        }
214        match serde_json::from_str::<serde_json::Value>(s) {
215            Ok(v) => std::borrow::Cow::Owned(v),
216            Err(e) => {
217                tracing::warn!(
218                    error = %e,
219                    preview = %s.chars().take(80).collect::<String>(),
220                    "commconfig: guaranteed_ip_for_conn string-to-json parse failed"
221                );
222                return (platform, broker, web);
223            }
224        }
225    } else {
226        std::borrow::Cow::Borrowed(value)
227    };
228    let Some(arr) = arr_value.as_array() else {
229        tracing::warn!(
230            kind = ?value_kind(value),
231            "commconfig: guaranteed_ip_for_conn is neither array nor array-string"
232        );
233        return (platform, broker, web);
234    };
235
236    for entry in arr {
237        let Some(obj) = entry.as_object() else {
238            continue;
239        };
240        let Some(identity) = json_u32_field(obj, "identity", 0, "guaranteed_ip_for_conn.identity")
241        else {
242            continue;
243        };
244        let Some(port) = json_u16_field(obj, "port", 9595, "guaranteed_ip_for_conn.port") else {
245            continue;
246        };
247        let ips = obj.get("ip").and_then(|v| v.as_array());
248        let Some(ips) = ips else {
249            continue;
250        };
251
252        let mut pool: Vec<(String, u16)> = Vec::new();
253        for ip_v in ips {
254            if let Some(ip) = ip_v.as_str()
255                && !ip.is_empty()
256            {
257                pool.push((ip.to_string(), port));
258            }
259        }
260        if pool.is_empty() {
261            continue;
262        }
263
264        if let Some(attr) = UserAttribution::from_u32(identity) {
265            // Platform identity (1-6)
266            tracing::debug!(
267                identity,
268                port,
269                count = pool.len(),
270                "commconfig: platform guaranteed_ip loaded"
271            );
272            platform.insert(attr, pool);
273        } else if is_broker_identity(identity) {
274            // Broker identity (CONN_BROKER_FUTU_*)
275            tracing::debug!(
276                identity,
277                port,
278                count = pool.len(),
279                "commconfig: broker guaranteed_ip loaded"
280            );
281            broker.insert(identity, pool);
282        } else if is_web_identity(identity) {
283            // WebTCP-short identity (CONN_WEB_*)
284            tracing::debug!(
285                identity,
286                port,
287                count = pool.len(),
288                "commconfig: web guaranteed_ip loaded"
289            );
290            web.insert(identity, pool);
291        } else {
292            tracing::debug!(
293                identity,
294                "commconfig: skipping unknown guaranteed_ip identity"
295            );
296        }
297    }
298    (platform, broker, web)
299}
300
301/// 解析 C++ `web_tcp_config` 的全局 WebTCP-short identity。
302///
303/// 服务端可能把 `web_tcp_config` 作为 JSON 字符串或 object 下发。C++
304/// `WebRequestManager::UpdateCommConfig()` 只使用其中 `web_conn_identity`
305/// 来决定 WebTCP-short 目标 identity;它不是 broker 维度字段。
306pub fn parse_web_tcp_config_identity(value: &serde_json::Value) -> Option<u32> {
307    let obj_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
308        if s.is_empty() {
309            return None;
310        }
311        match serde_json::from_str::<serde_json::Value>(s) {
312            Ok(v) => std::borrow::Cow::Owned(v),
313            Err(e) => {
314                tracing::warn!(
315                    error = %e,
316                    preview = %s.chars().take(80).collect::<String>(),
317                    "commconfig: web_tcp_config string-to-json parse failed"
318                );
319                return None;
320            }
321        }
322    } else {
323        std::borrow::Cow::Borrowed(value)
324    };
325
326    let Some(obj) = obj_value.as_object() else {
327        tracing::debug!(
328            kind = value_kind(value),
329            "commconfig: web_tcp_config is not object/object-string"
330        );
331        return None;
332    };
333    let identity = json_u32_field(
334        obj,
335        "web_conn_identity",
336        0,
337        "web_tcp_config.web_conn_identity",
338    )?;
339    if is_web_identity(identity) {
340        Some(identity)
341    } else {
342        tracing::warn!(
343            identity,
344            "commconfig: ignoring invalid web_tcp_config.web_conn_identity"
345        );
346        None
347    }
348}
349
350/// 解析 C++ `auth_guaranteed_domain_list` 动态兜底域名表。
351///
352/// 服务端可能把它作为 JSON string 或 object 下发;key 是原始鉴权域名,
353/// value 是失败后用于 retry-domain 阶段的域名。
354pub fn parse_auth_guaranteed_domain_list(
355    value: &serde_json::Value,
356) -> (AuthGuaranteedDomainMap, bool) {
357    let mut out = AuthGuaranteedDomainMap::new();
358    if value.is_null() {
359        return (out, false);
360    }
361
362    let obj_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
363        if s.is_empty() {
364            return (out, false);
365        }
366        match serde_json::from_str::<serde_json::Value>(s) {
367            Ok(v) => std::borrow::Cow::Owned(v),
368            Err(e) => {
369                tracing::warn!(
370                    error = %e,
371                    preview = %s.chars().take(80).collect::<String>(),
372                    "commconfig: auth_guaranteed_domain_list string-to-json parse failed"
373                );
374                return (out, false);
375            }
376        }
377    } else {
378        std::borrow::Cow::Borrowed(value)
379    };
380
381    let Some(obj) = obj_value.as_object() else {
382        tracing::warn!(
383            kind = value_kind(value),
384            "commconfig: auth_guaranteed_domain_list is neither object nor object-string"
385        );
386        return (out, false);
387    };
388
389    for (domain, retry_domain) in obj {
390        let Some(retry_domain) = retry_domain.as_str() else {
391            continue;
392        };
393        if domain.is_empty() || retry_domain.is_empty() {
394            continue;
395        }
396        out.insert(domain.clone(), retry_domain.to_string());
397    }
398    (out, true)
399}
400
401/// 对齐 C++ `FTLogin/Src/ftlogin/channel/channel.h:61-75` `kAllConnIdentity`
402/// 里的 broker identity 集合。C++ enum 里有 `CONN_BROKER_AIR_STAR = 1022`,
403/// 但当前 `kAllConnIdentity` 没有列它,所以这里也不把 1022 当作 commconfig
404/// broker IP 池 identity。
405#[inline]
406pub fn is_broker_identity(identity: u32) -> bool {
407    matches!(identity, 1001 | 1007 | 1008 | 1009 | 1012 | 1017 | 1019)
408}
409
410/// 对齐 C++ `FTConnCmn.proto` 的 `CONN_WEB_*` identity。
411#[inline]
412pub fn is_web_identity(identity: u32) -> bool {
413    matches!(
414        identity,
415        CONN_WEB_CN
416            | CONN_WEB_US
417            | CONN_WEB_SG
418            | CONN_WEB_AU
419            | CONN_WEB_JP
420            | CONN_WEB_HK
421            | CONN_WEB_MY
422            | CONN_WEB_CA
423    )
424}