futu_backend/auth/
login.rs1use std::sync::Arc;
2
3use futu_core::error::{FutuError, Result};
4
5use super::endpoints;
6use super::http_client::build_primary_auth_http_client;
7use super::password_auth::password_auth;
8use super::phone::normalize_phone_account;
9use super::redact::{self, account_log_fingerprint};
10use super::{AuthChallengePort, AuthConfig, AuthResult, AuthSession};
11
12tokio::task_local! {
13 static AUTH_CHALLENGE_PORT: Arc<dyn AuthChallengePort>;
14}
15
16pub(super) fn current_auth_challenge_port() -> Option<Arc<dyn AuthChallengePort>> {
17 AUTH_CHALLENGE_PORT.try_with(Arc::clone).ok()
18}
19
20mod cached_credentials;
21#[cfg(test)]
22pub(in crate::auth) use cached_credentials::install_test_cached_verify_origin_override;
23use cached_credentials::try_cached_credentials_login;
24
25#[cfg(test)]
26pub(in crate::auth) async fn try_cached_credentials_login_for_test(
27 http: &reqwest::Client,
28 effective_config: &AuthConfig,
29 region_code: Option<&str>,
30 verify_cb: Option<&(dyn Fn() -> Option<String> + Send + Sync)>,
31 primary_webtcp: Option<&endpoints::PrimaryAuthWebTcpContext>,
32) -> Result<Option<AuthResult>> {
33 try_cached_credentials_login(
34 http,
35 effective_config,
36 region_code,
37 verify_cb,
38 primary_webtcp,
39 )
40 .await
41}
42
43fn attach_primary_auth_site_config(
44 auth_result: AuthResult,
45 context: Option<&endpoints::PrimaryAuthWebTcpContext>,
46) -> AuthSession {
47 let bootstrap_site_config = context
48 .map_or_else(crate::auth::site_config::empty_shared, |context| {
49 Arc::clone(&context.site_config)
50 });
51 AuthSession {
52 auth_result,
53 bootstrap_site_config,
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(in crate::auth) enum CachedSmsPreflight {
59 DirectVerify,
60 AwaitInput,
61 Continue,
62}
63
64pub(in crate::auth) fn plan_cached_sms_preflight(
68 dvs_fresh: bool,
69 dcs_fresh: bool,
70 has_verify_cb: bool,
71) -> CachedSmsPreflight {
72 if dvs_fresh && dcs_fresh {
73 if has_verify_cb {
74 CachedSmsPreflight::DirectVerify
75 } else {
76 CachedSmsPreflight::AwaitInput
77 }
78 } else {
79 CachedSmsPreflight::Continue
80 }
81}
82
83pub(in crate::auth) fn should_fallback_to_password_auth_after_remember_error(
84 err: &FutuError,
85) -> bool {
86 !matches!(
87 err,
88 FutuError::ServerError { ret_type: 20, msg }
89 if msg.starts_with("remember-login device verification did not complete:")
90 )
91}
92
93pub type VerifyCodeCallback = Box<dyn Fn() -> Option<String> + Send + Sync>;
98
99pub async fn authenticate(config: &AuthConfig) -> Result<AuthSession> {
104 authenticate_with_callback(config, None).await
105}
106
107pub async fn authenticate_with_callback(
109 config: &AuthConfig,
110 verify_cb: Option<VerifyCodeCallback>,
111) -> Result<AuthSession> {
112 redact::emit_debug_log_security_warn_once();
115
116 let http = build_primary_auth_http_client(config.protocol_identity.client_type())?;
117 let primary_webtcp = endpoints::primary_auth_webtcp_context_for_auth_server(
118 config.protocol_identity,
119 &config.auth_server,
120 );
121 let _primary_webtcp_prefetch = primary_webtcp.as_ref().map(|context| {
122 endpoints::spawn_primary_auth_site_config_prefetch(context, &config.device_id)
123 });
124
125 let (normalized_account, region_code) = normalize_phone_account(&config.account);
130 if region_code.is_some() {
131 tracing::info!(
133 original_fp = %account_log_fingerprint(&config.account),
134 account_fp = %account_log_fingerprint(&normalized_account),
135 region_no = %region_code.as_deref().unwrap_or(""),
136 "parsed phone account with region code"
137 );
138 }
139 let mut effective_config = config.clone();
141 effective_config.account = normalized_account;
142
143 if let Some(auth_result) = try_cached_credentials_login(
144 &http,
145 &effective_config,
146 region_code.as_deref(),
147 verify_cb.as_deref(),
148 primary_webtcp.as_ref(),
149 )
150 .await?
151 {
152 return Ok(attach_primary_auth_site_config(
153 auth_result,
154 primary_webtcp.as_ref(),
155 ));
156 }
157
158 let auth_result = password_auth(
182 &effective_config,
183 region_code.as_deref(),
184 &http,
185 verify_cb.as_deref(),
186 primary_webtcp.as_ref(),
187 )
188 .await?;
189 Ok(attach_primary_auth_site_config(
190 auth_result,
191 primary_webtcp.as_ref(),
192 ))
193}
194
195pub async fn authenticate_with_challenge_port(
196 config: &AuthConfig,
197 port: Arc<dyn AuthChallengePort>,
198) -> Result<AuthSession> {
199 with_auth_challenge_port(port, authenticate_with_callback(config, None)).await
200}
201
202pub async fn with_auth_challenge_port<F>(port: Arc<dyn AuthChallengePort>, future: F) -> F::Output
203where
204 F: std::future::Future,
205{
206 AUTH_CHALLENGE_PORT.scope(port, future).await
207}