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> {
45 let csrf = self.csrf()?;
46 let timestamp = SystemTime::now()
48 .duration_since(UNIX_EPOCH)
49 .map_err(|e| BpiError::network(format!("获取时间戳失败: {}", e)))?
50 .as_secs();
51
52 let message = format!("ts{}", timestamp);
54 let hexsign = self.hmac_sha256("XgwSnGZ1p", &message)?;
55
56 let params = [
60 ("key_id", "ec02"),
61 ("hexsign", &hexsign),
62 ("context[ts]", ×tamp.to_string()),
63 ("csrf", csrf.as_str()),
64 ];
65
66 self.post("https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket")
68 .query(¶ms)
69 .send_bpi("生成bili_ticket")
70 .await
71 }
72
73 pub async fn misc_sign_bili_ticket_string(&self) -> Result<String, BpiError> {
75 let resp = self.misc_sign_bili_ticket().await?;
76 let data = resp.data.ok_or_else(BpiError::missing_data)?;
77 Ok(data.ticket)
78 }
79
80 fn hmac_sha256(&self, key: &str, message: &str) -> Result<String, BpiError> {
82 let mut mac = HmacSha256::new_from_slice(key.as_bytes())
83 .map_err(|e| BpiError::parse(format!("HMAC 密钥错误: {}", e)))?;
84
85 mac.update(message.as_bytes());
86 let result = mac.finalize();
87 Ok(hex::encode(result.into_bytes()))
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[tokio::test]
96 async fn test_hmac_sha256() {
97 let bpi = BpiClient::new();
98 let result = bpi.hmac_sha256("XgwSnGZ1p", "ts1234567890").unwrap();
99
100 assert_eq!(result.len(), 64);
102 assert!(result.chars().all(|c| c.is_ascii_hexdigit()));
103 tracing::info!("HMAC-SHA256 测试通过: {}", result);
104 }
105
106 #[tokio::test]
107 async fn test_generate_bili_ticket() {
108 let bpi = BpiClient::new();
109
110 match bpi.misc_sign_bili_ticket().await {
111 Ok(resp) => {
112 if resp.code == 0 {
113 let data = resp.data.unwrap();
114 tracing::info!("Ticket: {}", data.ticket);
115 tracing::info!("创建时间: {}", data.created_at);
116 tracing::info!(
117 "有效时长: {} 秒 ({:.1} 天)",
118 data.ttl,
119 (data.ttl as f64) / 86400.0
120 );
121 tracing::info!("WBI img: {}", data.nav.img);
122 tracing::info!("WBI sub: {}", data.nav.sub);
123
124 assert!(data.ticket.contains('.'));
126 assert!(data.ttl > 250000); } else {
128 panic!("API 返回错误: code={}, message={}", resp.code, resp.message);
129 }
130 }
131
132 Err(err) => {
133 panic!("生成 bili_ticket 失败: {}", err);
134 }
135 }
136 }
137
138 #[tokio::test]
139 async fn test_get_bili_ticket_string() {
140 let bpi = BpiClient::new();
141
142 match bpi.misc_sign_bili_ticket_string().await {
143 Ok(ticket) => {
144 tracing::info!("获取到的 bili_ticket: {}", ticket);
145
146 assert!(!ticket.is_empty());
148 assert!(ticket.contains('.'));
149
150 let parts: Vec<&str> = ticket.split('.').collect();
152 assert_eq!(parts.len(), 3);
153 }
154 Err(err) => {
155 panic!("获取 bili_ticket 字符串失败: {}", err);
156 }
157 }
158 }
159
160 #[tokio::test]
161 async fn test_with_csrf() {
162 let bpi = BpiClient::new();
163
164 match bpi.misc_sign_bili_ticket().await {
166 Ok(resp) => {
167 tracing::info!(
168 "带 CSRF 的 bili_ticket 生成成功: {}",
169 resp.data.unwrap().ticket
170 );
171 }
172 Err(err) => {
173 tracing::info!("带 CSRF 测试失败(预期可能失败): {}", err);
174 }
176 }
177 }
178}
179
180impl BpiClient {
182 pub fn _usage_example() {}
206}