Skip to main content

tdm_server_rust/utils/
cos_cdn.rs

1//! 腾讯云 CDN TypeD 鉴权链接生成
2//!
3//! 对齐 Java `OssServiceImpl.generateCdnUrl`。
4//! 生成带时间戳和 MD5 签名的 CDN 鉴权下载链接,防止盗链。
5//!
6//! ## 鉴权算法
7//!
8//! TypeD 鉴权:`sign = md5(key + encoded_path + timestamp)`
9//! URL 格式:`https://{domain}{path}?sign={sign}&t={ts}`
10//!
11//! ## 配置要求
12//!
13//! 需要 `tencent.cdn_domain` 和 `tencent.cdn_key` 均已配置且非空,
14//! 否则返回 `None`(降级为无鉴权访问)。
15
16use std::time::{SystemTime, UNIX_EPOCH};
17
18/// 生成腾讯云 CDN TypeD 鉴权下载链接
19///
20/// 对指定对象生成有时效的 CDN 下载链接,防止盗链。
21/// 签名有效期为长期(时间戳生成后永久可访问),
22/// 若需更严格的控制可修改为添加过期参数。
23///
24/// # 参数
25///
26/// - `cdn_domain`: CDN 加速域名,如 `"ossdev.yuriful.top"`
27/// - `cdn_key`: CDN 鉴权密钥(从配置加载)
28/// - `object_key`: COS 对象 Key,如 `"manga_1/episode_2/file.7z"`
29///
30/// # 返回值
31///
32/// - `Some(url)` — 生成成功,返回完整的鉴权下载链接
33/// - `None` — CDN 域名或密钥为空,无法生成鉴权 URL
34///
35/// # 路径编码
36///
37/// 对象 Key 中的特殊字符(中文、空格、`!`、`*` 等)会被 URL 编码,
38/// 保留 `/` 分隔符以保持路径结构。
39///
40/// # 示例
41///
42/// ```rust,ignore
43/// let url = generate_cdn_url("cdn.example.com", "mykey", "manga/cover.jpg");
44/// // => "https://cdn.example.com/manga/cover.jpg?sign=abc123&t=1710000000"
45/// ```
46pub fn generate_cdn_url(cdn_domain: &str, cdn_key: &str, object_key: &str) -> Option<String> {
47    let domain = cdn_domain.trim();
48    let key = cdn_key.trim();
49    if domain.is_empty() || key.is_empty() {
50        return None;
51    }
52    let raw_path = if object_key.starts_with('/') {
53        object_key.to_string()
54    } else {
55        format!("/{object_key}")
56    };
57    let encoded_path = encode_cdn_path(&raw_path);
58    let ts = SystemTime::now()
59        .duration_since(UNIX_EPOCH)
60        .ok()?
61        .as_secs();
62    let sign_content = format!("{key}{encoded_path}{ts}");
63    let sign = format!("{:x}", md5::compute(sign_content.as_bytes()));
64    let domain = domain.trim_end_matches('/');
65    let protocol = if domain.starts_with("http://") || domain.starts_with("https://") {
66        ""
67    } else {
68        "https://"
69    };
70    Some(format!("{protocol}{domain}{encoded_path}?sign={sign}&t={ts}"))
71}
72
73/// CDN 路径编码:保留 `/`,编码中文及特殊字符
74///
75/// 对路径中每一段分别进行 URL 编码,同时对 `!` `'` `(` `)` `*` 做额外百分号编码,
76/// 对齐 Java `OssServiceImpl` 的编码行为。
77fn encode_cdn_path(path: &str) -> String {
78    path.split('/')
79        .map(|seg| {
80            if seg.is_empty() {
81                String::new()
82            } else {
83                urlencoding::encode(seg)
84                    .replace('!', "%21")
85                    .replace('\'', "%27")
86                    .replace('(', "%28")
87                    .replace(')', "%29")
88                    .replace('*', "%2A")
89            }
90        })
91        .collect::<Vec<_>>()
92        .join("/")
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_generate_cdn_url_has_sign_and_t() {
101        let url = generate_cdn_url(
102            "ossdev.yuriful.top",
103            "testkey",
104            "manga_1/episode_2/manga-raw/file.7z",
105        )
106        .unwrap();
107        assert!(url.contains("?sign="));
108        assert!(url.contains("&t="));
109        assert!(url.starts_with("https://ossdev.yuriful.top/"));
110    }
111}