use crate::IPRange;
use crate::{ClientEndpoint, HyperClientEndpoint};
use azure_sdk_core::errors::AzureError;
use azure_sdk_core::headers;
use azure_sdk_core::util::{format_header_value, HeaderMapExt, RequestBuilderExt};
use chrono::{DateTime, Utc};
use http::request::Builder;
use hyper::{self, header, HeaderMap, Method};
use ring::hmac;
use std::fmt::Write;
use url::form_urlencoded;
#[derive(Debug, Clone, Copy)]
pub enum ServiceType {
Blob,
Table,
}
pub(crate) const AZURE_VERSION: &str = "2019-07-07";
pub(crate) const SAS_VERSION: &str = "2019-02-02";
pub(crate) const HEADER_VERSION: &str = "x-ms-version"; pub(crate) const HEADER_DATE: &str = "x-ms-date";
fn generate_authorization<CE: ClientEndpoint>(
client_endpoint: &CE,
h: &HeaderMap,
u: &url::Url,
method: &Method,
service_type: ServiceType,
) -> String {
let str_to_sign = string_to_sign(client_endpoint, h, u, method, service_type);
let auth = encode_str_to_sign(&str_to_sign, client_endpoint.key());
format!("SharedKey {}:{}", client_endpoint.account(), auth)
}
fn encode_str_to_sign(str_to_sign: &str, hmac_key: &str) -> String {
let key = hmac::Key::new(ring::hmac::HMAC_SHA256, &base64::decode(hmac_key).unwrap());
let sig = hmac::sign(&key, str_to_sign.as_bytes());
base64::encode(sig.as_ref())
}
#[inline]
fn add_if_exists<K: header::AsHeaderName>(h: &HeaderMap, key: K) -> &str {
match h.get(key) {
Some(ce) => ce.to_str().unwrap(),
None => "",
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum SASType {
Container,
Blob,
FileShare,
File,
Queue,
Table,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn generate_storage_sas<CE: ClientEndpoint>(
client_endpoint: &CE,
start: Option<&DateTime<Utc>>,
end: &DateTime<Utc>,
path: &url::Url,
permission: &str,
identifier: &str,
ip_range: Option<&IPRange>,
resource: SASType,
snapshot_time: Option<&DateTime<Utc>>,
cache_control: &str,
content_disposition: &str,
content_encoding: &str,
content_language: &str,
content_type: &str,
starting_pk: &str,
ending_pk: &str,
starting_rk: &str,
ending_rk: &str,
) -> String {
let canonicalized_resource = canonicalized_resource(client_endpoint, path);
debug!("canonicalized_resource == {}", canonicalized_resource);
let canonicalized_resource = canonicalized_resource.lines().next().unwrap();
let start_string = if let Some(start) = start {
start.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
} else {
"".to_owned()
};
debug!("start_string == {}", start_string);
let end_string = end.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
debug!("end_string == {}", end_string);
let snapshot_time_string = if let Some(snapshot_time) = snapshot_time {
snapshot_time.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
} else {
"".to_owned()
};
debug!("snapshot_time_string == {}", snapshot_time_string);
let protocol = path.scheme();
debug!("protocol == {}", protocol);
let resource_char = match resource {
SASType::Blob => "b",
SASType::Container => "c",
SASType::FileShare => "s", SASType::File => "f", SASType::Queue => "q",
SASType::Table => "t",
};
debug!("resource_char == {}", resource_char);
let type_canonicalized_resource = format!(
"{}{}",
match resource {
SASType::Blob => "/blob",
SASType::Container => "/blob",
SASType::FileShare => "/file",
SASType::File => "/file",
SASType::Queue => "/queue",
SASType::Table => "/table",
},
canonicalized_resource
);
debug!(
"type_canonicalized_resource == {}",
type_canonicalized_resource
);
let ip_range_string = if let Some(ip_range) = ip_range {
format!(
"{}-{}",
ip_range.start.to_string(),
ip_range.end.to_string()
)
} else {
"".to_owned()
};
let string_to_sign = match resource {
SASType::Blob | SASType::Container | SASType::File | SASType::FileShare => format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
permission,
start_string,
end_string,
type_canonicalized_resource,
identifier,
ip_range_string,
protocol,
SAS_VERSION,
resource_char,
snapshot_time_string,
cache_control,
content_disposition,
content_encoding,
content_language,
content_type
),
SASType::Table => format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
permission,
start_string,
end_string,
type_canonicalized_resource,
identifier,
ip_range_string,
protocol,
SAS_VERSION,
starting_pk,
starting_rk,
ending_pk,
ending_rk,
),
SASType::Queue => format!(
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
permission,
start_string,
end_string,
type_canonicalized_resource,
identifier,
ip_range_string,
protocol,
SAS_VERSION,
),
};
debug!("string_to_sign == \n{}", string_to_sign);
let key = hmac::Key::new(
ring::hmac::HMAC_SHA256,
&base64::decode(client_endpoint.key()).unwrap(),
);
let sig = hmac::sign(&key, string_to_sign.as_bytes());
let result = base64::encode(sig.as_ref());
let token = format!(
"{}{}{}{}{}{}{}{}{}{}{}{}se={}&sp={}&sr={}&spr={}&sv={}&sig={}",
if start.is_some() {
format!(
"st={}&",
form_urlencoded::byte_serialize(start_string.as_bytes()).collect::<String>()
)
} else {
"".to_owned()
},
if snapshot_time.is_some() {
format!(
"sst={}&",
form_urlencoded::byte_serialize(snapshot_time_string.as_bytes())
.collect::<String>()
)
} else {
"".to_owned()
},
if ip_range.is_some() {
format!(
"sip={}&",
form_urlencoded::byte_serialize(ip_range_string.as_bytes()).collect::<String>()
)
} else {
"".to_owned()
},
if cache_control.is_empty() {
"".to_owned()
} else {
format!(
"rscc={}&",
form_urlencoded::byte_serialize(cache_control.as_bytes()).collect::<String>()
)
},
if content_disposition.is_empty() {
"".to_owned()
} else {
format!(
"rscd={}&",
form_urlencoded::byte_serialize(content_disposition.as_bytes()).collect::<String>()
)
},
if content_encoding.is_empty() {
"".to_owned()
} else {
format!(
"rsce={}&",
form_urlencoded::byte_serialize(content_encoding.as_bytes()).collect::<String>()
)
},
if content_language.is_empty() {
"".to_owned()
} else {
format!(
"rscl={}&",
form_urlencoded::byte_serialize(content_language.as_bytes()).collect::<String>()
)
},
if content_type.is_empty() {
"".to_owned()
} else {
format!(
"rsct={}&",
form_urlencoded::byte_serialize(content_type.as_bytes()).collect::<String>()
)
},
if starting_pk.is_empty() {
"".to_owned()
} else {
format!(
"spk={}&",
form_urlencoded::byte_serialize(starting_pk.as_bytes()).collect::<String>()
)
},
if ending_pk.is_empty() {
"".to_owned()
} else {
format!(
"epk={}&",
form_urlencoded::byte_serialize(ending_pk.as_bytes()).collect::<String>()
)
},
if starting_rk.is_empty() {
"".to_owned()
} else {
format!(
"srk={}&",
form_urlencoded::byte_serialize(starting_rk.as_bytes()).collect::<String>()
)
},
if ending_rk.is_empty() {
"".to_owned()
} else {
format!(
"erk={}&",
form_urlencoded::byte_serialize(ending_rk.as_bytes()).collect::<String>()
)
},
form_urlencoded::byte_serialize(end_string.as_bytes()).collect::<String>(),
permission,
resource_char,
protocol,
SAS_VERSION,
form_urlencoded::byte_serialize(result.as_bytes()).collect::<String>()
);
token
}
#[allow(unknown_lints)]
fn string_to_sign<CE: ClientEndpoint>(
client_endpoint: &CE,
h: &HeaderMap,
u: &url::Url,
method: &Method,
service_type: ServiceType,
) -> String {
match service_type {
ServiceType::Table => {
let mut s = String::new();
write!(
s,
"{}\n{}\n{}\n{}\n{}",
method.as_str(),
add_if_exists(h, headers::CONTENT_MD5),
add_if_exists(h, header::CONTENT_TYPE),
add_if_exists(h, HEADER_DATE),
canonicalized_resource_table(client_endpoint, u)
)
.unwrap();
s
}
_ => {
let cl = h
.get_as_str(header::CONTENT_LENGTH)
.map(|s| if s == "0" { "" } else { s })
.unwrap_or("");
let mut s = String::new();
write!(
s,
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}{}",
method.as_str(),
add_if_exists(h, header::CONTENT_ENCODING),
add_if_exists(h, header::CONTENT_LANGUAGE),
cl,
add_if_exists(h, headers::CONTENT_MD5),
add_if_exists(h, header::CONTENT_TYPE),
add_if_exists(h, header::DATE),
add_if_exists(h, header::IF_MODIFIED_SINCE),
add_if_exists(h, header::IF_MATCH),
add_if_exists(h, header::IF_NONE_MATCH),
add_if_exists(h, header::IF_UNMODIFIED_SINCE),
add_if_exists(h, header::RANGE),
canonicalize_header(h),
canonicalized_resource(client_endpoint, u)
)
.unwrap();
s
}
}
}
fn canonicalize_header(h: &HeaderMap) -> String {
let mut v_headers = h
.iter()
.filter(|(k, _v)| k.as_str().starts_with("x-ms"))
.map(|(k, _)| k.as_str())
.collect::<Vec<_>>();
v_headers.sort_unstable();
let mut can = String::new();
for header_name in v_headers {
let s = h.get_as_str(header_name).unwrap();
can = can + header_name + ":" + s + "\n";
}
can
}
fn canonicalized_resource_table<CE: ClientEndpoint>(client_endpoint: &CE, u: &url::Url) -> String {
format!("/{}{}", client_endpoint.account(), u.path())
}
fn canonicalized_resource<CE: ClientEndpoint>(client_endpoint: &CE, u: &url::Url) -> String {
let mut can_res: String = String::new();
can_res += "/";
let account = client_endpoint.account();
can_res += &account;
let paths = u.path_segments().unwrap();
{
let mut path = String::new();
for p in paths {
path.push('/');
path.push_str(&*p);
}
can_res += &path;
}
can_res += "\n";
let query_pairs = u.query_pairs(); {
let mut qps = Vec::new();
{
for qp in query_pairs {
trace!("adding to qps {:?}", qp);
if !(qps.iter().any(|x: &String| x == &qp.0)) {
qps.push(qp.0.into_owned());
}
}
}
qps.sort();
for qparam in qps {
let ret = lexy_sort(&query_pairs, &qparam);
can_res = can_res + &qparam.to_lowercase() + ":";
for (i, item) in ret.iter().enumerate() {
if i > 0 {
can_res += ","
}
can_res += item;
}
can_res += "\n";
}
};
can_res[0..can_res.len() - 1].to_owned()
}
fn lexy_sort(vec: &url::form_urlencoded::Parse, query_param: &str) -> Vec<String> {
let mut v_values: Vec<String> = Vec::new();
for item in vec.filter(|x| x.0 == *query_param) {
v_values.push(item.1.into_owned())
}
v_values.sort();
v_values
}
#[allow(unknown_lints)]
pub fn perform_request<HCE: HyperClientEndpoint>(
hyper_client_endpoint: &HCE,
uri: &str,
http_method: &Method,
http_header_adder: &dyn Fn(Builder) -> Builder,
request_body: Option<&[u8]>,
service_type: ServiceType,
) -> Result<hyper::client::ResponseFuture, AzureError> {
let dt = chrono::Utc::now();
let time = format!("{}", dt.format("%a, %d %h %Y %T GMT"));
let url = url::Url::parse(uri)?;
let mut request = hyper::Request::builder();
request = request.method(http_method.clone()).uri(uri);
match request_body {
Some(ref b) => {
request = request.header(header::CONTENT_LENGTH, &b.len().to_string() as &str)
}
None => request = request.header_static(header::CONTENT_LENGTH, "0"),
};
request = http_header_adder(request);
request = request
.header_bytes(HEADER_DATE, time)
.header_static(HEADER_VERSION, AZURE_VERSION);
let b = request_body
.map(|v| Vec::from(v).into())
.unwrap_or_else(hyper::Body::empty);
let mut request = request.body(b)?;
if url.query_pairs().find(|p| p.0 == "sig").is_none() {
let auth = generate_authorization(
hyper_client_endpoint,
request.headers(),
&url,
http_method,
service_type,
);
request
.headers_mut()
.insert(header::AUTHORIZATION, format_header_value(auth)?);
}
Ok(hyper_client_endpoint.hyper_client().request(request))
}
#[inline]
pub fn get_default_json_mime() -> &'static str {
"application/json; charset=utf-8"
}
#[inline]
pub fn get_json_mime_nometadata() -> &'static str {
"application/json; odata=nometadata"
}
#[inline]
pub fn get_json_mime_fullmetadata() -> &'static str {
"application/json; odata=fullmetadata"
}
mod test {
use super::*;
struct MockClientEndpoint {
account: String,
key: String,
}
impl ClientEndpoint for MockClientEndpoint {
fn account(&self) -> &str {
&self.account
}
fn key(&self) -> &str {
&self.key
}
}
#[test]
fn test_canonicalize_header() {
use super::*;
let dt = chrono::DateTime::parse_from_rfc2822("Fri, 28 Nov 2014 21:00:09 +0900").unwrap();
let time = format!("{}", dt.format("%a, %d %h %Y %T GMT%Z"));
debug!("time == {}", time);
let mut h = hyper::header::HeaderMap::new();
h.insert(HEADER_DATE, format_header_value(time).unwrap());
h.insert(
HEADER_VERSION,
header::HeaderValue::from_static("2015-04-05"),
);
assert_eq!(
super::canonicalize_header(&h),
"x-ms-date:Fri, 28 Nov 2014 21:00:09 GMT+09:00\nx-ms-version:2015-04-05\n"
);
}
#[test]
fn str_to_sign_test() {
use super::*;
let mut headers: HeaderMap = HeaderMap::new();
headers.insert(
header::ACCEPT,
header::HeaderValue::from_static(get_json_mime_nometadata()),
);
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(get_default_json_mime()),
);
let u: url::Url =
url::Url::parse("https://mindrust.table.core.windows.net/TABLES").unwrap();
let method: Method = Method::POST;
let service_type: ServiceType = ServiceType::Table;
let dt = chrono::DateTime::parse_from_rfc2822("Wed, 3 May 2017 14:04:56 +0000").unwrap();
let time = format!("{}", dt.format("%a, %d %h %Y %T GMT"));
headers.insert(HEADER_DATE, format_header_value(time).unwrap());
headers.insert(
HEADER_VERSION,
header::HeaderValue::from_static(AZURE_VERSION),
);
let s = string_to_sign(
&MockClientEndpoint {
account: "mindrust".to_owned(),
key: "useless".to_owned(),
},
&headers,
&u,
&method,
service_type,
);
assert_eq!(
s,
"POST
application/json; charset=utf-8
Wed, 03 May 2017 14:04:56 GMT
/mindrust/TABLES"
);
}
#[test]
fn test_canonicalize_resource_10() {
let url = url::Url::parse("https://mindrust.table.core.windows.net/TABLES").unwrap();
assert_eq!(
super::canonicalized_resource(
&MockClientEndpoint {
account: "mindrust".to_owned(),
key: "useless".to_owned(),
},
&url
),
"/mindrust/TABLES"
);
}
#[test]
fn test_canonicalize_resource_1() {
let url = url::Url::parse(
"http://myaccount.blob.core.windows.\
net/mycontainer?restype=container&comp=metadata",
)
.unwrap();
assert_eq!(
super::canonicalized_resource(
&MockClientEndpoint {
account: "myaccount".to_owned(),
key: "useless".to_owned(),
},
&url
),
"/myaccount/mycontainer\ncomp:metadata\nrestype:container"
);
}
#[test]
fn test_canonicalize_resource_2() {
let url = url::Url::parse(
"http://myaccount.blob.core.windows.\
net/mycontainer?restype=container&comp=list&include=snapshots&\
include=metadata&include=uncommittedblobs",
)
.unwrap();
assert_eq!(
super::canonicalized_resource(
&MockClientEndpoint {
account: "myaccount".to_owned(),
key: "useless".to_owned(),
},
&url
),
"/myaccount/mycontainer\ncomp:list\ninclude:metadata,snapshots,\
uncommittedblobs\nrestype:container"
);
}
#[test]
fn test_canonicalize_resource_3() {
let url = url::Url::parse(
"https://myaccount-secondary.blob.core.windows.\
net/mycontainer/myblob",
)
.unwrap();
assert_eq!(
super::canonicalized_resource(
&MockClientEndpoint {
account: "myaccount-secondary".to_owned(),
key: "useless".to_owned(),
},
&url
),
"/myaccount-secondary/mycontainer/myblob"
);
}
#[test]
fn test_encode_str_to_sign_1() {
let str_to_sign = "53d7e14aee681a00340300032015-01-01T10:00:00.0000000".to_owned();
let hmac_key = "pXeTVaaaaU9XxH6fPcPlq8Y9D9G3Cdo5Eh2nMSgKj/DWqeSFFXDdmpz5Trv+L2hQNM+nGa704R\
f8Z22W9O1jdQ=="
.to_owned();
assert_eq!(
super::encode_str_to_sign(&str_to_sign, &hmac_key),
"gZzaRaIkvC9jYRY123tq3xXZdsMAcgAbjKQo8y0p0Fs=".to_owned()
);
}
#[test]
fn test_encode_str_to_sign_2() {
let str_to_sign = "This is the data to sign".to_owned();
let hmac_key = "pXeTVaaaaU9XxH6fPcPlq8Y9D9G3Cdo5Eh2nMSgKj/DWqeSFFXDdmpz5Trv+L2hQNM+nGa704R\
f8Z22W9O1jdQ=="
.to_owned();
assert_eq!(
super::encode_str_to_sign(&str_to_sign, &hmac_key),
"YuKoXELO9M9HXeeGaSXBr4Nk+CgPAEQhcwJ6tVtBRCw=".to_owned()
);
}
#[test]
fn test_encode_str_to_sign_3() {
let str_to_sign = "This is the data to sign".to_owned();
let hmac_key = "pXeTVaaaaU9XxH6fPcPlq8Y9D9G3Cdo5Eh2nMSgKj/DWqeSFFXDdmpz5Trv+L2hQNM+nGa704R\
f8Z22W9O1jdQ=="
.to_owned();
assert_eq!(
super::encode_str_to_sign(&str_to_sign, &hmac_key),
"YuKoXELO9M9HXeeGaSXBr4Nk+CgPAEQhcwJ6tVtBRCw=".to_owned()
);
}
}