feat: 实验性加入刷新凭据(暂未测试)

This commit is contained in:
amtoaer
2024-03-20 02:00:55 +08:00
parent ed27467d16
commit 8af5350772
9 changed files with 669 additions and 68 deletions

View File

@@ -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
View 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)
}
}

View File

@@ -11,6 +11,7 @@ pub struct FavoriteList {
fid: String,
}
#[allow(dead_code)]
#[derive(Debug, serde::Deserialize)]
pub struct FavoriteListInfo {
id: u64,

View File

@@ -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;

View File

@@ -27,6 +27,7 @@ pub struct Tag {
pub tag_name: String,
}
#[allow(dead_code)]
#[derive(Debug, serde::Deserialize)]
pub struct Page {
cid: u64,

View File

@@ -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())
}
}

View File

@@ -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());