1use crate::{Error, fields};
4use alloc::{
5 borrow::ToOwned,
6 boxed::Box,
7 collections::BTreeMap,
8 format,
9 string::{String, ToString},
10 vec::Vec,
11};
12use base64::{Engine as _, engine::general_purpose::STANDARD};
13use http::{HeaderMap, Method, StatusCode, header::HeaderName};
14use sfv::{FieldType, Item};
15use url::Url;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum FieldSection {
20 Headers,
22 Trailers,
24}
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum StructuredFieldType {
29 Item,
31 List,
33 Dictionary,
35}
36
37#[derive(Clone, Debug)]
38enum Kind {
39 Request { method: Method, target: Url },
40 Response(StatusCode),
41}
42
43#[derive(Clone)]
55pub struct Message {
56 kind: Kind,
57 pub headers: HeaderMap,
59 pub trailers: HeaderMap,
61 request: Option<Box<Message>>,
62 request_target: Option<String>,
63 structured: BTreeMap<String, StructuredFieldType>,
64}
65
66impl core::fmt::Debug for Message {
67 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
68 f.debug_struct("Message")
69 .field("is_request", &matches!(self.kind, Kind::Request { .. }))
70 .field("header_names", &self.headers.keys().collect::<Vec<_>>())
71 .field("trailer_names", &self.trailers.keys().collect::<Vec<_>>())
72 .finish_non_exhaustive()
73 }
74}
75
76impl Message {
77 pub fn request(method: Method, target: &str, headers: HeaderMap) -> Result<Self, Error> {
80 let target = Url::parse(target).map_err(|_| Error::InvalidTargetUri)?;
81 if !matches!(target.scheme(), "http" | "https")
82 || target.host_str().is_none()
83 || !target.username().is_empty()
84 || target.password().is_some()
85 || target.fragment().is_some()
86 {
87 return Err(Error::InvalidTargetUri);
88 }
89 Ok(Self::new(Kind::Request { method, target }, headers))
90 }
91
92 pub fn from_request<B>(request: &http::Request<B>, effective_uri: &str) -> Result<Self, Error> {
96 let mut result = Self::request(
97 request.method().clone(),
98 effective_uri,
99 request.headers().clone(),
100 )?;
101 let target = if request.version() == http::Version::HTTP_2 {
102 if request.method() == Method::CONNECT {
103 request.uri().authority().map(ToString::to_string)
104 } else {
105 request.uri().path_and_query().map(ToString::to_string)
106 }
107 } else {
108 Some(request.uri().to_string())
109 };
110 result.request_target = target;
111 Ok(result)
112 }
113
114 pub fn response(
116 status: StatusCode,
117 headers: HeaderMap,
118 request: Option<Self>,
119 ) -> Result<Self, Error> {
120 if request
121 .as_ref()
122 .is_some_and(|r| !matches!(r.kind, Kind::Request { .. }))
123 {
124 return Err(Error::InvalidComponent("response request context".into()));
125 }
126 let mut result = Self::new(Kind::Response(status), headers);
127 result.request = request.map(Box::new);
128 Ok(result)
129 }
130
131 fn new(kind: Kind, headers: HeaderMap) -> Self {
132 let structured = [
133 "content-digest",
134 "repr-digest",
135 "want-content-digest",
136 "want-repr-digest",
137 "signature",
138 "signature-input",
139 ]
140 .into_iter()
141 .map(|name| (name.into(), StructuredFieldType::Dictionary))
142 .collect();
143 Self {
144 kind,
145 headers,
146 trailers: HeaderMap::new(),
147 request: None,
148 request_target: None,
149 structured,
150 }
151 }
152
153 pub fn with_structured_field(mut self, name: HeaderName, kind: StructuredFieldType) -> Self {
157 self.structured.insert(name.as_str().into(), kind);
158 self
159 }
160
161 pub fn with_request_target(mut self, target: impl Into<String>) -> Self {
164 self.request_target = Some(target.into());
165 self
166 }
167
168 pub fn fields(&self, section: FieldSection) -> &HeaderMap {
170 match section {
171 FieldSection::Headers => &self.headers,
172 FieldSection::Trailers => &self.trailers,
173 }
174 }
175
176 pub fn fields_mut(&mut self, section: FieldSection) -> &mut HeaderMap {
178 match section {
179 FieldSection::Headers => &mut self.headers,
180 FieldSection::Trailers => &mut self.trailers,
181 }
182 }
183
184 pub(crate) fn permits_response_trailers(&self) -> bool {
185 let Kind::Response(status) = self.kind else {
186 return false;
187 };
188 !status.is_informational() && status != StatusCode::NO_CONTENT && status != StatusCode::RESET_CONTENT
189 && status != StatusCode::NOT_MODIFIED
190 && !self.request.as_ref().is_some_and(|request| matches!(&request.kind,
191 Kind::Request { method, .. } if *method == Method::HEAD || (*method == Method::CONNECT && status.is_success())))
192 }
193
194 fn source(&self, component: &Component) -> Result<&Self, Error> {
195 if component.flag("req") {
196 if !matches!(self.kind, Kind::Response(_)) {
197 return Err(Error::InvalidComponent("req on a request".into()));
198 }
199 self.request
200 .as_deref()
201 .ok_or_else(|| Error::MissingComponent("request context".into()))
202 } else {
203 Ok(self)
204 }
205 }
206
207 pub(crate) fn value(&self, component: &Component) -> Result<String, Error> {
208 let source = self.source(component)?;
209 let name = component.name();
210 let value = if name.starts_with('@') {
211 source.derived(component)?
212 } else {
213 source.field(component)?
214 };
215 if !value.is_ascii() || value.contains(['\r', '\n']) {
216 return Err(Error::InvalidComponent(
217 "non-ASCII or multiline component value".into(),
218 ));
219 }
220 Ok(value)
221 }
222
223 fn derived(&self, component: &Component) -> Result<String, Error> {
224 if component.name() == "@status" {
225 return match self.kind {
226 Kind::Response(status) => Ok(status.as_str().into()),
227 _ => Err(Error::InvalidComponent("@status on a request".into())),
228 };
229 }
230 let Kind::Request { method, target } = &self.kind else {
231 return Err(Error::InvalidComponent(
232 "request-derived component without req".into(),
233 ));
234 };
235 Ok(match component.name() {
236 "@method" => method.as_str().into(), "@target-uri" => target.as_str().into(),
238 "@authority" => target[url::Position::BeforeHost..url::Position::AfterPort].into(),
239 "@scheme" => target.scheme().into(),
240 "@path" => target.path().into(),
241 "@query" => format!("?{}", target.query().unwrap_or("")),
242 "@request-target" => self
243 .request_target
244 .clone()
245 .ok_or_else(|| Error::MissingComponent("actual request-target".into()))?,
246 "@query-param" => {
247 let name = component
248 .string_param("name")
249 .ok_or_else(|| Error::InvalidComponent("query parameter name".into()))?;
250 let encoded = |value: &str| {
253 url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>()
254 };
255 let mut matches = target.query_pairs().filter(|(key, _)| encoded(key) == name);
256 let (_, value) = matches
257 .next()
258 .ok_or_else(|| Error::MissingComponent(name.into()))?;
259 if matches.next().is_some() {
260 return Err(Error::InvalidComponent("duplicate query parameter".into()));
261 }
262 encoded(&value)
263 },
264 _ => return Err(Error::InvalidComponent(component.name().into())),
265 })
266 }
267
268 fn field(&self, component: &Component) -> Result<String, Error> {
269 let map = if component.flag("tr") {
270 &self.trailers
271 } else {
272 &self.headers
273 };
274 let name = component.name();
275 if component.flag("bs") {
276 let values = fields::values(&[map], name)?;
277 if values.is_empty() {
278 return Err(Error::MissingComponent(name.into()));
279 }
280 return Ok(values
281 .iter()
282 .map(|value| format!(":{}:", STANDARD.encode(value)))
283 .collect::<Vec<_>>()
284 .join(", "));
285 }
286 let value =
287 fields::combined(&[map], name)?.ok_or_else(|| Error::MissingComponent(name.into()))?;
288 if let Some(key) = component.string_param("key") {
289 return fields::serialize_entry(
290 fields::dictionary(&value)?
291 .get(key)
292 .ok_or_else(|| Error::MissingComponent(format!("{name};key={key}")))?,
293 );
294 }
295 if component.flag("sf") {
296 return match self
297 .structured
298 .get(name)
299 .ok_or_else(|| Error::InvalidComponent("unknown structured field type".into()))?
300 {
301 StructuredFieldType::Item => {
302 Ok(fields::parser(&value)?.parse::<Item>()?.serialize())
303 },
304 StructuredFieldType::List => Ok(fields::parser(&value)?
305 .parse::<sfv::List>()?
306 .serialize()
307 .unwrap_or_default()),
308 StructuredFieldType::Dictionary => {
309 Ok(fields::dictionary(&value)?.serialize().unwrap_or_default())
310 },
311 };
312 }
313 Ok(value)
314 }
315}
316
317#[derive(Clone, Debug)]
323pub struct Component(pub(crate) Item);
324
325impl Component {
326 pub fn new(name: &str) -> Result<Self, Error> {
328 Self::from_item(Item::new(sfv::StringRef::from_str(name)?))
329 }
330
331 pub fn parse(value: &str) -> Result<Self, Error> {
333 Self::from_item(fields::parser(value)?.parse()?)
334 }
335
336 pub fn trailer(name: &str) -> Result<Self, Error> {
338 let mut component = Self::new(name)?;
339 component
340 .0
341 .params
342 .insert(sfv::key_ref("tr").to_owned(), true.into());
343 Self::from_item(component.0)
344 }
345
346 pub(crate) fn from_item(item: Item) -> Result<Self, Error> {
347 let name = item
348 .bare_item
349 .as_string()
350 .ok_or_else(|| Error::InvalidComponent("identifier must be a string".into()))?
351 .as_str();
352 if name.starts_with('@') {
353 if !matches!(
354 name,
355 "@method"
356 | "@target-uri"
357 | "@authority"
358 | "@scheme"
359 | "@path"
360 | "@query"
361 | "@query-param"
362 | "@request-target"
363 | "@status"
364 ) {
365 return Err(Error::InvalidComponent(name.into()));
366 }
367 } else if name.bytes().any(|b| b.is_ascii_uppercase())
368 || HeaderName::from_bytes(name.as_bytes()).is_err()
369 {
370 return Err(Error::InvalidComponent(name.into()));
371 }
372 for (key, value) in &item.params {
373 let allowed = match key.as_str() {
374 "req" => name != "@status" && value.as_boolean() == Some(true),
375 "name" => name == "@query-param" && value.as_string().is_some(),
376 "key" => !name.starts_with('@') && value.as_string().is_some(),
377 "sf" | "bs" | "tr" => !name.starts_with('@') && value.as_boolean() == Some(true),
378 _ => false,
379 };
380 if !allowed {
381 return Err(Error::InvalidComponent(format!("{name};{key}")));
382 }
383 }
384 if name == "@query-param" && !item.params.contains_key("name") {
385 return Err(Error::InvalidComponent(
386 "query parameter requires name".into(),
387 ));
388 }
389 if item.params.contains_key("bs")
390 && (item.params.contains_key("sf") || item.params.contains_key("key"))
391 {
392 return Err(Error::InvalidComponent(
393 "bs cannot be combined with sf/key".into(),
394 ));
395 }
396 Ok(Self(item))
397 }
398
399 pub fn name(&self) -> &str {
401 self.0
402 .bare_item
403 .as_string()
404 .expect("validated component string")
405 .as_str()
406 }
407
408 pub fn to_field_value(&self) -> String {
410 self.0.serialize()
411 }
412
413 pub(crate) fn flag(&self, name: &str) -> bool {
414 self.0.params.contains_key(name)
415 }
416
417 pub(crate) fn string_param(&self, name: &str) -> Option<&str> {
418 self.0.params.get(name)?.as_string().map(|v| v.as_str())
419 }
420}
421
422impl PartialEq for Component {
423 fn eq(&self, other: &Self) -> bool {
424 self.0.bare_item == other.0.bare_item
425 && self.0.params.len() == other.0.params.len()
426 && self
427 .0
428 .params
429 .iter()
430 .all(|(key, value)| other.0.params.get(key) == Some(value))
431 }
432}
433impl Eq for Component {}