wasmtime-wasi-http 48.0.2

Experimental HTTP library for WebAssembly in Wasmtime
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
#[cfg(feature = "p2")]
use crate::p2::bindings::http::types as p2;
#[cfg(feature = "p3")]
use crate::p3::bindings::http::types as p3;
use crate::{DEFAULT_FORBIDDEN_HEADERS, Error, RequestOptions, Result};
use bytes::Bytes;
use http::{HeaderName, uri::Scheme};
use http_body_util::combinators::UnsyncBoxBody;
use wasmtime::component::{HasData, ResourceTable};

/// A helper struct which implements [`HasData`] for the `wasi:http` APIs.
///
/// This can be useful when directly calling `add_to_linker` functions directly,
/// such as [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`] as
/// the `D` type parameter. See [`HasData`] for more information about the type
/// parameter's purpose.
///
/// When using this type you can skip the [`WasiHttpView`] trait, for example.
///
/// [`wasmtime_wasi_http::p3::bindings::http::types::add_to_linker`]: crate::p3::bindings::http::types::add_to_linker
///
/// # Examples
///
/// ```
/// use wasmtime::component::Linker;
/// use wasmtime::{Engine, Result};
/// use wasmtime_wasi_http::{WasiHttp, WasiHttpCtxView};
///
/// struct MyStoreState {
///     // ...
/// }
///
/// impl MyStoreState {
///     fn http(&mut self) -> WasiHttpCtxView<'_> {
///         // ...
/// #       todo!()
///     }
/// }
///
/// fn main() -> Result<()> {
///     let engine = Engine::default();
///     let mut linker = Linker::new(&engine);
///
///     wasmtime_wasi_http::p3::bindings::http::types::add_to_linker::<MyStoreState, WasiHttp>(
///         &mut linker,
///         |state| state.http(),
///     )?;
///     Ok(())
/// }
/// ```
pub struct WasiHttp;

impl HasData for WasiHttp {
    type Data<'a> = WasiHttpCtxView<'a>;
}

/// A trait which provides internal WASI HTTP state.
///
/// This trait is used by the [`add_to_linker`] convenience functions of this
/// crate. This trait can be implemented for the `T` in `Store<T>` to provide
/// access to wasi:http information at runtime.
///
/// [`add_to_linker`]: crate::p3::add_to_linker
///
/// # Example
///
/// ```
/// use wasmtime::component::ResourceTable;
/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView};
///
/// struct MyState {
///     http_ctx: WasiHttpCtx,
///     table: ResourceTable,
/// }
///
/// impl WasiHttpView for MyState {
///     fn http(&mut self) -> WasiHttpCtxView<'_> {
///         WasiHttpCtxView {
///             ctx: &mut self.http_ctx,
///             table: &mut self.table,
///             hooks: Default::default(),
///         }
///     }
/// }
/// ```
pub trait WasiHttpView: Send {
    /// Return a [WasiHttpCtxView] from mutable reference to self.
    fn http(&mut self) -> WasiHttpCtxView<'_>;
}

/// Basis of implementation of all `wasi:http` APIs in this crate.
///
/// This type provides a temporary view into information such as a WASI
/// resource table, HTTP context information, and embedder-provided hooks if
/// so desired. THe fields in this structure are typically stored within the
/// `T` of `Store<T>` and this struct borrows from there. All `Host` traits
/// generated by `bindgen!` are implemented for this type.
pub struct WasiHttpCtxView<'a> {
    /// Mutable reference to the WASI HTTP hooks.
    ///
    /// Note that [`default_hooks`] or [`Default::default()`] can be used if
    /// you don't want or need to customize this.
    pub hooks: &'a mut dyn WasiHttpHooks,

    /// Mutable reference to table used to manage resources.
    pub table: &'a mut ResourceTable,

    /// Mutable reference to the WASI HTTP context.
    pub ctx: &'a mut WasiHttpCtx,
}

/// Default maximum size for the contents of a fields resource.
///
/// Typically, HTTP proxies limit headers to 8k. This number is higher than that
/// because it not only includes the wire-size of headers but it additionally
/// includes factors for the in-memory representation of `HeaderMap`. This is in
/// theory high enough that no one runs into it but low enough such that a
/// completely full `HeaderMap` doesn't break the bank in terms of memory
/// consumption.
const DEFAULT_FIELD_SIZE_LIMIT: usize = 128 * 1024;

/// Capture the state necessary for use in the wasi-http API implementation.
#[derive(Debug, Clone)]
pub struct WasiHttpCtx {
    pub(crate) field_size_limit: usize,
}

impl WasiHttpCtx {
    /// Create a new context.
    pub fn new() -> Self {
        Self {
            field_size_limit: DEFAULT_FIELD_SIZE_LIMIT,
        }
    }

    /// Set the maximum size for any fields resources created by this context.
    ///
    /// The limit specified here is roughly a byte limit for the size of the
    /// in-memory representation of headers. This means that the limit needs to
    /// be larger than the literal representation of headers on the wire to
    /// account for in-memory Rust-side data structures representing the header
    /// names/values/etc.
    pub fn set_field_size_limit(&mut self, limit: usize) {
        self.field_size_limit = limit;
    }
}

impl Default for WasiHttpCtx {
    fn default() -> Self {
        Self::new()
    }
}

/// Convenience type definition for the bodies used in this crate.
pub type WasiBody = UnsyncBoxBody<Bytes, Error>;

/// A trait which provides hooks into internal WASI HTTP operations.
///
/// Note that when using this type if state is needed to implement the methods
/// the state will need to be stored separately in a distinct structure to
/// implement this trait as the same type can't implement both [`WasiHttpView`]
/// and [`WasiHttpHooks`] and be usable.
///
/// # Example
///
/// ```
/// use wasmtime::component::ResourceTable;
/// use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView, WasiHttpCtxView, WasiHttpHooks};
///
/// struct MyState {
///     http_ctx: WasiHttpCtx,
///     table: ResourceTable,
///     hooks: MyHooks,
/// }
///
/// impl MyState {
///     fn new() -> MyState {
///         MyState {
///             table: ResourceTable::new(),
///             http_ctx: WasiHttpCtx::new(),
///             hooks: MyHooks,
///         }
///     }
/// }
///
/// impl WasiHttpView for MyState {
///     fn http(&mut self) -> WasiHttpCtxView<'_> {
///         WasiHttpCtxView {
///             ctx: &mut self.http_ctx,
///             table: &mut self.table,
///             hooks: &mut self.hooks,
///         }
///     }
/// }
///
/// struct MyHooks;
///
/// impl WasiHttpHooks for MyHooks {
///     fn is_forbidden_header(&mut self, name: &http::HeaderName) -> bool {
///         *name == http::header::AUTHORIZATION ||
///             wasmtime_wasi_http::DEFAULT_FORBIDDEN_HEADERS.contains(name)
///     }
/// }
/// ```
pub trait WasiHttpHooks: Send {
    /// Whether a given header should be considered forbidden and not allowed.
    fn is_forbidden_header(&mut self, name: &HeaderName) -> bool {
        DEFAULT_FORBIDDEN_HEADERS.contains(name)
    }

    /// Whether a given scheme should be considered supported.
    ///
    /// `handle` will return [Error::HttpProtocolError] for unsupported schemes.
    fn is_supported_scheme(&mut self, scheme: &Scheme) -> bool {
        *scheme == Scheme::HTTP || *scheme == Scheme::HTTPS
    }

    /// Whether to set `host` header in the request passed to `send_request`.
    fn set_host_header(&mut self) -> bool {
        true
    }

    /// Scheme to default to, when not set by the guest.
    ///
    /// If [None], `handle` will return [Error::HttpProtocolError]
    /// for requests missing a scheme.
    fn default_scheme(&mut self) -> Option<Scheme> {
        Some(Scheme::HTTPS)
    }

    /// Send an outgoing request.
    ///
    /// This function will be used by the `wasi:http/handler#handle` implementation.
    ///
    /// The specified [Future] `fut` will be used to communicate
    /// a response processing error, if any.
    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
    /// a result will be sent on `fut`.
    ///
    /// The returned [Future] can be used to communicate
    /// a request processing error, if any, to the constructor of the request.
    /// For example, if the request was constructed via `wasi:http/types.request#new`,
    /// a result resolved from it will be forwarded to the guest on the future handle returned.
    ///
    /// `Content-Length` of the request passed to this function will be validated, however no
    /// `Content-Length` validation will be performed for the received response.
    #[cfg(feature = "default-send-request")]
    fn send_request(
        &mut self,
        request: http::Request<WasiBody>,
        options: Option<RequestOptions>,
        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
    ) -> Box<
        dyn Future<
                Output = Result<(
                    http::Response<WasiBody>,
                    Box<dyn Future<Output = Result<(), Error>> + Send>,
                )>,
            > + Send,
    > {
        _ = fut;
        Box::new(async move {
            use http_body_util::BodyExt;

            let (res, io) = crate::default_send_request(request, options).await?;
            Ok((
                res.map(BodyExt::boxed_unsync),
                Box::new(io) as Box<dyn Future<Output = _> + Send>,
            ))
        })
    }

    /// Send an outgoing request.
    ///
    /// This function will be used by the `wasi:http/handler#handle` implementation.
    ///
    /// The specified [Future] `fut` will be used to communicate
    /// a response processing error, if any.
    /// For example, if the response body is consumed via `wasi:http/types.response#consume-body`,
    /// a result will be sent on `fut`.
    ///
    /// The returned [Future] can be used to communicate
    /// a request processing error, if any, to the constructor of the request.
    /// For example, if the request was constructed via `wasi:http/types.request#new`,
    /// a result resolved from it will be forwarded to the guest on the future handle returned.
    ///
    /// `Content-Length` of the request passed to this function will be validated, however no
    /// `Content-Length` validation will be performed for the received response.
    #[cfg(not(feature = "default-send-request"))]
    fn send_request(
        &mut self,
        request: http::Request<WasiBody>,
        options: Option<RequestOptions>,
        fut: Box<dyn Future<Output = Result<(), Error>> + Send>,
    ) -> Box<
        dyn Future<
                Output = Result<(
                    http::Response<WasiBody>,
                    Box<dyn Future<Output = Result<(), Error>> + Send>,
                )>,
            > + Send,
    >;

    /// Number of distinct write calls to the outgoing body's output-stream
    /// that the implementation will buffer.
    /// Default: 1.
    #[cfg(feature = "p2")]
    fn p2_outgoing_body_buffer_chunks(&mut self) -> usize {
        crate::p2::DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS
    }

    /// Maximum size allowed in a write call to the outgoing body's
    /// output-stream.  Default: 1024 * 1024.
    #[cfg(feature = "p2")]
    fn p2_outgoing_body_chunk_size(&mut self) -> usize {
        crate::p2::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
    }

    /// Optional hook to configure the error code for hyper errors.
    #[cfg(feature = "p2")]
    fn p2_error_from_hyper(&mut self, err: &hyper::Error) -> p2::ErrorCode {
        tracing::warn!("hyper error: {err:?}");
        p2::ErrorCode::HttpProtocolError
    }

    /// Optional hook to configure the error code for connect I/O errors.
    #[cfg(feature = "p2")]
    fn p2_error_from_connect(&mut self, err: &std::io::Error) -> p2::ErrorCode {
        tracing::warn!("connect error: {err:?}");
        p2::ErrorCode::ConnectionRefused
    }

    /// Optional hook to configure the error code for TLS I/O errors.
    #[cfg(feature = "p2")]
    fn p2_error_from_tls(&mut self, err: &std::io::Error) -> p2::ErrorCode {
        tracing::warn!("tls error: {err:?}");
        p2::ErrorCode::TlsProtocolError
    }

    /// Optional hook to configure the error code for DNS errors.
    #[cfg(all(feature = "p2", feature = "default-send-request"))]
    fn p2_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p2::ErrorCode {
        tracing::warn!("dns lookup error: {err:?}");
        p2::ErrorCode::DnsError(p2::DnsErrorPayload {
            rcode: Some("invalid dns name".to_string()),
            info_code: None,
        })
    }

    /// Maximum number of bytes the implementation will copy out of the guest in
    /// a single write to an outgoing body's stream.
    #[cfg(feature = "p3")]
    fn p3_outgoing_body_chunk_size(&mut self) -> usize {
        crate::p3::DEFAULT_OUTGOING_BODY_CHUNK_SIZE
    }

    /// Optional hook to configure the error code for hyper errors.
    #[cfg(feature = "p3")]
    fn p3_error_from_hyper(&mut self, err: &hyper::Error) -> p3::ErrorCode {
        tracing::warn!("hyper error: {err:?}");
        p3::ErrorCode::HttpProtocolError
    }

    /// Optional hook to configure the error code for connect I/O errors.
    #[cfg(feature = "p3")]
    fn p3_error_from_connect(&mut self, err: &std::io::Error) -> p3::ErrorCode {
        tracing::warn!("connect error: {err:?}");
        p3::ErrorCode::ConnectionRefused
    }

    /// Optional hook to configure the error code for TLS I/O errors.
    #[cfg(feature = "p3")]
    fn p3_error_from_tls(&mut self, err: &std::io::Error) -> p3::ErrorCode {
        tracing::warn!("tls error: {err:?}");
        p3::ErrorCode::TlsProtocolError
    }

    /// Optional hook to configure the error code for DNS errors.
    #[cfg(all(feature = "p3", feature = "default-send-request"))]
    fn p3_error_from_dns(&mut self, err: &rustls::pki_types::InvalidDnsNameError) -> p3::ErrorCode {
        tracing::warn!("dns lookup error: {err:?}");
        p3::ErrorCode::DnsError(p3::DnsErrorPayload {
            rcode: Some("invalid dns name".to_string()),
            info_code: None,
        })
    }
}

/// Returns a value suitable for the `WasiHttpCtxView::hooks` field which has
/// the default behavior for `wasi:http`.
#[cfg(feature = "default-send-request")]
pub fn default_hooks() -> &'static mut dyn WasiHttpHooks {
    Default::default()
}

#[cfg(feature = "default-send-request")]
impl<'a> Default for &'a mut dyn WasiHttpHooks {
    fn default() -> Self {
        let x: &mut [(); 0] = &mut [];
        x
    }
}

#[doc(hidden)]
#[cfg(feature = "default-send-request")]
impl WasiHttpHooks for [(); 0] {}