finance_query_core/client/
yahoo_auth.rs1use crate::client::error::YahooError;
6use chrono::{DateTime, Utc};
7use reqwest::{cookie::Jar, ClientBuilder};
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::Mutex;
11use tracing::{debug, error, info, warn};
12
13const MIN_REFRESH_INTERVAL_SECS: i64 = 30;
14
15struct AuthState {
16 crumb: Option<String>,
17 cookie_jar: Arc<Jar>,
18 last_update: Option<DateTime<Utc>>,
19}
20
21pub struct YahooAuthManager {
26 state: Arc<Mutex<AuthState>>,
27 proxy: Option<String>,
28}
29
30impl YahooAuthManager {
31 pub fn new(proxy: Option<String>, cookie_jar: Arc<Jar>) -> Self {
37 Self {
38 state: Arc::new(Mutex::new(AuthState {
39 crumb: None,
40 cookie_jar,
41 last_update: None,
42 })),
43 proxy,
44 }
45 }
46
47 pub async fn refresh(&self) -> Result<(), YahooError> {
49 info!("Refreshing Yahoo authentication...");
50 let state = self.state.lock().await;
51 let cookie_jar = state.cookie_jar.clone();
52 drop(state);
53
54 let mut builder = ClientBuilder::new()
55 .timeout(Duration::from_secs(30))
56 .cookie_provider(cookie_jar.clone())
57 .redirect(reqwest::redirect::Policy::limited(10));
58
59
60 if let Some(proxy_url) = &self.proxy {
61 info!(
62 "Using proxy for Yahoo auth: {}...",
63 &proxy_url.chars().take(30).collect::<String>()
64 );
65 builder = builder
66 .proxy(
67 reqwest::Proxy::all(proxy_url).map_err(YahooError::NetworkError)?,
68 )
69 .danger_accept_invalid_certs(true);
71 }
72
73 let client = builder.build().map_err(YahooError::NetworkError)?;
74
75 info!("Step 1: Visiting Yahoo Finance homepage");
77 let _ = client
78 .get("https://finance.yahoo.com/")
79 .header(
80 "User-Agent",
81 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
82 )
83 .header(
84 "Accept",
85 "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
86 )
87 .header("Accept-Language", "en-US,en;q=0.9")
88 .header("Accept-Encoding", "gzip, deflate, br")
89 .header("Connection", "keep-alive")
90 .header("Upgrade-Insecure-Requests", "1")
91 .send()
92 .await
93 .map_err(YahooError::NetworkError)?;
94
95 debug!("Finance homepage visited");
96
97 info!("Step 2: Visiting quote page");
99 let _ = client
100 .get("https://finance.yahoo.com/quote/AAPL")
101 .header(
102 "User-Agent",
103 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
104 )
105 .header(
106 "Accept",
107 "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
108 )
109 .header("Accept-Language", "en-US,en;q=0.9")
110 .header("Referer", "https://finance.yahoo.com/")
111 .send()
112 .await
113 .map_err(YahooError::NetworkError)?;
114
115 debug!("Quote page visited");
116
117 info!("Step 3: Attempting to get crumb from query1 endpoint");
119 let crumb_response = client
120 .get("https://query1.finance.yahoo.com/v1/test/getcrumb")
121 .header(
122 "User-Agent",
123 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
124 )
125 .header("Accept", "*/*")
126 .header("Referer", "https://finance.yahoo.com/")
127 .send()
128 .await
129 .map_err(YahooError::NetworkError)?;
130
131 let status = crumb_response.status();
132 debug!("getcrumb response status: {}", status);
133
134 let crumb_text = crumb_response
135 .text()
136 .await
137 .map_err(YahooError::NetworkError)?;
138 let crumb = crumb_text.trim().to_string();
139 debug!(
140 "Crumb received (length: {}): {}",
141 crumb.len(),
142 if crumb.len() < 50 { &crumb } else { "..." }
143 );
144
145
146 if crumb.is_empty() || crumb.contains("<html") || crumb.contains("Unauthorized") {
148 warn!("Crumb from query1 is invalid, trying query2");
149
150 let crumb_response2 = client
151 .get("https://query2.finance.yahoo.com/v1/test/getcrumb")
152 .header(
153 "User-Agent",
154 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
155 )
156 .header("Accept", "*/*")
157 .header("Referer", "https://finance.yahoo.com/")
158 .send()
159 .await
160 .map_err(YahooError::NetworkError)?;
161
162 let crumb_text2 = crumb_response2
163 .text()
164 .await
165 .map_err(YahooError::NetworkError)?;
166 let crumb2 = crumb_text2.trim().to_string();
167
168 if crumb2.is_empty() || crumb2.contains("<html") || crumb2.contains("Unauthorized") {
169 error!("Failed to get valid crumb from both endpoints");
170 return Err(YahooError::AuthFailed(
171 "Could not obtain valid crumb from Yahoo".to_string(),
172 ));
173 }
174
175 info!(
176 "Successfully obtained crumb from query2 (length: {})",
177 crumb2.len()
178 );
179 let mut state = self.state.lock().await;
180 state.crumb = Some(crumb2);
181 state.last_update = Some(Utc::now());
182 return Ok(());
183 }
184
185 info!("Successfully obtained crumb (length: {})", crumb.len());
187 let mut state = self.state.lock().await;
188 state.crumb = Some(crumb);
189 state.last_update = Some(Utc::now());
190
191 Ok(())
192 }
193
194 pub async fn get_or_refresh(&self) -> Result<(Arc<Jar>, String), YahooError> {
198 let mut state = self.state.lock().await;
199
200 let needs_refresh = state.crumb.is_none()
201 || state.last_update.is_none()
202 || (Utc::now() - state.last_update.unwrap()).num_seconds() > MIN_REFRESH_INTERVAL_SECS;
203
204 if needs_refresh {
205 debug!(
206 "Auth refresh needed. Crumb exists: {}, Last update: {:?}",
207 state.crumb.is_some(),
208 state.last_update
209 );
210 drop(state); self.refresh().await?;
212 state = self.state.lock().await;
213 } else {
214 debug!(
215 "Using cached crumb (age: {}s)",
216 state
217 .last_update
218 .map(|t| (Utc::now() - t).num_seconds())
219 .unwrap_or(0)
220 );
221 }
222
223 let crumb = state.crumb.clone().ok_or_else(|| {
224 error!("No crumb available after refresh attempt");
225 YahooError::AuthFailed("No crumb available".to_string())
226 })?;
227
228 debug!("Returning crumb (length: {})", crumb.len());
229 Ok((state.cookie_jar.clone(), crumb))
230 }
231
232 pub async fn crumb(&self) -> Option<String> {
234 self.state.lock().await.crumb.clone()
235 }
236}