lapce_wasi_experimental_http/
lib.rs1use anyhow::{Context, Error};
2use bytes::Bytes;
3use http::{self, header::HeaderName, HeaderMap, HeaderValue, Request, StatusCode};
4use std::{
5 convert::{TryFrom, TryInto},
6 str::FromStr,
7};
8
9#[allow(dead_code)]
10#[allow(clippy::mut_from_ref)]
11#[allow(clippy::too_many_arguments)]
12pub(crate) mod raw;
13
14#[derive(Debug, thiserror::Error)]
16pub enum HttpError {
17 #[error("Invalid handle")]
18 InvalidHandle,
19 #[error("Memory not found")]
20 MemoryNotFound,
21 #[error("Memory access error")]
22 MemoryAccessError,
23 #[error("Buffer too small")]
24 BufferTooSmall,
25 #[error("Header not found")]
26 HeaderNotFound,
27 #[error("UTF-8 error")]
28 Utf8Error,
29 #[error("Destination not allowed")]
30 DestinationNotAllowed,
31 #[error("Invalid method")]
32 InvalidMethod,
33 #[error("Invalid encoding")]
34 InvalidEncoding,
35 #[error("Invalid URL")]
36 InvalidUrl,
37 #[error("HTTP error")]
38 RequestError,
39 #[error("Runtime error")]
40 RuntimeError,
41 #[error("Too many sessions")]
42 TooManySessions,
43 #[error("Unknown WASI error")]
44 UnknownError,
45}
46
47impl From<raw::Error> for HttpError {
51 fn from(e: raw::Error) -> Self {
52 match e {
53 raw::Error::WasiError(errno) => match errno {
54 1 => HttpError::InvalidHandle,
55 2 => HttpError::MemoryNotFound,
56 3 => HttpError::MemoryAccessError,
57 4 => HttpError::BufferTooSmall,
58 5 => HttpError::HeaderNotFound,
59 6 => HttpError::Utf8Error,
60 7 => HttpError::DestinationNotAllowed,
61 8 => HttpError::InvalidMethod,
62 9 => HttpError::InvalidEncoding,
63 10 => HttpError::InvalidUrl,
64 11 => HttpError::RequestError,
65 12 => HttpError::RuntimeError,
66 13 => HttpError::TooManySessions,
67
68 _ => HttpError::UnknownError,
69 },
70 }
71 }
72}
73
74pub struct Response {
76 handle: raw::ResponseHandle,
77 pub status_code: StatusCode,
78}
79
80impl Drop for Response {
83 fn drop(&mut self) {
84 raw::close(self.handle).unwrap();
85 }
86}
87
88impl Response {
89 pub fn body_read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
94 let read = raw::body_read(self.handle, buf.as_mut_ptr(), buf.len())?;
95 Ok(read)
96 }
97
98 pub fn body_read_all(&mut self) -> Result<Vec<u8>, Error> {
100 let mut chunk = [0u8; 4096];
104 let mut v = vec![];
105 loop {
106 let read = self.body_read(&mut chunk)?;
107 if read == 0 {
108 return Ok(v);
109 }
110 v.extend_from_slice(&chunk[0..read]);
111 }
112 }
113
114 pub fn header_get(&self, name: String) -> Result<String, Error> {
117 let name = name;
118
119 let mut capacity = 4 * 1024;
129 let max_capacity: usize = 64 * 1024;
130
131 loop {
132 let mut buf = vec![0u8; capacity];
133 match raw::header_get(
134 self.handle,
135 name.as_ptr(),
136 name.len(),
137 buf.as_mut_ptr(),
138 buf.len(),
139 ) {
140 Ok(written) => {
141 buf.truncate(written);
142 return Ok(String::from_utf8(buf)?);
143 }
144 Err(e) => match Into::<HttpError>::into(e) {
145 HttpError::BufferTooSmall => {
146 if capacity < max_capacity {
147 capacity *= 2;
148 continue;
149 } else {
150 return Err(e.into());
151 }
152 }
153 _ => return Err(e.into()),
154 },
155 };
156 }
157 }
158
159 pub fn headers_get_all(&self) -> Result<HeaderMap, Error> {
164 let capacity = 64 * 1024;
175 let mut buf = vec![0u8; capacity];
176
177 match raw::headers_get_all(self.handle, buf.as_mut_ptr(), buf.len()) {
178 Ok(written) => {
179 buf.truncate(written);
180 let str = String::from_utf8(buf)?;
181 Ok(string_to_header_map(&str)?)
182 }
183 Err(e) => Err(e.into()),
184 }
185 }
186}
187
188#[tracing::instrument]
192pub fn request(req: Request<Option<Bytes>>) -> Result<Response, Error> {
193 let url = req.uri().to_string();
194 tracing::debug!(%url, headers = ?req.headers(), "performing http request using wasmtime function");
195
196 let headers = header_map_to_string(req.headers())?;
197 let method = req.method().as_str().to_string();
198 let body = match req.body() {
199 None => Default::default(),
200 Some(body) => body.as_ref(),
201 };
202 let (status_code, handle) = raw::req(
203 url.as_ptr(),
204 url.len(),
205 method.as_ptr(),
206 method.len(),
207 headers.as_ptr(),
208 headers.len(),
209 body.as_ptr(),
210 body.len(),
211 )?;
212 Ok(Response {
213 handle,
214 status_code: StatusCode::from_u16(status_code)?,
215 })
216}
217
218pub fn send_request(
220 req: http::Request<Option<Bytes>>,
221) -> Result<http::Response<Option<Bytes>>, Error> {
222 request(req)?.try_into()
223}
224
225impl TryFrom<Response> for http::Response<Option<Bytes>> {
226 type Error = anyhow::Error;
227
228 fn try_from(outbound_res: Response) -> Result<Self, Self::Error> {
229 let mut outbound_res = outbound_res;
230 let status = outbound_res.status_code.as_u16();
231 let headers = outbound_res.headers_get_all()?;
232 let body = Some(Bytes::from(outbound_res.body_read_all()?));
233
234 let mut res = http::Response::builder().status(status);
235 append_response_headers(&mut res, &headers)?;
236 Ok(res.body(body)?)
237 }
238}
239
240fn append_response_headers(
241 http_res: &mut http::response::Builder,
242 hm: &HeaderMap,
243) -> Result<(), Error> {
244 let headers = http_res
245 .headers_mut()
246 .context("error building the response headers")?;
247
248 for (k, v) in hm {
249 headers.insert(k, v.clone());
250 }
251
252 Ok(())
253}
254
255pub fn header_map_to_string(hm: &HeaderMap) -> Result<String, Error> {
257 let mut res = String::new();
258 for (name, value) in hm
259 .iter()
260 .map(|(name, value)| (name.as_str(), std::str::from_utf8(value.as_bytes())))
261 {
262 let value = value?;
263 anyhow::ensure!(
264 !name
265 .chars()
266 .any(|x| x.is_control() || "(),/:;<=>?@[\\]{}".contains(x)),
267 "Invalid header name"
268 );
269 anyhow::ensure!(
270 !value.chars().any(|x| x.is_control()),
271 "Invalid header value"
272 );
273 res.push_str(&format!("{}:{}\n", name, value));
274 }
275 Ok(res)
276}
277
278pub fn string_to_header_map(s: &str) -> Result<HeaderMap, Error> {
280 let mut headers = HeaderMap::new();
281 for entry in s.lines() {
282 let mut parts = entry.splitn(2, ':');
283 #[allow(clippy::or_fun_call)]
284 let k = parts.next().ok_or(anyhow::format_err!(
285 "Invalid serialized header: [{}]",
286 entry
287 ))?;
288 let v = parts.next().unwrap();
289 headers.insert(HeaderName::from_str(k)?, HeaderValue::from_str(v)?);
290 }
291 Ok(headers)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use http::{HeaderMap, HeaderValue};
298
299 #[test]
300 fn test_header_map_to_string() {
301 let mut hm = HeaderMap::new();
302 hm.insert("custom-header", HeaderValue::from_static("custom-value"));
303 hm.insert("custom-header2", HeaderValue::from_static("custom-value2"));
304 let str = header_map_to_string(&hm).unwrap();
305 assert_eq!(
306 "custom-header:custom-value\ncustom-header2:custom-value2\n",
307 str
308 );
309 }
310}