1use chrono::{DateTime, TimeZone, Utc};
5use directories::UserDirs;
6use log::{debug, info, warn};
7use sqlite::Connection;
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone)]
13pub struct Cookie {
14 pub name: String,
15 pub value: String,
16 pub domain: String,
17 pub path: String,
18 pub expires: Option<DateTime<Utc>>,
19 pub secure: bool,
20 pub http_only: bool,
21}
22
23#[derive(Debug)]
24pub enum Browser {
25 Chrome,
26 Firefox,
27 Edge,
28 Chromium,
29 Opera,
30}
31
32impl Browser {
33 pub fn get_cookie_db_path(&self) -> Option<PathBuf> {
34 let user_dirs = UserDirs::new()?;
35 let home_dir = user_dirs.home_dir();
36
37 match self {
38 Browser::Chrome => {
39 #[cfg(target_os = "linux")]
40 {
41 Some(home_dir.join(".config/google-chrome/Default/Cookies"))
42 }
43 #[cfg(target_os = "macos")]
44 {
45 Some(home_dir.join("Library/Application Support/Google/Chrome/Default/Cookies"))
46 }
47 #[cfg(target_os = "windows")]
48 {
49 Some(
50 home_dir
51 .join("AppData/Local/Google/Chrome/User Data/Default/Network/Cookies"),
52 )
53 }
54 }
55 Browser::Firefox => {
56 #[cfg(target_os = "linux")]
57 {
58 let firefox_dir = home_dir.join(".mozilla/firefox");
59 Self::find_firefox_profile_cookies(&firefox_dir)
60 }
61 #[cfg(target_os = "macos")]
62 {
63 let firefox_dir = home_dir.join("Library/Application Support/Firefox/Profiles");
64 Self::find_firefox_profile_cookies(&firefox_dir)
65 }
66 #[cfg(target_os = "windows")]
67 {
68 let firefox_dir = home_dir.join("AppData/Roaming/Mozilla/Firefox/Profiles");
69 Self::find_firefox_profile_cookies(&firefox_dir)
70 }
71 }
72 Browser::Edge => {
73 #[cfg(target_os = "linux")]
74 {
75 Some(home_dir.join(".config/microsoft-edge/Default/Cookies"))
76 }
77 #[cfg(target_os = "macos")]
78 {
79 Some(
80 home_dir.join("Library/Application Support/Microsoft Edge/Default/Cookies"),
81 )
82 }
83 #[cfg(target_os = "windows")]
84 {
85 Some(
86 home_dir
87 .join("AppData/Local/Microsoft/Edge/User Data/Default/Network/Cookies"),
88 )
89 }
90 }
91 Browser::Chromium => {
92 #[cfg(target_os = "linux")]
93 {
94 Some(home_dir.join(".config/chromium/Default/Cookies"))
95 }
96 #[cfg(target_os = "macos")]
97 {
98 Some(home_dir.join("Library/Application Support/Chromium/Default/Cookies"))
99 }
100 #[cfg(target_os = "windows")]
101 {
102 Some(home_dir.join("AppData/Local/Chromium/User Data/Default/Network/Cookies"))
103 }
104 }
105 Browser::Opera => {
106 #[cfg(target_os = "linux")]
107 {
108 Some(home_dir.join(".config/opera/Default/Cookies"))
109 }
110 #[cfg(target_os = "macos")]
111 {
112 Some(home_dir.join(
113 "Library/Application Support/com.operasoftware.Opera/Default/Cookies",
114 ))
115 }
116 #[cfg(target_os = "windows")]
117 {
118 Some(
119 home_dir
120 .join("AppData/Roaming/Opera Software/Opera Stable/Network/Cookies"),
121 )
122 }
123 }
124 }
125 }
126
127 fn find_firefox_profile_cookies(firefox_dir: &Path) -> Option<PathBuf> {
128 if !firefox_dir.exists() {
129 return None;
130 }
131
132 let entries = fs::read_dir(firefox_dir).ok()?;
134 for entry in entries {
135 if let Ok(entry) = entry {
136 let path = entry.path();
137 if path.is_dir() {
138 let dir_name = path.file_name()?.to_str()?;
139 if dir_name.contains(".default") || dir_name.contains(".default-release") {
140 let cookies_path = path.join("cookies.sqlite");
141 if cookies_path.exists() {
142 return Some(cookies_path);
143 }
144 }
145 }
146 }
147 }
148 None
149 }
150
151 pub fn get_all_supported() -> Vec<Browser> {
152 vec![
153 Browser::Chrome,
154 Browser::Firefox,
155 Browser::Edge,
156 Browser::Chromium,
157 Browser::Opera,
158 ]
159 }
160}
161
162pub fn read_cookies_from_browser(
164 browser: &Browser,
165 domain_filter: Option<&str>,
166) -> Result<Vec<Cookie>, String> {
167 let db_path = browser
168 .get_cookie_db_path()
169 .ok_or_else(|| "Could not determine cookie database path".to_string())?;
170
171 if !db_path.exists() {
172 return Err(format!("Cookie database not found at: {:?}", db_path));
173 }
174
175 debug!("Reading cookies from: {:?}", db_path);
176
177 let temp_path = std::env::temp_dir().join(format!("temp_cookies_{}.db", std::process::id()));
179 if let Err(e) = fs::copy(&db_path, &temp_path) {
180 return Err(format!("Failed to copy cookie database: {}", e));
181 }
182
183 let result = match browser {
184 Browser::Firefox => read_firefox_cookies(&temp_path, domain_filter),
185 _ => read_chromium_cookies(&temp_path, domain_filter),
186 };
187
188 let _ = fs::remove_file(&temp_path);
190
191 result
192}
193
194fn read_chromium_cookies(
195 db_path: &Path,
196 domain_filter: Option<&str>,
197) -> Result<Vec<Cookie>, String> {
198 let connection =
199 Connection::open(db_path).map_err(|e| format!("Failed to open cookie database: {}", e))?;
200
201 let mut query =
202 "SELECT name, value, host_key, path, expires_utc, is_secure, is_httponly FROM cookies"
203 .to_string();
204
205 if let Some(domain) = domain_filter {
206 query.push_str(&format!(" WHERE host_key LIKE '%{}'", domain));
207 }
208
209 let mut cookies = Vec::new();
210
211 connection
212 .iterate(query, |pairs| {
213 let mut cookie_data = HashMap::new();
214 for &(column, value) in pairs.iter() {
215 cookie_data.insert(column, value.unwrap_or(""));
216 }
217
218 let expires = if let Some(expires_str) = cookie_data.get("expires_utc") {
219 if let Ok(expires_microseconds) = expires_str.parse::<i64>() {
220 let windows_epoch_offset = 11644473600_i64; let unix_timestamp = (expires_microseconds / 1_000_000) - windows_epoch_offset;
224 Utc.timestamp_opt(unix_timestamp, 0).single()
225 } else {
226 None
227 }
228 } else {
229 None
230 };
231
232 let cookie = Cookie {
233 name: cookie_data.get("name").unwrap_or(&"").to_string(),
234 value: cookie_data.get("value").unwrap_or(&"").to_string(),
235 domain: cookie_data.get("host_key").unwrap_or(&"").to_string(),
236 path: cookie_data.get("path").unwrap_or(&"").to_string(),
237 expires,
238 secure: cookie_data.get("is_secure").unwrap_or(&"0") == &"1",
239 http_only: cookie_data.get("is_httponly").unwrap_or(&"0") == &"1",
240 };
241
242 cookies.push(cookie);
243 true
244 })
245 .map_err(|e| format!("Failed to query cookies: {}", e))?;
246
247 Ok(cookies)
248}
249
250fn read_firefox_cookies(
251 db_path: &Path,
252 domain_filter: Option<&str>,
253) -> Result<Vec<Cookie>, String> {
254 let connection =
255 Connection::open(db_path).map_err(|e| format!("Failed to open cookie database: {}", e))?;
256
257 let mut query =
258 "SELECT name, value, host, path, expiry, isSecure, isHttpOnly FROM moz_cookies".to_string();
259
260 if let Some(domain) = domain_filter {
261 query.push_str(&format!(" WHERE host LIKE '%{}'", domain));
262 }
263
264 let mut cookies = Vec::new();
265
266 connection
267 .iterate(query, |pairs| {
268 let mut cookie_data = HashMap::new();
269 for &(column, value) in pairs.iter() {
270 cookie_data.insert(column, value.unwrap_or(""));
271 }
272
273 let expires = if let Some(expires_str) = cookie_data.get("expiry") {
274 if let Ok(expires_timestamp) = expires_str.parse::<i64>() {
275 Utc.timestamp_opt(expires_timestamp, 0).single()
276 } else {
277 None
278 }
279 } else {
280 None
281 };
282
283 let cookie = Cookie {
284 name: cookie_data.get("name").unwrap_or(&"").to_string(),
285 value: cookie_data.get("value").unwrap_or(&"").to_string(),
286 domain: cookie_data.get("host").unwrap_or(&"").to_string(),
287 path: cookie_data.get("path").unwrap_or(&"").to_string(),
288 expires,
289 secure: cookie_data.get("isSecure").unwrap_or(&"0") == &"1",
290 http_only: cookie_data.get("isHttpOnly").unwrap_or(&"0") == &"1",
291 };
292
293 cookies.push(cookie);
294 true
295 })
296 .map_err(|e| format!("Failed to query cookies: {}", e))?;
297
298 Ok(cookies)
299}
300
301pub fn find_bilibili_cookies_as_string() -> Option<String> {
303 let browsers = Browser::get_all_supported();
304 let mut all_cookies = vec![];
305
306 for browser in browsers {
307 info!("Checking browser: {:?}", browser);
308
309 if let Ok(cookies) = read_cookies_from_browser(&browser, Some("bilibili.com")) {
310 all_cookies.extend(cookies);
311 }
312 }
313
314 let mut valid_cookies = all_cookies
315 .into_iter()
316 .filter(|cookie| {
317 if let Some(expires) = cookie.expires {
318 if Utc::now() > expires {
319 warn!(
320 "Found expired {} cookie, expires: {:?}",
321 cookie.name, expires
322 );
323 return false;
324 }
325 }
326 true
327 })
328 .collect::<Vec<_>>();
329
330 valid_cookies.sort_by(|a, b| {
332 if a.name != b.name {
333 a.name.cmp(&b.name)
334 } else {
335 b.expires.cmp(&a.expires) }
337 });
338 valid_cookies.dedup_by(|a, b| a.name == b.name);
339
340 if valid_cookies.is_empty() {
341 warn!("No valid bilibili cookies found in any browser");
342 return None;
343 }
344
345 info!("Found {} valid bilibili cookies", valid_cookies.len());
346
347 let cookie_string = valid_cookies
348 .iter()
349 .map(|c| format!("{}={}", c.name, c.value))
350 .collect::<Vec<String>>()
351 .join("; ");
352
353 if cookie_string.contains("SESSDATA") {
354 Some(cookie_string)
355 } else {
356 warn!("No SESSDATA cookie found among the valid cookies");
357 None
358 }
359}
360
361pub fn get_all_bilibili_cookies() -> HashMap<String, String> {
363 let mut all_cookies = HashMap::new();
364 let browsers = Browser::get_all_supported();
365
366 for browser in browsers {
367 if let Ok(cookies) = read_cookies_from_browser(&browser, Some("bilibili.com")) {
368 for cookie in cookies {
369 if let Some(expires) = cookie.expires {
371 if Utc::now() > expires {
372 continue;
373 }
374 }
375
376 let key = format!("{}_{}", cookie.name, cookie.domain);
378 all_cookies.insert(key, cookie.value);
379 }
380 }
381 }
382
383 all_cookies
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_browser_path_detection() {
392 for browser in Browser::get_all_supported() {
393 let path = browser.get_cookie_db_path();
394 println!("{:?} cookie path: {:?}", browser, path);
395 }
396 }
397
398 #[test]
399 fn test_find_sessdata() {
400 if let Some(sessdata) = find_bilibili_cookies_as_string() {
402 println!("Found SESSDATA: {}", &sessdata[..20.min(sessdata.len())]);
403 assert!(!sessdata.is_empty());
404 } else {
405 println!("No SESSDATA found - this is normal if you're not logged into bilibili");
406 }
407 }
408}