1use crate::StorageCredentials;
2use azure_core::{
3 auth::Secret,
4 error::{Error, ErrorKind},
5};
6use tracing::warn;
7
8pub const ACCOUNT_KEY_KEY_NAME: &str = "AccountKey";
10pub const ACCOUNT_NAME_KEY_NAME: &str = "AccountName";
11pub const SAS_KEY_NAME: &str = "SharedAccessSignature";
12pub const ENDPOINT_SUFFIX_KEY_NAME: &str = "EndpointSuffix";
13pub const DEFAULT_ENDPOINTS_PROTOCOL_KEY_NAME: &str = "DefaultEndpointsProtocol";
14pub const USE_DEVELOPMENT_STORAGE_KEY_NAME: &str = "UseDevelopmentStorage";
15pub const DEVELOPMENT_STORAGE_PROXY_URI_KEY_NAME: &str = "DevelopmentStorageProxyUri";
16pub const BLOB_ENDPOINT_KEY_NAME: &str = "BlobEndpoint";
17pub const BLOB_SECONDARY_ENDPOINT_KEY_NAME: &str = "BlobSecondaryEndpoint";
18pub const TABLE_ENDPOINT_KEY_NAME: &str = "TableEndpoint";
19pub const TABLE_SECONDARY_ENDPOINT_KEY_NAME: &str = "TableSecondaryEndpoint";
20pub const QUEUE_ENDPOINT_KEY_NAME: &str = "QueueEndpoint";
21pub const QUEUE_SECONDARY_ENDPOINT_KEY_NAME: &str = "QueueSecondaryEndpoint";
22pub const FILE_ENDPOINT_KEY_NAME: &str = "FileEndpoint";
23pub const FILE_SECONDARY_ENDPOINT_KEY_NAME: &str = "FileSecondaryEndpoint";
24
25#[derive(Debug, PartialEq, Eq)]
26pub enum EndpointProtocol {
27 Http,
28 Https,
29}
30
31impl std::fmt::Display for EndpointProtocol {
32 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
33 let s = match self {
34 EndpointProtocol::Https => "https",
35 EndpointProtocol::Http => "http",
36 };
37 write!(f, "{s}")
38 }
39}
40
41#[derive(Debug, Default, PartialEq, Eq)]
46pub struct ConnectionString<'a> {
47 pub account_name: Option<&'a str>,
49 pub account_key: Option<&'a str>,
51 pub sas: Option<&'a str>,
53 pub default_endpoints_protocol: Option<EndpointProtocol>,
55 pub endpoint_suffix: Option<&'a str>,
57 pub use_development_storage: Option<bool>,
59 pub development_storage_proxy_uri: Option<&'a str>,
61 pub blob_endpoint: Option<&'a str>,
63 pub blob_secondary_endpoint: Option<&'a str>,
65 pub table_endpoint: Option<&'a str>,
67 pub table_secondary_endpoint: Option<&'a str>,
69 pub queue_endpoint: Option<&'a str>,
71 pub queue_secondary_endpoint: Option<&'a str>,
73 pub file_endpoint: Option<&'a str>,
75 pub file_secondary_endpoint: Option<&'a str>,
77}
78
79impl<'a> ConnectionString<'a> {
80 pub fn new(connection_string: &'a str) -> azure_core::Result<Self> {
81 let mut account_name = None;
82 let mut account_key = None;
83 let mut sas = None;
84 let mut endpoint_suffix = None;
85 let mut default_endpoints_protocol = None;
86 let mut use_development_storage = None;
87 let mut development_storage_proxy_uri = None;
88 let mut blob_endpoint = None;
89 let mut blob_secondary_endpoint = None;
90 let mut table_endpoint = None;
91 let mut table_secondary_endpoint = None;
92 let mut queue_endpoint = None;
93 let mut queue_secondary_endpoint = None;
94 let mut file_endpoint = None;
95 let mut file_secondary_endpoint = None;
96
97 let kv_str_pairs = connection_string
98 .split(';')
99 .filter(|s| !s.chars().all(char::is_whitespace));
100
101 for kv_pair_str in kv_str_pairs {
102 let kv = kv_pair_str.trim().split_once('=');
103
104 let (k, v) = match kv {
105 Some((k, _)) if (k.chars().all(char::is_whitespace) || k.trim() == "") => {
106 return Err(Error::with_message(ErrorKind::Other, || {
107 format!("no key found in connection string: {connection_string}")
108 }))
109 }
110 Some((k, v)) if (v.chars().all(char::is_whitespace) || v.trim() == "") => {
111 return Err(Error::with_message(ErrorKind::Other, || {
112 format!("missing value in connection string: {connection_string} key: {k}")
113 }))
114 }
115 Some((k, v)) => (k.trim(), v.trim()),
116 None => {
117 return Err(Error::with_message(ErrorKind::Other, || {
118 format!("no key/value found in connection string: {connection_string}")
119 }))
120 }
121 };
122
123 match k {
124 ACCOUNT_NAME_KEY_NAME => account_name = Some(v),
125 ACCOUNT_KEY_KEY_NAME => account_key = Some(v),
126 SAS_KEY_NAME => sas = Some(v),
127 ENDPOINT_SUFFIX_KEY_NAME => endpoint_suffix = Some(v),
128 DEFAULT_ENDPOINTS_PROTOCOL_KEY_NAME => {
129 let protocol = match v {
130 "http" => EndpointProtocol::Http,
131 "https" => EndpointProtocol::Https,
132 _ => {
133 return Err(Error::with_message(ErrorKind::Other, || {
134 format!("connection string unsupported protocol: {v}")
135 }));
136 }
137 };
138 default_endpoints_protocol = Some(protocol);
139 }
140 USE_DEVELOPMENT_STORAGE_KEY_NAME => match v {
141 "true" => use_development_storage = Some(true),
142 "false" => use_development_storage = Some(false),
143 _ => {
144 return Err(Error::with_message(ErrorKind::Other, || {
145 format!("connection string unexpected value. {USE_DEVELOPMENT_STORAGE_KEY_NAME}: {v}. Please specify true or false.")
146 }));
147 }
148 },
149 DEVELOPMENT_STORAGE_PROXY_URI_KEY_NAME => development_storage_proxy_uri = Some(v),
150 BLOB_ENDPOINT_KEY_NAME => blob_endpoint = Some(v),
151 BLOB_SECONDARY_ENDPOINT_KEY_NAME => blob_secondary_endpoint = Some(v),
152 TABLE_ENDPOINT_KEY_NAME => table_endpoint = Some(v),
153 TABLE_SECONDARY_ENDPOINT_KEY_NAME => table_secondary_endpoint = Some(v),
154 QUEUE_ENDPOINT_KEY_NAME => queue_endpoint = Some(v),
155 QUEUE_SECONDARY_ENDPOINT_KEY_NAME => queue_secondary_endpoint = Some(v),
156 FILE_ENDPOINT_KEY_NAME => file_endpoint = Some(v),
157 FILE_SECONDARY_ENDPOINT_KEY_NAME => file_secondary_endpoint = Some(v),
158 k => {
159 return Err(Error::with_message(ErrorKind::Other, || {
160 format!("connection string unexpected key: {k}")
161 }))
162 }
163 }
164 }
165
166 Ok(Self {
167 account_name,
168 account_key,
169 sas,
170 default_endpoints_protocol,
171 endpoint_suffix,
172 use_development_storage,
173 development_storage_proxy_uri,
174 blob_endpoint,
175 blob_secondary_endpoint,
176 table_endpoint,
177 table_secondary_endpoint,
178 queue_endpoint,
179 queue_secondary_endpoint,
180 file_endpoint,
181 file_secondary_endpoint,
182 })
183 }
184
185 pub fn storage_credentials(&self) -> azure_core::Result<StorageCredentials> {
186 match self {
187 ConnectionString {
188 sas: Some(sas_token),
189 ..
190 } => {
191 if self.account_key.is_some() {
192 warn!("Both account key and SAS defined in connection string. Using only the provided SAS.");
193 }
194 StorageCredentials::sas_token(*sas_token)
195 }
196 ConnectionString {
197 account_name: Some(account),
198 account_key: Some(key),
199 ..
200 } => Ok(StorageCredentials::access_key(*account, Secret::new((*key).to_string()))),
201 _ => {
202 Err(Error::message(ErrorKind::Credential,
203 "Could not create a `StorageCredentail` from the provided connection string. Please validate that you have specified a means of authentication (key, SAS, etc.)."
204 ))
205 }
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 #[allow(unused_imports)]
213 use super::*;
214
215 #[test]
216 fn it_returns_expected_errors() {
217 assert!(ConnectionString::new("AccountName=").is_err());
218 assert!(ConnectionString::new("AccountName =").is_err());
219 assert!(ConnectionString::new("MissingEquals").is_err());
220 assert!(ConnectionString::new("=").is_err());
221 assert!(ConnectionString::new("x=123;").is_err());
222 }
223
224 #[test]
225 fn it_parses_empty_connection_string() {
226 assert_eq!(
227 ConnectionString::new("").unwrap(),
228 ConnectionString::default()
229 );
230 }
231
232 #[test]
233 fn it_parses_basic_cases() {
234 assert!(matches!(
235 ConnectionString::new("AccountName=guy"),
236 Ok(ConnectionString {
237 account_name: Some("guy"),
238 ..
239 })
240 ));
241 assert!(matches!(
242 ConnectionString::new("AccountName=guy;"),
243 Ok(ConnectionString {
244 account_name: Some("guy"),
245 ..
246 })
247 ));
248 assert!(matches!(
249 ConnectionString::new("AccountName=guywald;AccountKey=1234"),
250 Ok(ConnectionString {
251 account_name: Some("guywald"),
252 account_key: Some("1234"),
253 ..
254 })
255 ));
256 assert!(matches!(
257 ConnectionString::new("AccountName=guywald;SharedAccessSignature=s"),
258 Ok(ConnectionString {
259 account_name: Some("guywald"),
260 sas: Some("s"),
261 ..
262 })
263 ));
264 assert!(matches!(
265 ConnectionString::new("AccountName=guywald;SharedAccessSignature=se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
266 Ok(ConnectionString {
267 account_name: Some("guywald"),
268 sas: Some("se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
269 ..
270 })
271 ));
272
273 assert!(matches!(
274 ConnectionString::new("AccountName = guywald;SharedAccessSignature = se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
275 Ok(ConnectionString {
276 account_name: Some("guywald"),
277 sas: Some("se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
278 ..
279 })
280 ));
281 }
282
283 #[test]
284 fn it_parses_all_properties() {
285 assert!(matches!(
286 ConnectionString::new("AccountName=a;AccountKey=b;DefaultEndpointsProtocol=https;UseDevelopmentStorage=true;DevelopmentStorageProxyUri=c;BlobEndpoint=d;TableEndpoint=e;QueueEndpoint=f;SharedAccessSignature=g;"),
287 Ok(ConnectionString {
288 account_name: Some("a"),
289 account_key: Some("b"),
290 default_endpoints_protocol: Some(EndpointProtocol::Https),
291 use_development_storage: Some(true),
292 development_storage_proxy_uri: Some("c"),
293 blob_endpoint: Some("d"),
294 table_endpoint: Some("e"),
295 sas: Some("g"),
296 ..
297 })
298 ));
299 assert!(matches!(
300 ConnectionString::new("BlobEndpoint=b1;BlobSecondaryEndpoint=b2;TableEndpoint=t1;TableSecondaryEndpoint=t2;QueueEndpoint=q1;QueueSecondaryEndpoint=q2;FileEndpoint=f1;FileSecondaryEndpoint=f2;"),
301 Ok(ConnectionString {
302 blob_endpoint: Some("b1"),
303 blob_secondary_endpoint: Some("b2"),
304 table_endpoint: Some("t1"),
305 table_secondary_endpoint: Some("t2"),
306 queue_endpoint: Some("q1"),
307 queue_secondary_endpoint: Some("q2"),
308 file_endpoint: Some("f1"),
309 file_secondary_endpoint: Some("f2"),
310 ..
311 })
312 ));
313 }
314
315 #[test]
316 fn it_parses_correct_endpoint_protocols() {
317 assert!(matches!(
318 ConnectionString::new("DefaultEndpointsProtocol=https"),
319 Ok(ConnectionString {
320 default_endpoints_protocol: Some(EndpointProtocol::Https),
321 ..
322 })
323 ));
324 assert!(matches!(
325 ConnectionString::new("DefaultEndpointsProtocol=http"),
326 Ok(ConnectionString {
327 default_endpoints_protocol: Some(EndpointProtocol::Http),
328 ..
329 })
330 ));
331 }
332}