futu_backend/auth/
repull.rs1use futu_core::error::{FutuError, Result};
56use futu_domain_broker_reconnect::{
57 BrokerRepullAuthCodeRequestFacts, BrokerRepullAuthCodeRequestReject,
58 BrokerRepullAuthCodeSuccessFacts, BrokerRepullAuthCodeSuccessReject,
59 validate_broker_repull_auth_code_request, validate_broker_repull_auth_code_success,
60};
61
62use super::redact::uid_log_fingerprint;
63use super::{BrokerAuthCode, UserAttribution};
64
65const REPULL_AUTH_CODE_PATH: &str = "/authority/repull_auth_code";
68
69pub const ERROR_CODE_NO_VALID_CID: i64 = 20029;
73
74pub async fn repull_auth_code(
95 http: &reqwest::Client,
96 attribution: UserAttribution,
97 uid: u64,
98 web_sig: &str,
99 device_id: &str,
100 broker_id: u32,
101) -> Result<BrokerAuthCode> {
102 let client_type = match attribution {
106 UserAttribution::Cn | UserAttribution::Hk => 40,
107 _ => 60,
108 };
109 repull_auth_code_with_client_type(
110 http,
111 client_type,
112 attribution,
113 uid,
114 web_sig,
115 device_id,
116 broker_id,
117 )
118 .await
119}
120
121pub async fn repull_auth_code_with_client_type(
124 http: &reqwest::Client,
125 client_type: u8,
126 attribution: UserAttribution,
127 uid: u64,
128 web_sig: &str,
129 device_id: &str,
130 broker_id: u32,
131) -> Result<BrokerAuthCode> {
132 let request = validate_broker_repull_auth_code_request(BrokerRepullAuthCodeRequestFacts {
133 uid,
134 web_sig,
135 broker_id,
136 })
137 .map_err(repull_request_reject_error)?;
138 let uid = request.uid;
139 let web_sig = request.web_sig;
140 let broker_id = request.broker_id;
141
142 let url = repull_auth_code_url(attribution);
143
144 let body = serde_json::json!({
145 "uid": uid,
146 "device_id": device_id,
147 "web_sig": web_sig,
148 "broker_id": broker_id,
149 });
150
151 let uid_fp = uid_log_fingerprint(uid);
152 tracing::info!(
153 broker_id,
154 uid_fp = %uid_fp,
155 url = %url,
156 attribution = ?attribution,
157 "v1.4.93 G2: POST /authority/repull_auth_code (broker auth_code refresh)"
158 );
159
160 let headers = super::http_client::auth_business_headers(client_type, device_id)?;
163 let resp: serde_json::Value = http
164 .post(&url)
165 .headers(headers)
166 .json(&body)
167 .send()
168 .await
169 .map_err(|e| FutuError::Network(std::io::Error::other(e.to_string())))?
170 .json()
171 .await
172 .map_err(|e| FutuError::Codec(format!("repull_auth_code: response not JSON: {e}")))?;
173
174 if let Some(err) = resp.get("error").and_then(|e| e.as_object()) {
176 let code = err.get("error_code").and_then(|v| v.as_i64()).unwrap_or(-1);
177 let msg = err
178 .get("error_msg")
179 .and_then(|v| v.as_str())
180 .unwrap_or("unknown");
181 if code != 0 {
182 tracing::warn!(
183 broker_id,
184 uid_fp = %uid_fp,
185 error_code = code,
186 error_msg = %msg,
187 no_valid_cid = code == ERROR_CODE_NO_VALID_CID,
188 "v1.4.93 G2: RepullAuthCode failed"
189 );
190 return Err(FutuError::ServerError {
191 ret_type: code as i32,
192 msg: format!("repull_auth_code broker_id={broker_id}: {msg}"),
193 });
194 }
195 }
196
197 let result = resp
198 .get("result")
199 .and_then(|r| r.as_object())
200 .ok_or_else(|| {
201 FutuError::Codec("repull_auth_code: missing result + missing error".into())
202 })?;
203
204 parse_repull_success_response(result, uid, broker_id)
205}
206
207fn repull_request_reject_error(reject: BrokerRepullAuthCodeRequestReject) -> FutuError {
208 match reject {
209 BrokerRepullAuthCodeRequestReject::EmptyWebSig => FutuError::Codec(
210 "repull_auth_code: web_sig empty (legacy credentials before v1.4.93 G3 \
211 or device-verify shell path) — caller should fallback to platform refresh"
212 .into(),
213 ),
214 BrokerRepullAuthCodeRequestReject::ZeroUid => {
215 FutuError::Codec("repull_auth_code: uid is 0 (invalid)".into())
216 }
217 BrokerRepullAuthCodeRequestReject::UnsupportedBrokerId { broker_id } => {
218 FutuError::Codec(format!("repull_auth_code: unknown broker_id {broker_id}"))
219 }
220 }
221}
222
223fn repull_auth_code_url(attribution: UserAttribution) -> String {
224 format!("{}{}", attribution.auth_domain(), REPULL_AUTH_CODE_PATH)
228}
229
230fn parse_repull_success_response(
231 result: &serde_json::Map<String, serde_json::Value>,
232 uid: u64,
233 broker_id: u32,
234) -> Result<BrokerAuthCode> {
235 let validated = validate_broker_repull_auth_code_success(BrokerRepullAuthCodeSuccessFacts {
237 expected_uid: uid,
238 expected_broker_id: broker_id,
239 response_uid: result.get("uid").and_then(|v| v.as_u64()),
240 response_broker_id: result.get("broker_id").and_then(|v| v.as_u64()),
241 auth_code: result.get("auth_code").and_then(|v| v.as_str()),
242 invalid_time: result.get("invalid_time").and_then(|v| v.as_u64()),
243 })
244 .map_err(repull_success_reject_error)?;
245
246 let auth_code = validated.auth_code.to_string();
247 let invalid_time = validated.invalid_time;
248
249 tracing::info!(
250 broker_id,
251 uid_fp = %uid_log_fingerprint(uid),
252 invalid_time,
253 auth_code_len = auth_code.len(),
254 "v1.4.93 G2: RepullAuthCode success — broker auth_code refreshed"
255 );
256
257 Ok(BrokerAuthCode {
258 broker_id: validated.broker_id,
259 auth_code,
260 invalid_time,
261 })
262}
263
264fn repull_success_reject_error(reject: BrokerRepullAuthCodeSuccessReject) -> FutuError {
265 match reject {
266 BrokerRepullAuthCodeSuccessReject::UidMismatch {
267 expected_uid,
268 response_uid,
269 } => {
270 let expected_uid_fp = uid_log_fingerprint(expected_uid);
271 let got_uid_fp = uid_log_fingerprint(response_uid);
272 FutuError::Codec(format!(
273 "repull_auth_code: response uid mismatch (expected {expected_uid_fp}, \
274 got {got_uid_fp})"
275 ))
276 }
277 BrokerRepullAuthCodeSuccessReject::MissingBrokerId => {
278 FutuError::Codec("repull_auth_code: response broker_id invalid".into())
279 }
280 BrokerRepullAuthCodeSuccessReject::InvalidBrokerId { response_broker_id } => {
281 FutuError::Codec(format!(
282 "repull_auth_code: response broker_id invalid (got {response_broker_id})"
283 ))
284 }
285 BrokerRepullAuthCodeSuccessReject::BrokerIdMismatch {
286 expected_broker_id,
287 response_broker_id,
288 } => FutuError::Codec(format!(
289 "repull_auth_code: response broker_id mismatch (expected {expected_broker_id}, \
290 got {response_broker_id})"
291 )),
292 BrokerRepullAuthCodeSuccessReject::EmptyAuthCodeOrInvalidTime {
293 auth_code_len,
294 invalid_time,
295 } => FutuError::Codec(format!(
296 "repull_auth_code: empty auth_code or invalid_time (auth_code_len={auth_code_len}, \
297 invalid_time={invalid_time})"
298 )),
299 }
300}
301
302#[cfg(test)]
303mod tests;