1use futu_core::error::{FutuError, Result};
11
12use crate::auth::AuthResult;
13use crate::conn::BackendConn;
14
15pub const CMD_LOGIN_PLATFORM: u16 = 6001;
17pub const CMD_LOGIN_BROKER: u16 = 1001;
19
20const FTGTW_CLIENT_BUILD: u32 = 6208;
25
26const NET_TYPE_ETHERNET: u32 = 3;
30
31fn os_name() -> &'static str {
38 if cfg!(target_os = "macos") {
39 "Mac"
40 } else if cfg!(target_os = "windows") {
41 "Windows"
42 } else if cfg!(target_os = "linux") {
43 "Ubuntu"
44 } else {
45 "Unknown"
46 }
47}
48
49fn net_type_str_ethernet() -> &'static str {
52 "LAN"
53}
54
55fn format_session_key_len_marker(session_key_len: usize) -> String {
56 format!("session_key_len={session_key_len}")
57}
58
59#[derive(Debug, Clone)]
61pub struct LoginResult {
62 pub user_id: u64,
63 pub session_key: Vec<u8>,
67 pub keep_alive_interval: u32,
68 pub sec_data: u32,
69 pub server_time: u64,
70 pub client_ip: String,
76}
77
78#[derive(Debug, Clone, Copy)]
80pub struct TcpLoginTarget<'a> {
81 pub is_new_login: bool,
82 pub redirect_ttl: u32,
83 pub host_ip: &'a str,
84 pub host_port: u32,
85}
86
87impl<'a> TcpLoginTarget<'a> {
88 pub fn new(is_new_login: bool, redirect_ttl: u32, host_ip: &'a str, host_port: u32) -> Self {
89 Self {
90 is_new_login,
91 redirect_ttl,
92 host_ip,
93 host_port,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy)]
101pub struct TcpLoginChannel<'a> {
102 pub cmd_id: u16,
103 pub conn_identity: u32,
104 pub effective_user_id: u64,
105 pub client_sig: &'a [u8],
106}
107
108impl<'a> TcpLoginChannel<'a> {
109 pub fn platform(auth: &'a AuthResult) -> Self {
110 Self {
111 cmd_id: CMD_LOGIN_PLATFORM,
112 conn_identity: auth.user_attribution.to_conn_identity(),
113 effective_user_id: auth.user_id,
114 client_sig: &auth.client_sig,
115 }
116 }
117
118 pub fn broker(conn_identity: u32, customer_id: u64, broker_client_sig: &'a [u8]) -> Self {
119 Self {
120 cmd_id: CMD_LOGIN_BROKER,
121 conn_identity,
122 effective_user_id: customer_id,
123 client_sig: broker_client_sig,
124 }
125 }
126}
127
128pub async fn tcp_login(
131 conn: &BackendConn,
132 auth: &AuthResult,
133 client_key: &[u8],
134 target: TcpLoginTarget<'_>,
135) -> Result<LoginResult> {
136 tcp_login_raw(conn, client_key, target, TcpLoginChannel::platform(auth)).await
137}
138
139pub async fn tcp_login_raw(
155 conn: &BackendConn,
156 client_key: &[u8],
157 target: TcpLoginTarget<'_>,
158 channel: TcpLoginChannel<'_>,
159) -> Result<LoginResult> {
160 let TcpLoginTarget {
161 is_new_login,
162 redirect_ttl,
163 host_ip,
164 host_port,
165 } = target;
166 let TcpLoginChannel {
167 cmd_id,
168 conn_identity,
169 effective_user_id,
170 client_sig,
171 } = channel;
172
173 let mut req_encrypt = Vec::with_capacity(128);
175 prost::encoding::uint64::encode(1, &effective_user_id, &mut req_encrypt);
177 let mac_addr = "00:00:00:00:00:00".to_string();
179 prost::encoding::string::encode(2, &mac_addr, &mut req_encrypt);
180 prost::encoding::uint32::encode(3, &40u32, &mut req_encrypt);
182 prost::encoding::uint32::encode(4, &FTGTW_CLIENT_BUILD, &mut req_encrypt);
185 prost::encoding::uint32::encode(5, &NET_TYPE_ETHERNET, &mut req_encrypt);
187 prost::encoding::uint32::encode(6, &redirect_ttl, &mut req_encrypt);
189 let host_ip_str = host_ip.to_string();
191 prost::encoding::string::encode(7, &host_ip_str, &mut req_encrypt);
192 prost::encoding::uint32::encode(8, &conn_identity, &mut req_encrypt);
195 prost::encoding::uint32::encode(9, &host_port, &mut req_encrypt);
197 let client_feature = build_client_feature();
200 prost::encoding::bytes::encode(10, &client_feature, &mut req_encrypt);
201 let os = os_name().to_string();
205 prost::encoding::string::encode(12, &os, &mut req_encrypt);
206
207 let encrypted_data = if client_key.is_empty() {
212 req_encrypt.clone()
213 } else {
214 futu_net::encrypt::aes_cbc_md5_encrypt_var(client_key, &req_encrypt)?
215 };
216
217 let mut login_req = Vec::with_capacity(256);
220 prost::encoding::uint64::encode(1, &effective_user_id, &mut login_req);
221 prost::encoding::bool::encode(2, &is_new_login, &mut login_req);
222 prost::encoding::bytes::encode(3, &client_sig.to_vec(), &mut login_req);
223 prost::encoding::bytes::encode(4, &encrypted_data, &mut login_req);
224
225 let chan_desc = match cmd_id {
226 CMD_LOGIN_PLATFORM => "platform",
227 CMD_LOGIN_BROKER => "broker",
228 _ => "other",
229 };
230 tracing::info!(
231 user_id = effective_user_id,
232 cmd_id = cmd_id,
233 channel = chan_desc,
234 conn_identity = conn_identity,
235 host = %format!("{host_ip}:{host_port}"),
236 "sending TCP login request"
237 );
238 tracing::debug!(
239 login_req_len = login_req.len(),
240 req_encrypt_plain_len = req_encrypt.len(),
241 client_key_len = client_key.len(),
242 client_sig_len = client_sig.len(),
243 encrypted_data_len = encrypted_data.len(),
244 "TCP login request details"
245 );
246
247 let resp_frame = conn.request(cmd_id, login_req).await?;
248
249 let resp_body = &resp_frame.body;
251 tracing::debug!(
257 resp_body_len = resp_body.len(),
258 resp_body_first4 = hex::encode(&resp_body[..resp_body.len().min(4)]),
259 "TCP login response (body redacted, F-003 v1.4.102 fix)"
260 );
261
262 let result_code = extract_int32_field(resp_body, 1).unwrap_or(-1);
263
264 if result_code != 0 {
265 let desc = extract_bytes_field(resp_body, 3).unwrap_or_default();
266 let desc_str = String::from_utf8_lossy(&desc).to_string();
267
268 if result_code == 1 {
269 let redirect = parse_login_redirect(resp_body)?;
271 tracing::warn!(
272 addr = %redirect.addr,
273 port = redirect.port,
274 ttl = redirect.ttl,
275 "login redirect"
276 );
277 return Err(FutuError::ServerError {
278 ret_type: result_code,
279 msg: format!("redirect to {}:{}", redirect.addr, redirect.port),
280 });
281 }
282
283 return Err(FutuError::ServerError {
284 ret_type: result_code,
285 msg: desc_str,
286 });
287 }
288
289 let enc_data = extract_bytes_field(resp_body, 2)
291 .ok_or(FutuError::Codec("missing encrypt_data in LoginRsp".into()))?;
292
293 let dec_data = if client_key.is_empty() {
294 enc_data
295 } else {
296 futu_net::encrypt::aes_cbc_md5_decrypt_var(client_key, &enc_data)?
297 };
298
299 let result = parse_rsp_encrypt_login_result(&dec_data, effective_user_id)?;
300 let session_key_len = result.session_key.len();
301
302 let session_key_len_marker = format_session_key_len_marker(session_key_len);
303 tracing::info!(
304 user_id = result.user_id,
305 keep_alive = result.keep_alive_interval,
306 session_key_len,
307 session_key_len_marker = %session_key_len_marker,
308 client_ip_present = !result.client_ip.is_empty(),
309 "TCP login succeeded, got session key; {session_key_len_marker}"
310 );
311
312 Ok(result)
313}
314
315#[derive(Debug, Clone, PartialEq, Eq)]
316struct LoginRedirect {
317 addr: String,
318 port: u32,
319 ttl: u32,
320}
321
322fn parse_login_redirect(resp_body: &[u8]) -> Result<LoginRedirect> {
323 let addr = extract_string_field(resp_body, 5)
327 .ok_or_else(|| FutuError::Codec("missing redir_svr_addr in redirect LoginRsp".into()))?;
328 let port = extract_uint32_field(resp_body, 6)
329 .ok_or_else(|| FutuError::Codec("missing redir_svr_port in redirect LoginRsp".into()))?;
330 let ttl = extract_uint32_field(resp_body, 8)
331 .ok_or_else(|| FutuError::Codec("missing redirect_ttl in redirect LoginRsp".into()))?;
332
333 Ok(LoginRedirect { addr, port, ttl })
334}
335
336fn parse_rsp_encrypt_login_result(dec_data: &[u8], effective_user_id: u64) -> Result<LoginResult> {
337 let user_id = extract_uint64_field(dec_data, 1).unwrap_or(effective_user_id);
340 let session_key_bytes = extract_bytes_field(dec_data, 4)
341 .ok_or_else(|| FutuError::Codec("missing session_key in RspEncryptData".into()))?;
342 let session_key_len = session_key_bytes.len();
343 let keep_alive = extract_uint32_field(dec_data, 8).unwrap_or(10);
344 let sec_data = extract_uint32_field(dec_data, 9).unwrap_or(1);
345 let server_time = extract_uint64_field(dec_data, 10).unwrap_or(0);
346 let client_ip = extract_string_field(dec_data, 14).unwrap_or_default();
347
348 if !matches!(session_key_len, 16 | 24 | 32) {
350 return Err(FutuError::Encryption(format!(
351 "session key has unexpected length: {} bytes (expected 16/24/32)",
352 session_key_len
353 )));
354 }
355
356 Ok(LoginResult {
357 user_id,
358 session_key: session_key_bytes,
359 keep_alive_interval: keep_alive,
360 sec_data,
361 server_time,
362 client_ip,
363 })
364}
365
366fn build_client_feature() -> Vec<u8> {
370 let mut out = Vec::with_capacity(32);
371 let device_model = os_name().to_string();
374 prost::encoding::string::encode(1, &device_model, &mut out);
375 let net_type = net_type_str_ethernet().to_string();
378 prost::encoding::string::encode(2, &net_type, &mut out);
379 out
381}
382
383fn extract_int32_field(data: &[u8], field_num: u32) -> Option<i32> {
386 extract_varint_field(data, field_num).map(|v| v as i32)
387}
388
389fn extract_uint32_field(data: &[u8], field_num: u32) -> Option<u32> {
390 extract_varint_field(data, field_num).map(|v| v as u32)
391}
392
393fn extract_uint64_field(data: &[u8], field_num: u32) -> Option<u64> {
394 extract_varint_field(data, field_num)
395}
396
397fn extract_string_field(data: &[u8], field_num: u32) -> Option<String> {
398 extract_bytes_field(data, field_num).map(|b| String::from_utf8_lossy(&b).to_string())
399}
400
401fn extract_bytes_field(data: &[u8], field_num: u32) -> Option<Vec<u8>> {
402 let mut pos = 0;
403 while pos < data.len() {
404 let (tag, new_pos) = decode_varint(data, pos)?;
405 pos = new_pos;
406
407 let wire_type = (tag & 0x07) as u8;
408 let num = (tag >> 3) as u32;
409
410 match wire_type {
411 0 => {
412 let (_val, new_pos) = decode_varint(data, pos)?;
414 if num == field_num {
415 return Some(vec![]); }
417 pos = new_pos;
418 }
419 2 => {
420 let (len, new_pos) = decode_varint(data, pos)?;
422 pos = new_pos;
423 let len = len as usize;
424 if pos + len > data.len() {
425 return None;
426 }
427 if num == field_num {
428 return Some(data[pos..pos + len].to_vec());
429 }
430 pos += len;
431 }
432 1 => {
433 pos += 8;
434 } 5 => {
436 pos += 4;
437 } _ => return None,
439 }
440 }
441 None
442}
443
444fn extract_varint_field(data: &[u8], field_num: u32) -> Option<u64> {
445 let mut pos = 0;
446 while pos < data.len() {
447 let (tag, new_pos) = decode_varint(data, pos)?;
448 pos = new_pos;
449
450 let wire_type = (tag & 0x07) as u8;
451 let num = (tag >> 3) as u32;
452
453 match wire_type {
454 0 => {
455 let (val, new_pos) = decode_varint(data, pos)?;
456 if num == field_num {
457 return Some(val);
458 }
459 pos = new_pos;
460 }
461 2 => {
462 let (len, new_pos) = decode_varint(data, pos)?;
463 pos = new_pos + len as usize;
464 }
465 1 => {
466 pos += 8;
467 }
468 5 => {
469 pos += 4;
470 }
471 _ => return None,
472 }
473 }
474 None
475}
476
477fn decode_varint(data: &[u8], start: usize) -> Option<(u64, usize)> {
478 let mut result: u64 = 0;
479 let mut shift = 0;
480 let mut pos = start;
481 loop {
482 if pos >= data.len() {
483 return None;
484 }
485 let byte = data[pos];
486 result |= ((byte & 0x7F) as u64) << shift;
487 pos += 1;
488 if byte & 0x80 == 0 {
489 return Some((result, pos));
490 }
491 shift += 7;
492 if shift >= 64 {
493 return None;
494 }
495 }
496}
497
498#[cfg(test)]
499mod tests;