bpi_rs/transport/
request.rs1use reqwest::{Method, RequestBuilder, Url};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct RequestMetadata {
6 pub method: Method,
7 pub endpoint: String,
8 pub sanitized_url: String,
9}
10
11impl RequestMetadata {
12 pub fn from_builder(builder: &RequestBuilder, endpoint: impl Into<String>) -> Option<Self> {
14 let request = builder.try_clone()?.build().ok()?;
15
16 Some(Self {
17 method: request.method().clone(),
18 endpoint: endpoint.into(),
19 sanitized_url: sanitize_url_for_logging(request.url().as_str()),
20 })
21 }
22}
23
24pub fn sanitize_url_for_logging(url: &str) -> String {
26 let Ok(mut parsed) = Url::parse(url) else {
27 return "<invalid-url>".to_string();
28 };
29
30 let safe_pairs = parsed
31 .query_pairs()
32 .filter(|(key, _)| !is_sensitive_query_key(key))
33 .map(|(key, value)| (key.into_owned(), value.into_owned()))
34 .collect::<Vec<_>>();
35
36 parsed.set_query(None);
37
38 if !safe_pairs.is_empty() {
39 let query = safe_pairs
40 .into_iter()
41 .map(|(key, value)| format!("{key}={value}"))
42 .collect::<Vec<_>>()
43 .join("&");
44 parsed.set_query(Some(&query));
45 }
46
47 parsed.to_string()
48}
49
50pub fn sanitize_header_for_logging(name: &str, value: &str) -> Option<(String, String)> {
52 if is_sensitive_header_name(name) {
53 return None;
54 }
55
56 Some((name.to_string(), value.to_string()))
57}
58
59fn is_sensitive_query_key(key: &str) -> bool {
60 matches!(
61 key.to_ascii_lowercase().as_str(),
62 "sessdata"
63 | "dedeuserid"
64 | "dedeuserid__ckmd5"
65 | "bili_jct"
66 | "csrf"
67 | "csrf_token"
68 | "w_rid"
69 | "access_key"
70 | "token"
71 | "cookie"
72 )
73}
74
75fn is_sensitive_header_name(name: &str) -> bool {
76 matches!(
77 name.to_ascii_lowercase().as_str(),
78 "cookie" | "authorization" | "proxy-authorization" | "set-cookie"
79 )
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn sanitize_url_for_logging_removes_sensitive_query_values() {
88 let sanitized = sanitize_url_for_logging(
89 "https://api.bilibili.com/x/test?mid=1&SESSDATA=secret&csrf=token&bili_jct=token&w_rid=signed",
90 );
91
92 assert_eq!(sanitized, "https://api.bilibili.com/x/test?mid=1");
93 }
94
95 #[test]
96 fn sanitize_url_for_logging_keeps_path_when_all_query_values_are_sensitive() {
97 let sanitized =
98 sanitize_url_for_logging("https://api.bilibili.com/x/test?SESSDATA=secret&csrf=token");
99
100 assert_eq!(sanitized, "https://api.bilibili.com/x/test");
101 }
102
103 #[test]
104 fn sanitize_url_for_logging_handles_invalid_url_without_panicking() {
105 let sanitized = sanitize_url_for_logging("not a url with SESSDATA=secret");
106
107 assert_eq!(sanitized, "<invalid-url>");
108 }
109
110 #[test]
111 fn sanitize_header_for_logging_removes_raw_cookie_header() {
112 let sanitized = sanitize_header_for_logging("Cookie", "SESSDATA=secret; bili_jct=token");
113
114 assert!(sanitized.is_none());
115 }
116
117 #[test]
118 fn request_metadata_from_builder_sanitizes_url_and_preserves_method() {
119 let client = reqwest::Client::new();
120 let builder = client.post("https://api.bilibili.com/x/test?mid=1&csrf=secret&w_rid=signed");
121
122 let metadata = RequestMetadata::from_builder(&builder, "test.endpoint").unwrap();
123
124 assert_eq!(metadata.method, Method::POST);
125 assert_eq!(metadata.endpoint, "test.endpoint");
126 assert_eq!(
127 metadata.sanitized_url,
128 "https://api.bilibili.com/x/test?mid=1"
129 );
130 }
131}