futu_backend/auth/
http_client.rs1use futu_core::error::{FutuError, Result};
11
12pub(crate) const AUTH_HTTP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
13pub(crate) const AUTH_HTTP_MAX_REDIRECTS: usize = 3;
14pub(crate) const PRIMARY_AUTH_HTTP_MAX_REDIRECTS: usize = 6;
15
16pub fn build_http_client(client_type: u8) -> Result<reqwest::Client> {
17 build_http_client_with_resolve(client_type, None)
18}
19
20pub(crate) fn build_http_client_with_resolve(
21 client_type: u8,
22 resolve: Option<(&str, std::net::SocketAddr)>,
23) -> Result<reqwest::Client> {
24 build_http_client_with_policy(
25 client_type,
26 resolve,
27 AuthHttpRedirectPolicy::SharedLimited(AUTH_HTTP_MAX_REDIRECTS),
28 )
29}
30
31pub(crate) fn build_primary_auth_http_client(client_type: u8) -> Result<reqwest::Client> {
32 build_primary_auth_http_client_with_resolve(client_type, None)
33}
34
35pub(crate) fn build_primary_auth_http_client_with_resolve(
36 client_type: u8,
37 resolve: Option<(&str, std::net::SocketAddr)>,
38) -> Result<reqwest::Client> {
39 build_http_client_with_policy(
40 client_type,
41 resolve,
42 AuthHttpRedirectPolicy::PrimaryRequesterOwned,
43 )
44}
45
46enum AuthHttpRedirectPolicy {
47 SharedLimited(usize),
48 PrimaryRequesterOwned,
49}
50
51fn build_http_client_with_policy(
52 client_type: u8,
53 resolve: Option<(&str, std::net::SocketAddr)>,
54 redirect_policy: AuthHttpRedirectPolicy,
55) -> Result<reqwest::Client> {
56 let _ = client_type;
75 let redirect_policy = match redirect_policy {
84 AuthHttpRedirectPolicy::SharedLimited(max_redirects) => {
85 reqwest::redirect::Policy::limited(max_redirects)
86 }
87 AuthHttpRedirectPolicy::PrimaryRequesterOwned => reqwest::redirect::Policy::none(),
88 };
89 let mut builder = futu_core::http_client::webpki_builder()
90 .timeout(AUTH_HTTP_TIMEOUT)
91 .redirect(redirect_policy);
92 if let Some((domain, addr)) = resolve {
93 builder = builder.resolve(domain, addr);
94 }
95 builder
96 .build()
97 .map_err(|e| FutuError::Encryption(format!("http client: {e}")))
98}
99
100pub(crate) fn auth_http_default_headers(client_type: u8) -> Result<reqwest::header::HeaderMap> {
109 let mut default_headers = reqwest::header::HeaderMap::new();
110 default_headers.insert(
111 "X-Futu-Client-Type",
112 http_header_value("X-Futu-Client-Type", client_type)?,
113 );
114 default_headers.insert(
115 "X-Futu-Client-Version",
116 http_header_value(
117 "X-Futu-Client-Version",
118 crate::conn::BackendConn::CLIENT_VER_FTGTW,
119 )?,
120 );
121 default_headers.insert(
122 "X-Futu-Client-Lang",
123 reqwest::header::HeaderValue::from_static("sc"),
124 );
125 default_headers.insert(
126 "Content-Type",
127 reqwest::header::HeaderValue::from_static("application/json"),
128 );
129 Ok(default_headers)
130}
131
132pub(crate) fn auth_business_headers(
133 client_type: u8,
134 device_id: &str,
135) -> Result<reqwest::header::HeaderMap> {
136 let mut headers = reqwest::header::HeaderMap::new();
137 headers.insert(
138 reqwest::header::CONTENT_TYPE,
139 reqwest::header::HeaderValue::from_static("application/json"),
140 );
141 headers.insert(
142 reqwest::header::COOKIE,
143 http_header_value("Cookie", format!("device_id={device_id}"))?,
144 );
145 headers.insert(
146 reqwest::header::USER_AGENT,
147 http_header_value("User-Agent", opend_auth_user_agent(client_type))?,
148 );
149 AuthTraceHeaders::new().insert_http_headers(&mut headers)?;
150 Ok(headers)
151}
152
153#[derive(Debug, Clone)]
154pub(crate) struct AuthTraceHeaders {
155 trace_id: String,
156 parent_span_id: String,
157 span_id: String,
158}
159
160impl AuthTraceHeaders {
161 pub(crate) fn new() -> Self {
162 Self {
166 trace_id: hex::encode(rand::random::<[u8; 16]>()),
167 parent_span_id: hex::encode(rand::random::<[u8; 8]>()),
168 span_id: hex::encode(rand::random::<[u8; 8]>()),
169 }
170 }
171
172 pub(crate) fn entries(&self) -> [(&'static str, &str); 3] {
173 [
174 ("x-b3-traceid", self.trace_id.as_str()),
175 ("x-b3-parentspanid", self.parent_span_id.as_str()),
176 ("x-b3-spanid", self.span_id.as_str()),
177 ]
178 }
179
180 fn insert_http_headers(&self, headers: &mut reqwest::header::HeaderMap) -> Result<()> {
181 for (name, value) in self.entries() {
182 headers.insert(name, http_header_value(name, value)?);
183 }
184 Ok(())
185 }
186}
187
188pub(crate) fn opend_auth_user_agent(client_type: u8) -> String {
189 format!(
191 "ClientType/{client_type} ClientVersion/{} CliLang/zh-cn ClientHourClock/24 OsType/{} RequestSource/Http",
192 crate::conn::BackendConn::CLIENT_VER_FTGTW,
193 opend_user_agent_os_type(),
194 )
195}
196
197fn opend_user_agent_os_type() -> &'static str {
198 if cfg!(target_os = "macos") {
199 "11"
200 } else if cfg!(target_os = "linux") {
201 "14"
202 } else {
203 "10"
204 }
205}
206
207fn http_header_value(
208 name: &'static str,
209 value: impl std::fmt::Display,
210) -> Result<reqwest::header::HeaderValue> {
211 reqwest::header::HeaderValue::from_str(&value.to_string())
212 .map_err(|e| FutuError::Codec(format!("{name}: invalid header value: {e}")))
213}