Skip to main content

wasmtime_wasi_http/
lib.rs

1//! Wasmtime's implementation of `wasi:http`
2//!
3//! This crate is organized similarly to [`wasmtime_wasi`] where there is a
4//! top-level [`p2`] and [`p3`] module corresponding to the implementation for
5//! WASIp2 and WASIp3.
6
7#![deny(missing_docs)]
8#![doc(test(attr(deny(warnings))))]
9#![doc(test(attr(allow(dead_code, unused_variables, unused_mut))))]
10#![cfg_attr(docsrs, feature(doc_cfg))]
11
12use http::{HeaderName, header};
13
14#[cfg(any(feature = "p2", feature = "p3"))]
15mod authority;
16#[cfg(any(feature = "p2", feature = "p3"))]
17pub(crate) use authority::parse_authority;
18
19mod ctx;
20#[cfg(feature = "default-send-request")]
21mod default_send_request;
22mod error;
23mod field_map;
24#[cfg(feature = "component-model-async")]
25pub mod handler;
26pub mod io;
27#[cfg(feature = "p2")]
28pub mod p2;
29#[cfg(feature = "p3")]
30pub mod p3;
31mod request_options;
32
33pub use ctx::*;
34#[cfg(feature = "default-send-request")]
35pub use default_send_request::*;
36pub use error::*;
37pub use field_map::*;
38pub use request_options::*;
39
40/// Extract the `Content-Length` header value from a [`http::HeaderMap`], returning `None` if it's not
41/// present. This function will return `Err` if it's not possible to parse the `Content-Length`
42/// header.
43#[cfg(any(feature = "p2", feature = "p3"))]
44fn get_content_length(headers: &http::HeaderMap) -> wasmtime::Result<Option<u64>> {
45    let mut values = headers.get_all(header::CONTENT_LENGTH).iter();
46    let Some(v) = values.next() else {
47        return Ok(None);
48    };
49    // `FieldMap` rejects multiple `Content-Length` values, but double-check
50    // here since the framing of the body depends on there being exactly one.
51    if values.next().is_some() {
52        wasmtime::bail!("multiple `content-length` header values");
53    }
54    let v = v.to_str()?;
55    // RFC 9110 defines `Content-Length` as `1*DIGIT`. `u64`'s `FromStr` is more
56    // lenient and also accepts a leading `+`, so reject anything that isn't a
57    // non-empty run of decimal digits before parsing.
58    if v.is_empty() || !v.bytes().all(|b| b.is_ascii_digit()) {
59        wasmtime::bail!("invalid `content-length` header value: {v:?}");
60    }
61    let v = v.parse()?;
62    Ok(Some(v))
63}
64
65/// Parse the specified string as an `http::uri::PathAndQuery`, if possible.
66///
67/// As of `http` v1.4.1, `PathAndQuery::from_str` will return an error if given
68/// an empty string per https://github.com/hyperium/http/pull/826.  However,
69/// https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 says an empty path
70/// is okay:
71///
72/// > If a URI contains an authority component, then the path component must
73/// > either be empty or begin with a slash ("/") character.
74///
75/// ...and they are still allowed when using
76/// `http::uri::Builder::path_and_query` per
77/// https://github.com/hyperium/http/pull/853, so we use that here.
78///
79/// The same section of RFC 3986 also requires a non-empty path to begin with
80/// `/`, which `PathAndQuery` doesn't check: it accepts `http://example.com/`,
81/// `*`, and `x`. Those are rejected here, as they would otherwise end up
82/// verbatim in the request line of outgoing requests, and an absolute URI
83/// there addresses the request to a different host than the one it's sent to.
84#[cfg(any(feature = "p2", feature = "p3"))]
85fn parse_path_with_query(path_with_query: &str) -> Option<http::uri::PathAndQuery> {
86    if !(path_with_query.is_empty()
87        || path_with_query.starts_with('/')
88        || path_with_query.starts_with('?'))
89    {
90        return None;
91    }
92    http::uri::Builder::new()
93        .path_and_query(path_with_query)
94        .build()
95        .ok()
96        .and_then(|uri| uri.path_and_query().cloned())
97}
98
99#[cfg(all(test, any(feature = "p2", feature = "p3")))]
100mod content_length_tests {
101    use super::get_content_length;
102    use http::{HeaderMap, HeaderValue, header};
103
104    fn headers(value: &str) -> HeaderMap {
105        let mut map = HeaderMap::new();
106        map.insert(
107            header::CONTENT_LENGTH,
108            HeaderValue::from_str(value).unwrap(),
109        );
110        map
111    }
112
113    #[test]
114    fn content_length_must_be_decimal_digits() {
115        assert_eq!(get_content_length(&HeaderMap::new()).unwrap(), None);
116        assert_eq!(get_content_length(&headers("0")).unwrap(), Some(0));
117        assert_eq!(get_content_length(&headers("1234")).unwrap(), Some(1234));
118
119        // `u64::from_str` accepts these but they are not `1*DIGIT` per RFC 9110.
120        assert!(get_content_length(&headers("+5")).is_err());
121        assert!(get_content_length(&headers("-5")).is_err());
122        assert!(get_content_length(&headers(" 5")).is_err());
123        assert!(get_content_length(&headers("")).is_err());
124
125        // Duplicate content-length definition is forbidden
126        let mut map = headers("5");
127        map.append(header::CONTENT_LENGTH, HeaderValue::from_static("5"));
128        assert!(get_content_length(&map).is_err());
129    }
130}
131
132/// Set of [http::header::HeaderName], that are forbidden by default
133/// for requests and responses originating in the guest.
134pub const DEFAULT_FORBIDDEN_HEADERS: [HeaderName; 9] = [
135    header::CONNECTION,
136    HeaderName::from_static("keep-alive"),
137    header::PROXY_AUTHENTICATE,
138    header::PROXY_AUTHORIZATION,
139    HeaderName::from_static("proxy-connection"),
140    header::TRANSFER_ENCODING,
141    header::UPGRADE,
142    header::HOST,
143    HeaderName::from_static("http2-settings"),
144];