Files
bili-sync-ai/src/bilibili/credential.rs

216 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::HashSet;
use anyhow::{anyhow, bail, Result};
use cookie::Cookie;
use regex::Regex;
use reqwest::{header, Method};
use rsa::pkcs8::DecodePublicKey;
use rsa::sha2::Sha256;
use rsa::{Oaep, RsaPublicKey};
use serde::{Deserialize, Serialize};
use super::error::BiliError;
use crate::bilibili::Client;
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
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 need_refresh(&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?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
res["data"]["refresh"].as_bool().ok_or(anyhow!("check refresh failed"))
}
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.confirm_refresh(client, &new_credential).await?;
*self = new_credential;
Ok(())
}
fn get_correspond_path() -> String {
// 调用频率很低,让 key 在函数内部构造影响不大
let key = RsaPublicKey::from_public_key_pem(
"-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLgd2OAkcGVtoE3ThUREbio0Eg
Uc/prcajMKXvkCKFCWhJYJcLkcM2DKKcSeFpD/j6Boy538YXnR6VhcuUJOhH2x71
nzPjfdTcqMz7djHum0qSZA0AyCBDABUqCrfNgCiJ00Ra7GmRj+YCK1NJEuewlb40
JNrRuoEUXpabUzGB8QIDAQAB
-----END PUBLIC KEY-----",
)
.unwrap();
let ts = chrono::Local::now().timestamp_millis();
let data = format!("refresh_{}", ts).into_bytes();
let mut rng = rand::rngs::OsRng;
let encrypted = key.encrypt(&mut rng, Oaep::new::<Sha256>(), &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?
.error_for_status()?;
regex_find(r#"<div id="1-name">(.+?)</div>"#, res.text().await?.as_str())
}
async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result<Credential> {
let mut res = client
.request(
Method::POST,
"https://passport.bilibili.com/x/passport-login/web/cookie/refresh",
Some(self),
)
.header(header::COOKIE, "Domain=.bilibili.com")
.form(&[
// 这里不是 json而是 form data
("csrf", self.bili_jct.as_str()),
("refresh_csrf", csrf),
("refresh_token", self.ac_time_value.as_str()),
("source", "main_web"),
])
.send()
.await?
.error_for_status()?;
// 必须在 .json 前取出 headers否则 res 会被消耗
let headers = std::mem::take(res.headers_mut());
let res = res.json::<serde_json::Value>().await?;
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
let set_cookies = headers.get_all(header::SET_COOKIE);
let mut credential = Credential {
buvid3: self.buvid3.clone(),
..Default::default()
};
let required_cookies = HashSet::from(["SESSDATA", "bili_jct", "DedeUserID"]);
let cookies: Vec<Cookie> = set_cookies
.iter()
.filter_map(|x| x.to_str().ok())
.filter_map(|x| Cookie::parse(x).ok())
.filter(|x| required_cookies.contains(x.name()))
.collect();
if cookies.len() != required_cookies.len() {
bail!("not all required cookies found");
}
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(),
_ => unreachable!(),
}
}
if !res["data"]["refresh_token"].is_string() {
bail!("refresh_token not found");
}
credential.ac_time_value = res["data"]["refresh_token"].as_str().unwrap().to_string();
Ok(credential)
}
async fn confirm_refresh(&self, client: &Client, new_credential: &Credential) -> Result<()> {
let res = client
.request(
Method::POST,
"https://passport.bilibili.com/x/passport-login/web/confirm/refresh",
// 此处用的是新的凭证
Some(new_credential),
)
.form(&[
("csrf", new_credential.bili_jct.as_str()),
("refresh_token", self.ac_time_value.as_str()),
])
.send()
.await?
.error_for_status()?
.json::<serde_json::Value>()
.await?;
let (code, msg) = match (res["code"].as_i64(), res["message"].as_str()) {
(Some(code), Some(msg)) => (code, msg),
_ => bail!("no code or message found"),
};
if code != 0 {
bail!(BiliError::RequestFailed(code, msg.to_owned()));
}
Ok(())
}
}
// 用指定的 pattern 正则表达式在 doc 中查找,返回第一个匹配的捕获组
fn regex_find(pattern: &str, doc: &str) -> Result<String> {
let re = Regex::new(pattern)?;
Ok(re
.captures(doc)
.ok_or(anyhow!("pattern not match"))?
.get(1)
.unwrap()
.as_str()
.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_and_find() {
let doc = r#"
<html lang="zh-Hans">
<body>
<div id="1-name">b0cc8411ded2f9db2cff2edb3123acac</div>
</body>
</html>
"#;
assert_eq!(
regex_find(r#"<div id="1-name">(.+?)</div>"#, doc).unwrap(),
"b0cc8411ded2f9db2cff2edb3123acac",
);
}
}