feat: add cookies.txt parser, add cookie auth + history cmds to CLI
This commit is contained in:
parent
3c95b52cea
commit
cf498e4a8f
3 changed files with 328 additions and 43 deletions
|
|
@ -1463,23 +1463,80 @@ impl RustyPipe {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get a copy of the authentication cookie from the cache
|
||||
fn user_auth_cookie(&self) -> Result<AuthCookie, Error> {
|
||||
self.inner
|
||||
.cache
|
||||
.auth_cookie
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or(Error::Auth(AuthError::NoLogin))
|
||||
}
|
||||
|
||||
/// Set the user authentication cookie
|
||||
///
|
||||
/// The cookie is used for authenticated requests with browser-based clients
|
||||
/// (Desktop, DesktopMusic, Mobile).
|
||||
///
|
||||
/// **Note:** YouTube rotates cookies every few minutes when using the web applications.
|
||||
/// So you should not use the session you obtained cookies from afterwards or it will
|
||||
/// **Note:** YouTube rotates cookies every few minutes when using the web application.
|
||||
/// Do not use the session you obtained cookies from afterwards or it will
|
||||
/// become invalid.
|
||||
///
|
||||
/// I recommend to log in using Incognito mode, get the cookies from the devtools
|
||||
/// and then close the page.
|
||||
pub async fn set_auth_cookie<S: Into<String>>(&self, cookie: S) -> Result<(), Error> {
|
||||
let mut auth_cookie = AuthCookie::new(cookie.into());
|
||||
pub async fn user_auth_set_cookie<S: Into<String>>(&self, cookie: S) -> Result<(), Error> {
|
||||
let cookie = cookie.into();
|
||||
if cookie.is_empty() {
|
||||
return Err(Error::Auth(AuthError::NoLogin));
|
||||
}
|
||||
let mut auth_cookie = AuthCookie::new(cookie);
|
||||
self.extract_session_headers(&mut auth_cookie).await?;
|
||||
{
|
||||
let mut c = self.inner.cache.auth_cookie.write().unwrap();
|
||||
*c = Some(auth_cookie);
|
||||
}
|
||||
self.store_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut c = self.inner.cache.auth_cookie.write().unwrap();
|
||||
*c = Some(auth_cookie);
|
||||
/// Parse the user authentication cookie from a Netscape HTTP Cookie File
|
||||
///
|
||||
/// The cookie is used for authenticated requests with browser-based clients
|
||||
/// (Desktop, DesktopMusic, Mobile).
|
||||
///
|
||||
/// cookie.txt files can be extracted using browser plugins like
|
||||
/// "Get cookies.txt LOCALLY" ([Firefox](https://addons.mozilla.org/de/firefox/addon/get-cookies-txt-locally/))
|
||||
/// ([Chromium](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)).
|
||||
///
|
||||
/// **Note:** YouTube rotates cookies every few minutes when using the web application.
|
||||
/// Do not use the session you obtained cookies from afterwards or it will
|
||||
/// become invalid.
|
||||
///
|
||||
/// I recommend to log in using Incognito mode, obtain the cookies and then close the page.
|
||||
pub async fn user_auth_set_cookie_txt(&self, cookies: &str) -> Result<(), Error> {
|
||||
let cookie = util::parse_netscape_cookies(cookies, ".youtube.com")?;
|
||||
self.user_auth_set_cookie(cookie).await
|
||||
}
|
||||
|
||||
/// Remove the user authentication cookie from cache storage
|
||||
pub async fn user_auth_remove_cookie(&self) -> Result<(), Error> {
|
||||
{
|
||||
let mut cookie = self.inner.cache.auth_cookie.write().unwrap();
|
||||
if cookie.is_none() {
|
||||
return Err(Error::Auth(AuthError::NoLogin));
|
||||
}
|
||||
*cookie = None;
|
||||
}
|
||||
self.store_cache().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to fetch the YouTube website with login cookies to check if the user is successfully logged in
|
||||
/// and the session is still valid.
|
||||
pub async fn user_auth_check_cookie(&self) -> Result<(), Error> {
|
||||
let mut cookie = self.user_auth_cookie()?;
|
||||
self.extract_session_headers(&mut cookie).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -1488,7 +1545,7 @@ impl RustyPipe {
|
|||
///
|
||||
/// The header values are included in the ytcfg object which is embedded in the html code.
|
||||
async fn extract_session_headers(&self, auth_cookie: &mut AuthCookie) -> Result<(), Error> {
|
||||
let re_session_id = Regex::new(r#"""USER_SESSION_ID"":"[\d]+?""#).unwrap();
|
||||
let re_session_id = Regex::new(r#""USER_SESSION_ID":"[\d]+?""#).unwrap();
|
||||
let re_sync_id = Regex::new(r#""datasyncId":"([\w|]+?)""#).unwrap();
|
||||
let re_session_index = Regex::new(r#""SESSION_INDEX":"([\d]+?)""#).unwrap();
|
||||
|
||||
|
|
@ -1501,6 +1558,7 @@ impl RustyPipe {
|
|||
let html = self.http_request_txt(&req).await?;
|
||||
|
||||
if !re_session_id.is_match(&html) {
|
||||
tracing::debug!("session check failed: USER_SESSION_ID not found in reponse");
|
||||
return Err(Error::Auth(AuthError::NoLogin));
|
||||
}
|
||||
|
||||
|
|
@ -1904,15 +1962,7 @@ impl RustyPipeQuery {
|
|||
|
||||
if self.opts.auth == Some(true) {
|
||||
if ctype.is_web() {
|
||||
let auth_cookie = self
|
||||
.client
|
||||
.inner
|
||||
.cache
|
||||
.auth_cookie
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.ok_or(Error::Auth(AuthError::NoLogin))?;
|
||||
let auth_cookie = self.client.user_auth_cookie()?;
|
||||
|
||||
if let Some(auth_header) = Self::sapisidhash_header(&auth_cookie.cookie, ctype) {
|
||||
r = r.header(header::AUTHORIZATION, auth_header);
|
||||
|
|
|
|||
|
|
@ -608,6 +608,69 @@ pub fn map_internal_playlist_err(e: Error) -> Error {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse cookies from a Netscape HTTP Cookie File and return a cookie header value
|
||||
pub fn parse_netscape_cookies(cookies: &str, filter_domain: &str) -> Result<String, Error> {
|
||||
let mut res = cookies
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(line_n, line)| parse_netscape_cookie_line(line, line_n + 1, filter_domain))
|
||||
.try_fold(String::new(), |mut acc, itm| {
|
||||
if let Some((k, v)) = itm? {
|
||||
acc += k;
|
||||
acc.push('=');
|
||||
acc += v;
|
||||
acc += "; ";
|
||||
}
|
||||
Ok::<_, Error>(acc)
|
||||
})?;
|
||||
|
||||
if !res.is_empty() {
|
||||
res.truncate(res.len() - 2);
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn parse_netscape_cookie_line<'a>(
|
||||
line: &'a str,
|
||||
line_n: usize,
|
||||
filter_domain: &str,
|
||||
) -> Result<Option<(&'a str, &'a str)>, Error> {
|
||||
let mut line = line.trim();
|
||||
|
||||
if let Some(s) = line.strip_prefix("#HttpOnly_") {
|
||||
line = s;
|
||||
} else if line.is_empty() || line.starts_with('#') {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mkerr = || Error::Other(format!("line {line_n}: too few fields, expected 7").into());
|
||||
|
||||
let mut cols = line.split('\t');
|
||||
let domain = cols.next().ok_or_else(mkerr)?;
|
||||
if domain != filter_domain {
|
||||
return Ok(None);
|
||||
}
|
||||
let include_subdomains = cols.next().ok_or_else(mkerr)?.to_ascii_lowercase() == "true";
|
||||
if !include_subdomains {
|
||||
return Ok(None);
|
||||
}
|
||||
let path = cols.next().ok_or_else(mkerr)?;
|
||||
if path != "/" {
|
||||
return Ok(None);
|
||||
}
|
||||
// skip secure, expire
|
||||
let name = cols.nth(2).ok_or_else(mkerr)?;
|
||||
let value = cols.next().ok_or_else(mkerr)?;
|
||||
|
||||
if cols.next().is_some() {
|
||||
return Err(Error::Other(
|
||||
format!("line {line_n}: too many fields, expected 7").into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some((name, value)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::{fs::File, io::BufReader, path::PathBuf};
|
||||
|
|
@ -810,4 +873,35 @@ pub(crate) mod tests {
|
|||
Err(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t_parse_netscape_cookies() {
|
||||
let cookies = r#"# Netscape HTTP Cookie File
|
||||
# http://curl.haxx.se/rfc/cookie_spec.html
|
||||
# This is a generated file! Do not edit.
|
||||
|
||||
# Domain Subdomain Path Secure Expire Name Value
|
||||
.www.youtube.com TRUE / FALSE 1769704561 yt-dev.storage-integrity true
|
||||
.youtube.com TRUE / TRUE 1763481937 SOCS Abcdefg
|
||||
.youtube.com TRUE / TRUE 1744905937 __Secure-BUCKET IE7E
|
||||
"#;
|
||||
let filter_domain = ".youtube.com";
|
||||
let parsed = parse_netscape_cookies(cookies, filter_domain).unwrap();
|
||||
assert_eq!(&parsed, "SOCS=Abcdefg; __Secure-BUCKET=IE7E");
|
||||
|
||||
let cookies_too_few_cols = r#".youtube.com TRUE / TRUE 1763481937 SOCS"#;
|
||||
let cookies_too_many_cols = r#".youtube.com TRUE / TRUE 1763481937 SOCS Abcdefg foo"#;
|
||||
assert_eq!(
|
||||
parse_netscape_cookies(cookies_too_few_cols, filter_domain)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"error: line 1: too few fields, expected 7"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_netscape_cookies(cookies_too_many_cols, filter_domain)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"error: line 1: too many fields, expected 7"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue