wae_storage/providers/
azure_blob.rs1use crate::{StorageConfig, StorageProvider, StorageResult};
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use chrono::{Duration, Utc};
7use hmac::{Hmac, Mac};
8use sha2::Sha256;
9use url::Url;
10use wae_types::WaeError;
11
12pub struct AzureBlobProvider;
16
17impl AzureBlobProvider {
18 fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
20 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC can take key of any size");
21 mac.update(data);
22 mac.finalize().into_bytes().to_vec()
23 }
24}
25
26impl StorageProvider for AzureBlobProvider {
27 fn sign_url(&self, path: &str, config: &StorageConfig) -> StorageResult<Url> {
28 if path.is_empty() {
29 return Err(WaeError::invalid_params("path", "Empty path"));
30 }
31
32 if path.starts_with("http://") || path.starts_with("https://") {
33 return Url::parse(path).map_err(|e| WaeError::invalid_params("path", e.to_string()));
34 }
35
36 let clean_path = path.trim_start_matches('/');
37 let now = Utc::now();
38 let _expiration = (now + Duration::seconds(3600)).timestamp();
39 let x_ms_version = "2023-11-03";
40 let _x_ms_date = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
41
42 let host_str = config.cdn_url.clone().unwrap_or_else(|| {
43 if let Some(endpoint) = &config.endpoint {
44 endpoint.clone()
45 }
46 else {
47 format!("https://{}.blob.core.windows.net/{}", config.secret_id, config.bucket)
48 }
49 });
50
51 let host_url =
52 if host_str.starts_with("http") { Url::parse(&host_str) } else { Url::parse(&format!("https://{}", host_str)) }
53 .map_err(|e| WaeError::invalid_params("host", e.to_string()))?;
54
55 let mut url = host_url.join(clean_path).map_err(|e| WaeError::invalid_params("path", e.to_string()))?;
56
57 let _method = "r";
58 let start = "";
59 let now = Utc::now();
60 let expiry = (now + Duration::seconds(3600)).format("%Y-%m-%dT%H:%M:%SZ").to_string();
61 let resource = "b";
62 let permissions = "r";
63 let version = x_ms_version;
64
65 let account_key = STANDARD
66 .decode(&config.secret_key)
67 .map_err(|e| WaeError::invalid_params("secret_key", format!("Invalid base64: {}", e)))?;
68
69 let resource_path = format!("/{}/{}/{}", config.secret_id, config.bucket, clean_path);
70 let string_to_sign = format!(
71 "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
72 permissions, start, expiry, resource_path, "", "", "", "", "", "", "", version, "", ""
73 );
74
75 let signature = Self::hmac_sha256(&account_key, string_to_sign.as_bytes());
76 let signature_base64 = STANDARD.encode(signature);
77
78 url.query_pairs_mut()
79 .append_pair("sv", version)
80 .append_pair("sr", resource)
81 .append_pair("sp", permissions)
82 .append_pair("se", &expiry)
83 .append_pair("sig", &signature_base64);
84
85 Ok(url)
86 }
87
88 fn get_presigned_put_url(&self, key: &str, config: &StorageConfig) -> StorageResult<Url> {
89 let clean_key = key.trim_start_matches('/');
90 let now = Utc::now();
91 let _expiration = (now + Duration::seconds(900)).timestamp();
92 let x_ms_version = "2023-11-03";
93
94 let host_str = config
95 .endpoint
96 .clone()
97 .unwrap_or_else(|| format!("https://{}.blob.core.windows.net/{}", config.secret_id, config.bucket));
98
99 let host_url =
100 if host_str.starts_with("http") { Url::parse(&host_str) } else { Url::parse(&format!("https://{}", host_str)) }
101 .map_err(|e| WaeError::invalid_params("host", e.to_string()))?;
102
103 let mut url = host_url.join(clean_key).map_err(|e| WaeError::invalid_params("key", e.to_string()))?;
104
105 let permissions = "w";
106 let resource = "b";
107 let version = x_ms_version;
108 let expiry = (Utc::now() + Duration::seconds(900)).format("%Y-%m-%dT%H:%M:%SZ").to_string();
109 let resource_path = format!("/{}/{}/{}", config.secret_id, config.bucket, clean_key);
110
111 let account_key = STANDARD
112 .decode(&config.secret_key)
113 .map_err(|e| WaeError::invalid_params("secret_key", format!("Invalid base64: {}", e)))?;
114
115 let string_to_sign = format!(
116 "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
117 permissions, "", expiry, resource_path, "", "", "", "", "", "", "", version, "", ""
118 );
119
120 let signature = Self::hmac_sha256(&account_key, string_to_sign.as_bytes());
121 let signature_base64 = STANDARD.encode(signature);
122
123 url.query_pairs_mut()
124 .append_pair("sv", version)
125 .append_pair("sr", resource)
126 .append_pair("sp", permissions)
127 .append_pair("se", &expiry)
128 .append_pair("sig", &signature_base64);
129
130 Ok(url)
131 }
132}