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 std::marker;
10use wasmtime::component::{HasData, ResourceTable};
11use wasmtime_wasi::{NamedId, WasiCtxNamedView};
12
13/// A helper struct which implements [`HasData`] for the `wasi:http` APIs.
14///
15/// This can be useful when directly calling `add_to_linker` functions directly,
16/// such as [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`] as
17/// the `D` type parameter. See [`HasData`] for more information about the type
18/// parameter's purpose.
19///
20/// When using this type you can skip the [`WasiHttpView`] trait, for example.
21///
22/// [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`]: crate::p3::bindings::http::types::add_to_linker
23///
24/// # Examples
25///
26/// ```
27/// use wasmtime::component::Linker;
28/// use wasmtime::{Engine, Result};
29/// use wasmtime_wasi_http::{WasiHttp, WasiHttpCtxView};
30///
31/// struct MyStoreState {
32///     // ...
33/// }
34///
35/// impl MyStoreState {
36///     fn http(&mut self) -> WasiHttpCtxView<'_> {
37///         // ...
38/// #       todo!()
39///     }
40/// }
41///
42/// fn main() -> Result<()> {
43///     let engine = Engine::default();
44///     let mut linker = Linker::new(&engine);
45///
46///     wasmtime_wasi_http::p3::bindings::http::types::add_to_linker::<MyStoreState, WasiHttp>(
47///         &mut linker,
48///         |state| state.http(),
49///     )?;
50///     Ok(())
51/// }
52/// ```
53pub struct WasiHttp;
54
55impl HasData for WasiHttp {
56    type Data<'a> = WasiHttpCtxView<'a>;
57}
58
59/// A trait which provides internal WASI HTTP state.
60///
61/// This trait is used by the [`add_to_linker`] convenience functions of this
62/// crate. This trait can be implemented for the `T` in `Store<T>` to provide
63/// access to wasi:http information at runtime.
64///
65/// [`add_to_linker`]: crate::p3::add_to_linker
66///
67/// # Example
68///
69/// ```
70/// use wasmtime::component::ResourceTable;
71/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView};
72///
73/// struct MyState {
74///     http_ctx: WasiHttpCtx,
75///     table: ResourceTable,
76/// }
77///
78/// impl WasiHttpView for MyState {
79///     fn http(&mut self) -> WasiHttpCtxView<'_> {
80///         WasiHttpCtxView {
81///             ctx: &mut self.http_ctx,
82///             table: &mut self.table,
83///             hooks: Default::default(),
84///         }
85///     }
86/// }
87/// ```
88pub trait WasiHttpView: Send {
89    /// Return a [WasiHttpCtxView] from mutable reference to self.
90    fn http(&mut self) -> WasiHttpCtxView<'_>;
91}
92
93/// Basis of implementation of all `wasi:http` APIs in this crate.
94///
95/// This type provides a temporary view into information such as a WASI
96/// resource table, HTTP context information, and embedder-provided hooks if
97/// so desired. THe fields in this structure are typically stored within the
98/// `T` of `Store<T>` and this struct borrows from there. All `Host` traits
99/// generated by `bindgen!` are implemented for this type.
100pub struct WasiHttpCtxView<'a> {
101    /// Mutable reference to the WASI HTTP hooks.
102    ///
103    /// Note that [`default_hooks`] or [`Default::default()`] can be used if
104    /// you don't want or need to customize this.
105    pub hooks: &'a mut dyn WasiHttpHooks,
106
107    /// Mutable reference to table used to manage resources.
108    pub table: &'a mut ResourceTable,
109
110    /// Mutable reference to the WASI HTTP context.
111    pub ctx: &'a mut WasiHttpCtx,
112}
113
114/// Default maximum size for the contents of a fields resource.
115///
116/// Typically, HTTP proxies limit headers to 8k. This number is higher than that
117/// because it not only includes the wire-size of headers but it additionally
118/// includes factors for the in-memory representation of `HeaderMap`. This is in
119/// theory high enough that no one runs into it but low enough such that a
120/// completely full `HeaderMap` doesn't break the bank in terms of memory
121/// consumption.
122const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024;
123
124/// Capture the state necessary for use in the wasi-http API implementation.
125#[derive(Debug, Clone)]
126pub struct WasiHttpCtx {
127    pub(crate) field_size_limit: usize,
128}
129
130impl WasiHttpCtx {
131    /// Create a new context.
132    pub fn new() -> Self {
133        Self {
134            field_size_limit: DEFAULT_FIELD_SIZE_LIMIT,
135        }
136    }
137
138    /// Set the maximum size for any fields resources created by this context.
139    ///
140    /// The limit specified here is roughly a byte limit for the size of the
141    /// in-memory representation of headers. This means that the limit needs to
142    /// be larger than the literal representation of headers on the wire to
143    /// account for in-memory Rust-side data structures representing the header
144    /// names/values/etc.
145    pub fn set_field_size_limit(&mut self, limit: usize) {
146        self.field_size_limit = limit;
147    }
148}
149
150impl Default for WasiHttpCtx {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156/// Convenience type definition for the bodies used in this crate.
157pub type WasiBody = UnsyncBoxBody<Bytes, Error>;
158
159/// A trait which provides hooks into internal WASI HTTP operations.
160///
161/// Note that when using this type if state is needed to implement the methods
162/// the state will need to be stored separately in a distinct structure to
163/// implement this trait as the same type can't implement both [`WasiHttpView`]
164/// and [`WasiHttpHooks`] and be usable.
165///
166/// # Example
167///
168/// ```
169/// use wasmtime::component::ResourceTable;
170/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView, WasiHttpHooks};
171///
172/// struct MyState {
173///     http_ctx: WasiHttpCtx,
174///     table: ResourceTable,
175///     hooks: MyHooks,
176/// }
177///
178/// impl MyState {
179///     fn new() -> MyState {
180///         MyState {
181///             table: ResourceTable::new(),
182///             http_ctx: WasiHttpCtx::new(),
183///             hooks: MyHooks,
184///         }
185///     }
186/// }
187///
188/// impl WasiHttpView for MyState {
189///     fn http(&mut self) -> WasiHttpCtxView<'_> {
190///         WasiHttpCtxView {
191///             ctx: &mut self.http_ctx,
192///             table: &mut self.table,
193///             hooks: &mut self.hooks,
194///         }
195///     }
196/// }
197///
198/// struct MyHooks;
199///
200/// impl WasiHttpHooks for MyHooks {
201///     fn is_forbidden_header(&mut self, name: &http::HeaderName) -> bool {
202///         *name == http::header::AUTHORIZATION ||
203///             wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS.contains(name)
204///     }
205/// }
206/// ```
207pub trait WasiHttpHooks: Send {
208    /// Whether a given header should be considered forbidden and not allowed.
209    fn is_forbidden_header(&mut self, name: &HeaderName) -> bool {
210        DEFAULT_FORBIDDEN_HEADERS.contains(name)
211    }
212
213    /// Whether a given scheme should be considered supported.
214    ///
215    /// `handle` will return [Error::HttpProtocolError] for unsupported schemes.
216    fn is_supported_scheme(&mut self, scheme: &Scheme) -> bool {
217        *scheme == Scheme::HTTP || *scheme == Scheme::HTTPS
218    }
219
220    /// Whether to set `host` header in the request passed to `send_request`.
221    fn set_host_header(&mut self) -> bool {
222        true
223    }
224
225    /// Scheme to default to, when not set by the guest.
226    ///
227    /// If [None], `handle` will return [Error::HttpProtocolError]
228    /// for requests missing a scheme.
229    fn default_scheme(&mut self) -> Option<Scheme> {
230        Some(Scheme::HTTPS)
231    }
232
233    /// Send an outgoing request.
234    ///
235    /// This function will be used by the `wasi:http/handler#handle` implementation.
236    ///
237    /// The specified [Future] `fut` will be used to communicate
238    /// a response processing error, if any.
239    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
240    /// a result will be sent on `fut`.
241    ///
242    /// The returned [Future] can be used to communicate
243    /// a request processing error, if any, to the constructor of the request.
244    /// For example, if the request was constructed via `wasi:http/types.request#new`,
245    /// a result resolved from it will be forwarded to the guest on the future handle returned.
246    ///
247    /// `Content-Length` of the request passed to this function will be validated, however no
248    /// `Content-Length` validation will be performed for the received response.
249    #[cfg(feature = "default-send-request")]
250    fn send_request(
251        &mut self,
252        request: http::Request<WasiBody>,
253        options: Option<RequestOptions>,
254        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
255    ) -> Box<
256        dyn Future<
257                Output = Result<(
258                    http::Response<WasiBody>,
259                    Box<dyn Future<Output = Result<(), Error>> + Send>,
260                )>,
261            > + Send,
262    > {
263        _ = fut;
264        Box::new(async move {
265            use http_body_util::BodyExt;
266
267            let (res, io) = crate::default_send_request(request, options).await?;
268            Ok((
269                res.map(BodyExt::boxed_unsync),
270                Box::new(io) as Box<dyn Future<Output = _> + Send>,
271            ))
272        })
273    }
274
275    /// Send an outgoing request.
276    ///
277    /// This function will be used by the `wasi:http/handler#handle` implementation.
278    ///
279    /// The specified [Future] `fut` will be used to communicate
280    /// a response processing error, if any.
281    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
282    /// a result will be sent on `fut`.
283    ///
284    /// The returned [Future] can be used to communicate
285    /// a request processing error, if any, to the constructor of the request.
286    /// For example, if the request was constructed via `wasi:http/types.request#new`,
287    /// a result resolved from it will be forwarded to the guest on the future handle returned.
288    ///
289    /// `Content-Length` of the request passed to this function will be validated, however no
290    /// `Content-Length` validation will be performed for the received response.
291    #[cfg(not(feature = "default-send-request"))]
292    fn send_request(
293        &mut self,
294        request: http::Request<WasiBody>,
295        options: Option<RequestOptions>,
296        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
297    ) -> Box<
298        dyn Future<
299                Output = Result<(
300                    http::Response<WasiBody>,
301                    Box<dyn Future<Output = Result<(), Error>> + Send>,
302                )>,
303            > + Send,
304    >;
305
306    /// Number of distinct write calls to the outgoing body's output-stream
307    /// that the implementation will buffer.
308    /// Default: 1.
309    #[cfg(feature = "p2")]
310    fn p2_outgoing_body_buffer_chunks(&mut self) -> usize {
311        crate::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS
312    }
313
314    /// Maximum size allowed in a write call to the outgoing body's
315    /// output-stream.  Default: 1024 * 1024.
316    #[cfg(feature = "p2")]
317    fn p2_outgoing_body_chunk_size(&mut self) -> usize {
318        crate::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
319    }
320
321    /// Optional hook to configure the error code for hyper errors.
322    #[cfg(feature = "p2")]
323    fn p2_error_from_hyper(&mut self, err: &hyper::Error) -> p2::ErrorCode {
324        tracing::warn!("hyper error: {err:?}");
325        p2::ErrorCode::HttpProtocolError
326    }
327
328    /// Optional hook to configure the error code for connect I/O errors.
329    #[cfg(feature = "p2")]
330    fn p2_error_from_connect(&mut self, err: &std::io::Error) -> p2::ErrorCode {
331        tracing::warn!("connect error: {err:?}");
332        p2::ErrorCode::ConnectionRefused
333    }
334
335    /// Optional hook to configure the error code for TLS I/O errors.
336    #[cfg(feature = "p2")]
337    fn p2_error_from_tls(&mut self, err: &std::io::Error) -> p2::ErrorCode {
338        tracing::warn!("tls error: {err:?}");
339        p2::ErrorCode::TlsProtocolError
340    }
341
342    /// Optional hook to configure the error code for DNS errors.
343    #[cfg(all(feature = "p2", feature = "default-send-request"))]
344    fn p2_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p2::ErrorCode {
345        tracing::warn!("dns lookup error: {err:?}");
346        p2::ErrorCode::DnsError(p2::DnsErrorPayload {
347            rcode: Some("invalid dns name".to_string()),
348            info_code: None,
349        })
350    }
351
352    /// Maximum number of bytes the implementation will copy out of the guest in
353    /// a single write to an outgoing body's stream.
354    #[cfg(feature = "p3")]
355    fn p3_outgoing_body_chunk_size(&mut self) -> usize {
356        crate::p3::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
357    }
358
359    /// Optional hook to configure the error code for hyper errors.
360    #[cfg(feature = "p3")]
361    fn p3_error_from_hyper(&mut self, err: &hyper::Error) -> p3::ErrorCode {
362        tracing::warn!("hyper error: {err:?}");
363        p3::ErrorCode::HttpProtocolError
364    }
365
366    /// Optional hook to configure the error code for connect I/O errors.
367    #[cfg(feature = "p3")]
368    fn p3_error_from_connect(&mut self, err: &std::io::Error) -> p3::ErrorCode {
369        tracing::warn!("connect error: {err:?}");
370        p3::ErrorCode::ConnectionRefused
371    }
372
373    /// Optional hook to configure the error code for TLS I/O errors.
374    #[cfg(feature = "p3")]
375    fn p3_error_from_tls(&mut self, err: &std::io::Error) -> p3::ErrorCode {
376        tracing::warn!("tls error: {err:?}");
377        p3::ErrorCode::TlsProtocolError
378    }
379
380    /// Optional hook to configure the error code for DNS errors.
381    #[cfg(all(feature = "p3", feature = "default-send-request"))]
382    fn p3_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p3::ErrorCode {
383        tracing::warn!("dns lookup error: {err:?}");
384        p3::ErrorCode::DnsError(p3::DnsErrorPayload {
385            rcode: Some("invalid dns name".to_string()),
386            info_code: None,
387        })
388    }
389}
390
391/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has
392/// the default behavior for `wasi:http`.
393#[cfg(feature = "default-send-request")]
394pub fn default_hooks() -> &'static mut dyn WasiHttpHooks {
395    Default::default()
396}
397
398#[cfg(feature = "default-send-request")]
399impl<'a> Default for &'a mut dyn WasiHttpHooks {
400    fn default() -> Self {
401        let x: &mut [(); 0] = &mut [];
402        x
403    }
404}
405
406#[doc(hidden)]
407#[cfg(feature = "default-send-request")]
408impl WasiHttpHooks for [(); 0] {}
409
410/// A helper struct which implements [`HasData`] for the `wasi:http` APIs when
411/// used in combination with named imports.
412///
413/// This structure is similar in purpose to [`WasiHttp`] and is used
414/// when using the [`named_imports`] module for `wasi:http`. This structure
415/// serves as the `D` type parameter for `add_to_linker` functions.
416///
417/// [`named_imports`]: crate::p3::bindings::named_imports::wasi::http
418///
419/// # Meaning of the `T` parameter
420///
421/// Here the `T` must be something that implements [`WasiHttpNamedView`]. The
422/// corresponding `Data` for this type is [`WasiCtxNamedView`] which internally
423/// will contain `&mut T`.
424///
425/// Effectively you're going to implement [`WasiHttpNamedView`] for something in
426/// your embedding, and that's the `T` you'll fill in here.
427///
428/// # Examples
429///
430/// ```
431/// use wasmtime::component::{Linker, Component, ResourceTable};
432/// use wasmtime::{Engine, Result};
433/// use wasmtime_wasi::{NamedId, WasiCtxNamedView};
434/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpCtxView, WasiHttpNamed, WasiHttpNamedView};
435/// use wasmtime_wasi_http::p2::bindings::named_imports;
436/// use std::collections::HashMap;
437///
438/// struct MyStoreState {
439///     table: ResourceTable,
440///     states: HashMap<NamedId, WasiHttpCtx>,
441/// }
442///
443/// fn main() -> Result<()> {
444///     let engine = Engine::default();
445///     let mut linker = Linker::new(&engine);
446///     let component = Component::new(&engine, "(component)")?;
447///     let mut name_map = HashMap::new();
448///
449///     named_imports::wasi::http::outgoing_handler::add_to_linker::<MyStoreState, WasiHttpNamed<MyStoreState>>(
450///         &mut linker,
451///         &component,
452///         |name| {
453///             let len = name_map.len();
454///             Ok(NamedId(*name_map.entry(name.to_string()).or_insert(len)))
455///         },
456///         |state| WasiCtxNamedView(state),
457///     )?;
458///     Ok(())
459/// }
460///
461/// impl WasiHttpNamedView for MyStoreState {
462///     fn http(&mut self, id: NamedId) -> WasiHttpCtxView<'_> {
463///         let ctx = self.states.get_mut(&id).expect("state for id");
464///         WasiHttpCtxView {
465///             ctx,
466///             table: &mut self.table,
467///             hooks: Default::default(),
468///         }
469///     }
470/// }
471/// ```
472pub struct WasiHttpNamed<T>(marker::PhantomData<fn() -> T>);
473
474impl<T> HasData for WasiHttpNamed<T>
475where
476    T: WasiHttpNamedView,
477{
478    type Data<'a> = WasiCtxNamedView<'a, T>;
479}
480
481/// A trait used to look up a specific `wasi:http` context for a named import.
482///
483/// This trait is used in conjunction with the [`named_imports`] bindings
484/// generated for all `wasi:http` interfaces. The purpose of this trait is for
485/// embedders to define how a [`NamedId`] maps to a particular `wasi:http`
486/// context, here returned as [`WasiHttpCtxView`]. Embedders are responsible
487/// for assigning meaning to [`NamedId`] values themselves. These IDs are
488/// assigned when [`add_named_to_linker`] is called, for example, as the
489/// `lookup` argument to that function.
490///
491/// When using [`add_named_to_linker`] it's sufficient to implement this trait
492/// for the `T` in `Store<T>`.
493///
494/// When using `add_to_linker` in the generated `bindings::named_imports`
495/// module then values implementing this live within the `T` of `Store<T>`, and
496/// be temporarily referenced in [`WasiCtxNamedView`] where internally that'll
497/// hold `WasiCtxNamedView(&mut your_type)`.
498///
499/// [`named_imports`]: crate::p3::bindings::named_imports
500/// [`add_named_to_linker`]: crate::p3::add_named_to_linker
501///
502/// # Examples
503///
504/// ```
505/// use wasmtime::component::{Linker, Component, ResourceTable};
506/// use wasmtime::{Engine, Result};
507/// use wasmtime_wasi::NamedId;
508/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpCtxView, WasiHttpNamedView};
509/// use std::collections::HashMap;
510///
511/// struct MyStoreState {
512///     table: ResourceTable,
513///     states: HashMap<NamedId, WasiHttpCtx>,
514/// }
515///
516/// fn main() -> Result<()> {
517///     let engine = Engine::default();
518///     let mut linker = Linker::new(&engine);
519///     let component = Component::new(&engine, "(component)")?;
520///     let mut name_map = HashMap::new();
521///
522///     wasmtime_wasi_http::p3::add_named_to_linker::<MyStoreState>(
523///         &mut linker,
524///         &component,
525///         |_, name| {
526///             let len = name_map.len();
527///             Ok(NamedId(*name_map.entry(name.to_string()).or_insert(len)))
528///         },
529///     )?;
530///     Ok(())
531/// }
532///
533/// impl WasiHttpNamedView for MyStoreState {
534///     fn http(&mut self, id: NamedId) -> WasiHttpCtxView<'_> {
535///         let ctx = self.states.get_mut(&id).expect("state for id");
536///         WasiHttpCtxView {
537///             ctx,
538///             table: &mut self.table,
539///             hooks: Default::default(),
540///         }
541///     }
542/// }
543/// ```
544pub trait WasiHttpNamedView: Send + 'static {
545    /// Looks up the [`WasiHttpCtxView`] for the given [`NamedId`].
546    ///
547    /// This method will resolve the `id` specified to a specific HTTP context
548    /// that is available to be used. Note that this method is specifically
549    /// infallible meaning that an HTTP context must be returned and this cannot
550    /// generate a trap or panic or similar.
551    ///
552    /// Embedders are responsible for allocating [`NamedId`] and assigning
553    /// meaning to ids. When a `Linker` is populated embedders will have the
554    /// ability to generate a `NamedId` for all imports found, and then that
555    /// embedder-allocated id is then passed back here when the corresponding
556    /// imported function is invoked.
557    ///
558    /// Note that the [`ResourceTable`] referenced in the returned
559    /// [`WasiHttpCtxView`] need not be unique. It's ok to use the same
560    /// [`ResourceTable`] for all imports. This is not a guest-visible
561    /// abstraction and just helps the host allocate and manage state.
562    fn http(&mut self, id: NamedId) -> WasiHttpCtxView<'_>;
563}