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