Skip to main content

wasmtime_wasi_http/p3/host/
types.rs

1use crate::FieldMap;
2use crate::p3::bindings::clocks::monotonic_clock::Duration;
3use crate::p3::bindings::http::types::{
4    ErrorCode, FieldName, FieldValue, Fields, HeaderError, Headers, Host, HostFields, HostRequest,
5    HostRequestOptions, HostRequestWithStore, HostResponse, HostResponseWithStore, Method, Request,
6    RequestOptions, RequestOptionsError, Response, Scheme, StatusCode, Trailers,
7};
8use crate::p3::body::Body;
9use crate::p3::{HeaderResult, HttpError, RequestOptionsResult};
10use crate::{WasiHttp, WasiHttpCtxView};
11use std::sync::Arc;
12use wasmtime::component::{Access, FutureReader, Resource, ResourceTable, StreamReader};
13use wasmtime::error::Context as _;
14
15fn get_fields<'a>(
16    table: &'a ResourceTable,
17    fields: &Resource<Fields>,
18) -> wasmtime::Result<&'a Fields> {
19    table
20        .get(&fields)
21        .context("failed to get fields from table")
22}
23
24fn get_fields_mut<'a>(
25    table: &'a mut ResourceTable,
26    fields: &Resource<Fields>,
27) -> HeaderResult<&'a mut Fields> {
28    table
29        .get_mut(&fields)
30        .context("failed to get fields from table")
31        .map_err(crate::p3::HeaderError::trap)
32}
33
34fn push_fields(table: &mut ResourceTable, fields: Fields) -> wasmtime::Result<Resource<Fields>> {
35    table.push(fields).context("failed to push fields to table")
36}
37
38fn delete_fields(table: &mut ResourceTable, fields: Resource<Fields>) -> wasmtime::Result<Fields> {
39    let mut fields = table
40        .delete(fields)
41        .context("failed to delete fields from table")?;
42    // When fields are passed by ownership to the host that flags them as
43    // immutable within `wasi:http`, and this semantically means that putting
44    // fields in a request, then getting them back out, will return an immutable
45    // view of the headers rather than mutable for example.
46    fields.set_immutable();
47    Ok(fields)
48}
49
50fn get_request<'a>(
51    table: &'a ResourceTable,
52    req: &Resource<Request>,
53) -> wasmtime::Result<&'a Request> {
54    table.get(req).context("failed to get request from table")
55}
56
57fn get_request_mut<'a>(
58    table: &'a mut ResourceTable,
59    req: &Resource<Request>,
60) -> wasmtime::Result<&'a mut Request> {
61    table
62        .get_mut(req)
63        .context("failed to get request from table")
64}
65
66fn get_response<'a>(
67    table: &'a ResourceTable,
68    res: &Resource<Response>,
69) -> wasmtime::Result<&'a Response> {
70    table.get(res).context("failed to get response from table")
71}
72
73fn get_response_mut<'a>(
74    table: &'a mut ResourceTable,
75    res: &Resource<Response>,
76) -> wasmtime::Result<&'a mut Response> {
77    table
78        .get_mut(res)
79        .context("failed to get response from table")
80}
81
82fn get_request_options<'a>(
83    table: &'a ResourceTable,
84    opts: &Resource<RequestOptions>,
85) -> wasmtime::Result<&'a RequestOptions> {
86    table
87        .get(opts)
88        .context("failed to get request options from table")
89}
90
91fn get_request_options_mut<'a>(
92    table: &'a mut ResourceTable,
93    opts: &Resource<RequestOptions>,
94) -> RequestOptionsResult<&'a mut RequestOptions> {
95    table
96        .get_mut(opts)
97        .context("failed to get request options from table")
98        .map_err(crate::p3::RequestOptionsError::trap)
99}
100
101fn push_request_options(
102    table: &mut ResourceTable,
103    opts: RequestOptions,
104) -> wasmtime::Result<Resource<RequestOptions>> {
105    table
106        .push(opts)
107        .context("failed to push request options to table")
108}
109
110fn delete_request_options(
111    table: &mut ResourceTable,
112    opts: Resource<RequestOptions>,
113) -> wasmtime::Result<RequestOptions> {
114    table
115        .delete(opts)
116        .context("failed to delete request options from table")
117}
118
119/// Parse an outgoing request `authority`, rejecting a malformed value.
120///
121/// `http::uri::Authority` accepts an authority whose port section is empty or
122/// non-numeric (for example `example.com:` or `example.com:abc`), so the port
123/// is validated here as well. A `:` inside an IPv6 literal host such as `[::1]`
124/// is part of the host rather than a port delimiter, so the port is only looked
125/// for after any closing bracket.
126fn parse_authority(authority: String) -> Result<http::uri::Authority, ()> {
127    let has_port = match authority.rfind(']') {
128        Some(i) => authority[i..].contains(':'),
129        None => authority.contains(':'),
130    };
131    let authority = http::uri::Authority::try_from(authority).map_err(|_| ())?;
132    if has_port && authority.port_u16().is_none() {
133        return Err(());
134    }
135    Ok(authority)
136}
137
138impl HostFields for WasiHttpCtxView<'_> {
139    fn new(&mut self) -> wasmtime::Result<Resource<Fields>> {
140        push_fields(self.table, FieldMap::new_mutable(self.ctx.field_size_limit))
141    }
142
143    fn from_list(
144        &mut self,
145        entries: Vec<(FieldName, FieldValue)>,
146    ) -> HeaderResult<Resource<Fields>> {
147        let mut fields = FieldMap::new_mutable(self.ctx.field_size_limit);
148        for (name, value) in entries {
149            fields.append(self.hooks, name, value)?;
150        }
151        let fields = push_fields(self.table, fields).map_err(crate::p3::HeaderError::trap)?;
152        Ok(fields)
153    }
154
155    fn get(
156        &mut self,
157        fields: Resource<Fields>,
158        name: FieldName,
159    ) -> wasmtime::Result<Vec<FieldValue>> {
160        let fields = get_fields(self.table, &fields)?;
161        Ok(fields
162            .get_all(name)
163            .into_iter()
164            .map(|val| val.as_bytes().into())
165            .collect())
166    }
167
168    fn has(&mut self, fields: Resource<Fields>, name: FieldName) -> wasmtime::Result<bool> {
169        let fields = get_fields(self.table, &fields)?;
170        Ok(fields.contains_key(name))
171    }
172
173    fn set(
174        &mut self,
175        fields: Resource<Fields>,
176        name: FieldName,
177        values: Vec<FieldValue>,
178    ) -> HeaderResult<()> {
179        get_fields_mut(self.table, &fields)?.set(self.hooks, name, values)?;
180        Ok(())
181    }
182
183    fn delete(&mut self, fields: Resource<Fields>, name: FieldName) -> HeaderResult<()> {
184        get_fields_mut(self.table, &fields)?.remove_all(self.hooks, name)?;
185        Ok(())
186    }
187
188    fn get_and_delete(
189        &mut self,
190        fields: Resource<Fields>,
191        name: FieldName,
192    ) -> HeaderResult<Vec<FieldValue>> {
193        let name = name.parse().or(Err(HeaderError::InvalidSyntax))?;
194        let values = get_fields_mut(self.table, &fields)?
195            .remove_all(self.hooks, name)?
196            .into_iter();
197        Ok(values.map(|value| value.as_bytes().into()).collect())
198    }
199
200    fn append(
201        &mut self,
202        fields: Resource<Fields>,
203        name: FieldName,
204        value: FieldValue,
205    ) -> HeaderResult<()> {
206        get_fields_mut(self.table, &fields)?.append(self.hooks, name, value)?;
207        Ok(())
208    }
209
210    fn copy_all(
211        &mut self,
212        fields: Resource<Fields>,
213    ) -> wasmtime::Result<Vec<(FieldName, FieldValue)>> {
214        let fields = get_fields(self.table, &fields)?;
215        let fields = fields
216            .iter()
217            .map(|(name, value)| (name.as_str().into(), value.as_bytes().into()))
218            .collect();
219        Ok(fields)
220    }
221
222    fn clone(&mut self, fields: Resource<Fields>) -> wasmtime::Result<Resource<Fields>> {
223        let mut fields = get_fields(self.table, &fields)?.clone();
224        fields.set_mutable(self.ctx.field_size_limit);
225        push_fields(self.table, fields)
226    }
227
228    fn drop(&mut self, fields: Resource<Fields>) -> wasmtime::Result<()> {
229        delete_fields(self.table, fields)?;
230        Ok(())
231    }
232}
233
234impl<T> HostRequestWithStore<T> for WasiHttp {
235    fn new(
236        mut store: Access<T, Self>,
237        headers: Resource<Headers>,
238        contents: Option<StreamReader<u8>>,
239        trailers: FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
240        options: Option<Resource<RequestOptions>>,
241    ) -> wasmtime::Result<(Resource<Request>, FutureReader<Result<(), ErrorCode>>)> {
242        let (body, body_result) = Body::new_guest(&mut store, contents, trailers)?;
243        let WasiHttpCtxView { table, .. } = store.get();
244        let headers = delete_fields(table, headers)?;
245        let options = options
246            .map(|options| delete_request_options(table, options))
247            .transpose()?;
248        let req = Request {
249            method: http::Method::GET,
250            scheme: None,
251            authority: None,
252            path_with_query: None,
253            headers,
254            options: options.map(Into::into),
255            body,
256        };
257        let req = table.push(req).context("failed to push request to table")?;
258        Ok((req, body_result))
259    }
260
261    fn consume_body(
262        mut store: Access<T, Self>,
263        req: Resource<Request>,
264        fut: FutureReader<Result<(), ErrorCode>>,
265    ) -> wasmtime::Result<(
266        StreamReader<u8>,
267        FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
268    )> {
269        let getter = store.getter();
270        let Request { body, .. } = store
271            .get()
272            .table
273            .delete(req)
274            .context("failed to delete request from table")?;
275        body.consume(store, fut, getter)
276    }
277
278    fn drop(mut store: Access<'_, T, Self>, req: Resource<Request>) -> wasmtime::Result<()> {
279        let Request { body, .. } = store
280            .get()
281            .table
282            .delete(req)
283            .context("failed to delete request from table")?;
284        body.drop(store)?;
285        Ok(())
286    }
287}
288
289impl HostRequest for WasiHttpCtxView<'_> {
290    fn get_method(&mut self, req: Resource<Request>) -> wasmtime::Result<Method> {
291        let Request { method, .. } = get_request(self.table, &req)?;
292        Ok(method.into())
293    }
294
295    fn set_method(
296        &mut self,
297        req: Resource<Request>,
298        method: Method,
299    ) -> wasmtime::Result<Result<(), ()>> {
300        let req = get_request_mut(self.table, &req)?;
301        let Ok(method) = method.try_into() else {
302            return Ok(Err(()));
303        };
304        req.method = method;
305        Ok(Ok(()))
306    }
307
308    fn get_path_with_query(&mut self, req: Resource<Request>) -> wasmtime::Result<Option<String>> {
309        let Request {
310            path_with_query, ..
311        } = get_request(self.table, &req)?;
312        Ok(path_with_query.as_ref().map(|pq| pq.as_str().into()))
313    }
314
315    fn set_path_with_query(
316        &mut self,
317        req: Resource<Request>,
318        path_with_query: Option<String>,
319    ) -> wasmtime::Result<Result<(), ()>> {
320        let req = get_request_mut(self.table, &req)?;
321        let Some(path_with_query) = path_with_query else {
322            req.path_with_query = None;
323            return Ok(Ok(()));
324        };
325        let Ok(path_with_query) = path_with_query.try_into() else {
326            return Ok(Err(()));
327        };
328        req.path_with_query = Some(path_with_query);
329        Ok(Ok(()))
330    }
331
332    fn get_scheme(&mut self, req: Resource<Request>) -> wasmtime::Result<Option<Scheme>> {
333        let Request { scheme, .. } = get_request(self.table, &req)?;
334        Ok(scheme.as_ref().map(Into::into))
335    }
336
337    fn set_scheme(
338        &mut self,
339        req: Resource<Request>,
340        scheme: Option<Scheme>,
341    ) -> wasmtime::Result<Result<(), ()>> {
342        let req = get_request_mut(self.table, &req)?;
343        let Some(scheme) = scheme else {
344            req.scheme = None;
345            return Ok(Ok(()));
346        };
347        let Ok(scheme) = scheme.try_into() else {
348            return Ok(Err(()));
349        };
350        req.scheme = Some(scheme);
351        Ok(Ok(()))
352    }
353
354    fn get_authority(&mut self, req: Resource<Request>) -> wasmtime::Result<Option<String>> {
355        let Request { authority, .. } = get_request(self.table, &req)?;
356        Ok(authority.as_ref().map(|auth| auth.as_str().into()))
357    }
358
359    fn set_authority(
360        &mut self,
361        req: Resource<Request>,
362        authority: Option<String>,
363    ) -> wasmtime::Result<Result<(), ()>> {
364        let req = get_request_mut(self.table, &req)?;
365        let Some(authority) = authority else {
366            req.authority = None;
367            return Ok(Ok(()));
368        };
369        let Ok(authority) = parse_authority(authority) else {
370            return Ok(Err(()));
371        };
372        req.authority = Some(authority);
373        Ok(Ok(()))
374    }
375
376    fn get_options(
377        &mut self,
378        req: Resource<Request>,
379    ) -> wasmtime::Result<Option<Resource<RequestOptions>>> {
380        let Request { options, .. } = get_request(self.table, &req)?;
381        if let Some(options) = options {
382            let options = push_request_options(
383                self.table,
384                RequestOptions::new_immutable(Arc::clone(options)),
385            )?;
386            Ok(Some(options))
387        } else {
388            Ok(None)
389        }
390    }
391
392    fn get_headers(&mut self, req: Resource<Request>) -> wasmtime::Result<Resource<Headers>> {
393        let Request { headers, .. } = get_request(self.table, &req)?;
394        push_fields(self.table, headers.clone())
395    }
396}
397
398impl HostRequestOptions for WasiHttpCtxView<'_> {
399    fn new(&mut self) -> wasmtime::Result<Resource<RequestOptions>> {
400        push_request_options(self.table, RequestOptions::new_mutable_default())
401    }
402
403    fn get_connect_timeout(
404        &mut self,
405        opts: Resource<RequestOptions>,
406    ) -> wasmtime::Result<Option<Duration>> {
407        let opts = get_request_options(self.table, &opts)?;
408        let Some(connect_timeout) = opts.connect_timeout else {
409            return Ok(None);
410        };
411        let ns = connect_timeout.as_nanos();
412        let ns = Duration::try_from(ns)
413            .context("connect timeout duration nanoseconds do not fit in u64")?;
414        Ok(Some(ns))
415    }
416
417    fn set_connect_timeout(
418        &mut self,
419        opts: Resource<RequestOptions>,
420        duration: Option<Duration>,
421    ) -> RequestOptionsResult<()> {
422        let opts = get_request_options_mut(self.table, &opts)?;
423        let opts = opts.get_mut().ok_or(RequestOptionsError::Immutable)?;
424        opts.connect_timeout = duration.map(core::time::Duration::from_nanos);
425        Ok(())
426    }
427
428    fn get_first_byte_timeout(
429        &mut self,
430        opts: Resource<RequestOptions>,
431    ) -> wasmtime::Result<Option<Duration>> {
432        let opts = get_request_options(self.table, &opts)?;
433        let Some(first_byte_timeout) = opts.first_byte_timeout else {
434            return Ok(None);
435        };
436        let ns = first_byte_timeout.as_nanos();
437        let ns = Duration::try_from(ns)
438            .context("first byte timeout duration nanoseconds do not fit in u64")?;
439        Ok(Some(ns))
440    }
441
442    fn set_first_byte_timeout(
443        &mut self,
444        opts: Resource<RequestOptions>,
445        duration: Option<Duration>,
446    ) -> RequestOptionsResult<()> {
447        let opts = get_request_options_mut(self.table, &opts)?;
448        let opts = opts.get_mut().ok_or(RequestOptionsError::Immutable)?;
449        opts.first_byte_timeout = duration.map(core::time::Duration::from_nanos);
450        Ok(())
451    }
452
453    fn get_between_bytes_timeout(
454        &mut self,
455        opts: Resource<RequestOptions>,
456    ) -> wasmtime::Result<Option<Duration>> {
457        let opts = get_request_options(self.table, &opts)?;
458        let Some(between_bytes_timeout) = opts.between_bytes_timeout else {
459            return Ok(None);
460        };
461        let ns = between_bytes_timeout.as_nanos();
462        let ns = Duration::try_from(ns)
463            .context("between bytes timeout duration nanoseconds do not fit in u64")?;
464        Ok(Some(ns))
465    }
466
467    fn set_between_bytes_timeout(
468        &mut self,
469        opts: Resource<RequestOptions>,
470        duration: Option<Duration>,
471    ) -> RequestOptionsResult<()> {
472        let opts = get_request_options_mut(self.table, &opts)?;
473        let opts = opts.get_mut().ok_or(RequestOptionsError::Immutable)?;
474        opts.between_bytes_timeout = duration.map(core::time::Duration::from_nanos);
475        Ok(())
476    }
477
478    fn clone(
479        &mut self,
480        opts: Resource<RequestOptions>,
481    ) -> wasmtime::Result<Resource<RequestOptions>> {
482        let opts = get_request_options(self.table, &opts)?;
483        push_request_options(self.table, RequestOptions::new_mutable(Arc::clone(opts)))
484    }
485
486    fn drop(&mut self, opts: Resource<RequestOptions>) -> wasmtime::Result<()> {
487        delete_request_options(self.table, opts)?;
488        Ok(())
489    }
490}
491
492impl<T> HostResponseWithStore<T> for WasiHttp {
493    fn new(
494        mut store: Access<T, Self>,
495        headers: Resource<Headers>,
496        contents: Option<StreamReader<u8>>,
497        trailers: FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
498    ) -> wasmtime::Result<(Resource<Response>, FutureReader<Result<(), ErrorCode>>)> {
499        let (body, body_result) = Body::new_guest(&mut store, contents, trailers)?;
500        let WasiHttpCtxView { table, .. } = store.get();
501        let headers = delete_fields(table, headers)?;
502        let res = Response {
503            status: http::StatusCode::OK,
504            headers,
505            body,
506        };
507        let res = table
508            .push(res)
509            .context("failed to push response to table")?;
510        Ok((res, body_result))
511    }
512
513    fn consume_body(
514        mut store: Access<T, Self>,
515        res: Resource<Response>,
516        fut: FutureReader<Result<(), ErrorCode>>,
517    ) -> wasmtime::Result<(
518        StreamReader<u8>,
519        FutureReader<Result<Option<Resource<Trailers>>, ErrorCode>>,
520    )> {
521        let getter = store.getter();
522        let Response { body, .. } = store
523            .get()
524            .table
525            .delete(res)
526            .context("failed to delete response from table")?;
527        body.consume(store, fut, getter)
528    }
529
530    fn drop(mut store: Access<'_, T, Self>, res: Resource<Response>) -> wasmtime::Result<()> {
531        let Response { body, .. } = store
532            .get()
533            .table
534            .delete(res)
535            .context("failed to delete response from table")?;
536        body.drop(store)?;
537        Ok(())
538    }
539}
540
541impl HostResponse for WasiHttpCtxView<'_> {
542    fn get_status_code(&mut self, res: Resource<Response>) -> wasmtime::Result<StatusCode> {
543        let res = get_response(self.table, &res)?;
544        Ok(res.status.into())
545    }
546
547    fn set_status_code(
548        &mut self,
549        res: Resource<Response>,
550        status_code: StatusCode,
551    ) -> wasmtime::Result<Result<(), ()>> {
552        let res = get_response_mut(self.table, &res)?;
553        match http::StatusCode::from_u16(status_code) {
554            Ok(status) if matches!(status_code, 100..=599) => {
555                res.status = status;
556                Ok(Ok(()))
557            }
558            _ => Ok(Err(())),
559        }
560    }
561
562    fn get_headers(&mut self, res: Resource<Response>) -> wasmtime::Result<Resource<Headers>> {
563        let Response { headers, .. } = get_response(self.table, &res)?;
564        push_fields(self.table, headers.clone())
565    }
566}
567
568impl Host for WasiHttpCtxView<'_> {
569    fn convert_error_code(&mut self, error: HttpError) -> wasmtime::Result<ErrorCode> {
570        error.downcast()
571    }
572
573    fn convert_header_error(
574        &mut self,
575        error: crate::p3::HeaderError,
576    ) -> wasmtime::Result<HeaderError> {
577        error.downcast()
578    }
579
580    fn convert_request_options_error(
581        &mut self,
582        error: crate::p3::RequestOptionsError,
583    ) -> wasmtime::Result<RequestOptionsError> {
584        error.downcast()
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    #[test]
591    fn authority_accepts_ipv6_and_validates_ports() {
592        use super::parse_authority;
593
594        // Host names and IPv4 literals, with and without an explicit port.
595        assert!(parse_authority("example.com".into()).is_ok());
596        assert!(parse_authority("example.com:443".into()).is_ok());
597        assert!(parse_authority("127.0.0.1:80".into()).is_ok());
598
599        // Bracketed IPv6 literals: the colons belong to the host, so a missing
600        // port must still be accepted and not mistaken for an empty port.
601        assert!(parse_authority("[::1]".into()).is_ok());
602        assert!(parse_authority("[2001:db8::1]".into()).is_ok());
603        assert!(parse_authority("[::1]:443".into()).is_ok());
604
605        // When a port section is present it must be a valid number.
606        assert!(parse_authority("example.com:".into()).is_err());
607        assert!(parse_authority("example.com:abc".into()).is_err());
608    }
609}