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}