pub use crate::utils::crypto::{generate_digest, verify_digest};
pub use crate::utils::encoding::{encode_url_base64, encode_url_hex};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Encoding {
#[default]
Hex,
Base64,
}
#[derive(Debug, Clone)]
pub struct SignedUrl {
pub original_url: String,
pub digest: String,
pub encoded_url: String,
pub encoding: Encoding,
}
impl SignedUrl {
pub fn to_url(&self, base: &str) -> String {
let base = base.trim_end_matches('/');
format!("{}/{}/{}", base, self.digest, self.encoded_url)
}
pub fn to_path(&self) -> String {
format!("/{}/{}", self.digest, self.encoded_url)
}
pub fn base64(mut self) -> Self {
if self.encoding != Encoding::Base64 {
self.encoded_url = encode_url_base64(&self.original_url);
self.encoding = Encoding::Base64;
}
self
}
pub fn hex(mut self) -> Self {
if self.encoding != Encoding::Hex {
self.encoded_url = encode_url_hex(&self.original_url);
self.encoding = Encoding::Hex;
}
self
}
}
#[derive(Debug, Clone)]
pub struct CamoUrl {
key: String,
default_encoding: Encoding,
}
impl CamoUrl {
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
default_encoding: Encoding::Hex,
}
}
pub fn with_encoding(mut self, encoding: Encoding) -> Self {
self.default_encoding = encoding;
self
}
pub fn sign(&self, url: impl AsRef<str>) -> SignedUrl {
let url = url.as_ref();
let digest = generate_digest(&self.key, url);
let encoded_url = match self.default_encoding {
Encoding::Hex => encode_url_hex(url),
Encoding::Base64 => encode_url_base64(url),
};
SignedUrl {
original_url: url.to_string(),
digest,
encoded_url,
encoding: self.default_encoding,
}
}
pub fn sign_url(&self, url: impl AsRef<str>, base: &str) -> String {
self.sign(url).to_url(base)
}
pub fn verify(&self, url: impl AsRef<str>, digest: &str) -> bool {
verify_digest(&self.key, url.as_ref(), digest)
}
}
pub fn sign_url(key: &str, url: &str, base: &str) -> String {
CamoUrl::new(key).sign_url(url, base)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_url() {
let camo = CamoUrl::new("test-secret");
let signed = camo.sign("http://example.com/image.png");
assert!(!signed.digest.is_empty());
assert!(!signed.encoded_url.is_empty());
assert_eq!(signed.encoding, Encoding::Hex);
}
#[test]
fn test_sign_url_base64() {
let camo = CamoUrl::new("test-secret").with_encoding(Encoding::Base64);
let signed = camo.sign("http://example.com/image.png");
assert_eq!(signed.encoding, Encoding::Base64);
}
#[test]
fn test_to_url() {
let camo = CamoUrl::new("test-secret");
let url = camo.sign_url("http://example.com/image.png", "https://camo.example.com");
assert!(url.starts_with("https://camo.example.com/"));
assert!(url.contains('/'));
}
#[test]
fn test_verify() {
let camo = CamoUrl::new("test-secret");
let signed = camo.sign("http://example.com/image.png");
assert!(camo.verify("http://example.com/image.png", &signed.digest));
assert!(!camo.verify("http://example.com/image.png", "invalid-digest"));
}
#[test]
fn test_encoding_switch() {
let camo = CamoUrl::new("test-secret");
let signed = camo.sign("http://example.com/image.png");
let hex_encoded = signed.encoded_url.clone();
let signed = signed.base64();
assert_ne!(signed.encoded_url, hex_encoded);
assert_eq!(signed.encoding, Encoding::Base64);
let signed = signed.hex();
assert_eq!(signed.encoded_url, hex_encoded);
assert_eq!(signed.encoding, Encoding::Hex);
}
#[test]
fn test_convenience_function() {
let url = sign_url(
"secret",
"http://example.com/image.png",
"https://camo.example.com",
);
assert!(url.starts_with("https://camo.example.com/"));
}
}