wasi_experimental_http/
lib.rs

1use 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/// HTTP errors
15#[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
47// TODO(@radu-matei)
48//
49// This error is not really used in the public API.
50impl 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
74/// An HTTP response
75pub struct Response {
76    handle: raw::ResponseHandle,
77    pub status_code: StatusCode,
78}
79
80/// Automatically call `close` to remove the current handle
81/// when the response object goes out of scope.
82impl Drop for Response {
83    fn drop(&mut self) {
84        raw::close(self.handle).unwrap();
85    }
86}
87
88impl Response {
89    /// Read a response body in a streaming fashion.
90    /// `buf` is an arbitrary large buffer, that may be partially filled after each call.
91    /// The function returns the actual number of bytes that were written, and `0`
92    /// when the end of the stream has been reached.
93    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    /// Read the entire body until the end of the stream.
99    pub fn body_read_all(&mut self) -> Result<Vec<u8>, Error> {
100        // TODO(@radu-matei)
101        //
102        // Do we want to have configurable chunk sizes?
103        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    /// Get the value of the `name` header.
115    /// Returns `HttpError::HeaderNotFound` if no such header was found.
116    pub fn header_get(&self, name: String) -> Result<String, Error> {
117        let name = name;
118
119        // Set the initial capacity of the expected header value to 4 kilobytes.
120        // If the response value size is larger, double the capacity and
121        // attempt to read again, but only until reaching 64 kilobytes.
122        //
123        // This is to avoid a potentially malicious web server from returning a
124        // response header that would make the guest allocate all of its possible
125        // memory.
126        // The maximum is set to 64 kilobytes, as it is usually the maximum value
127        // known servers will allow until returning 413 Entity Too Large.
128        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    /// Get the entire response header map for a given request.
160    // If clients know the specific header key, they should use
161    // `header_get` to avoid allocating memory for the entire
162    // header map.
163    pub fn headers_get_all(&self) -> Result<HeaderMap, Error> {
164        // The fixed capacity for the header map is 64 kilobytes.
165        // If a server sends a header map that is larger than this,
166        // the client will return an error.
167        // The same note applies - most known servers will limit
168        // response headers to 64 kilobytes at most before returning
169        // 413 Entity Too Large.
170        //
171        // It might make sense to increase the size here in the same
172        // way it is done in `header_get`, if there are valid use
173        // cases where it is required.
174        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/// Send an HTTP request.
189/// The function returns a `Response` object, that includes the status,
190/// as well as methods to access the headers and the body.
191#[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
218/// Send an HTTP request and get a fully formed HTTP response.
219pub 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
255/// Encode a header map as a string.
256pub 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
278/// Decode a header map from a string.
279pub 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}