use std::fmt;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use http::uri::{Authority, InvalidUri};
use sha2::{Digest, Sha256};
use crate::{profile::Component, signature::SignatureParams};
#[derive(Debug, Clone)]
pub struct CanonicalRequest<'a> {
method: &'a str,
scheme: &'a str,
authority: Authority,
path: &'a str,
query: Option<&'a str>,
body: &'a [u8],
}
impl<'a> CanonicalRequest<'a> {
pub fn new(
method: &'a str,
scheme: &'a str,
authority: &'a str,
path: &'a str,
query: Option<&'a str>,
body: &'a [u8],
) -> Result<Self, InvalidUri> {
Ok(Self {
method,
scheme,
authority: authority.parse()?,
path,
query,
body,
})
}
pub fn from_http(
method: &'a http::Method,
uri: &'a http::Uri,
scheme: &'a str,
authority: &'a str,
body: &'a [u8],
) -> Result<Self, InvalidUri> {
Self::new(
method.as_str(),
scheme,
authority,
uri.path(),
uri.query(),
body,
)
}
#[must_use]
pub fn component_values(&self, integrity_token: &str) -> ComponentValues {
let scheme = self.scheme.to_ascii_lowercase();
let authority = match (scheme.as_str(), self.authority.port_u16()) {
("http", Some(80)) | ("https", Some(443)) => self.authority.host(),
_ => self.authority.as_str(),
}
.to_ascii_lowercase();
ComponentValues {
method: self.method.to_owned(),
scheme,
authority,
path: if self.path.is_empty() { "/" } else { self.path }.to_owned(),
query: format!("?{}", self.query.unwrap_or_default()),
content_digest: content_digest(self.body),
integrity_token: integrity_token.to_owned(),
}
}
#[must_use]
pub fn signature_base(&self, params: &SignatureParams, integrity_token: &str) -> SignatureBase {
self.component_values(integrity_token)
.signature_base(params)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ComponentValues {
pub method: String,
pub scheme: String,
pub authority: String,
pub path: String,
pub query: String,
pub content_digest: String,
pub integrity_token: String,
}
impl ComponentValues {
#[must_use]
pub fn get(&self, component: Component) -> &str {
match component {
Component::Method => &self.method,
Component::Scheme => &self.scheme,
Component::Authority => &self.authority,
Component::Path => &self.path,
Component::Query => &self.query,
Component::ContentDigest => &self.content_digest,
Component::IntegrityToken => &self.integrity_token,
}
}
#[must_use]
pub fn signature_base(&self, params: &SignatureParams) -> SignatureBase {
let mut lines: Vec<String> = params
.components()
.iter()
.map(|component| format!("\"{component}\": {}", self.get(*component)))
.collect();
lines.push(format!("\"@signature-params\": {}", params.serialize()));
SignatureBase(lines.join("\n"))
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SignatureBase(String);
impl SignatureBase {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn client_data_hash(&self) -> [u8; 32] {
Sha256::digest(self.0.as_bytes()).into()
}
#[must_use]
pub fn request_binding(&self) -> String {
STANDARD.encode(self.client_data_hash())
}
}
impl fmt::Debug for SignatureBase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SignatureBase(..)")
}
}
#[must_use]
pub fn content_digest(body: &[u8]) -> String {
format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(body)))
}