use crate::StorageCredentials;
use azure_core::{
auth::Secret,
error::{Error, ErrorKind},
};
use tracing::warn;
pub const ACCOUNT_KEY_KEY_NAME: &str = "AccountKey";
pub const ACCOUNT_NAME_KEY_NAME: &str = "AccountName";
pub const SAS_KEY_NAME: &str = "SharedAccessSignature";
pub const ENDPOINT_SUFFIX_KEY_NAME: &str = "EndpointSuffix";
pub const DEFAULT_ENDPOINTS_PROTOCOL_KEY_NAME: &str = "DefaultEndpointsProtocol";
pub const USE_DEVELOPMENT_STORAGE_KEY_NAME: &str = "UseDevelopmentStorage";
pub const DEVELOPMENT_STORAGE_PROXY_URI_KEY_NAME: &str = "DevelopmentStorageProxyUri";
pub const BLOB_ENDPOINT_KEY_NAME: &str = "BlobEndpoint";
pub const BLOB_SECONDARY_ENDPOINT_KEY_NAME: &str = "BlobSecondaryEndpoint";
pub const TABLE_ENDPOINT_KEY_NAME: &str = "TableEndpoint";
pub const TABLE_SECONDARY_ENDPOINT_KEY_NAME: &str = "TableSecondaryEndpoint";
pub const QUEUE_ENDPOINT_KEY_NAME: &str = "QueueEndpoint";
pub const QUEUE_SECONDARY_ENDPOINT_KEY_NAME: &str = "QueueSecondaryEndpoint";
pub const FILE_ENDPOINT_KEY_NAME: &str = "FileEndpoint";
pub const FILE_SECONDARY_ENDPOINT_KEY_NAME: &str = "FileSecondaryEndpoint";
#[derive(Debug, PartialEq, Eq)]
pub enum EndpointProtocol {
Http,
Https,
}
impl std::fmt::Display for EndpointProtocol {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s = match self {
EndpointProtocol::Https => "https",
EndpointProtocol::Http => "http",
};
write!(f, "{s}")
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ConnectionString<'a> {
pub account_name: Option<&'a str>,
pub account_key: Option<&'a str>,
pub sas: Option<&'a str>,
pub default_endpoints_protocol: Option<EndpointProtocol>,
pub endpoint_suffix: Option<&'a str>,
pub use_development_storage: Option<bool>,
pub development_storage_proxy_uri: Option<&'a str>,
pub blob_endpoint: Option<&'a str>,
pub blob_secondary_endpoint: Option<&'a str>,
pub table_endpoint: Option<&'a str>,
pub table_secondary_endpoint: Option<&'a str>,
pub queue_endpoint: Option<&'a str>,
pub queue_secondary_endpoint: Option<&'a str>,
pub file_endpoint: Option<&'a str>,
pub file_secondary_endpoint: Option<&'a str>,
}
impl<'a> ConnectionString<'a> {
pub fn new(connection_string: &'a str) -> azure_core::Result<Self> {
let mut account_name = None;
let mut account_key = None;
let mut sas = None;
let mut endpoint_suffix = None;
let mut default_endpoints_protocol = None;
let mut use_development_storage = None;
let mut development_storage_proxy_uri = None;
let mut blob_endpoint = None;
let mut blob_secondary_endpoint = None;
let mut table_endpoint = None;
let mut table_secondary_endpoint = None;
let mut queue_endpoint = None;
let mut queue_secondary_endpoint = None;
let mut file_endpoint = None;
let mut file_secondary_endpoint = None;
let kv_str_pairs = connection_string
.split(';')
.filter(|s| !s.chars().all(char::is_whitespace));
for kv_pair_str in kv_str_pairs {
let kv = kv_pair_str.trim().split_once('=');
let (k, v) = match kv {
Some((k, _)) if (k.chars().all(char::is_whitespace) || k.trim() == "") => {
return Err(Error::with_message(ErrorKind::Other, || {
format!("no key found in connection string: {connection_string}")
}))
}
Some((k, v)) if (v.chars().all(char::is_whitespace) || v.trim() == "") => {
return Err(Error::with_message(ErrorKind::Other, || {
format!("missing value in connection string: {connection_string} key: {k}")
}))
}
Some((k, v)) => (k.trim(), v.trim()),
None => {
return Err(Error::with_message(ErrorKind::Other, || {
format!("no key/value found in connection string: {connection_string}")
}))
}
};
match k {
ACCOUNT_NAME_KEY_NAME => account_name = Some(v),
ACCOUNT_KEY_KEY_NAME => account_key = Some(v),
SAS_KEY_NAME => sas = Some(v),
ENDPOINT_SUFFIX_KEY_NAME => endpoint_suffix = Some(v),
DEFAULT_ENDPOINTS_PROTOCOL_KEY_NAME => {
let protocol = match v {
"http" => EndpointProtocol::Http,
"https" => EndpointProtocol::Https,
_ => {
return Err(Error::with_message(ErrorKind::Other, || {
format!("connection string unsupported protocol: {v}")
}));
}
};
default_endpoints_protocol = Some(protocol);
}
USE_DEVELOPMENT_STORAGE_KEY_NAME => match v {
"true" => use_development_storage = Some(true),
"false" => use_development_storage = Some(false),
_ => {
return Err(Error::with_message(ErrorKind::Other, || {
format!("connection string unexpected value. {USE_DEVELOPMENT_STORAGE_KEY_NAME}: {v}. Please specify true or false.")
}));
}
},
DEVELOPMENT_STORAGE_PROXY_URI_KEY_NAME => development_storage_proxy_uri = Some(v),
BLOB_ENDPOINT_KEY_NAME => blob_endpoint = Some(v),
BLOB_SECONDARY_ENDPOINT_KEY_NAME => blob_secondary_endpoint = Some(v),
TABLE_ENDPOINT_KEY_NAME => table_endpoint = Some(v),
TABLE_SECONDARY_ENDPOINT_KEY_NAME => table_secondary_endpoint = Some(v),
QUEUE_ENDPOINT_KEY_NAME => queue_endpoint = Some(v),
QUEUE_SECONDARY_ENDPOINT_KEY_NAME => queue_secondary_endpoint = Some(v),
FILE_ENDPOINT_KEY_NAME => file_endpoint = Some(v),
FILE_SECONDARY_ENDPOINT_KEY_NAME => file_secondary_endpoint = Some(v),
k => {
return Err(Error::with_message(ErrorKind::Other, || {
format!("connection string unexpected key: {k}")
}))
}
}
}
Ok(Self {
account_name,
account_key,
sas,
default_endpoints_protocol,
endpoint_suffix,
use_development_storage,
development_storage_proxy_uri,
blob_endpoint,
blob_secondary_endpoint,
table_endpoint,
table_secondary_endpoint,
queue_endpoint,
queue_secondary_endpoint,
file_endpoint,
file_secondary_endpoint,
})
}
pub fn storage_credentials(&self) -> azure_core::Result<StorageCredentials> {
match self {
ConnectionString {
sas: Some(sas_token),
..
} => {
if self.account_key.is_some() {
warn!("Both account key and SAS defined in connection string. Using only the provided SAS.");
}
StorageCredentials::sas_token(*sas_token)
}
ConnectionString {
account_name: Some(account),
account_key: Some(key),
..
} => Ok(StorageCredentials::access_key(*account, Secret::new((*key).to_string()))),
_ => {
Err(Error::message(ErrorKind::Credential,
"Could not create a `StorageCredentail` from the provided connection string. Please validate that you have specified a means of authentication (key, SAS, etc.)."
))
}
}
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn it_returns_expected_errors() {
assert!(ConnectionString::new("AccountName=").is_err());
assert!(ConnectionString::new("AccountName =").is_err());
assert!(ConnectionString::new("MissingEquals").is_err());
assert!(ConnectionString::new("=").is_err());
assert!(ConnectionString::new("x=123;").is_err());
}
#[test]
fn it_parses_empty_connection_string() {
assert_eq!(
ConnectionString::new("").unwrap(),
ConnectionString::default()
);
}
#[test]
fn it_parses_basic_cases() {
assert!(matches!(
ConnectionString::new("AccountName=guy"),
Ok(ConnectionString {
account_name: Some("guy"),
..
})
));
assert!(matches!(
ConnectionString::new("AccountName=guy;"),
Ok(ConnectionString {
account_name: Some("guy"),
..
})
));
assert!(matches!(
ConnectionString::new("AccountName=guywald;AccountKey=1234"),
Ok(ConnectionString {
account_name: Some("guywald"),
account_key: Some("1234"),
..
})
));
assert!(matches!(
ConnectionString::new("AccountName=guywald;SharedAccessSignature=s"),
Ok(ConnectionString {
account_name: Some("guywald"),
sas: Some("s"),
..
})
));
assert!(matches!(
ConnectionString::new("AccountName=guywald;SharedAccessSignature=se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
Ok(ConnectionString {
account_name: Some("guywald"),
sas: Some("se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
..
})
));
assert!(matches!(
ConnectionString::new("AccountName = guywald;SharedAccessSignature = se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
Ok(ConnectionString {
account_name: Some("guywald"),
sas: Some("se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
..
})
));
}
#[test]
fn it_parses_all_properties() {
assert!(matches!(
ConnectionString::new("AccountName=a;AccountKey=b;DefaultEndpointsProtocol=https;UseDevelopmentStorage=true;DevelopmentStorageProxyUri=c;BlobEndpoint=d;TableEndpoint=e;QueueEndpoint=f;SharedAccessSignature=g;"),
Ok(ConnectionString {
account_name: Some("a"),
account_key: Some("b"),
default_endpoints_protocol: Some(EndpointProtocol::Https),
use_development_storage: Some(true),
development_storage_proxy_uri: Some("c"),
blob_endpoint: Some("d"),
table_endpoint: Some("e"),
sas: Some("g"),
..
})
));
assert!(matches!(
ConnectionString::new("BlobEndpoint=b1;BlobSecondaryEndpoint=b2;TableEndpoint=t1;TableSecondaryEndpoint=t2;QueueEndpoint=q1;QueueSecondaryEndpoint=q2;FileEndpoint=f1;FileSecondaryEndpoint=f2;"),
Ok(ConnectionString {
blob_endpoint: Some("b1"),
blob_secondary_endpoint: Some("b2"),
table_endpoint: Some("t1"),
table_secondary_endpoint: Some("t2"),
queue_endpoint: Some("q1"),
queue_secondary_endpoint: Some("q2"),
file_endpoint: Some("f1"),
file_secondary_endpoint: Some("f2"),
..
})
));
}
#[test]
fn it_parses_correct_endpoint_protocols() {
assert!(matches!(
ConnectionString::new("DefaultEndpointsProtocol=https"),
Ok(ConnectionString {
default_endpoints_protocol: Some(EndpointProtocol::Https),
..
})
));
assert!(matches!(
ConnectionString::new("DefaultEndpointsProtocol=http"),
Ok(ConnectionString {
default_endpoints_protocol: Some(EndpointProtocol::Http),
..
})
));
}
}