feat: 实验性加入刷新凭据(暂未测试)
This commit is contained in:
@@ -1,79 +1,83 @@
|
||||
use reqwest::{header, Method};
|
||||
|
||||
pub struct Credential {
|
||||
sessdata: String,
|
||||
bili_jct: String,
|
||||
buvid3: String,
|
||||
dedeuserid: String,
|
||||
ac_time_value: String,
|
||||
}
|
||||
use crate::bilibili::Credential;
|
||||
use crate::Result;
|
||||
|
||||
impl Credential {
|
||||
pub fn new(
|
||||
sessdata: String,
|
||||
bili_jct: String,
|
||||
buvid3: String,
|
||||
dedeuserid: String,
|
||||
ac_time_value: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
sessdata,
|
||||
bili_jct,
|
||||
buvid3,
|
||||
dedeuserid,
|
||||
ac_time_value,
|
||||
pub struct Client(reqwest::Client);
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
header::USER_AGENT,
|
||||
header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54"));
|
||||
headers.insert(
|
||||
header::REFERER,
|
||||
header::HeaderValue::from_static("https://www.bilibili.com"),
|
||||
);
|
||||
Self(
|
||||
reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn request(
|
||||
&self,
|
||||
method: Method,
|
||||
url: &str,
|
||||
credential: Option<&Credential>,
|
||||
) -> reqwest::RequestBuilder {
|
||||
let mut req = self.0.request(method, url);
|
||||
if let Some(credential) = credential {
|
||||
req = req
|
||||
.header(header::COOKIE, format!("SESSDATA={}", credential.sessdata))
|
||||
.header(header::COOKIE, format!("bili_jct={}", credential.bili_jct))
|
||||
.header(header::COOKIE, format!("buvid3={}", credential.buvid3))
|
||||
.header(
|
||||
header::COOKIE,
|
||||
format!("DedeUserID={}", credential.dedeuserid),
|
||||
)
|
||||
.header(
|
||||
header::COOKIE,
|
||||
format!("ac_time_value={}", credential.ac_time_value),
|
||||
);
|
||||
}
|
||||
req
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_with_header() -> reqwest::Client {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
header::USER_AGENT,
|
||||
header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54"));
|
||||
headers.insert(
|
||||
header::REFERER,
|
||||
header::HeaderValue::from_static("https://www.bilibili.com"),
|
||||
);
|
||||
reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap()
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BiliClient {
|
||||
credential: Option<Credential>,
|
||||
client: reqwest::Client,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl BiliClient {
|
||||
pub fn anonymous() -> Self {
|
||||
let credential = None;
|
||||
let client = client_with_header();
|
||||
pub fn new(credential: Option<Credential>) -> Self {
|
||||
let client = Client::new();
|
||||
Self { credential, client }
|
||||
}
|
||||
|
||||
pub fn authenticated(credential: Credential) -> Self {
|
||||
let credential = Some(credential);
|
||||
let client = client_with_header();
|
||||
Self { credential, client }
|
||||
}
|
||||
|
||||
fn set_header(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||
let Some(credential) = &self.credential else {
|
||||
return req;
|
||||
};
|
||||
req.header("cookie", format!("SESSDATA={}", credential.sessdata))
|
||||
.header("cookie", format!("bili_jct={}", credential.bili_jct))
|
||||
.header("cookie", format!("buvid3={}", credential.buvid3))
|
||||
.header("cookie", format!("DedeUserID={}", credential.dedeuserid))
|
||||
.header(
|
||||
"cookie",
|
||||
format!("ac_time_value={}", credential.ac_time_value),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn request(&self, method: Method, url: &str) -> reqwest::RequestBuilder {
|
||||
self.set_header(self.client.request(method, url))
|
||||
self.client.request(method, url, self.credential.as_ref())
|
||||
}
|
||||
|
||||
pub async fn check_refresh(&mut self) -> Result<()> {
|
||||
let Some(credential) = self.credential.as_mut() else {
|
||||
// no credential, just ignore it
|
||||
return Ok(());
|
||||
};
|
||||
if credential.check(&self.client).await? {
|
||||
// is valid, no need to refresh
|
||||
return Ok(());
|
||||
}
|
||||
credential.refresh(&self.client).await
|
||||
}
|
||||
}
|
||||
|
||||
152
src/bilibili/credential.rs
Normal file
152
src/bilibili/credential.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::collections::HashSet;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use cookie::Cookie;
|
||||
use regex::Regex;
|
||||
use reqwest::{header, Method};
|
||||
use rsa::pkcs8::DecodePublicKey;
|
||||
use rsa::{Pkcs1v15Encrypt, RsaPublicKey};
|
||||
|
||||
use crate::bilibili::Client;
|
||||
use crate::Result;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Credential {
|
||||
pub sessdata: String,
|
||||
pub bili_jct: String,
|
||||
pub buvid3: String,
|
||||
pub dedeuserid: String,
|
||||
pub ac_time_value: String,
|
||||
}
|
||||
|
||||
impl Credential {
|
||||
pub fn new(
|
||||
sessdata: String,
|
||||
bili_jct: String,
|
||||
buvid3: String,
|
||||
dedeuserid: String,
|
||||
ac_time_value: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
sessdata,
|
||||
bili_jct,
|
||||
buvid3,
|
||||
dedeuserid,
|
||||
ac_time_value,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check(&self, client: &Client) -> Result<bool> {
|
||||
let res = client
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://passport.bilibili.com/x/passport-login/web/cookie/info",
|
||||
Some(self),
|
||||
)
|
||||
.send()
|
||||
.await?
|
||||
.json::<serde_json::Value>()
|
||||
.await?;
|
||||
res["refresh"]
|
||||
.as_bool()
|
||||
.ok_or("check refresh failed".into())
|
||||
}
|
||||
|
||||
pub async fn refresh(&mut self, client: &Client) -> Result<()> {
|
||||
let correspond_path = Self::get_correspond_path();
|
||||
let csrf = self.get_refresh_csrf(client, correspond_path).await?;
|
||||
let new_credential = self.get_new_credential(client, &csrf).await?;
|
||||
self.sessdata = new_credential.sessdata;
|
||||
self.bili_jct = new_credential.bili_jct;
|
||||
self.dedeuserid = new_credential.dedeuserid;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_correspond_path() -> String {
|
||||
// maybe as a static value
|
||||
let key = RsaPublicKey::from_public_key_pem(
|
||||
"-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLgd2OAkcGVtoE3ThUREbio0Eg
|
||||
Uc/prcajMKXvkCKFCWhJYJcLkcM2DKKcSeFpD/j6Boy538YXnR6VhcuUJOhH2x71
|
||||
nzPjfdTcqMz7djHum0qSZA0AyCBDABUqCrfNgCiJ00Ra7GmRj+YCK1NJEuewlb40
|
||||
JNrRuoEUXpabUzGB8QIDAQAB
|
||||
-----END PUBLIC KEY-----",
|
||||
)
|
||||
.unwrap();
|
||||
let ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
let data = format!("refresh_{}", ts).into_bytes();
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
let encrypted = key.encrypt(&mut rng, Pkcs1v15Encrypt, &data).unwrap();
|
||||
hex::encode(encrypted)
|
||||
}
|
||||
|
||||
async fn get_refresh_csrf(&self, client: &Client, correspond_path: String) -> Result<String> {
|
||||
let res = client
|
||||
.request(
|
||||
Method::GET,
|
||||
format!("https://www.bilibili.com/correspond/1/{}", correspond_path).as_str(),
|
||||
Some(self),
|
||||
)
|
||||
.header(header::COOKIE, "Domain=.bilibili.com")
|
||||
.send()
|
||||
.await?;
|
||||
if !res.status().is_success() {
|
||||
return Err("error get csrf".into());
|
||||
}
|
||||
let re = Regex::new("<div id=\"1-name\">(.+?)</div>").unwrap();
|
||||
if let Some(res) = re.find(&res.text().await?) {
|
||||
return Ok(res.as_str().to_string());
|
||||
}
|
||||
Err("error get csrf".into())
|
||||
}
|
||||
|
||||
async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result<Credential> {
|
||||
let res = client
|
||||
.request(
|
||||
Method::POST,
|
||||
"https://passport.bilibili.com/x/passport-login/web/cookie/refresh",
|
||||
Some(self),
|
||||
)
|
||||
.header(header::COOKIE, "Domain=.bilibili.com")
|
||||
.json(&serde_json::json!({
|
||||
"csrf": self.bili_jct,
|
||||
"refresh_csrf": csrf,
|
||||
"refresh_token": self.ac_time_value,
|
||||
"source": "main_web",
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
let set_cookie = res
|
||||
.headers()
|
||||
.get(header::SET_COOKIE)
|
||||
.ok_or("error refresh credential")?
|
||||
.to_str()
|
||||
.unwrap();
|
||||
let mut credential = Credential::default();
|
||||
let required_cookies = HashSet::from(["SESSDATA", "bili_jct", "DedeUserID"]);
|
||||
let cookies: Vec<Cookie> = Cookie::split_parse_encoded(set_cookie)
|
||||
.filter(|x| {
|
||||
x.as_ref()
|
||||
.is_ok_and(|x| required_cookies.contains(x.name()))
|
||||
})
|
||||
.map(|x| x.unwrap())
|
||||
.collect();
|
||||
for cookie in cookies {
|
||||
match cookie.name() {
|
||||
"SESSDATA" => credential.sessdata = cookie.value().to_string(),
|
||||
"bili_jct" => credential.bili_jct = cookie.value().to_string(),
|
||||
"DedeUserID" => credential.dedeuserid = cookie.value().to_string(),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
let json = res.json::<serde_json::Value>().await?;
|
||||
if !json["data"]["refresh_token"].is_string() {
|
||||
return Err("error refresh credential".into());
|
||||
}
|
||||
credential.ac_time_value = json["data"]["refresh_token"].as_str().unwrap().to_string();
|
||||
Ok(credential)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub struct FavoriteList {
|
||||
fid: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct FavoriteListInfo {
|
||||
id: u64,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
mod analyzer;
|
||||
mod client;
|
||||
mod credential;
|
||||
mod favorite_list;
|
||||
mod video;
|
||||
|
||||
pub use analyzer::{
|
||||
AudioQuality, BestStream, FilterOption, PageAnalyzer, VideoCodecs, VideoQuality,
|
||||
};
|
||||
pub use client::{client_with_header, BiliClient, Credential};
|
||||
pub use client::{BiliClient, Client};
|
||||
pub use credential::Credential;
|
||||
pub use favorite_list::FavoriteList;
|
||||
pub use video::Video;
|
||||
|
||||
@@ -27,6 +27,7 @@ pub struct Tag {
|
||||
pub tag_name: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Page {
|
||||
cid: u64,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
use std::path::Path;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use reqwest::Method;
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io;
|
||||
|
||||
use crate::bilibili::client_with_header;
|
||||
use crate::bilibili::Client;
|
||||
use crate::Result;
|
||||
pub struct Downloader {
|
||||
client: reqwest::Client,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
pub fn new(client: reqwest::Client) -> Self {
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
@@ -20,7 +21,12 @@ impl Downloader {
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let mut file = File::create(path).await?;
|
||||
let mut res = self.client.get(url).send().await?.bytes_stream();
|
||||
let mut res = self
|
||||
.client
|
||||
.request(Method::GET, url, None)
|
||||
.send()
|
||||
.await?
|
||||
.bytes_stream();
|
||||
while let Some(item) = res.next().await {
|
||||
io::copy(&mut item?.as_ref(), &mut file).await?;
|
||||
}
|
||||
@@ -59,6 +65,6 @@ impl Downloader {
|
||||
|
||||
impl Default for Downloader {
|
||||
fn default() -> Self {
|
||||
Self::new(client_with_header())
|
||||
Self::new(Client::new())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use futures_util::{pin_mut, StreamExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let bili_client = Rc::new(BiliClient::anonymous());
|
||||
let bili_client = Rc::new(BiliClient::new(None));
|
||||
let favorite_list = FavoriteList::new(bili_client.clone(), "52642258".to_string());
|
||||
dbg!(favorite_list.get_info().await.unwrap());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user