bpi_rs/misc/sign/
bili_ticket.rs1use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
9use hmac::{ Hmac, Mac };
10use serde::{ Deserialize, Serialize };
11use sha2::Sha256;
12use std::time::{ SystemTime, UNIX_EPOCH };
13
14type HmacSha256 = Hmac<Sha256>;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TicketData {
19 pub ticket: String,
21 pub created_at: i64,
23 pub ttl: i32,
25 pub context: serde_json::Value,
27 pub nav: NavData,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct NavData {
34 pub img: String,
36 pub sub: String,
38}
39
40impl BpiClient {
41 pub async fn misc_sign_bili_ticket(&self) -> Result<BpiResponse<TicketData>, BpiError> {
46 let csrf = self.csrf()?;
47 let timestamp = SystemTime::now()
49 .duration_since(UNIX_EPOCH)
50 .map_err(|e| BpiError::network(format!("获取时间戳失败: {}", e)))?
51 .as_secs();
52
53 let message = format!("ts{}", timestamp);
55 let hexsign = self.hmac_sha256("XgwSnGZ1p", &message)?;
56
57 let params = [
61 ("key_id", "ec02"),
62 ("hexsign", &hexsign),
63 ("context[ts]", ×tamp.to_string()),
64 ("csrf", csrf.as_str()),
65 ];
66
67 self
69 .post("https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket")
70 .query(¶ms)
71 .send_bpi("生成bili_ticket").await
72 }
73
74 pub async fn misc_sign_bili_ticket_string(&self) -> Result<String, BpiError> {
76 let resp = self.misc_sign_bili_ticket().await?;
77 let data = resp.data.ok_or_else(BpiError::missing_data)?;
78 Ok(data.ticket)
79 }
80
81 fn hmac_sha256(&self, key: &str, message: &str) -> Result<String, BpiError> {
83 let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(|e|
84 BpiError::parse(format!("HMAC 密钥错误: {}", e))
85 )?;
86
87 mac.update(message.as_bytes());
88 let result = mac.finalize();
89 Ok(hex::encode(result.into_bytes()))
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[tokio::test]
98 async fn test_hmac_sha256() {
99 let bpi = BpiClient::new();
100 let result = bpi.hmac_sha256("XgwSnGZ1p", "ts1234567890").unwrap();
101
102 assert_eq!(result.len(), 64);
104 assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
105 tracing::info!("HMAC-SHA256 测试通过: {}", result);
106 }
107
108 #[tokio::test]
109 async fn test_generate_bili_ticket() {
110 let bpi = BpiClient::new();
111
112 match bpi.misc_sign_bili_ticket().await {
113 Ok(resp) => {
114 if resp.code == 0 {
115 let data = resp.data.unwrap();
116 tracing::info!("Ticket: {}", data.ticket);
117 tracing::info!("创建时间: {}", data.created_at);
118 tracing::info!(
119 "有效时长: {} 秒 ({:.1} 天)",
120 data.ttl,
121 (data.ttl as f64) / 86400.0
122 );
123 tracing::info!("WBI img: {}", data.nav.img);
124 tracing::info!("WBI sub: {}", data.nav.sub);
125
126 assert!(data.ticket.contains('.'));
128 assert!(data.ttl > 250000); } else {
130 panic!("API 返回错误: code={}, message={}", resp.code, resp.message);
131 }
132 }
133
134 Err(err) => {
135 panic!("生成 bili_ticket 失败: {}", err);
136 }
137 }
138 }
139
140 #[tokio::test]
141 async fn test_get_bili_ticket_string() {
142 let bpi = BpiClient::new();
143
144 match bpi.misc_sign_bili_ticket_string().await {
145 Ok(ticket) => {
146 tracing::info!("获取到的 bili_ticket: {}", ticket);
147
148 assert!(!ticket.is_empty());
150 assert!(ticket.contains('.'));
151
152 let parts: Vec<&str> = ticket.split('.').collect();
154 assert_eq!(parts.len(), 3);
155 }
156 Err(err) => {
157 panic!("获取 bili_ticket 字符串失败: {}", err);
158 }
159 }
160 }
161
162 #[tokio::test]
163 async fn test_with_csrf() {
164 let bpi = BpiClient::new();
165
166 match bpi.misc_sign_bili_ticket().await {
168 Ok(resp) => {
169 tracing::info!("带 CSRF 的 bili_ticket 生成成功: {}", resp.data.unwrap().ticket);
170 }
171 Err(err) => {
172 tracing::info!("带 CSRF 测试失败(预期可能失败): {}", err);
173 }
175 }
176 }
177}