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, CommConfigSource, CommonConfigSnapshot, ForcedIpEntry,
12 ForcedIpMap, GuaranteedBrokerIpMap, GuaranteedIpMap, GuaranteedWebIpMap,
13 OrderAbilityCapability, OrderAbilityConfig, OrderAbilityConfigError, OrderAbilityTradeType,
14};
15
16pub fn parse_forced_ip(value: &serde_json::Value) -> ForcedIpMap {
27 let mut map: ForcedIpMap = HashMap::new();
28 if value.is_null() {
29 tracing::debug!("commconfig: forced_ip_for_conn is null");
30 return map;
31 }
32 let obj_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
34 if s.is_empty() {
35 return map;
36 }
37 match serde_json::from_str::<serde_json::Value>(s) {
38 Ok(v) => std::borrow::Cow::Owned(v),
39 Err(e) => {
40 tracing::warn!(
41 error = %e,
42 "commconfig: forced_ip_for_conn string-to-json parse failed"
43 );
44 return map;
45 }
46 }
47 } else {
48 std::borrow::Cow::Borrowed(value)
49 };
50 let arr = obj_value
52 .as_object()
53 .and_then(|o| o.get("forced_ip_for_conn"))
54 .and_then(|v| v.as_array());
55 let Some(arr) = arr else {
56 tracing::warn!(
57 kind = value_kind(value),
58 "commconfig: forced_ip_for_conn missing nested `forced_ip_for_conn` array"
59 );
60 return map;
61 };
62
63 for entry in arr {
64 let Some(o) = entry.as_object() else {
65 continue;
66 };
67 let Some(identity) = json_u32_field(o, "identity", 0, "forced_ip_for_conn.identity") else {
68 continue;
69 };
70 let ip = o
71 .get("ip")
72 .and_then(|v| v.as_str())
73 .unwrap_or("")
74 .to_string();
75 let Some(port) = json_u16_field(o, "port", 9595, "forced_ip_for_conn.port") else {
76 continue;
77 };
78 let expire_ts = o.get("expire").and_then(|v| v.as_i64()).unwrap_or(0);
79
80 if ip.is_empty() {
81 continue;
82 }
83 let Some(attr) = UserAttribution::from_u32(identity) else {
84 tracing::debug!(
85 identity,
86 "commconfig: forced_ip skipping non-platform identity"
87 );
88 continue;
89 };
90 tracing::debug!(
91 identity,
92 ip = %ip,
93 port,
94 expire_ts,
95 "commconfig: forced_ip loaded"
96 );
97 map.insert(
98 attr,
99 ForcedIpEntry {
100 ip,
101 port,
102 expire_ts,
103 },
104 );
105 }
106 map
107}
108
109pub fn value_kind(v: &serde_json::Value) -> &'static str {
112 match v {
113 serde_json::Value::Null => "Null",
114 serde_json::Value::Bool(_) => "Bool",
115 serde_json::Value::Number(_) => "Number",
116 serde_json::Value::String(_) => "String",
117 serde_json::Value::Array(_) => "Array",
118 serde_json::Value::Object(_) => "Object",
119 }
120}
121
122fn json_u32_field(
123 obj: &serde_json::Map<String, serde_json::Value>,
124 field: &'static str,
125 default: u32,
126 context: &'static str,
127) -> Option<u32> {
128 let Some(value) = obj.get(field) else {
129 return Some(default);
130 };
131 if let Some(raw) = value.as_i64()
132 && let Ok(parsed) = u32::try_from(raw)
133 {
134 return Some(parsed);
135 }
136 if let Some(raw) = value.as_u64()
137 && let Ok(parsed) = u32::try_from(raw)
138 {
139 return Some(parsed);
140 }
141 tracing::warn!(
142 context,
143 field,
144 kind = value_kind(value),
145 value = ?value,
146 "commconfig: skipping invalid u32 field"
147 );
148 None
149}
150
151fn json_u16_field(
152 obj: &serde_json::Map<String, serde_json::Value>,
153 field: &'static str,
154 default: u16,
155 context: &'static str,
156) -> Option<u16> {
157 let Some(value) = obj.get(field) else {
158 return Some(default);
159 };
160 if let Some(raw) = value.as_i64()
161 && let Ok(parsed) = u16::try_from(raw)
162 {
163 return Some(parsed);
164 }
165 if let Some(raw) = value.as_u64()
166 && let Ok(parsed) = u16::try_from(raw)
167 {
168 return Some(parsed);
169 }
170 tracing::warn!(
171 context,
172 field,
173 kind = value_kind(value),
174 value = ?value,
175 "commconfig: skipping invalid u16 field"
176 );
177 None
178}
179
180pub fn parse_guaranteed_ip(
197 value: &serde_json::Value,
198) -> (GuaranteedIpMap, GuaranteedBrokerIpMap, GuaranteedWebIpMap) {
199 let mut platform: GuaranteedIpMap = HashMap::new();
200 let mut broker: GuaranteedBrokerIpMap = HashMap::new();
201 let mut web: GuaranteedWebIpMap = HashMap::new();
202 if value.is_null() {
204 tracing::debug!(
205 "commconfig: guaranteed_ip_for_conn is null (no guaranteed IPs for this account)"
206 );
207 return (platform, broker, web);
208 }
209 let arr_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
211 if s.is_empty() {
212 tracing::debug!("commconfig: guaranteed_ip_for_conn is empty string");
213 return (platform, broker, web);
214 }
215 match serde_json::from_str::<serde_json::Value>(s) {
216 Ok(v) => std::borrow::Cow::Owned(v),
217 Err(e) => {
218 tracing::warn!(
219 error = %e,
220 preview = %s.chars().take(80).collect::<String>(),
221 "commconfig: guaranteed_ip_for_conn string-to-json parse failed"
222 );
223 return (platform, broker, web);
224 }
225 }
226 } else {
227 std::borrow::Cow::Borrowed(value)
228 };
229 let Some(arr) = arr_value.as_array() else {
230 tracing::warn!(
231 kind = ?value_kind(value),
232 "commconfig: guaranteed_ip_for_conn is neither array nor array-string"
233 );
234 return (platform, broker, web);
235 };
236
237 for entry in arr {
238 let Some(obj) = entry.as_object() else {
239 continue;
240 };
241 let Some(identity) = json_u32_field(obj, "identity", 0, "guaranteed_ip_for_conn.identity")
242 else {
243 continue;
244 };
245 let Some(port) = json_u16_field(obj, "port", 9595, "guaranteed_ip_for_conn.port") else {
246 continue;
247 };
248 let ips = obj.get("ip").and_then(|v| v.as_array());
249 let Some(ips) = ips else {
250 continue;
251 };
252
253 let mut pool: Vec<(String, u16)> = Vec::new();
254 for ip_v in ips {
255 if let Some(ip) = ip_v.as_str()
256 && !ip.is_empty()
257 {
258 pool.push((ip.to_string(), port));
259 }
260 }
261 if pool.is_empty() {
262 continue;
263 }
264
265 if let Some(attr) = UserAttribution::from_u32(identity) {
266 tracing::debug!(
268 identity,
269 port,
270 count = pool.len(),
271 "commconfig: platform guaranteed_ip loaded"
272 );
273 platform.insert(attr, pool);
274 } else if is_broker_identity(identity) {
275 tracing::debug!(
277 identity,
278 port,
279 count = pool.len(),
280 "commconfig: broker guaranteed_ip loaded"
281 );
282 broker.insert(identity, pool);
283 } else if is_web_identity(identity) {
284 tracing::debug!(
286 identity,
287 port,
288 count = pool.len(),
289 "commconfig: web guaranteed_ip loaded"
290 );
291 web.insert(identity, pool);
292 } else {
293 tracing::debug!(
294 identity,
295 "commconfig: skipping unknown guaranteed_ip identity"
296 );
297 }
298 }
299 (platform, broker, web)
300}
301
302pub fn parse_web_tcp_config_identity(value: &serde_json::Value) -> Option<u32> {
308 let obj_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
309 if s.is_empty() {
310 return None;
311 }
312 match serde_json::from_str::<serde_json::Value>(s) {
313 Ok(v) => std::borrow::Cow::Owned(v),
314 Err(e) => {
315 tracing::warn!(
316 error = %e,
317 preview = %s.chars().take(80).collect::<String>(),
318 "commconfig: web_tcp_config string-to-json parse failed"
319 );
320 return None;
321 }
322 }
323 } else {
324 std::borrow::Cow::Borrowed(value)
325 };
326
327 let Some(obj) = obj_value.as_object() else {
328 tracing::debug!(
329 kind = value_kind(value),
330 "commconfig: web_tcp_config is not object/object-string"
331 );
332 return None;
333 };
334 let identity = json_u32_field(
335 obj,
336 "web_conn_identity",
337 0,
338 "web_tcp_config.web_conn_identity",
339 )?;
340 if is_web_identity(identity) {
341 Some(identity)
342 } else {
343 tracing::warn!(
344 identity,
345 "commconfig: ignoring invalid web_tcp_config.web_conn_identity"
346 );
347 None
348 }
349}
350
351pub fn parse_auth_guaranteed_domain_list(
356 value: &serde_json::Value,
357) -> (AuthGuaranteedDomainMap, bool) {
358 let mut out = AuthGuaranteedDomainMap::new();
359 if value.is_null() {
360 return (out, false);
361 }
362
363 let obj_value: std::borrow::Cow<serde_json::Value> = if let Some(s) = value.as_str() {
364 if s.is_empty() {
365 return (out, false);
366 }
367 match serde_json::from_str::<serde_json::Value>(s) {
368 Ok(v) => std::borrow::Cow::Owned(v),
369 Err(e) => {
370 tracing::warn!(
371 error = %e,
372 preview = %s.chars().take(80).collect::<String>(),
373 "commconfig: auth_guaranteed_domain_list string-to-json parse failed"
374 );
375 return (out, false);
376 }
377 }
378 } else {
379 std::borrow::Cow::Borrowed(value)
380 };
381
382 let Some(obj) = obj_value.as_object() else {
383 tracing::warn!(
384 kind = value_kind(value),
385 "commconfig: auth_guaranteed_domain_list is neither object nor object-string"
386 );
387 return (out, false);
388 };
389
390 for (domain, retry_domain) in obj {
391 let Some(retry_domain) = retry_domain.as_str() else {
392 continue;
393 };
394 if domain.is_empty() || retry_domain.is_empty() {
395 continue;
396 }
397 out.insert(domain.clone(), retry_domain.to_string());
398 }
399 (out, true)
400}
401
402#[inline]
407pub fn is_broker_identity(identity: u32) -> bool {
408 matches!(identity, 1001 | 1007 | 1008 | 1009 | 1012 | 1017 | 1019)
409}
410
411#[inline]
413pub fn is_web_identity(identity: u32) -> bool {
414 matches!(
415 identity,
416 CONN_WEB_CN
417 | CONN_WEB_US
418 | CONN_WEB_SG
419 | CONN_WEB_AU
420 | CONN_WEB_JP
421 | CONN_WEB_HK
422 | CONN_WEB_MY
423 | CONN_WEB_CA
424 )
425}
426
427pub fn project_order_ability_config(
428 value: &serde_json::Value,
429) -> Result<Option<OrderAbilityConfig>, OrderAbilityConfigError> {
430 let owned;
431 let root = if let Some(encoded) = value.as_str() {
432 owned = serde_json::from_str::<serde_json::Value>(encoded)
433 .map_err(|_| OrderAbilityConfigError::InvalidShape)?;
434 &owned
435 } else {
436 value
437 };
438 let desktop = root
439 .get("Desktop")
440 .ok_or(OrderAbilityConfigError::MissingDesktop)?
441 .as_object()
442 .ok_or(OrderAbilityConfigError::InvalidShape)?;
443 let mut config = OrderAbilityConfig::default();
444 for (server_name, broker_value) in desktop {
445 let trade_types: &[OrderAbilityTradeType] = match server_name.as_str() {
446 "OCO" => &[OrderAbilityTradeType::Oco],
447 "TWAP_VWAP" => &[OrderAbilityTradeType::Twap, OrderAbilityTradeType::Vwap],
448 "POV" => &[OrderAbilityTradeType::Pov],
449 "Slicer" => &[OrderAbilityTradeType::Slicer],
450 "Peg" => &[OrderAbilityTradeType::Peg],
451 "Iceberg" => &[OrderAbilityTradeType::Iceberg],
452 "MOC" => continue,
454 _ => return Err(OrderAbilityConfigError::UnknownTradeType),
457 };
458 let brokers = broker_value
459 .as_object()
460 .ok_or(OrderAbilityConfigError::InvalidShape)?;
461 for (broker_key, market_value) in brokers {
462 let broker_id = parse_order_ability_key(broker_key);
463 if !is_valid_order_ability_broker(broker_id) {
464 continue;
465 }
466 let markets = market_value
467 .as_object()
468 .ok_or(OrderAbilityConfigError::InvalidShape)?;
469 for (market_key, instrument_value) in markets {
470 let market = parse_order_ability_key(market_key);
471 if !is_valid_order_ability_market(market) {
472 continue;
473 }
474 let instruments = instrument_value
475 .as_object()
476 .ok_or(OrderAbilityConfigError::InvalidShape)?;
477 for (instrument_key, version_value) in instruments {
478 let instrument_v2 = parse_order_ability_key(instrument_key);
479 if instrument_v2 == 0 {
480 continue;
481 }
482 let Some(min_desktop_version) = parse_order_ability_version(version_value)
483 else {
484 continue;
485 };
486 for trade_type in trade_types {
487 config.cells.insert(
488 (*trade_type, broker_id, market, instrument_v2),
489 min_desktop_version.clone(),
490 );
491 }
492 }
493 }
494 }
495 }
496 Ok(Some(config))
497}
498
499fn parse_order_ability_key(value: &str) -> u32 {
500 value.parse::<u32>().unwrap_or(0)
501}
502
503fn is_valid_order_ability_broker(value: u32) -> bool {
509 matches!(value, 1001 | 1007 | 1008 | 1009 | 1012 | 1017 | 1019 | 1022)
510}
511
512fn is_valid_order_ability_market(value: u32) -> bool {
518 matches!(
519 value,
520 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24
521 )
522}
523
524fn parse_order_ability_version(value: &serde_json::Value) -> Option<String> {
528 let parsed = match value {
529 serde_json::Value::Number(number) => {
530 if let Some(value) = number.as_i64() {
531 Some(value)
532 } else if let Some(value) = number.as_u64() {
533 i64::try_from(value).ok()
534 } else {
535 number.as_f64().and_then(|value| {
536 let rounded = value.round();
537 (rounded.is_finite()
538 && rounded >= i64::MIN as f64
539 && rounded <= i64::MAX as f64)
540 .then_some(rounded as i64)
541 })
542 }
543 }
544 serde_json::Value::String(value) => value
545 .rsplit_once('.')
546 .map_or(value.as_str(), |(_, tail)| tail)
547 .parse::<i64>()
548 .ok(),
549 _ => None,
550 }?;
551 (parsed > 0).then(|| parsed.to_string())
552}
553
554pub fn resolve_order_ability_capability(
555 snapshot: &CommonConfigSnapshot,
556 trade_type: OrderAbilityTradeType,
557 broker_id: u32,
558 market: u32,
559 instrument_v2: u32,
560 now_ts: i64,
561) -> OrderAbilityCapability {
562 if snapshot.source != CommConfigSource::Live
563 || snapshot.generation == 0
564 || snapshot.next_refresh_ts <= now_ts
565 {
566 return OrderAbilityCapability::Stale;
567 }
568 if !is_valid_order_ability_broker(broker_id)
569 || !is_valid_order_ability_market(market)
570 || instrument_v2 == 0
571 {
572 return OrderAbilityCapability::Unknown;
573 }
574 let Some(config) = snapshot.order_ability.as_ref() else {
575 return OrderAbilityCapability::Unknown;
576 };
577 let Some(min_desktop_version) =
578 config
579 .cells
580 .get(&(trade_type, broker_id, market, instrument_v2))
581 else {
582 return OrderAbilityCapability::Unknown;
583 };
584 OrderAbilityCapability::Ready {
585 generation: snapshot.generation,
586 min_desktop_version: min_desktop_version.clone(),
587 }
588}