camo_url/
lib.rs

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}