1use futu_command_spec::CommandSpecId;
11use futu_core::error::{FutuError, Result};
12use futu_core::log_redact::endpoint_log_fingerprint;
13use futu_core::server_time::ServerTimeAnchorUpdate;
14use futu_domain_auth::{
15 TcpLoginChannelKind, TcpLoginCredentialFacts, TcpLoginIgnoredResponseFieldPresence,
16 TcpLoginOuterRequestFacts, TcpLoginTargetReason, plan_tcp_login_outer_request_like_cpp,
17 plan_tcp_login_request_encrypt_data_like_cpp, tcp_login_is_new_login_like_cpp,
18 tcp_login_redirect_ttl_like_cpp, validate_tcp_login_credentials_like_cpp,
19};
20use std::time::{Instant, SystemTime, UNIX_EPOCH};
21
22use crate::auth::{AuthResult, redact::uid_log_fingerprint};
23use crate::conn::BackendConn;
24use crate::connection_lifecycle_runtime::execute_connection_lifecycle;
25
26mod request_encode;
27mod request_env;
28mod response_parse;
29mod wire_decode;
30
31pub use futu_domain_auth::{
33 TCP_LOGIN_BROKER_CMD as CMD_LOGIN_BROKER, TCP_LOGIN_PLATFORM_CMD as CMD_LOGIN_PLATFORM,
34 TcpLoginSessionPhase,
35};
36
37pub fn format_session_key_len_marker(session_key_len: usize) -> String {
38 format!("session_key_len={session_key_len}")
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42enum TcpLoginStage {
43 LoginRspParsed,
44 SessionKeyAccepted,
45}
46
47impl TcpLoginStage {
48 const fn as_str(self) -> &'static str {
49 match self {
50 Self::LoginRspParsed => "login_rsp_parsed",
51 Self::SessionKeyAccepted => "session_key_accepted",
52 }
53 }
54}
55
56fn format_tcp_login_stage_marker(
57 cmd_id: u16,
58 stage: TcpLoginStage,
59 session_key_len: Option<usize>,
60) -> String {
61 match session_key_len {
62 Some(session_key_len) => format!(
63 "cmd_id={cmd_id} stage={} session_key_len={session_key_len}",
64 stage.as_str()
65 ),
66 None => format!("cmd_id={cmd_id} stage={}", stage.as_str()),
67 }
68}
69
70fn trace_tcp_login_stage(cmd_id: u16, stage: TcpLoginStage, session_key_len: Option<usize>) {
71 let marker = format_tcp_login_stage_marker(cmd_id, stage, session_key_len);
72 tracing::debug!(
73 cmd_id,
74 stage = stage.as_str(),
75 session_key_len,
76 marker = %marker,
77 "TCP login response stage"
78 );
79}
80
81pub const TCP_LOGIN_REDIRECT_SAFETY_LIMIT: usize = 3;
89
90#[derive(Debug, Clone)]
92pub struct LoginResult {
93 pub user_id: u64,
94 pub session_key: Vec<u8>,
98 pub session_key_update_interval: u32,
101 pub web_url_head: Option<String>,
103 pub keep_alive_interval: u32,
104 pub sec_data: Option<u32>,
105 pub server_time_update: Option<ServerTimeAnchorUpdate>,
109 pub client_ip: String,
115 pub update_flag: Option<u32>,
116 pub web_session_id: Option<u64>,
117 pub ignored_field_presence: TcpLoginIgnoredResponseFieldPresence,
120}
121
122#[derive(Debug, Clone, Copy)]
124pub struct TcpLoginTarget<'a> {
125 pub is_new_login: bool,
126 pub redirect_ttl: u32,
127 pub host_ip: &'a str,
128 pub host_port: u32,
129}
130
131impl<'a> TcpLoginTarget<'a> {
132 pub fn fresh(phase: TcpLoginSessionPhase, host_ip: &'a str, host_port: u32) -> Self {
133 Self::new(
134 tcp_login_is_new_login_like_cpp(phase),
135 tcp_login_redirect_ttl_like_cpp(TcpLoginTargetReason::FreshConnection),
136 host_ip,
137 host_port,
138 )
139 }
140
141 pub fn redirected(
142 phase: TcpLoginSessionPhase,
143 server_ttl: u32,
144 host_ip: &'a str,
145 host_port: u32,
146 ) -> Self {
147 Self::new(
148 tcp_login_is_new_login_like_cpp(phase),
149 tcp_login_redirect_ttl_like_cpp(TcpLoginTargetReason::ServerRedirect { server_ttl }),
150 host_ip,
151 host_port,
152 )
153 }
154
155 pub fn new(is_new_login: bool, redirect_ttl: u32, host_ip: &'a str, host_port: u32) -> Self {
159 Self {
160 is_new_login,
161 redirect_ttl,
162 host_ip,
163 host_port,
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct TcpLoginRedirect {
170 pub result_code: i32,
171 pub addr: String,
172 pub port: u32,
173 pub ttl: u32,
174 pub condition_flag: i32,
178}
179
180impl TcpLoginRedirect {
181 pub fn endpoint(&self) -> String {
182 format!("{}:{}", self.addr, self.port)
183 }
184}
185
186#[derive(Debug, Clone)]
187pub enum TcpLoginAttemptOutcome {
188 Success(LoginResult),
189 Redirect(TcpLoginRedirect),
190}
191
192#[derive(Debug, Clone, Copy)]
195pub struct TcpLoginChannel<'a> {
196 pub cmd_id: u16,
197 pub conn_identity: u32,
198 pub effective_user_id: u64,
199 pub client_sig: &'a [u8],
200}
201
202impl<'a> TcpLoginChannel<'a> {
203 pub fn platform(auth: &'a AuthResult) -> Self {
204 Self {
205 cmd_id: CMD_LOGIN_PLATFORM,
206 conn_identity: auth.user_attribution.to_conn_identity(),
207 effective_user_id: auth.user_id,
208 client_sig: &auth.client_sig,
209 }
210 }
211
212 pub fn broker(conn_identity: u32, customer_id: u64, broker_client_sig: &'a [u8]) -> Self {
213 Self {
214 cmd_id: CMD_LOGIN_BROKER,
215 conn_identity,
216 effective_user_id: customer_id,
217 client_sig: broker_client_sig,
218 }
219 }
220}
221
222pub async fn tcp_login_attempt(
223 conn: &BackendConn,
224 auth: &AuthResult,
225 client_key: &[u8],
226 target: TcpLoginTarget<'_>,
227) -> Result<TcpLoginAttemptOutcome> {
228 tcp_login_raw_attempt(conn, client_key, target, TcpLoginChannel::platform(auth)).await
229}
230
231pub async fn tcp_login_raw_attempt(
247 conn: &BackendConn,
248 client_key: &[u8],
249 target: TcpLoginTarget<'_>,
250 channel: TcpLoginChannel<'_>,
251) -> Result<TcpLoginAttemptOutcome> {
252 let TcpLoginTarget {
253 is_new_login,
254 redirect_ttl,
255 host_ip,
256 host_port,
257 } = target;
258 let TcpLoginChannel {
259 cmd_id,
260 conn_identity,
261 effective_user_id,
262 client_sig,
263 } = channel;
264 let channel_kind = TcpLoginChannelKind::from_command_id(cmd_id)
265 .ok_or_else(|| FutuError::Codec(format!("unsupported TCP login command: {cmd_id}")))?;
266 validate_tcp_login_credentials_like_cpp(TcpLoginCredentialFacts {
267 client_key_len: client_key.len(),
268 client_sig_len: client_sig.len(),
269 })
270 .map_err(|error| FutuError::Codec(error.to_string()))?;
271
272 let req_encrypt_facts = request_env::tcp_login_request_encrypt_data_facts(
273 effective_user_id,
274 redirect_ttl,
275 host_ip,
276 host_port,
277 conn_identity,
278 );
279 let req_encrypt_plan = plan_tcp_login_request_encrypt_data_like_cpp(req_encrypt_facts);
280 let req_encrypt = request_encode::build_req_encrypt_data_from_plan(&req_encrypt_plan);
281
282 let encrypted_data = futu_net::encrypt::aes_cbc_md5_encrypt_var(client_key, &req_encrypt)?;
287
288 let outer_plan = plan_tcp_login_outer_request_like_cpp(TcpLoginOuterRequestFacts {
289 channel_kind,
290 user_id: effective_user_id,
291 new_login: is_new_login,
292 client_sig: client_sig.to_vec(),
293 encrypted_data,
294 });
295 let login_req = request_encode::build_login_req_from_plan(&outer_plan);
296
297 tracing::info!(
298 user_id_fp = %uid_log_fingerprint(effective_user_id),
299 cmd_id = outer_plan.cmd_id,
300 channel = outer_plan.channel_name,
301 conn_identity = conn_identity,
302 host_fp = %endpoint_log_fingerprint(&format!("{host_ip}:{host_port}")),
303 "sending TCP login request"
304 );
305 tracing::debug!(
306 login_req_len = login_req.len(),
307 req_encrypt_plain_len = req_encrypt.len(),
308 client_key_len = client_key.len(),
309 client_sig_len = client_sig.len(),
310 encrypted_data_len = outer_plan.encrypted_data.len(),
311 "TCP login request details"
312 );
313
314 let request_started_at = Instant::now();
315 let resp_frame = execute_connection_lifecycle(
316 conn,
317 CommandSpecId::LoginPlaintext(outer_plan.cmd_id),
318 login_req,
319 )
320 .await?;
321 let response_captured_at = Instant::now();
322 let response_timing = response_parse::LoginResponseTiming {
323 round_trip_micros: duration_micros_u64(
324 response_captured_at.saturating_duration_since(request_started_at),
325 ),
326 local_receive_unix_micros: local_unix_micros(),
327 captured_at: response_captured_at,
328 };
329
330 let resp_body = &resp_frame.body;
332 tracing::debug!(
338 resp_body_len = resp_body.len(),
339 "TCP login response (body redacted, F-003 v1.4.102 fix)"
340 );
341
342 let response_action = response_parse::parse_login_response_action(resp_body)?;
343 trace_tcp_login_stage(cmd_id, TcpLoginStage::LoginRspParsed, None);
344 match response_action {
345 response_parse::LoginResponseAction::Success => {}
346 response_parse::LoginResponseAction::Redirect {
347 result_code,
348 redirect,
349 } => {
350 let redirect_endpoint = format!("{}:{}", redirect.addr, redirect.port);
352 tracing::warn!(
353 addr_fp = %endpoint_log_fingerprint(&redirect_endpoint),
354 ttl = redirect.ttl,
355 "login redirect"
356 );
357 return Ok(TcpLoginAttemptOutcome::Redirect(TcpLoginRedirect {
358 result_code,
359 addr: redirect.addr,
360 port: redirect.port,
361 ttl: redirect.ttl,
362 condition_flag: redirect.condition_flag,
363 }));
364 }
365 response_parse::LoginResponseAction::Failure {
366 result_code,
367 message,
368 } => {
369 return Err(FutuError::ServerError {
370 ret_type: result_code,
371 msg: message,
372 });
373 }
374 }
375
376 let enc_data = response_parse::login_rsp_encrypt_data(resp_body)?;
378
379 let dec_data = futu_net::encrypt::aes_cbc_md5_decrypt_var(client_key, &enc_data)?;
380
381 let result = response_parse::parse_rsp_encrypt_login_result(
382 &dec_data,
383 effective_user_id,
384 response_timing,
385 )?;
386 let session_key_len = result.session_key.len();
387 trace_tcp_login_stage(
388 cmd_id,
389 TcpLoginStage::SessionKeyAccepted,
390 Some(session_key_len),
391 );
392
393 let session_key_len_marker = format_session_key_len_marker(session_key_len);
394 tracing::info!(
395 user_id_fp = %uid_log_fingerprint(result.user_id),
396 keep_alive = result.keep_alive_interval,
397 session_key_update_interval = result.session_key_update_interval,
398 session_key_len,
399 session_key_len_marker = %session_key_len_marker,
400 client_ip_present = !result.client_ip.is_empty(),
401 web_url_head_present = result.web_url_head.is_some(),
402 update_flag_present = result.update_flag.is_some(),
403 web_session_id_present = result.web_session_id.is_some(),
404 ignored_user_auth_flag_present = result.ignored_field_presence.user_auth_flag,
405 ignored_user_service_flag_present = result.ignored_field_presence.user_service_flag,
406 ignored_web_session_key_present = result.ignored_field_presence.web_session_key,
407 "TCP login succeeded, got session key; {session_key_len_marker}"
408 );
409
410 Ok(TcpLoginAttemptOutcome::Success(result))
411}
412
413fn duration_micros_u64(duration: std::time::Duration) -> u64 {
414 duration.as_micros().min(u128::from(u64::MAX)) as u64
415}
416
417fn local_unix_micros() -> Option<u64> {
418 match SystemTime::now().duration_since(UNIX_EPOCH) {
419 Ok(elapsed) => u64::try_from(elapsed.as_micros()).ok(),
420 Err(error) => {
421 tracing::warn!(%error, "system clock is before UNIX_EPOCH; TCP login clock not updated");
422 None
423 }
424 }
425}
426
427#[cfg(test)]
428mod tests;