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;
14
15pub fn build_http_client(client_type: u8) -> Result<reqwest::Client> {
16 build_http_client_with_resolve(client_type, None)
17}
18
19pub(crate) fn build_http_client_with_resolve(
20 client_type: u8,
21 resolve: Option<(&str, std::net::SocketAddr)>,
22) -> Result<reqwest::Client> {
23 let _ = client_type;
42 let mut builder = reqwest::Client::builder()
46 .timeout(AUTH_HTTP_TIMEOUT)
47 .redirect(reqwest::redirect::Policy::limited(AUTH_HTTP_MAX_REDIRECTS));
48 if let Some((domain, addr)) = resolve {
49 builder = builder.resolve(domain, addr);
50 }
51 builder
52 .build()
53 .map_err(|e| FutuError::Encryption(format!("http client: {e}")))
54}
55
56pub(crate) fn auth_http_default_headers(client_type: u8) -> Result<reqwest::header::HeaderMap> {
65 let mut default_headers = reqwest::header::HeaderMap::new();
66 default_headers.insert(
67 "X-Futu-Client-Type",
68 http_header_value("X-Futu-Client-Type", client_type)?,
69 );
70 default_headers.insert(
71 "X-Futu-Client-Version",
72 http_header_value(
73 "X-Futu-Client-Version",
74 crate::conn::BackendConn::CLIENT_VER_FTGTW,
75 )?,
76 );
77 default_headers.insert(
78 "X-Futu-Client-Lang",
79 reqwest::header::HeaderValue::from_static("sc"),
80 );
81 default_headers.insert(
82 "Content-Type",
83 reqwest::header::HeaderValue::from_static("application/json"),
84 );
85 Ok(default_headers)
86}
87
88pub(crate) fn auth_business_headers(
89 client_type: u8,
90 device_id: &str,
91) -> Result<reqwest::header::HeaderMap> {
92 let mut headers = reqwest::header::HeaderMap::new();
93 headers.insert(
94 reqwest::header::CONTENT_TYPE,
95 reqwest::header::HeaderValue::from_static("application/json"),
96 );
97 headers.insert(
98 reqwest::header::COOKIE,
99 http_header_value("Cookie", format!("device_id={device_id}"))?,
100 );
101 headers.insert(
102 reqwest::header::USER_AGENT,
103 http_header_value("User-Agent", opend_auth_user_agent(client_type))?,
104 );
105 AuthTraceHeaders::new().insert_http_headers(&mut headers)?;
106 Ok(headers)
107}
108
109#[derive(Debug, Clone)]
110pub(crate) struct AuthTraceHeaders {
111 trace_id: String,
112 parent_span_id: String,
113 span_id: String,
114}
115
116impl AuthTraceHeaders {
117 pub(crate) fn new() -> Self {
118 Self {
122 trace_id: hex::encode(rand::random::<[u8; 16]>()),
123 parent_span_id: hex::encode(rand::random::<[u8; 8]>()),
124 span_id: hex::encode(rand::random::<[u8; 8]>()),
125 }
126 }
127
128 pub(crate) fn entries(&self) -> [(&'static str, &str); 3] {
129 [
130 ("x-b3-traceid", self.trace_id.as_str()),
131 ("x-b3-parentspanid", self.parent_span_id.as_str()),
132 ("x-b3-spanid", self.span_id.as_str()),
133 ]
134 }
135
136 fn insert_http_headers(&self, headers: &mut reqwest::header::HeaderMap) -> Result<()> {
137 for (name, value) in self.entries() {
138 headers.insert(name, http_header_value(name, value)?);
139 }
140 Ok(())
141 }
142}
143
144pub(crate) fn opend_auth_user_agent(client_type: u8) -> String {
145 format!(
147 "ClientType/{client_type} ClientVersion/{} CliLang/zh-cn ClientHourClock/24 OsType/{} RequestSource/Http",
148 crate::conn::BackendConn::CLIENT_VER_FTGTW,
149 opend_user_agent_os_type(),
150 )
151}
152
153fn opend_user_agent_os_type() -> &'static str {
154 if cfg!(target_os = "macos") {
155 "11"
156 } else if cfg!(target_os = "linux") {
157 "14"
158 } else {
159 "10"
160 }
161}
162
163fn http_header_value(
164 name: &'static str,
165 value: impl std::fmt::Display,
166) -> Result<reqwest::header::HeaderValue> {
167 reqwest::header::HeaderValue::from_str(&value.to_string())
168 .map_err(|e| FutuError::Codec(format!("{name}: invalid header value: {e}")))
169}