1use hmac::{Hmac, Mac, NewMac};
2use std::{str::FromStr, time::Duration};
3use url::Url;
4
5#[derive(Clone, securefmt::Debug)]
6pub struct CamoConfig {
7 #[sensitive]
8 key: Vec<u8>,
9 host: url::Url,
10 lifetime: Option<std::time::Duration>,
11}
12
13type HmacSha1 = Hmac<crypto_hashes::sha1::Sha1>;
14type HmacBlake2b = Hmac<crypto_hashes::blake2::Blake2b>;
15
16impl CamoConfig {
17 pub fn new<S1: Into<String>, S2: Into<String>>(
18 key: S1,
19 host: S2,
20 ) -> Result<Self, Box<dyn std::error::Error>> {
21 let key: Vec<u8> = hex::decode(key.into())?;
22 let host = Url::from_str(&host.into())?;
23 Ok(Self::new_from(key, host))
24 }
25
26 pub fn new_with_lifetime<S1: Into<String>, S2: Into<String>>(
27 key: S1,
28 host: S2,
29 lifetime: Duration,
30 ) -> Result<Self, Box<dyn std::error::Error>> {
31 let key: Vec<u8> = hex::decode(key.into())?;
32 let host = Url::from_str(&host.into())?;
33 Ok(Self::new_from_with_lifetime(key, host, lifetime))
34 }
35
36 pub fn new_from(key: Vec<u8>, host: Url) -> Self {
37 Self {
38 key,
39 host,
40 lifetime: None,
41 }
42 }
43
44 pub fn new_from_with_lifetime(key: Vec<u8>, host: Url, lifetime: Duration) -> Self {
45 Self {
46 key,
47 host,
48 lifetime: Some(lifetime),
49 }
50 }
51
52 pub fn get_camo_url(&self, url: &Url) -> Result<Url, Box<dyn std::error::Error>> {
53 let urlstr = url.to_string();
54 let urldigest = self.digest(&urlstr);
55 let urldigest = hex::encode(urldigest);
56 let mut base = self.host.clone();
57 base.query_pairs_mut().append_pair("url", &urlstr);
58 base.path_segments_mut()
59 .map_err(|_| "could not append digest")?
60 .push(&urldigest);
61 Ok(base)
62 }
63
64 pub fn get_camo_url_inline(&self, url: &Url) -> Result<Url, Box<dyn std::error::Error>> {
65 let urlstr = url.to_string();
66 let urldigest = self.digest(&urlstr);
67 let urldigest = hex::encode(urldigest);
68 let mut base = self.host.clone();
69 base.path_segments_mut()
70 .map_err(|_| "could not append digest")?
71 .push(&urldigest)
72 .push(&hex::encode(urlstr))
73 .push("");
74 Ok(base)
75 }
76
77 pub fn get_camo_v2_url(
78 &self,
79 url: &Url,
80 base_time: std::time::SystemTime,
81 ) -> Result<Url, Box<dyn std::error::Error>> {
82 let urlstr = url.to_string();
83 let urldigest = self.digest(&urlstr);
84 let urldigest = hex::encode(urldigest);
85 let mut base = self.host.clone();
86 base.query_pairs_mut().append_pair("url", &urlstr);
87 base.path_segments_mut()
88 .map_err(|_| "could not append digest")?
89 .push(&urldigest);
90 let v2str = if let Some(lifetime) = self.lifetime {
91 let expiry = (base_time + lifetime)
92 .duration_since(std::time::UNIX_EPOCH)
93 .expect("system time is bad or lifetime negative")
94 .as_secs()
95 .to_string();
96 base.query_pairs_mut().append_pair("expires", &expiry);
97
98 let urldigestv2 = self.v2digest(&(expiry + &urlstr));
99 hex::encode(urldigestv2)
100 } else {
101 let urldigestv2 = self.v2digest(&urlstr);
102 hex::encode(urldigestv2)
103 };
104 base.query_pairs_mut().append_pair("urlv2", &v2str);
105 Ok(base)
106 }
107
108 fn digest(&self, urlstr: &str) -> Vec<u8> {
109 let mut hasher: HmacSha1 =
110 HmacSha1::new_varkey(&self.key).expect("could not take SHA1 key");
111 hasher.update(urlstr.as_bytes());
112 let result = hasher.finalize();
113 result.into_bytes().as_slice().to_vec()
114 }
115
116 fn v2digest(&self, urlstr: &str) -> Vec<u8> {
117 let mut hasher: HmacBlake2b =
118 HmacBlake2b::new_varkey(&self.key).expect("could not take Blake2b key");
119 hasher.update(urlstr.as_bytes());
120 let result = hasher.finalize();
121 result.into_bytes().as_slice().to_vec()
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 #[test]
129 fn camo_inline() {
130 let key = "somekeythatisuniqueandstufflikethat";
131 let key = hex::encode(key);
132 let host = "https://www.example.com";
133 let camo = CamoConfig::new(key, host).expect("must work");
134 let url = r#"http://40.media.tumblr.com/4574de09e1207dbb872f9c018adb57c8/tumblr_ngya1hYUBO1rq9ek2o1_1280.jpg"#;
135 let url = Url::from_str(url).expect("test url doesn't parse");
136 let d = camo.get_camo_url_inline(&url).expect("must work");
137 let expected = r#"https://www.example.com/3608e93ba99430a7fb28344e910330004ad51b84/687474703a2f2f34302e6d656469612e74756d626c722e636f6d2f34353734646530396531323037646262383732663963303138616462353763382f74756d626c725f6e67796131685955424f31727139656b326f315f313238302e6a7067/"#;
138 assert_eq!(expected, d.to_string());
139 }
140
141 #[test]
142 fn camo_urlquery() {
143 let key = "somekeythatisuniqueandstufflikethat";
144 let key = hex::encode(key);
145 let host = "https://www.example.com";
146 let camo = CamoConfig::new(key, host).expect("must work");
147 let url = r#"http://40.media.tumblr.com/4574de09e1207dbb872f9c018adb57c8/tumblr_ngya1hYUBO1rq9ek2o1_1280.jpg"#;
148 let url = Url::from_str(url).expect("test url doesn't parse");
149 let d = camo.get_camo_url(&url).expect("must work");
150 let expected = r#"https://www.example.com/3608e93ba99430a7fb28344e910330004ad51b84?url=http%3A%2F%2F40.media.tumblr.com%2F4574de09e1207dbb872f9c018adb57c8%2Ftumblr_ngya1hYUBO1rq9ek2o1_1280.jpg"#;
151 assert_eq!(expected, d.to_string());
152 }
153
154 #[test]
155 fn camo_v2() {
156 let key = "somekeythatisuniqueandstufflikethat";
157 let key = hex::encode(key);
158 let host = "https://www.example.com";
159 let camo =
160 CamoConfig::new_with_lifetime(key, host, Duration::from_secs(120)).expect("must work");
161 let url = r#"http://40.media.tumblr.com/4574de09e1207dbb872f9c018adb57c8/tumblr_ngya1hYUBO1rq9ek2o1_1280.jpg"#;
162 let url = Url::from_str(url).expect("test url doesn't parse");
163 let time = std::time::UNIX_EPOCH;
164 let d = camo.get_camo_v2_url(&url, time).expect("must work");
165 let expected = r#"https://www.example.com/3608e93ba99430a7fb28344e910330004ad51b84?url=http%3A%2F%2F40.media.tumblr.com%2F4574de09e1207dbb872f9c018adb57c8%2Ftumblr_ngya1hYUBO1rq9ek2o1_1280.jpg&expires=120&urlv2=441dc4e4d2dc89f3c7731b0c8b72f6f5fcba196a0fab70eac20508151e402eb092a7a9cf9dd73e9636a7854fbbf0f6aa6cd32e1228d89bf8668f5715f9116eb7"#;
166 assert_eq!(expected, d.to_string());
167 }
168}