1use std::fmt;
4
5use base64::{Engine as _, engine::general_purpose::STANDARD};
6use http::uri::{Authority, InvalidUri};
7use sha2::{Digest, Sha256};
8
9use crate::{profile::Component, signature::SignatureParams};
10
11#[derive(Debug, Clone)]
16pub struct CanonicalRequest<'a> {
17 method: &'a str,
18 scheme: &'a str,
19 authority: Authority,
20 path: &'a str,
21 query: Option<&'a str>,
22 body: &'a [u8],
23}
24
25impl<'a> CanonicalRequest<'a> {
26 pub fn new(
32 method: &'a str,
33 scheme: &'a str,
34 authority: &'a str,
35 path: &'a str,
36 query: Option<&'a str>,
37 body: &'a [u8],
38 ) -> Result<Self, InvalidUri> {
39 Ok(Self {
40 method,
41 scheme,
42 authority: authority.parse()?,
43 path,
44 query,
45 body,
46 })
47 }
48
49 pub fn from_http(
55 method: &'a http::Method,
56 uri: &'a http::Uri,
57 scheme: &'a str,
58 authority: &'a str,
59 body: &'a [u8],
60 ) -> Result<Self, InvalidUri> {
61 Self::new(
62 method.as_str(),
63 scheme,
64 authority,
65 uri.path(),
66 uri.query(),
67 body,
68 )
69 }
70
71 #[must_use]
74 pub fn component_values(&self, integrity_token: &str) -> ComponentValues {
75 let scheme = self.scheme.to_ascii_lowercase();
76 let authority = match (scheme.as_str(), self.authority.port_u16()) {
77 ("http", Some(80)) | ("https", Some(443)) => self.authority.host(),
78 _ => self.authority.as_str(),
79 }
80 .to_ascii_lowercase();
81
82 ComponentValues {
83 method: self.method.to_owned(),
84 scheme,
85 authority,
86 path: if self.path.is_empty() { "/" } else { self.path }.to_owned(),
87 query: format!("?{}", self.query.unwrap_or_default()),
88 content_digest: content_digest(self.body),
89 integrity_token: integrity_token.to_owned(),
90 }
91 }
92
93 #[must_use]
95 pub fn signature_base(&self, params: &SignatureParams, integrity_token: &str) -> SignatureBase {
96 self.component_values(integrity_token)
97 .signature_base(params)
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct ComponentValues {
104 pub method: String,
106 pub scheme: String,
108 pub authority: String,
110 pub path: String,
112 pub query: String,
114 pub content_digest: String,
116 pub integrity_token: String,
118}
119
120impl ComponentValues {
121 #[must_use]
123 pub fn get(&self, component: Component) -> &str {
124 match component {
125 Component::Method => &self.method,
126 Component::Scheme => &self.scheme,
127 Component::Authority => &self.authority,
128 Component::Path => &self.path,
129 Component::Query => &self.query,
130 Component::ContentDigest => &self.content_digest,
131 Component::IntegrityToken => &self.integrity_token,
132 }
133 }
134
135 #[must_use]
141 pub fn signature_base(&self, params: &SignatureParams) -> SignatureBase {
142 let mut lines: Vec<String> = params
143 .components()
144 .iter()
145 .map(|component| format!("\"{component}\": {}", self.get(*component)))
146 .collect();
147 lines.push(format!("\"@signature-params\": {}", params.serialize()));
148 SignatureBase(lines.join("\n"))
149 }
150}
151
152#[derive(Clone, PartialEq, Eq)]
154pub struct SignatureBase(String);
155
156impl SignatureBase {
157 #[must_use]
159 pub fn as_str(&self) -> &str {
160 &self.0
161 }
162
163 #[must_use]
165 pub fn client_data_hash(&self) -> [u8; 32] {
166 Sha256::digest(self.0.as_bytes()).into()
167 }
168
169 #[must_use]
172 pub fn request_binding(&self) -> String {
173 STANDARD.encode(self.client_data_hash())
174 }
175}
176
177impl fmt::Debug for SignatureBase {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.write_str("SignatureBase(..)")
181 }
182}
183
184#[must_use]
186pub fn content_digest(body: &[u8]) -> String {
187 format!("sha-256=:{}:", STANDARD.encode(Sha256::digest(body)))
188}