use std::fmt::Debug;
use std::fmt::Write;
use std::time::Duration;
use anyhow::{anyhow, Result};
use http::header::*;
use log::debug;
use super::super::constants::*;
use super::credential::Credential;
use crate::azure::storage::sas::account_sas;
use crate::ctx::SigningContext;
use crate::ctx::SigningMethod;
use crate::hash::base64_decode;
use crate::hash::base64_hmac_sha256;
use crate::request::SignableRequest;
use crate::time;
use crate::time::format_http_date;
use crate::time::DateTime;
#[derive(Debug, Default)]
pub struct Signer {
time: Option<DateTime>,
}
impl Signer {
pub fn new() -> Self {
Self::default()
}
#[cfg(test)]
pub fn time(&mut self, time: DateTime) -> &mut Self {
self.time = Some(time);
self
}
fn build(
&self,
req: &mut impl SignableRequest,
method: SigningMethod,
cred: &Credential,
) -> Result<SigningContext> {
let mut ctx = req.build()?;
match cred {
Credential::SharedAccessSignature(token) => {
ctx.query_append(token);
return Ok(ctx);
}
Credential::BearerToken(token) => match method {
SigningMethod::Query(_) => {
return Err(anyhow!("BearerToken can't be used in query string"));
}
SigningMethod::Header => {
ctx.headers
.insert(X_MS_DATE, format_http_date(time::now()).parse()?);
ctx.headers.insert(AUTHORIZATION, {
let mut value: HeaderValue = format!("Bearer {}", token).parse()?;
value.set_sensitive(true);
value
});
}
},
Credential::SharedKey(ak, sk) => match method {
SigningMethod::Query(d) => {
let signer = account_sas::AccountSharedAccessSignature::new(
ak.to_string(),
sk.to_string(),
time::now() + chrono::Duration::from_std(d)?,
);
let signer_token = signer.token()?;
signer_token.iter().for_each(|(k, v)| {
ctx.query_push(k, v);
});
}
SigningMethod::Header => {
let now = self.time.unwrap_or_else(time::now);
let string_to_sign = string_to_sign(&mut ctx, ak, now)?;
let decode_content = base64_decode(sk)?;
let signature = base64_hmac_sha256(&decode_content, string_to_sign.as_bytes());
ctx.headers.insert(AUTHORIZATION, {
let mut value: HeaderValue =
format!("SharedKey {ak}:{signature}").parse()?;
value.set_sensitive(true);
value
});
}
},
}
Ok(ctx)
}
pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> {
let ctx = self.build(req, SigningMethod::Header, cred)?;
req.apply(ctx)
}
pub fn sign_query(
&self,
req: &mut impl SignableRequest,
expire: Duration,
cred: &Credential,
) -> Result<()> {
let ctx = self.build(req, SigningMethod::Query(expire), cred)?;
req.apply(ctx)
}
}
fn string_to_sign(ctx: &mut SigningContext, ak: &str, now: DateTime) -> Result<String> {
let mut s = String::with_capacity(128);
writeln!(&mut s, "{}", ctx.method.as_str())?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&CONTENT_ENCODING)?)?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&CONTENT_LANGUAGE)?)?;
writeln!(
&mut s,
"{}",
ctx.header_get_or_default(&CONTENT_LENGTH)
.map(|v| if v == "0" { "" } else { v })?
)?;
writeln!(
&mut s,
"{}",
ctx.header_get_or_default(&CONTENT_MD5.parse()?)?
)?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&CONTENT_TYPE)?)?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&DATE)?)?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&IF_MODIFIED_SINCE)?)?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&IF_MATCH)?)?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&IF_NONE_MATCH)?)?;
writeln!(
&mut s,
"{}",
ctx.header_get_or_default(&IF_UNMODIFIED_SINCE)?
)?;
writeln!(&mut s, "{}", ctx.header_get_or_default(&RANGE)?)?;
writeln!(&mut s, "{}", canonicalize_header(ctx, now)?)?;
write!(&mut s, "{}", canonicalize_resource(ctx, ak))?;
debug!("string to sign: {}", &s);
Ok(s)
}
fn canonicalize_header(ctx: &mut SigningContext, now: DateTime) -> Result<String> {
ctx.headers
.insert(X_MS_DATE, format_http_date(now).parse()?);
Ok(SigningContext::header_to_string(
ctx.header_to_vec_with_prefix("x-ms-"),
":",
"\n",
))
}
fn canonicalize_resource(ctx: &mut SigningContext, ak: &str) -> String {
if ctx.query.is_empty() {
return format!("/{}{}", ak, ctx.path);
}
format!(
"/{}{}\n{}",
ak,
ctx.path,
SigningContext::query_to_string(ctx.query.clone(), ":", "\n")
)
}
#[cfg(test)]
mod tests {
use http::Request;
use std::time::Duration;
use super::super::config::Config;
use crate::azure::storage::loader::Loader;
use crate::{AzureStorageCredential, AzureStorageSigner};
#[tokio::test]
async fn test_sas_url() {
let _ = env_logger::builder().is_test(true).try_init();
let config = Config {
sas_token: Some("sv=2021-01-01&ss=b&srt=c&sp=rwdlaciytfx&se=2022-01-01T11:00:14Z&st=2022-01-02T03:00:14Z&spr=https&sig=KEllk4N8f7rJfLjQCmikL2fRVt%2B%2Bl73UBkbgH%2FK3VGE%3D".to_string()),
..Default::default()
};
let loader = Loader::new(config);
let cred = loader.load().await.unwrap().unwrap();
let signer = AzureStorageSigner::new();
let mut req = Request::builder()
.uri("https://test.blob.core.windows.net/testbucket/testblob")
.body(())
.unwrap();
assert!(signer
.sign_query(&mut req, Duration::from_secs(1), &cred)
.is_ok());
assert_eq!(req.uri(), "https://test.blob.core.windows.net/testbucket/testblob?sv=2021-01-01&ss=b&srt=c&sp=rwdlaciytfx&se=2022-01-01T11:00:14Z&st=2022-01-02T03:00:14Z&spr=https&sig=KEllk4N8f7rJfLjQCmikL2fRVt%2B%2Bl73UBkbgH%2FK3VGE%3D")
}
#[tokio::test]
async fn test_can_sign_request_use_bearer_token() {
let signer = AzureStorageSigner::new();
let mut req = Request::builder()
.uri("https://test.blob.core.windows.net/testbucket/testblob")
.body(())
.unwrap();
let cred = AzureStorageCredential::BearerToken("token".to_string());
assert!(signer.sign(&mut req, &cred).is_ok());
let authorization = req
.headers()
.get("Authorization")
.unwrap()
.to_str()
.unwrap();
assert_eq!("Bearer token", authorization);
*req.headers_mut() = http::header::HeaderMap::new();
assert!(signer
.sign_query(&mut req, Duration::from_secs(1), &cred)
.is_err());
}
}