Skip to main content

wasmtime_wasi_http/
ctx.rs

1#[cfg(feature = "p2")]
2use crate::p2::bindings::http::types as p2;
3#[cfg(feature = "p3")]
4use crate::p3::bindings::http::types as p3;
5use crate::{DEFAULT_FORBIDDEN_HEADERS, Error, RequestOptions, Result};
6use bytes::Bytes;
7use http::{HeaderName, uri::Scheme};
8use http_body_util::combinators::UnsyncBoxBody;
9use wasmtime::component::{HasData, ResourceTable};
10
11/// A helper struct which implements [`HasData`] for the `wasi:http` APIs.
12///
13/// This can be useful when directly calling `add_to_linker` functions directly,
14/// such as [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`] as
15/// the `D` type parameter. See [`HasData`] for more information about the type
16/// parameter's purpose.
17///
18/// When using this type you can skip the [`WasiHttpView`] trait, for example.
19///
20/// [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`]: crate::p3::bindings::http::types::add_to_linker
21///
22/// # Examples
23///
24/// ```
25/// use wasmtime::component::Linker;
26/// use wasmtime::{Engine, Result};
27/// use wasmtime_wasi_http::{WasiHttp, WasiHttpCtxView};
28///
29/// struct MyStoreState {
30///     // ...
31/// }
32///
33/// impl MyStoreState {
34///     fn http(&mut self) -> WasiHttpCtxView<'_> {
35///         // ...
36/// #       todo!()
37///     }
38/// }
39///
40/// fn main() -> Result<()> {
41///     let engine = Engine::default();
42///     let mut linker = Linker::new(&engine);
43///
44///     wasmtime_wasi_http::p3::bindings::http::types::add_to_linker::<MyStoreState, WasiHttp>(
45///         &mut linker,
46///         |state| state.http(),
47///     )?;
48///     Ok(())
49/// }
50/// ```
51pub struct WasiHttp;
52
53impl HasData for WasiHttp {
54    type Data<'a> = WasiHttpCtxView<'a>;
55}
56
57/// A trait which provides internal WASI HTTP state.
58///
59/// This trait is used by the [`add_to_linker`] convenience functions of this
60/// crate. This trait can be implemented for the `T` in `Store<T>` to provide
61/// access to wasi:http information at runtime.
62///
63/// [`add_to_linker`]: crate::p3::add_to_linker
64///
65/// # Example
66///
67/// ```
68/// use wasmtime::component::ResourceTable;
69/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView};
70///
71/// struct MyState {
72///     http_ctx: WasiHttpCtx,
73///     table: ResourceTable,
74/// }
75///
76/// impl WasiHttpView for MyState {
77///     fn http(&mut self) -> WasiHttpCtxView<'_> {
78///         WasiHttpCtxView {
79///             ctx: &mut self.http_ctx,
80///             table: &mut self.table,
81///             hooks: Default::default(),
82///         }
83///     }
84/// }
85/// ```
86pub trait WasiHttpView: Send {
87    /// Return a [WasiHttpCtxView] from mutable reference to self.
88    fn http(&mut self) -> WasiHttpCtxView<'_>;
89}
90
91/// Basis of implementation of all `wasi:http` APIs in this crate.
92///
93/// This type provides a temporary view into information such as a WASI
94/// resource table, HTTP context information, and embedder-provided hooks if
95/// so desired. THe fields in this structure are typically stored within the
96/// `T` of `Store<T>` and this struct borrows from there. All `Host` traits
97/// generated by `bindgen!` are implemented for this type.
98pub struct WasiHttpCtxView<'a> {
99    /// Mutable reference to the WASI HTTP hooks.
100    ///
101    /// Note that [`default_hooks`] or [`Default::default()`] can be used if
102    /// you don't want or need to customize this.
103    pub hooks: &'a mut dyn WasiHttpHooks,
104
105    /// Mutable reference to table used to manage resources.
106    pub table: &'a mut ResourceTable,
107
108    /// Mutable reference to the WASI HTTP context.
109    pub ctx: &'a mut WasiHttpCtx,
110}
111
112/// Default maximum size for the contents of a fields resource.
113///
114/// Typically, HTTP proxies limit headers to 8k. This number is higher than that
115/// because it not only includes the wire-size of headers but it additionally
116/// includes factors for the in-memory representation of `HeaderMap`. This is in
117/// theory high enough that no one runs into it but low enough such that a
118/// completely full `HeaderMap` doesn't break the bank in terms of memory
119/// consumption.
120const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024;
121
122/// Capture the state necessary for use in the wasi-http API implementation.
123#[derive(Debug, Clone)]
124pub struct WasiHttpCtx {
125    pub(crate) field_size_limit: usize,
126}
127
128impl WasiHttpCtx {
129    /// Create a new context.
130    pub fn new() -> Self {
131        Self {
132            field_size_limit: DEFAULT_FIELD_SIZE_LIMIT,
133        }
134    }
135
136    /// Set the maximum size for any fields resources created by this context.
137    ///
138    /// The limit specified here is roughly a byte limit for the size of the
139    /// in-memory representation of headers. This means that the limit needs to
140    /// be larger than the literal representation of headers on the wire to
141    /// account for in-memory Rust-side data structures representing the header
142    /// names/values/etc.
143    pub fn set_field_size_limit(&mut self, limit: usize) {
144        self.field_size_limit = limit;
145    }
146}
147
148impl Default for WasiHttpCtx {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154/// Convenience type definition for the bodies used in this crate.
155pub type WasiBody = UnsyncBoxBody<Bytes, Error>;
156
157/// A trait which provides hooks into internal WASI HTTP operations.
158///
159/// Note that when using this type if state is needed to implement the methods
160/// the state will need to be stored separately in a distinct structure to
161/// implement this trait as the same type can't implement both [`WasiHttpView`]
162/// and [`WasiHttpHooks`] and be usable.
163///
164/// # Example
165///
166/// ```
167/// use wasmtime::component::ResourceTable;
168/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView, WasiHttpHooks};
169///
170/// struct MyState {
171///     http_ctx: WasiHttpCtx,
172///     table: ResourceTable,
173///     hooks: MyHooks,
174/// }
175///
176/// impl MyState {
177///     fn new() -> MyState {
178///         MyState {
179///             table: ResourceTable::new(),
180///             http_ctx: WasiHttpCtx::new(),
181///             hooks: MyHooks,
182///         }
183///     }
184/// }
185///
186/// impl WasiHttpView for MyState {
187///     fn http(&mut self) -> WasiHttpCtxView<'_> {
188///         WasiHttpCtxView {
189///             ctx: &mut self.http_ctx,
190///             table: &mut self.table,
191///             hooks: &mut self.hooks,
192///         }
193///     }
194/// }
195///
196/// struct MyHooks;
197///
198/// impl WasiHttpHooks for MyHooks {
199///     fn is_forbidden_header(&mut self, name: &http::HeaderName) -> bool {
200///         *name == http::header::AUTHORIZATION ||
201///             wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS.contains(name)
202///     }
203/// }
204/// ```
205pub trait WasiHttpHooks: Send {
206    /// Whether a given header should be considered forbidden and not allowed.
207    fn is_forbidden_header(&mut self, name: &HeaderName) -> bool {
208        DEFAULT_FORBIDDEN_HEADERS.contains(name)
209    }
210
211    /// Whether a given scheme should be considered supported.
212    ///
213    /// `handle` will return [Error::HttpProtocolError] for unsupported schemes.
214    fn is_supported_scheme(&mut self, scheme: &Scheme) -> bool {
215        *scheme == Scheme::HTTP || *scheme == Scheme::HTTPS
216    }
217
218    /// Whether to set `host` header in the request passed to `send_request`.
219    fn set_host_header(&mut self) -> bool {
220        true
221    }
222
223    /// Scheme to default to, when not set by the guest.
224    ///
225    /// If [None], `handle` will return [Error::HttpProtocolError]
226    /// for requests missing a scheme.
227    fn default_scheme(&mut self) -> Option<Scheme> {
228        Some(Scheme::HTTPS)
229    }
230
231    /// Send an outgoing request.
232    ///
233    /// This function will be used by the `wasi:http/handler#handle` implementation.
234    ///
235    /// The specified [Future] `fut` will be used to communicate
236    /// a response processing error, if any.
237    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
238    /// a result will be sent on `fut`.
239    ///
240    /// The returned [Future] can be used to communicate
241    /// a request processing error, if any, to the constructor of the request.
242    /// For example, if the request was constructed via `wasi:http/types.request#new`,
243    /// a result resolved from it will be forwarded to the guest on the future handle returned.
244    ///
245    /// `Content-Length` of the request passed to this function will be validated, however no
246    /// `Content-Length` validation will be performed for the received response.
247    #[cfg(feature = "default-send-request")]
248    fn send_request(
249        &mut self,
250        request: http::Request<WasiBody>,
251        options: Option<RequestOptions>,
252        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
253    ) -> Box<
254        dyn Future<
255                Output = Result<(
256                    http::Response<WasiBody>,
257                    Box<dyn Future<Output = Result<(), Error>> + Send>,
258                )>,
259            > + Send,
260    > {
261        _ = fut;
262        Box::new(async move {
263            use http_body_util::BodyExt;
264
265            let (res, io) = crate::default_send_request(request, options).await?;
266            Ok((
267                res.map(BodyExt::boxed_unsync),
268                Box::new(io) as Box<dyn Future<Output = _> + Send>,
269            ))
270        })
271    }
272
273    /// Send an outgoing request.
274    ///
275    /// This function will be used by the `wasi:http/handler#handle` implementation.
276    ///
277    /// The specified [Future] `fut` will be used to communicate
278    /// a response processing error, if any.
279    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
280    /// a result will be sent on `fut`.
281    ///
282    /// The returned [Future] can be used to communicate
283    /// a request processing error, if any, to the constructor of the request.
284    /// For example, if the request was constructed via `wasi:http/types.request#new`,
285    /// a result resolved from it will be forwarded to the guest on the future handle returned.
286    ///
287    /// `Content-Length` of the request passed to this function will be validated, however no
288    /// `Content-Length` validation will be performed for the received response.
289    #[cfg(not(feature = "default-send-request"))]
290    fn send_request(
291        &mut self,
292        request: http::Request<WasiBody>,
293        options: Option<RequestOptions>,
294        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
295    ) -> Box<
296        dyn Future<
297                Output = Result<(
298                    http::Response<WasiBody>,
299                    Box<dyn Future<Output = Result<(), Error>> + Send>,
300                )>,
301            > + Send,
302    >;
303
304    /// Number of distinct write calls to the outgoing body's output-stream
305    /// that the implementation will buffer.
306    /// Default: 1.
307    #[cfg(feature = "p2")]
308    fn p2_outgoing_body_buffer_chunks(&mut self) -> usize {
309        crate::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS
310    }
311
312    /// Maximum size allowed in a write call to the outgoing body's
313    /// output-stream.  Default: 1024 * 1024.
314    #[cfg(feature = "p2")]
315    fn p2_outgoing_body_chunk_size(&mut self) -> usize {
316        crate::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
317    }
318
319    /// Optional hook to configure the error code for hyper errors.
320    #[cfg(feature = "p2")]
321    fn p2_error_from_hyper(&mut self, err: &hyper::Error) -> p2::ErrorCode {
322        tracing::warn!("hyper error: {err:?}");
323        p2::ErrorCode::HttpProtocolError
324    }
325
326    /// Optional hook to configure the error code for connect I/O errors.
327    #[cfg(feature = "p2")]
328    fn p2_error_from_connect(&mut self, err: &std::io::Error) -> p2::ErrorCode {
329        tracing::warn!("connect error: {err:?}");
330        p2::ErrorCode::ConnectionRefused
331    }
332
333    /// Optional hook to configure the error code for TLS I/O errors.
334    #[cfg(feature = "p2")]
335    fn p2_error_from_tls(&mut self, err: &std::io::Error) -> p2::ErrorCode {
336        tracing::warn!("tls error: {err:?}");
337        p2::ErrorCode::TlsProtocolError
338    }
339
340    /// Optional hook to configure the error code for DNS errors.
341    #[cfg(all(feature = "p2", feature = "default-send-request"))]
342    fn p2_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p2::ErrorCode {
343        tracing::warn!("dns lookup error: {err:?}");
344        p2::ErrorCode::DnsError(p2::DnsErrorPayload {
345            rcode: Some("invalid dns name".to_string()),
346            info_code: None,
347        })
348    }
349
350    /// Optional hook to configure the error code for hyper errors.
351    #[cfg(feature = "p3")]
352    fn p3_error_from_hyper(&mut self, err: &hyper::Error) -> p3::ErrorCode {
353        tracing::warn!("hyper error: {err:?}");
354        p3::ErrorCode::HttpProtocolError
355    }
356
357    /// Optional hook to configure the error code for connect I/O errors.
358    #[cfg(feature = "p3")]
359    fn p3_error_from_connect(&mut self, err: &std::io::Error) -> p3::ErrorCode {
360        tracing::warn!("connect error: {err:?}");
361        p3::ErrorCode::ConnectionRefused
362    }
363
364    /// Optional hook to configure the error code for TLS I/O errors.
365    #[cfg(feature = "p3")]
366    fn p3_error_from_tls(&mut self, err: &std::io::Error) -> p3::ErrorCode {
367        tracing::warn!("tls error: {err:?}");
368        p3::ErrorCode::TlsProtocolError
369    }
370
371    /// Optional hook to configure the error code for DNS errors.
372    #[cfg(all(feature = "p3", feature = "default-send-request"))]
373    fn p3_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p3::ErrorCode {
374        tracing::warn!("dns lookup error: {err:?}");
375        p3::ErrorCode::DnsError(p3::DnsErrorPayload {
376            rcode: Some("invalid dns name".to_string()),
377            info_code: None,
378        })
379    }
380}
381
382/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has
383/// the default behavior for `wasi:http`.
384#[cfg(feature = "default-send-request")]
385pub fn default_hooks() -> &'static mut dyn WasiHttpHooks {
386    Default::default()
387}
388
389#[cfg(feature = "default-send-request")]
390impl<'a> Default for &'a mut dyn WasiHttpHooks {
391    fn default() -> Self {
392        let x: &mut [(); 0] = &mut [];
393        x
394    }
395}
396
397#[doc(hidden)]
398#[cfg(feature = "default-send-request")]
399impl WasiHttpHooks for [(); 0] {}