Skip to main content

asimov_http/
message.rs

1// This is free and unencumbered software released into the public domain.
2
3use 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/// The field section containing a digest, signature, or covered HTTP field.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum FieldSection {
20    /// Fields preceding the message body.
21    Headers,
22    /// Fields following the message body.
23    Trailers,
24}
25
26/// The known type of a Structured Field covered using `;sf`.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum StructuredFieldType {
29    /// A single item and its parameters.
30    Item,
31    /// A list of items or inner lists.
32    List,
33    /// An ordered dictionary.
34    Dictionary,
35}
36
37#[derive(Clone, Debug)]
38enum Kind {
39    Request { method: Method, target: Url },
40    Response(StatusCode),
41}
42
43/// Owned signature context, independent of HTTP versions and body types.
44///
45/// Servers must supply the effective *external* target URI. The HTTP/1.1
46/// request target alone generally lacks scheme and authority. Only trusted
47/// routing/proxy configuration may provide missing context. HTTP/2 uses the
48/// same effective URI; pseudo-headers are represented by derived components.
49///
50/// Headers and trailers remain separate: `"content-digest"` and
51/// `"content-digest";tr` are different covered components. Responses may retain
52/// their originating request for `;req` components. The snapshot must describe
53/// the message actually transmitted, after content coding and header changes.
54#[derive(Clone)]
55pub struct Message {
56    kind: Kind,
57    /// The message's header section.
58    pub headers: HeaderMap,
59    /// The message's trailer section, populated when it becomes available.
60    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    /// Creates request context from a method, absolute HTTP(S) URI, and headers.
78    /// Userinfo and fragments are rejected because they are not HTTP targets.
79    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    /// Snapshots a request of any body type, including Axum's non-`Sync` body.
93    /// `effective_uri` supplies the external URI; the original request-target
94    /// form is also retained for HTTP/1.x `@request-target` signatures.
95    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    /// Creates response context. Supply a request snapshot to enable `;req`.
115    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    /// Registers a field's actual Structured Fields type for `;sf` processing.
154    /// Digest and signature dictionaries are already registered. Types are never
155    /// guessed by trying multiple parsers.
156    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    /// Supplies the actual protocol-specific request target for `@request-target`.
162    /// Prefer `@target-uri` when signatures must survive HTTP version changes.
163    pub fn with_request_target(mut self, target: impl Into<String>) -> Self {
164        self.request_target = Some(target.into());
165        self
166    }
167
168    /// Returns the chosen field section without merging headers and trailers.
169    pub fn fields(&self, section: FieldSection) -> &HeaderMap {
170        match section {
171            FieldSection::Headers => &self.headers,
172            FieldSection::Trailers => &self.trailers,
173        }
174    }
175
176    /// Returns the chosen mutable field section.
177    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(), // Methods are case-sensitive.
237            "@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                // RFC 9421 uses form parsing followed by form serialization, not
251                // raw substring matching or query/body parameter merging.
252                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/// An RFC 9421 component identifier with ordered parameters.
318///
319/// Supports ordinary fields, `sf`, `key`, `bs`, `tr`, `req`, and the standard
320/// request/response derived components. Boolean flags must be true. Unknown
321/// component names or parameters are rejected, never silently discarded.
322#[derive(Clone, Debug)]
323pub struct Component(pub(crate) Item);
324
325impl Component {
326    /// Creates a plain lowercase field or derived-component identifier.
327    pub fn new(name: &str) -> Result<Self, Error> {
328        Self::from_item(Item::new(sfv::StringRef::from_str(name)?))
329    }
330
331    /// Parses an identifier such as `"content-digest";tr` or `"@method";req`.
332    pub fn parse(value: &str) -> Result<Self, Error> {
333        Self::from_item(fields::parser(value)?.parse()?)
334    }
335
336    /// Creates a trailer field identifier.
337    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    /// Returns the field or derived-component name.
400    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    /// Serializes the identifier, preserving parameter order and escaping.
409    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 {}