1use crate::p3::bindings::http::types::ErrorCode;
2use crate::p3::body::{Body, BodyExt as _, GuestBody};
3use crate::p3::{HttpError, HttpResult};
4use crate::{
5 Error, FieldMap, RequestOptions, WasiHttpCtxView, WasiHttpHooks, WasiHttpView,
6 get_content_length,
7};
8use bytes::Bytes;
9use http::header::HOST;
10use http::uri::{Authority, PathAndQuery, Scheme};
11use http::{HeaderValue, Method, Uri};
12use http_body_util::BodyExt as _;
13use http_body_util::combinators::UnsyncBoxBody;
14use std::sync::Arc;
15use tokio::sync::oneshot;
16use tracing::debug;
17use wasmtime::AsContextMut;
18
19pub struct Request {
21 pub method: Method,
23 pub scheme: Option<Scheme>,
25 pub authority: Option<Authority>,
27 pub path_with_query: Option<PathAndQuery>,
29 pub headers: FieldMap,
31 pub options: Option<Arc<RequestOptions>>,
33 pub(crate) body: Body,
35}
36
37impl Request {
38 pub fn new<B>(
45 method: Method,
46 scheme: Option<Scheme>,
47 authority: Option<Authority>,
48 path_with_query: Option<PathAndQuery>,
49 headers: impl Into<FieldMap>,
50 options: Option<Arc<RequestOptions>>,
51 body: B,
52 ) -> (
53 Self,
54 impl Future<Output = Result<(), Error>> + Send + 'static,
55 )
56 where
57 B: http_body::Body<Data = Bytes> + Send + 'static,
58 B::Error: Into<Error>,
59 {
60 let (tx, rx) = oneshot::channel();
61 (
62 Self {
63 method,
64 scheme,
65 authority,
66 path_with_query,
67 headers: headers.into(),
68 options,
69 body: Body::Host {
70 body: body.map_err(Into::into).boxed_unsync(),
71 result_tx: tx,
72 },
73 },
74 async {
75 let Ok(fut) = rx.await else { return Ok(()) };
76 Box::into_pin(fut).await
77 },
78 )
79 }
80
81 pub fn from_http<T>(
88 hooks: &mut dyn WasiHttpHooks,
89 req: http::Request<T>,
90 ) -> (
91 Self,
92 impl Future<Output = Result<(), Error>> + Send + 'static + use<T>,
93 )
94 where
95 T: http_body::Body<Data = Bytes> + Send + 'static,
96 T::Error: Into<Error>,
97 {
98 let (
99 http::request::Parts {
100 method,
101 uri,
102 headers,
103 ..
104 },
105 body,
106 ) = req.into_parts();
107 let http::uri::Parts {
108 scheme,
109 authority,
110 path_and_query,
111 ..
112 } = uri.into_parts();
113 Self::new(
114 method,
115 scheme,
116 authority,
117 path_and_query,
118 FieldMap::new_immutable(hooks, headers),
119 None,
120 body,
121 )
122 }
123
124 pub fn into_http<T: WasiHttpView + 'static>(
130 self,
131 store: impl AsContextMut<Data = T>,
132 fut: impl Future<Output = Result<(), Error>> + Send + 'static,
133 ) -> HttpResult<(
134 http::Request<UnsyncBoxBody<Bytes, Error>>,
135 Option<Arc<RequestOptions>>,
136 )> {
137 self.into_http_with_getter(store, fut, T::http)
138 }
139
140 pub fn into_http_with_getter<T: 'static>(
142 self,
143 mut store: impl AsContextMut<Data = T>,
144 fut: impl Future<Output = Result<(), Error>> + Send + 'static,
145 mut getter: impl FnMut(&mut T) -> WasiHttpCtxView<'_> + Clone + Unpin + Send + 'static,
146 ) -> HttpResult<(
147 http::Request<UnsyncBoxBody<Bytes, Error>>,
148 Option<Arc<RequestOptions>>,
149 )> {
150 let Request {
151 method,
152 scheme,
153 authority,
154 path_with_query,
155 mut headers,
156 options,
157 body,
158 } = self;
159 let content_length = match get_content_length(&headers) {
161 Ok(content_length) => content_length,
162 Err(err) => {
163 body.drop(&mut store).map_err(HttpError::trap)?;
164 return Err(ErrorCode::InternalError(Some(format!("{err:#}"))).into());
165 }
166 };
167 let body = match body {
173 Body::Guest {
174 contents_rx,
175 trailers_rx,
176 result_tx,
177 } => GuestBody::new(
178 &mut store,
179 contents_rx,
180 trailers_rx,
181 result_tx,
182 fut,
183 content_length,
184 ErrorCode::HttpRequestBodySize,
185 getter.clone(),
186 )
187 .map_err(HttpError::trap)?
188 .boxed_unsync(),
189 Body::Host { body, result_tx } => {
190 let is_forbidden = getter(store.as_context_mut().data_mut())
191 .hooks
192 .forbidden_header_filter();
193 let body = body.with_filtered_trailers(is_forbidden).boxed_unsync();
194 if let Some(limit) = content_length {
195 let (http_result_tx, http_result_rx) = oneshot::channel();
196 _ = result_tx.send(Box::new(async move {
197 if let Ok(err) = http_result_rx.await {
198 return Err(err);
199 };
200 fut.await
201 }));
202 body.with_content_length(limit, http_result_tx, Error::HttpRequestBodySize)
203 .boxed_unsync()
204 } else {
205 _ = result_tx.send(Box::new(fut));
206 body
207 }
208 }
209 };
210 let mut store = store.as_context_mut();
211 let WasiHttpCtxView { hooks, ctx, .. } = getter(store.data_mut());
212 headers.set_mutable(ctx.field_size_limit);
213 if hooks.set_host_header() {
214 let host = if let Some(authority) = authority.as_ref() {
215 HeaderValue::try_from(authority.as_str())
216 .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?
217 } else {
218 HeaderValue::from_static("")
219 };
220 headers.append_raw(HOST, host).map_err(HttpError::trap)?;
221 }
222 let scheme = match scheme {
223 None => hooks.default_scheme().ok_or(ErrorCode::HttpProtocolError)?,
224 Some(scheme) if hooks.is_supported_scheme(&scheme) => scheme,
225 Some(..) => return Err(ErrorCode::HttpProtocolError.into()),
226 };
227 let mut uri = Uri::builder().scheme(scheme);
228 if let Some(authority) = authority {
229 uri = uri.authority(authority)
230 };
231 if let Some(path_with_query) = path_with_query {
232 uri = uri.path_and_query(path_with_query)
233 };
234 let uri = uri.build().map_err(|err| {
235 debug!(?err, "failed to build request URI");
236 ErrorCode::HttpRequestUriInvalid
237 })?;
238 let mut req = http::Request::builder();
239 *req.headers_mut().unwrap() = headers.into();
240 let req = req
241 .method(method)
242 .uri(uri)
243 .body(body)
244 .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?;
245 Ok((req, options))
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::WasiHttpCtx;
253 use core::future::Future;
254 use core::pin::pin;
255 use core::str::FromStr;
256 use core::task::{Context, Poll, Waker};
257 use http_body_util::{BodyExt, Empty, Full};
258 use wasmtime::Result;
259 use wasmtime::{Engine, Store};
260 use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
261
262 struct TestCtx {
263 table: ResourceTable,
264 wasi: WasiCtx,
265 http: WasiHttpCtx,
266 }
267
268 impl TestCtx {
269 fn new() -> Self {
270 Self {
271 table: ResourceTable::default(),
272 wasi: WasiCtxBuilder::new().build(),
273 http: Default::default(),
274 }
275 }
276 }
277
278 impl WasiView for TestCtx {
279 fn ctx(&mut self) -> WasiCtxView<'_> {
280 WasiCtxView {
281 ctx: &mut self.wasi,
282 table: &mut self.table,
283 }
284 }
285 }
286
287 impl WasiHttpView for TestCtx {
288 fn http(&mut self) -> WasiHttpCtxView<'_> {
289 WasiHttpCtxView {
290 ctx: &mut self.http,
291 table: &mut self.table,
292 hooks: crate::default_hooks(),
293 }
294 }
295 }
296
297 #[tokio::test]
298 async fn test_request_into_http_schemes() -> Result<()> {
299 let schemes = vec![Some(Scheme::HTTP), Some(Scheme::HTTPS), None];
300 let engine = Engine::default();
301
302 for scheme in schemes {
303 let (req, fut) = Request::new(
304 Method::POST,
305 scheme.clone(),
306 Some(Authority::from_static("example.com")),
307 Some(PathAndQuery::from_static("/path?query=1")),
308 FieldMap::default(),
309 None,
310 Full::new(Bytes::from_static(b"body")).boxed_unsync(),
311 );
312 let mut store = Store::new(&engine, TestCtx::new());
313 let (http_req, options) = req.into_http(&mut store, async { Ok(()) }).unwrap();
314 assert_eq!(options, None);
315 assert_eq!(http_req.method(), Method::POST);
316 let expected_scheme = scheme.unwrap_or(Scheme::HTTPS); assert_eq!(
318 http_req.uri(),
319 &http::Uri::from_str(&format!(
320 "{}://example.com/path?query=1",
321 expected_scheme.as_str()
322 ))
323 .unwrap()
324 );
325 let body_bytes = http_req.into_body().collect().await?;
326 assert_eq!(body_bytes.to_bytes(), b"body".as_slice());
327 let mut cx = Context::from_waker(Waker::noop());
328 let result = pin!(fut).poll(&mut cx);
329 assert!(matches!(result, Poll::Ready(Ok(()))));
330 }
331
332 Ok(())
333 }
334
335 #[tokio::test]
336 async fn test_request_into_http_uri_error() -> Result<()> {
337 let (req, fut) = Request::new(
338 Method::GET,
339 Some(Scheme::HTTP),
340 Some(Authority::from_static("example.com")),
341 None, FieldMap::default(),
343 None,
344 Empty::new().boxed_unsync(),
345 );
346 let mut store = Store::new(&Engine::default(), TestCtx::new());
347 let result = req
348 .into_http(&mut store, async {
349 Err(Error::InternalError(Some("uh oh".to_string())))
350 })
351 .unwrap_err();
352 assert!(matches!(
353 result.downcast()?,
354 ErrorCode::HttpRequestUriInvalid,
355 ));
356 let mut cx = Context::from_waker(Waker::noop());
357 let result = pin!(fut).poll(&mut cx);
358 assert!(matches!(
359 result,
360 Poll::Ready(Err(Error::InternalError(Some(_))))
361 ));
362
363 Ok(())
364 }
365}