camo/camo.rs
1pub use crate::utils::crypto::{generate_digest, verify_digest};
2pub use crate::utils::encoding::{encode_url_base64, encode_url_hex};
3
4/// URL encoding format
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
6pub enum Encoding {
7 /// Hexadecimal encoding (default, compatible with original Camo)
8 #[default]
9 Hex,
10 /// URL-safe Base64 encoding
11 Base64,
12}
13
14/// A signed Camo URL ready for use
15#[derive(Debug, Clone)]
16pub struct SignedUrl {
17 /// The original URL that was signed
18 pub original_url: String,
19 /// The HMAC-SHA1 digest
20 pub digest: String,
21 /// The encoded URL
22 pub encoded_url: String,
23 /// The encoding format used
24 pub encoding: Encoding,
25}
26
27impl SignedUrl {
28 /// Generate the full proxy URL with a base URL
29 ///
30 /// # Example
31 ///
32 /// ```rust
33 /// use camo::CamoUrl;
34 ///
35 /// let camo = CamoUrl::new("secret");
36 /// let url = camo.sign("http://example.com/image.png")
37 /// .to_url("https://camo.example.com");
38 /// ```
39 pub fn to_url(&self, base: &str) -> String {
40 let base = base.trim_end_matches('/');
41 format!("{}/{}/{}", base, self.digest, self.encoded_url)
42 }
43
44 /// Get just the path portion (without base URL)
45 ///
46 /// # Example
47 ///
48 /// ```rust
49 /// use camo::CamoUrl;
50 ///
51 /// let camo = CamoUrl::new("secret");
52 /// let path = camo.sign("http://example.com/image.png").to_path();
53 /// // Returns: /abc123.../68747470...
54 /// ```
55 pub fn to_path(&self) -> String {
56 format!("/{}/{}", self.digest, self.encoded_url)
57 }
58
59 /// Switch to Base64 encoding
60 pub fn base64(mut self) -> Self {
61 if self.encoding != Encoding::Base64 {
62 self.encoded_url = encode_url_base64(&self.original_url);
63 self.encoding = Encoding::Base64;
64 }
65 self
66 }
67
68 /// Switch to Hex encoding
69 pub fn hex(mut self) -> Self {
70 if self.encoding != Encoding::Hex {
71 self.encoded_url = encode_url_hex(&self.original_url);
72 self.encoding = Encoding::Hex;
73 }
74 self
75 }
76}
77
78/// Camo URL generator
79///
80/// Use this struct to generate signed URLs for a Camo proxy.
81///
82/// # Example
83///
84/// ```rust
85/// use camo::CamoUrl;
86///
87/// let camo = CamoUrl::new("your-secret-key");
88/// let signed = camo.sign("http://example.com/image.png");
89/// let url = signed.to_url("https://camo.example.com");
90/// ```
91#[derive(Debug, Clone)]
92pub struct CamoUrl {
93 key: String,
94 default_encoding: Encoding,
95}
96
97impl CamoUrl {
98 /// Create a new CamoUrl generator with the given HMAC key
99 ///
100 /// # Arguments
101 ///
102 /// * `key` - The HMAC secret key for signing URLs
103 ///
104 /// # Example
105 ///
106 /// ```rust
107 /// use camo::CamoUrl;
108 ///
109 /// let camo = CamoUrl::new("your-secret-key");
110 /// ```
111 pub fn new(key: impl Into<String>) -> Self {
112 Self {
113 key: key.into(),
114 default_encoding: Encoding::Hex,
115 }
116 }
117
118 /// Set the default encoding format for generated URLs
119 ///
120 /// # Example
121 ///
122 /// ```rust
123 /// use camo::{CamoUrl, Encoding};
124 ///
125 /// let camo = CamoUrl::new("secret")
126 /// .with_encoding(Encoding::Base64);
127 /// ```
128 pub fn with_encoding(mut self, encoding: Encoding) -> Self {
129 self.default_encoding = encoding;
130 self
131 }
132
133 /// Sign a URL and return a SignedUrl
134 ///
135 /// # Arguments
136 ///
137 /// * `url` - The URL to sign (typically an HTTP image URL)
138 ///
139 /// # Example
140 ///
141 /// ```rust
142 /// use camo::CamoUrl;
143 ///
144 /// let camo = CamoUrl::new("secret");
145 /// let signed = camo.sign("http://example.com/image.png");
146 ///
147 /// // Get the full URL
148 /// let url = signed.to_url("https://camo.example.com");
149 ///
150 /// // Or just the path
151 /// let path = camo.sign("http://example.com/image.png").to_path();
152 /// ```
153 pub fn sign(&self, url: impl AsRef<str>) -> SignedUrl {
154 let url = url.as_ref();
155 let digest = generate_digest(&self.key, url);
156 let encoded_url = match self.default_encoding {
157 Encoding::Hex => encode_url_hex(url),
158 Encoding::Base64 => encode_url_base64(url),
159 };
160
161 SignedUrl {
162 original_url: url.to_string(),
163 digest,
164 encoded_url,
165 encoding: self.default_encoding,
166 }
167 }
168
169 /// Convenience method to sign and generate a full URL in one call
170 ///
171 /// # Example
172 ///
173 /// ```rust
174 /// use camo::CamoUrl;
175 ///
176 /// let camo = CamoUrl::new("secret");
177 /// let url = camo.sign_url("http://example.com/image.png", "https://camo.example.com");
178 /// ```
179 pub fn sign_url(&self, url: impl AsRef<str>, base: &str) -> String {
180 self.sign(url).to_url(base)
181 }
182
183 /// Verify a digest matches the expected value for a URL
184 ///
185 /// # Example
186 ///
187 /// ```rust
188 /// use camo::CamoUrl;
189 ///
190 /// let camo = CamoUrl::new("secret");
191 /// let signed = camo.sign("http://example.com/image.png");
192 ///
193 /// assert!(camo.verify("http://example.com/image.png", &signed.digest));
194 /// assert!(!camo.verify("http://example.com/image.png", "invalid"));
195 /// ```
196 pub fn verify(&self, url: impl AsRef<str>, digest: &str) -> bool {
197 verify_digest(&self.key, url.as_ref(), digest)
198 }
199}
200
201/// Generate a signed Camo URL (convenience function)
202///
203/// This is a shorthand for creating a CamoUrl and calling sign_url.
204///
205/// # Arguments
206///
207/// * `key` - The HMAC secret key
208/// * `url` - The URL to sign
209/// * `base` - The Camo proxy base URL
210///
211/// # Example
212///
213/// ```rust
214/// let url = camo::sign_url("secret", "http://example.com/image.png", "https://camo.example.com");
215/// ```
216pub fn sign_url(key: &str, url: &str, base: &str) -> String {
217 CamoUrl::new(key).sign_url(url, base)
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_sign_url() {
226 let camo = CamoUrl::new("test-secret");
227 let signed = camo.sign("http://example.com/image.png");
228
229 assert!(!signed.digest.is_empty());
230 assert!(!signed.encoded_url.is_empty());
231 assert_eq!(signed.encoding, Encoding::Hex);
232 }
233
234 #[test]
235 fn test_sign_url_base64() {
236 let camo = CamoUrl::new("test-secret").with_encoding(Encoding::Base64);
237 let signed = camo.sign("http://example.com/image.png");
238
239 assert_eq!(signed.encoding, Encoding::Base64);
240 }
241
242 #[test]
243 fn test_to_url() {
244 let camo = CamoUrl::new("test-secret");
245 let url = camo.sign_url("http://example.com/image.png", "https://camo.example.com");
246
247 assert!(url.starts_with("https://camo.example.com/"));
248 assert!(url.contains('/'));
249 }
250
251 #[test]
252 fn test_verify() {
253 let camo = CamoUrl::new("test-secret");
254 let signed = camo.sign("http://example.com/image.png");
255
256 assert!(camo.verify("http://example.com/image.png", &signed.digest));
257 assert!(!camo.verify("http://example.com/image.png", "invalid-digest"));
258 }
259
260 #[test]
261 fn test_encoding_switch() {
262 let camo = CamoUrl::new("test-secret");
263 let signed = camo.sign("http://example.com/image.png");
264 let hex_encoded = signed.encoded_url.clone();
265
266 let signed = signed.base64();
267 assert_ne!(signed.encoded_url, hex_encoded);
268 assert_eq!(signed.encoding, Encoding::Base64);
269
270 let signed = signed.hex();
271 assert_eq!(signed.encoded_url, hex_encoded);
272 assert_eq!(signed.encoding, Encoding::Hex);
273 }
274
275 #[test]
276 fn test_convenience_function() {
277 let url = sign_url(
278 "secret",
279 "http://example.com/image.png",
280 "https://camo.example.com",
281 );
282 assert!(url.starts_with("https://camo.example.com/"));
283 }
284}