futu_backend/auth/commconfig/
parsers.rs1use 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
15pub 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 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 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
108pub 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
179pub 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 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 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 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 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 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
301pub 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
350pub 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#[inline]
406pub fn is_broker_identity(identity: u32) -> bool {
407 matches!(identity, 1001 | 1007 | 1008 | 1009 | 1012 | 1017 | 1019)
408}
409
410#[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}