use crate::{error::Error, signing, types::ObjectIdentifier};
use percent_encoding as perc_enc;
use std::borrow::Cow;
use url::Url;
pub struct UrlSigner<D, S> {
digester: D,
signer: S,
}
#[cfg(feature = "signing")]
impl UrlSigner<signing::RingDigest, signing::RingSigner> {
pub fn with_ring() -> UrlSigner<signing::RingDigest, signing::RingSigner> {
UrlSigner::new(signing::RingDigest, signing::RingSigner)
}
}
impl<D, S> UrlSigner<D, S>
where
D: signing::DigestCalulator,
S: signing::Signer,
{
pub fn new(digester: D, signer: S) -> Self {
Self { digester, signer }
}
pub fn generate<'a, K, OID>(
&self,
key_provider: &K,
id: &OID,
optional: SignedUrlOptional<'_>,
) -> Result<Url, Error>
where
K: signing::KeyProvider,
OID: ObjectIdentifier<'a>,
{
const SEVEN_DAYS: u64 = 7 * 24 * 60 * 60;
if optional.duration.as_secs() > SEVEN_DAYS {
return Err(Error::TooLongExpiration {
requested: optional.duration.as_secs(),
max: SEVEN_DAYS,
});
}
let mut signed_url =
Url::parse("https://storage.googleapis.com").map_err(Error::UrlParse)?;
let resource_path = format!(
"/{}/{}",
perc_enc::percent_encode(id.bucket().as_ref(), crate::util::PATH_ENCODE_SET),
perc_enc::percent_encode(id.object().as_ref(), crate::util::PATH_ENCODE_SET),
);
signed_url.set_path(&resource_path);
let mut headers = optional.headers;
headers.insert(
http::header::HOST,
http::header::HeaderValue::from_static("storage.googleapis.com"),
);
let headers = {
let mut hdrs = Vec::with_capacity(headers.keys_len());
for key in headers.keys() {
let vals_size = headers
.get_all(key)
.iter()
.fold(0, |acc, v| acc + v.len() + 1)
- 1;
let mut key_vals = String::with_capacity(vals_size);
for (i, val) in headers.get_all(key).iter().enumerate() {
if i > 0 {
key_vals.push(',');
}
key_vals.push_str(
val.to_str()
.map_err(|_| Error::OpaqueHeaderValue(val.clone()))?,
);
}
hdrs.push((key.as_str().to_lowercase(), key_vals));
}
hdrs.sort();
hdrs
};
let signed_headers = {
let signed_size =
headers.iter().fold(0, |acc, (name, _)| acc + name.len()) + headers.len() - 1;
let mut names = String::with_capacity(signed_size);
for (i, name) in headers.iter().map(|(name, _)| name).enumerate() {
if i > 0 {
names.push(';');
}
names.push_str(name);
}
assert_eq!(signed_size, names.capacity());
names
};
let timestamp = chrono::Utc::now();
let request_timestamp = timestamp.format("%Y%m%dT%H%M%SZ").to_string();
let datestamp = &request_timestamp[..8];
let credential_scope = format!("{}/{}/storage/goog4_request", datestamp, optional.region);
let credential_param = format!("{}/{}", key_provider.authorizer(), credential_scope);
let expiration = optional.duration.as_secs().to_string();
let mut query_params = optional.query_params;
query_params.extend(
[
("X-Goog-Algorithm", "GOOG4-RSA-SHA256"),
("X-Goog-Credential", &credential_param),
("X-Goog-Date", &request_timestamp),
("X-Goog-Expires", &expiration),
("X-Goog-SignedHeaders", &signed_headers),
]
.iter()
.map(|(k, v)| (Cow::Borrowed(*k), Cow::Borrowed(*v))),
);
query_params.sort();
let canonical_query = {
{
let mut query_pairs = signed_url.query_pairs_mut();
query_pairs.clear();
for (key, value) in &query_params {
query_pairs.append_pair(key, value);
}
}
signed_url.query().unwrap().to_owned()
};
let canonical_headers = {
let canonical_size = headers
.iter()
.fold(0, |acc, kv| acc + kv.0.len() + kv.1.len())
+ headers.len() * 2;
let mut hdrs = String::with_capacity(canonical_size);
for (k, v) in &headers {
hdrs.push_str(k);
hdrs.push(':');
hdrs.push_str(v);
hdrs.push('\n');
}
assert_eq!(canonical_size, hdrs.capacity());
hdrs
};
let canonical_request = format!(
"{verb}\n{resource}\n{query}\n{headers}\n{signed_headers}\nUNSIGNED-PAYLOAD",
verb = optional.method,
resource = resource_path,
query = canonical_query,
headers = canonical_headers,
signed_headers = signed_headers,
);
let mut digest = [0u8; 32];
self.digester.digest(
signing::DigestAlgorithm::Sha256,
canonical_request.as_bytes(),
&mut digest,
);
let mut digest_str_bytes = [0u8; 64];
let digest_str = crate::util::to_hex(&digest, &mut digest_str_bytes)
.expect("unable to construct digest string");
let string_to_sign = format!(
"GOOG4-RSA-SHA256\n{timestamp}\n{scope}\n{hash}",
timestamp = request_timestamp,
scope = credential_scope,
hash = digest_str,
);
let signature = self.signer.sign(
signing::SigningAlgorithm::RsaSha256,
key_provider.key(),
string_to_sign.as_bytes(),
)?;
let mut signature_str_bytes = vec![0; signature.len() * 2];
let signature_str = crate::util::to_hex(&signature, &mut signature_str_bytes)
.expect("unable to construct signature string");
signed_url
.query_pairs_mut()
.append_pair("X-Goog-Signature", signature_str);
Ok(signed_url)
}
}
pub struct SignedUrlOptional<'a> {
pub method: http::Method,
pub duration: std::time::Duration,
pub headers: http::HeaderMap,
pub region: &'a str,
pub query_params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
}
impl<'a> Default for SignedUrlOptional<'a> {
fn default() -> Self {
Self {
method: http::Method::GET,
duration: std::time::Duration::from_secs(60 * 60),
headers: http::HeaderMap::default(),
region: "auto",
query_params: Vec::new(),
}
}
}