futu_backend/auth/
repull.rs1use futu_core::error::{FutuError, Result};
56
57use super::redact::uid_log_fingerprint;
58use super::{BrokerAuthCode, UserAttribution};
59
60const REPULL_AUTH_CODE_PATH: &str = "/authority/repull_auth_code";
63
64pub const ERROR_CODE_NO_VALID_CID: i64 = 20029;
68
69pub async fn repull_auth_code(
90 http: &reqwest::Client,
91 attribution: UserAttribution,
92 uid: u64,
93 web_sig: &str,
94 device_id: &str,
95 broker_id: u32,
96) -> Result<BrokerAuthCode> {
97 let client_type = match attribution {
101 UserAttribution::Cn | UserAttribution::Hk => 40,
102 _ => 60,
103 };
104 repull_auth_code_with_client_type(
105 http,
106 client_type,
107 attribution,
108 uid,
109 web_sig,
110 device_id,
111 broker_id,
112 )
113 .await
114}
115
116pub async fn repull_auth_code_with_client_type(
119 http: &reqwest::Client,
120 client_type: u8,
121 attribution: UserAttribution,
122 uid: u64,
123 web_sig: &str,
124 device_id: &str,
125 broker_id: u32,
126) -> Result<BrokerAuthCode> {
127 if web_sig.is_empty() {
129 return Err(FutuError::Codec(
130 "repull_auth_code: web_sig empty (legacy credentials before v1.4.93 G3 \
131 or device-verify shell path) — caller should fallback to platform refresh"
132 .into(),
133 ));
134 }
135 if uid == 0 {
136 return Err(FutuError::Codec(
137 "repull_auth_code: uid is 0 (invalid)".into(),
138 ));
139 }
140 if super::broker_config(broker_id).is_none() {
141 return Err(FutuError::Codec(format!(
142 "repull_auth_code: unknown broker_id {broker_id}"
143 )));
144 }
145
146 let url = repull_auth_code_url(attribution);
147
148 let body = serde_json::json!({
149 "uid": uid,
150 "device_id": device_id,
151 "web_sig": web_sig,
152 "broker_id": broker_id,
153 });
154
155 let uid_fp = uid_log_fingerprint(uid);
156 tracing::info!(
157 broker_id,
158 uid_fp = %uid_fp,
159 url = %url,
160 attribution = ?attribution,
161 "v1.4.93 G2: POST /authority/repull_auth_code (broker auth_code refresh)"
162 );
163
164 let headers = super::http_client::auth_business_headers(client_type, device_id)?;
167 let resp: serde_json::Value = http
168 .post(&url)
169 .headers(headers)
170 .json(&body)
171 .send()
172 .await
173 .map_err(|e| FutuError::Network(std::io::Error::other(e.to_string())))?
174 .json()
175 .await
176 .map_err(|e| FutuError::Codec(format!("repull_auth_code: response not JSON: {e}")))?;
177
178 if let Some(err) = resp.get("error").and_then(|e| e.as_object()) {
180 let code = err.get("error_code").and_then(|v| v.as_i64()).unwrap_or(-1);
181 let msg = err
182 .get("error_msg")
183 .and_then(|v| v.as_str())
184 .unwrap_or("unknown");
185 if code != 0 {
186 tracing::warn!(
187 broker_id,
188 uid_fp = %uid_fp,
189 error_code = code,
190 error_msg = %msg,
191 no_valid_cid = code == ERROR_CODE_NO_VALID_CID,
192 "v1.4.93 G2: RepullAuthCode failed"
193 );
194 return Err(FutuError::ServerError {
195 ret_type: code as i32,
196 msg: format!("repull_auth_code broker_id={broker_id}: {msg}"),
197 });
198 }
199 }
200
201 let result = resp
202 .get("result")
203 .and_then(|r| r.as_object())
204 .ok_or_else(|| {
205 FutuError::Codec("repull_auth_code: missing result + missing error".into())
206 })?;
207
208 parse_repull_success_response(result, uid, broker_id)
209}
210
211fn repull_auth_code_url(attribution: UserAttribution) -> String {
212 format!("{}{}", attribution.auth_domain(), REPULL_AUTH_CODE_PATH)
216}
217
218fn parse_repull_success_response(
219 result: &serde_json::Map<String, serde_json::Value>,
220 uid: u64,
221 broker_id: u32,
222) -> Result<BrokerAuthCode> {
223 let resp_uid = result.get("uid").and_then(|v| v.as_u64()).unwrap_or(0);
225 let resp_broker_id_raw = result
226 .get("broker_id")
227 .and_then(|v| v.as_u64())
228 .ok_or_else(|| FutuError::Codec("repull_auth_code: response broker_id invalid".into()))?;
229 let resp_broker_id = u32::try_from(resp_broker_id_raw).map_err(|_| {
230 FutuError::Codec(format!(
231 "repull_auth_code: response broker_id invalid (got {resp_broker_id_raw})"
232 ))
233 })?;
234 let auth_code = result
235 .get("auth_code")
236 .and_then(|v| v.as_str())
237 .unwrap_or("")
238 .to_string();
239 let invalid_time = result
240 .get("invalid_time")
241 .and_then(|v| v.as_u64())
242 .unwrap_or(0);
243
244 if resp_uid != uid {
246 let expected_uid_fp = uid_log_fingerprint(uid);
247 let got_uid_fp = uid_log_fingerprint(resp_uid);
248 return Err(FutuError::Codec(format!(
249 "repull_auth_code: response uid mismatch (expected {expected_uid_fp}, \
250 got {got_uid_fp})"
251 )));
252 }
253 if resp_broker_id != broker_id {
254 return Err(FutuError::Codec(format!(
255 "repull_auth_code: response broker_id mismatch (expected {broker_id}, \
256 got {resp_broker_id})"
257 )));
258 }
259 if auth_code.is_empty() || invalid_time == 0 {
261 return Err(FutuError::Codec(format!(
262 "repull_auth_code: empty auth_code or invalid_time (auth_code_len={}, \
263 invalid_time={invalid_time})",
264 auth_code.len()
265 )));
266 }
267
268 tracing::info!(
269 broker_id,
270 uid_fp = %uid_log_fingerprint(uid),
271 invalid_time,
272 auth_code_len = auth_code.len(),
273 "v1.4.93 G2: RepullAuthCode success — broker auth_code refreshed"
274 );
275
276 Ok(BrokerAuthCode {
277 broker_id,
278 auth_code,
279 invalid_time,
280 })
281}
282
283#[cfg(test)]
284mod tests;