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,
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 getter: fn(&mut T) -> WasiHttpCtxView<'_>,
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,
186 )
187 .map_err(HttpError::trap)?
188 .boxed_unsync(),
189 Body::Host { body, result_tx } => {
190 if let Some(limit) = content_length {
191 let (http_result_tx, http_result_rx) = oneshot::channel();
192 _ = result_tx.send(Box::new(async move {
193 if let Ok(err) = http_result_rx.await {
194 return Err(err);
195 };
196 fut.await
197 }));
198 body.with_content_length(limit, http_result_tx, Error::HttpRequestBodySize)
199 .boxed_unsync()
200 } else {
201 _ = result_tx.send(Box::new(fut));
202 body
203 }
204 }
205 };
206 let mut store = store.as_context_mut();
207 let WasiHttpCtxView { hooks, ctx, .. } = getter(store.data_mut());
208 headers.set_mutable(ctx.field_size_limit);
209 if hooks.set_host_header() {
210 let host = if let Some(authority) = authority.as_ref() {
211 HeaderValue::try_from(authority.as_str())
212 .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?
213 } else {
214 HeaderValue::from_static("")
215 };
216 headers.append_raw(HOST, host).map_err(HttpError::trap)?;
217 }
218 let scheme = match scheme {
219 None => hooks.default_scheme().ok_or(ErrorCode::HttpProtocolError)?,
220 Some(scheme) if hooks.is_supported_scheme(&scheme) => scheme,
221 Some(..) => return Err(ErrorCode::HttpProtocolError.into()),
222 };
223 let mut uri = Uri::builder().scheme(scheme);
224 if let Some(authority) = authority {
225 uri = uri.authority(authority)
226 };
227 if let Some(path_with_query) = path_with_query {
228 uri = uri.path_and_query(path_with_query)
229 };
230 let uri = uri.build().map_err(|err| {
231 debug!(?err, "failed to build request URI");
232 ErrorCode::HttpRequestUriInvalid
233 })?;
234 let mut req = http::Request::builder();
235 *req.headers_mut().unwrap() = headers.into();
236 let req = req
237 .method(method)
238 .uri(uri)
239 .body(body)
240 .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?;
241 Ok((req, options))
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::WasiHttpCtx;
249 use core::future::Future;
250 use core::pin::pin;
251 use core::str::FromStr;
252 use core::task::{Context, Poll, Waker};
253 use http_body_util::{BodyExt, Empty, Full};
254 use wasmtime::Result;
255 use wasmtime::{Engine, Store};
256 use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
257
258 struct TestCtx {
259 table: ResourceTable,
260 wasi: WasiCtx,
261 http: WasiHttpCtx,
262 }
263
264 impl TestCtx {
265 fn new() -> Self {
266 Self {
267 table: ResourceTable::default(),
268 wasi: WasiCtxBuilder::new().build(),
269 http: Default::default(),
270 }
271 }
272 }
273
274 impl WasiView for TestCtx {
275 fn ctx(&mut self) -> WasiCtxView<'_> {
276 WasiCtxView {
277 ctx: &mut self.wasi,
278 table: &mut self.table,
279 }
280 }
281 }
282
283 impl WasiHttpView for TestCtx {
284 fn http(&mut self) -> WasiHttpCtxView<'_> {
285 WasiHttpCtxView {
286 ctx: &mut self.http,
287 table: &mut self.table,
288 hooks: crate::default_hooks(),
289 }
290 }
291 }
292
293 #[tokio::test]
294 async fn test_request_into_http_schemes() -> Result<()> {
295 let schemes = vec![Some(Scheme::HTTP), Some(Scheme::HTTPS), None];
296 let engine = Engine::default();
297
298 for scheme in schemes {
299 let (req, fut) = Request::new(
300 Method::POST,
301 scheme.clone(),
302 Some(Authority::from_static("example.com")),
303 Some(PathAndQuery::from_static("/path?query=1")),
304 FieldMap::default(),
305 None,
306 Full::new(Bytes::from_static(b"body")).boxed_unsync(),
307 );
308 let mut store = Store::new(&engine, TestCtx::new());
309 let (http_req, options) = req.into_http(&mut store, async { Ok(()) }).unwrap();
310 assert_eq!(options, None);
311 assert_eq!(http_req.method(), Method::POST);
312 let expected_scheme = scheme.unwrap_or(Scheme::HTTPS); assert_eq!(
314 http_req.uri(),
315 &http::Uri::from_str(&format!(
316 "{}://example.com/path?query=1",
317 expected_scheme.as_str()
318 ))
319 .unwrap()
320 );
321 let body_bytes = http_req.into_body().collect().await?;
322 assert_eq!(body_bytes.to_bytes(), b"body".as_slice());
323 let mut cx = Context::from_waker(Waker::noop());
324 let result = pin!(fut).poll(&mut cx);
325 assert!(matches!(result, Poll::Ready(Ok(()))));
326 }
327
328 Ok(())
329 }
330
331 #[tokio::test]
332 async fn test_request_into_http_uri_error() -> Result<()> {
333 let (req, fut) = Request::new(
334 Method::GET,
335 Some(Scheme::HTTP),
336 Some(Authority::from_static("example.com")),
337 None, FieldMap::default(),
339 None,
340 Empty::new().boxed_unsync(),
341 );
342 let mut store = Store::new(&Engine::default(), TestCtx::new());
343 let result = req
344 .into_http(&mut store, async {
345 Err(Error::InternalError(Some("uh oh".to_string())))
346 })
347 .unwrap_err();
348 assert!(matches!(
349 result.downcast()?,
350 ErrorCode::HttpRequestUriInvalid,
351 ));
352 let mut cx = Context::from_waker(Waker::noop());
353 let result = pin!(fut).poll(&mut cx);
354 assert!(matches!(
355 result,
356 Poll::Ready(Err(Error::InternalError(Some(_))))
357 ));
358
359 Ok(())
360 }
361}