use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone)]
pub struct YahooFinanceAuth {
pub cookie: Option<String>,
pub crumb: Option<String>,
pub crumb_timestamp: Option<u64>,
}
impl Default for YahooFinanceAuth {
fn default() -> Self {
Self::new()
}
}
impl YahooFinanceAuth {
pub fn new() -> Self {
Self {
cookie: None,
crumb: None,
crumb_timestamp: None,
}
}
pub fn from_env() -> Self {
let cookie = std::env::var("YAHOO_FINANCE_COOKIE").ok();
let crumb = std::env::var("YAHOO_FINANCE_CRUMB").ok();
let crumb_timestamp = crumb.as_ref().map(|_| current_timestamp());
Self {
cookie,
crumb,
crumb_timestamp,
}
}
pub fn with_cookie_crumb(cookie: impl Into<String>, crumb: impl Into<String>) -> Self {
Self {
cookie: Some(cookie.into()),
crumb: Some(crumb.into()),
crumb_timestamp: Some(current_timestamp()),
}
}
pub fn set_cookie(&mut self, cookie: impl Into<String>) {
self.cookie = Some(cookie.into());
}
pub fn set_crumb(&mut self, crumb: impl Into<String>) {
self.crumb = Some(crumb.into());
self.crumb_timestamp = Some(current_timestamp());
}
pub fn is_crumb_expired(&self) -> bool {
match self.crumb_timestamp {
Some(ts) => {
let current = current_timestamp();
current - ts > 1800 }
None => true, }
}
pub fn sign_headers(&self, headers: &mut HashMap<String, String>) {
if let Some(cookie) = &self.cookie {
headers.insert("Cookie".to_string(), cookie.clone());
}
if !headers.contains_key("User-Agent") {
headers.insert(
"User-Agent".to_string(),
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36".to_string(),
);
}
headers.entry("Accept".to_string())
.or_insert("application/json".to_string());
headers.entry("Accept-Language".to_string())
.or_insert("en-US,en;q=0.9".to_string());
headers.entry("Referer".to_string())
.or_insert("https://finance.yahoo.com/".to_string());
}
pub fn sign_query(&self, params: &mut HashMap<String, String>) {
if let Some(crumb) = &self.crumb {
params.insert("crumb".to_string(), crumb.clone());
}
}
pub fn has_download_auth(&self) -> bool {
self.cookie.is_some() && self.crumb.is_some() && !self.is_crumb_expired()
}
}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_creation() {
let auth = YahooFinanceAuth::new();
assert!(auth.cookie.is_none());
assert!(auth.crumb.is_none());
}
#[test]
fn test_auth_with_cookie_crumb() {
let auth = YahooFinanceAuth::with_cookie_crumb("test_cookie", "test_crumb");
assert_eq!(auth.cookie, Some("test_cookie".to_string()));
assert_eq!(auth.crumb, Some("test_crumb".to_string()));
assert!(auth.crumb_timestamp.is_some());
}
#[test]
fn test_sign_headers() {
let auth = YahooFinanceAuth::with_cookie_crumb("test_cookie", "test_crumb");
let mut headers = HashMap::new();
auth.sign_headers(&mut headers);
assert_eq!(headers.get("Cookie"), Some(&"test_cookie".to_string()));
assert!(headers.contains_key("User-Agent"));
assert!(headers.contains_key("Accept"));
}
#[test]
fn test_sign_query() {
let auth = YahooFinanceAuth::with_cookie_crumb("test_cookie", "test_crumb");
let mut params = HashMap::new();
auth.sign_query(&mut params);
assert_eq!(params.get("crumb"), Some(&"test_crumb".to_string()));
}
#[test]
fn test_has_download_auth() {
let auth = YahooFinanceAuth::new();
assert!(!auth.has_download_auth());
let auth = YahooFinanceAuth::with_cookie_crumb("cookie", "crumb");
assert!(auth.has_download_auth());
}
}