acril_http/client/
mod.rs

1//! Process HTTP connections on the client.
2
3use http_types::{Request, Response, StatusCode};
4
5#[cfg(not(target_arch = "wasm32"))]
6mod decode;
7#[cfg(not(target_arch = "wasm32"))]
8mod encode;
9
10#[cfg(not(target_arch = "wasm32"))]
11pub use decode::decode;
12#[cfg(not(target_arch = "wasm32"))]
13pub use encode::Encoder;
14use async_tls::TlsConnector;
15use async_std::net::TcpStream;
16
17use futures::io::{AsyncRead as Read, AsyncWrite as Write, self};
18
19#[cfg(not(target_arch = "wasm32"))]
20async fn native_connect<RW>(mut stream: RW, req: Request) -> http_types::Result<Response>
21where
22    RW: Read + Write + Send + Sync + Unpin + 'static,
23{
24    let mut req = Encoder::new(req);
25    log::trace!("> {:?}", &req);
26
27    io::copy(&mut req, &mut stream).await?;
28
29    let res = decode(stream).await?;
30    log::trace!("< {:?}", &res);
31
32    Ok(res)
33}
34
35/// Opens an HTTP/1.1 connection to a remote host.
36pub async fn connect(req: Request) -> http_types::Result<Response> {
37    #[cfg(target_arch = "wasm32")]
38    {
39        use futures::prelude::*;
40        use send_wrapper::SendWrapper;
41        SendWrapper::new(async move {
42            let req: fetch::Request = fetch::Request::new(req).await?;
43            let mut res = req.send().await?;
44
45            let body = res.body_bytes();
46            let mut response =
47                Response::new(http_types::StatusCode::try_from(res.status()).unwrap());
48            response.set_body(Body::from(body));
49            for (name, value) in res.headers() {
50                let name: http_types::headers::HeaderName = name.parse().unwrap();
51                response.append_header(&name, value);
52            }
53
54            Ok(response)
55        })
56        .await
57    }
58
59    #[cfg(not(target_arch = "wasm32"))]
60    if req.url().scheme() == "https" {
61        let stream = TcpStream::connect(format!(
62            "{}:{}",
63            req.url().host_str().ok_or_else(|| {
64                http_types::Error::from_str(
65                    StatusCode::UnprocessableEntity,
66                    "No host in request URL",
67                )
68            })?,
69            req.url().port_or_known_default().ok_or_else(|| {
70                http_types::Error::from_str(
71                    StatusCode::UnprocessableEntity,
72                    "No port in request URL",
73                )
74            })?
75        ))
76        .await?;
77        native_connect(
78            TlsConnector::default()
79                .connect(
80                    req.host().ok_or_else(|| {
81                        http_types::Error::from_str(
82                            StatusCode::UnprocessableEntity,
83                            "No host in request URL",
84                            )
85                    })?,
86                    stream,
87                )
88                .await?,
89            req,
90        )
91        .await
92    } else {
93        native_connect(
94            TcpStream::connect(format!(
95                "{}:{}",
96                req.url().host_str().ok_or_else(|| {
97                    http_types::Error::from_str(
98                        StatusCode::UnprocessableEntity,
99                        "No host in request URL",
100                    )
101                })?,
102                req.url().port_or_known_default().ok_or_else(|| {
103                    http_types::Error::from_str(
104                        StatusCode::UnprocessableEntity,
105                        "No port in request URL",
106                    )
107                })?
108            ))
109            .await?,
110            req,
111        )
112        .await
113    }
114}
115
116#[cfg(target_arch = "wasm32")]
117mod fetch {
118    use js_sys::{Array, ArrayBuffer, Reflect, Uint8Array};
119    use wasm_bindgen::{prelude::*, JsCast};
120    use wasm_bindgen_futures::JsFuture;
121    use web_sys::{RequestInit, Window, WorkerGlobalScope};
122
123    use std::iter::{IntoIterator, Iterator};
124    use std::pin::Pin;
125
126    use http_types::StatusCode;
127
128    use http_types::Error;
129
130    enum WindowOrWorker {
131        Window(Window),
132        Worker(WorkerGlobalScope),
133    }
134
135    impl WindowOrWorker {
136        fn new() -> Self {
137            #[wasm_bindgen]
138            extern "C" {
139                type Global;
140
141                #[wasm_bindgen(method, getter, js_name = Window)]
142                fn window(this: &Global) -> JsValue;
143
144                #[wasm_bindgen(method, getter, js_name = WorkerGlobalScope)]
145                fn worker(this: &Global) -> JsValue;
146            }
147
148            let global: Global = js_sys::global().unchecked_into();
149
150            if !global.window().is_undefined() {
151                Self::Window(global.unchecked_into())
152            } else if !global.worker().is_undefined() {
153                Self::Worker(global.unchecked_into())
154            } else {
155                panic!("Only supported in a browser or web worker");
156            }
157        }
158    }
159
160    /// Create a new fetch request.
161
162    /// An HTTP Fetch Request.
163    pub(crate) struct Request {
164        request: web_sys::Request,
165        /// This field stores the body of the request to ensure it stays allocated as long as the request needs it.
166        #[allow(dead_code)]
167        body_buf: Pin<Vec<u8>>,
168    }
169
170    impl Request {
171        /// Create a new instance.
172        pub(crate) async fn new(mut req: super::Request) -> Result<Self, Error> {
173            // create a fetch request initaliser
174            let mut init = RequestInit::new();
175
176            // set the fetch method
177            init.method(req.method().as_ref());
178
179            let uri = req.url().to_string();
180            let body = req.take_body();
181
182            // convert the body into a uint8 array
183            // needs to be pinned and retained inside the Request because the Uint8Array passed to
184            // js is just a portal into WASM linear memory, and if the underlying data is moved the
185            // js ref will become silently invalid
186            let body_buf = body.into_bytes().await.map_err(|_| {
187                Error::from_str(StatusCode::BadRequest, "could not read body into a buffer")
188            })?;
189            let body_pinned = Pin::new(body_buf);
190            if body_pinned.len() > 0 {
191                let uint_8_array = unsafe { js_sys::Uint8Array::view(&body_pinned) };
192                init.body(Some(&uint_8_array));
193            }
194
195            let request = web_sys::Request::new_with_str_and_init(&uri, &init).map_err(|e| {
196                Error::from_str(
197                    StatusCode::BadRequest,
198                    format!("failed to create request: {:?}", e),
199                )
200            })?;
201
202            // add any fetch headers
203            let headers: &mut super::Headers = req.as_mut();
204            for (name, value) in headers.iter() {
205                let name = name.as_str();
206                let value = value.as_str();
207
208                request.headers().set(name, value).map_err(|_| {
209                    Error::from_str(
210                        StatusCode::BadRequest,
211                        format!("could not add header: {} = {}", name, value),
212                    )
213                })?;
214            }
215
216            Ok(Self {
217                request,
218                body_buf: body_pinned,
219            })
220        }
221
222        /// Submit a request
223        // TODO(yoshuawuyts): turn this into a `Future` impl on `Request` instead.
224        pub(crate) async fn send(self) -> Result<Response, Error> {
225            // Send the request.
226            let scope = WindowOrWorker::new();
227            let promise = match scope {
228                WindowOrWorker::Window(window) => window.fetch_with_request(&self.request),
229                WindowOrWorker::Worker(worker) => worker.fetch_with_request(&self.request),
230            };
231            let resp = JsFuture::from(promise)
232                .await
233                .map_err(|e| Error::from_str(StatusCode::BadRequest, format!("{:?}", e)))?;
234
235            debug_assert!(resp.is_instance_of::<web_sys::Response>());
236            let res: web_sys::Response = resp.dyn_into().unwrap();
237
238            // Get the response body.
239            let promise = res.array_buffer().unwrap();
240            let resp = JsFuture::from(promise).await.unwrap();
241            debug_assert!(resp.is_instance_of::<js_sys::ArrayBuffer>());
242            let buf: ArrayBuffer = resp.dyn_into().unwrap();
243            let slice = Uint8Array::new(&buf);
244            let mut body: Vec<u8> = vec![0; slice.length() as usize];
245            slice.copy_to(&mut body);
246
247            Ok(Response::new(res, body))
248        }
249    }
250
251    /// An HTTP Fetch Response.
252    pub(crate) struct Response {
253        res: web_sys::Response,
254        body: Option<Vec<u8>>,
255    }
256
257    impl Response {
258        fn new(res: web_sys::Response, body: Vec<u8>) -> Self {
259            Self {
260                res,
261                body: Some(body),
262            }
263        }
264
265        /// Access the HTTP headers.
266        pub(crate) fn headers(&self) -> Headers {
267            Headers {
268                headers: self.res.headers(),
269            }
270        }
271
272        /// Get the request body as a byte vector.
273        ///
274        /// Returns an empty vector if the body has already been consumed.
275        pub(crate) fn body_bytes(&mut self) -> Vec<u8> {
276            self.body.take().unwrap_or_else(|| vec![])
277        }
278
279        /// Get the HTTP return status code.
280        pub(crate) fn status(&self) -> u16 {
281            self.res.status()
282        }
283    }
284
285    /// HTTP Headers.
286    pub(crate) struct Headers {
287        headers: web_sys::Headers,
288    }
289
290    impl IntoIterator for Headers {
291        type Item = (String, String);
292        type IntoIter = HeadersIter;
293
294        fn into_iter(self) -> Self::IntoIter {
295            HeadersIter {
296                iter: js_sys::try_iter(&self.headers).unwrap().unwrap(),
297            }
298        }
299    }
300
301    /// HTTP Headers Iterator.
302    pub(crate) struct HeadersIter {
303        iter: js_sys::IntoIter,
304    }
305
306    impl Iterator for HeadersIter {
307        type Item = (String, String);
308
309        fn next(&mut self) -> Option<Self::Item> {
310            let pair = self.iter.next()?;
311
312            let array: Array = pair.unwrap().into();
313            let vals = array.values();
314
315            let prop = String::from("value").into();
316            let key = Reflect::get(&vals.next().unwrap(), &prop).unwrap();
317            let value = Reflect::get(&vals.next().unwrap(), &prop).unwrap();
318
319            Some((
320                key.as_string().to_owned().unwrap(),
321                value.as_string().to_owned().unwrap(),
322            ))
323        }
324    }
325}