char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport fetch extension to typescriptf2c35fd

main
18.9 KiB534 linesraw
1use std::collections::HashMap;
2use std::convert::Infallible;
3use std::future::Future;
4use std::io;
5use std::pin::Pin;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex};
8
9use avril_caps::Capabilities;
10use avril_core::serde_v8::{JsBuffer, ToJsBuffer};
11use avril_core::{Extension, Op};
12use bytes::Bytes;
13use futures::StreamExt;
14use http_body_util::{BodyExt, Full, StreamBody, combinators::UnsyncBoxBody};
15use hyper::{
16    Method, Request,
17    body::Frame,
18    header::{HeaderMap, HeaderName, HeaderValue},
19};
20use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
21use hyper_util::{
22    client::legacy::{Client, connect::HttpConnector},
23    rt::TokioExecutor,
24};
25use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
26use tokio::sync::{Mutex as AsyncMutex, mpsc};
27use tokio_util::{io::StreamReader, sync::CancellationToken};
28
29type Error = Box<dyn std::error::Error + Send + Sync>;
30type Body = UnsyncBoxBody<Bytes, Error>;
31type Reader = Pin<Box<dyn AsyncRead + Send>>;
32/// Status, status text, headers, final URL, whether redirected, and whether a redirect
33/// abandoned the streamed request body.
34type Metadata = (u16, String, Vec<(String, String)>, String, bool, bool);
35type SendArgs = (
36    u64,
37    String,
38    String,
39    Vec<(String, String)>,
40    Option<JsBuffer>,
41    bool,
42    String,
43);
44type Upload = mpsc::Sender<Result<Frame<Bytes>, Error>>;
45
46const MAX_ID: u64 = 9_007_199_254_740_991;
47
48enum Redirect {
49    Follow,
50    Error,
51    Manual,
52}
53
54impl Redirect {
55    fn parse(mode: &str) -> Result<Self, Error> {
56        match mode {
57            "follow" => Ok(Self::Follow),
58            "error" => Ok(Self::Error),
59            "manual" => Ok(Self::Manual),
60            _ => Err("Invalid redirect mode".into()),
61        }
62    }
63}
64
65enum RequestBody {
66    Empty,
67    Bytes(Bytes),
68    Upload(mpsc::Receiver<Result<Frame<Bytes>, Error>>),
69}
70
71struct PendingRequest {
72    url: String,
73    method: String,
74    headers: Vec<(String, String)>,
75    body: RequestBody,
76    redirect: String,
77}
78
79struct Resource {
80    canceled: CancellationToken,
81    body: AsyncMutex<Option<Reader>>,
82    upload: Mutex<Option<Upload>>,
83    /// Releases writes blocked on a request body that will no longer be sent.
84    upload_closed: CancellationToken,
85}
86
87impl Resource {
88    fn close_upload(&self) {
89        self.upload.lock().unwrap().take();
90        self.upload_closed.cancel();
91    }
92}
93
94struct Fetch {
95    runtime: tokio::runtime::Handle,
96    client: Client<HttpsConnector<HttpConnector>, Body>,
97    resources: Arc<Mutex<HashMap<u64, Arc<Resource>>>>,
98    next: AtomicU64,
99    caps: Arc<Capabilities>,
100}
101
102impl Fetch {
103    fn resource(&self, id: u64) -> Result<Arc<Resource>, Error> {
104        self.resources
105            .lock()
106            .unwrap()
107            .get(&id)
108            .cloned()
109            .ok_or_else(|| "Fetch resource is closed".into())
110    }
111
112    fn run<T: Send + 'static, F: Future<Output = Result<T, Error>> + Send + 'static>(
113        &self,
114        future: F,
115    ) -> impl Future<Output = Result<T, Error>> + use<T, F> {
116        let handle = self.runtime.spawn(future);
117        async move {
118            // Dropping the handle detaches the task rather than aborting it.
119            handle
120                .await
121                .map_err(|_| io::Error::other("Fetch worker stopped"))?
122        }
123    }
124
125    fn cancel(&self, id: u64) {
126        if let Some(resource) = self.resources.lock().unwrap().remove(&id) {
127            resource.canceled.cancel();
128            resource.close_upload();
129        }
130    }
131
132    fn send(
133        &self,
134        (id, url, method, headers, bytes, streaming, redirect): SendArgs,
135    ) -> impl Future<Output = Result<Metadata, Error>> + use<> {
136        let resource = self.resource(id);
137        let client = self.client.clone();
138        let caps = self.caps.clone();
139        // Install the upload channel before returning the promise, so JS can submit its first chunk.
140        let body = if streaming {
141            let (send, receive) = mpsc::channel(1);
142            if let Ok(resource) = &resource {
143                *resource.upload.lock().unwrap() = Some(send);
144            }
145            RequestBody::Upload(receive)
146        } else {
147            bytes
148                .map(|bytes| Bytes::copy_from_slice(&bytes))
149                .map(RequestBody::Bytes)
150                .unwrap_or(RequestBody::Empty)
151        };
152        let request = PendingRequest {
153            url,
154            method,
155            headers,
156            body,
157            redirect,
158        };
159        self.run(async move {
160            let resource = resource?;
161            let canceled = resource.canceled.clone();
162            let result = tokio::select! {
163                _ = canceled.cancelled() => Err("Fetch aborted".into()),
164                result = send_request(client, caps, &resource, request) => result,
165            };
166            result
167        })
168    }
169}
170
171async fn send_request(
172    client: Client<HttpsConnector<HttpConnector>, Body>,
173    caps: Arc<Capabilities>,
174    resource: &Resource,
175    request: PendingRequest,
176) -> Result<Metadata, Error> {
177    let PendingRequest {
178        url,
179        method,
180        headers,
181        body,
182        redirect,
183    } = request;
184    let url = url::Url::parse(&url)?;
185    let method = Method::from_bytes(method.as_bytes())?;
186    if ["CONNECT", "TRACE", "TRACK"]
187        .iter()
188        .any(|name| method.as_str().eq_ignore_ascii_case(name))
189    {
190        return Err("Forbidden HTTP method".into());
191    }
192    let redirect = Redirect::parse(&redirect)?;
193    if !matches!(body, RequestBody::Empty) && (method == Method::GET || method == Method::HEAD) {
194        return Err("GET/HEAD requests cannot have a body".into());
195    }
196    let mut list = HeaderMap::new();
197    for (name, value) in headers {
198        let name = name.to_ascii_lowercase();
199        if [
200            "host",
201            "content-length",
202            "transfer-encoding",
203            "connection",
204            "upgrade",
205            "te",
206            "trailer",
207            "keep-alive",
208            "expect",
209        ]
210        .contains(&name.as_str())
211            || name.starts_with("proxy-")
212        {
213            return Err(format!("Forbidden request header: {name}").into());
214        }
215        let value = value
216            .chars()
217            .map(|c| u8::try_from(c as u32))
218            .collect::<Result<Vec<_>, _>>()?;
219        list.append(
220            HeaderName::from_bytes(name.as_bytes())?,
221            HeaderValue::from_bytes(&value)?,
222        );
223    }
224    if !list.contains_key("accept") {
225        list.insert("accept", HeaderValue::from_static("*/*"));
226    }
227    if !list.contains_key("accept-encoding") {
228        list.insert(
229            "accept-encoding",
230            HeaderValue::from_static("gzip, br, deflate"),
231        );
232    }
233    let streaming = matches!(body, RequestBody::Upload(_));
234    let (mut url, mut method, mut body) = (url, method, body);
235    let mut abandoned = false;
236    for hop in 0..=20 {
237        if !["http", "https"].contains(&url.scheme()) {
238            return Err("not yet implemented: non-HTTP fetch".into());
239        }
240        if !url.username().is_empty() || url.password().is_some() {
241            return Err("Fetch URLs cannot contain credentials".into());
242        }
243        url.set_fragment(None);
244        // No ambient proxies, automatic redirects, or snapshot permissions in the connector.
245        caps.check_net(
246            url.host_str().ok_or("Missing fetch host")?,
247            url.port_or_known_default().ok_or("Missing fetch port")?,
248        )?;
249        let request_body = match std::mem::replace(&mut body, RequestBody::Empty) {
250            RequestBody::Upload(receive) => {
251                let stream = futures::stream::unfold(receive, |mut receive| async move {
252                    receive.recv().await.map(|frame| (frame, receive))
253                });
254                StreamBody::new(stream).boxed_unsync()
255            }
256            RequestBody::Bytes(bytes) => {
257                let payload = Full::new(bytes.clone())
258                    .map_err(|never| match never {})
259                    .boxed_unsync();
260                body = RequestBody::Bytes(bytes);
261                payload
262            }
263            RequestBody::Empty => Full::new(Bytes::new())
264                .map_err(|never| match never {})
265                .boxed_unsync(),
266        };
267        let mut request = Request::builder()
268            .method(method.clone())
269            .uri(url.as_str())
270            .body(request_body)?;
271        *request.headers_mut() = list.clone();
272        let response = client.request(request).await?;
273        let status = response.status();
274        if matches!(status.as_u16(), 301 | 302 | 303 | 307 | 308)
275            && !matches!(redirect, Redirect::Manual)
276            && let Some(location) = response.headers().get("location")
277        {
278            if matches!(redirect, Redirect::Error) {
279                return Err("Redirect encountered in redirect:error mode".into());
280            }
281            if hop == 20 {
282                return Err("Too many redirects".into());
283            }
284            let location = String::from_utf8_lossy(location.as_bytes());
285            let next = url.join(&location)?;
286            if url.origin() != next.origin() {
287                for name in ["authorization", "proxy-authorization", "cookie", "host"] {
288                    list.remove(name);
289                }
290            }
291            if ((status.as_u16() == 301 || status.as_u16() == 302) && method == Method::POST)
292                || (status.as_u16() == 303 && method != Method::GET && method != Method::HEAD)
293            {
294                method = Method::GET;
295                body = RequestBody::Empty;
296                for name in [
297                    "content-type",
298                    "content-length",
299                    "content-encoding",
300                    "content-language",
301                    "content-location",
302                ] {
303                    list.remove(name);
304                }
305                resource.close_upload();
306                abandoned = streaming;
307            } else if streaming && method != Method::GET && method != Method::HEAD {
308                return Err(
309                    "not yet implemented: replaying streaming request bodies on redirects".into(),
310                );
311            }
312            url = next;
313            continue;
314        }
315        let headers = response
316            .headers()
317            .iter()
318            .map(|(key, value)| {
319                (
320                    key.to_string(),
321                    value.as_bytes().iter().map(|b| char::from(*b)).collect(),
322                )
323            })
324            .collect();
325        let status_text = response
326            .extensions()
327            .get::<hyper::ext::ReasonPhrase>()
328            .map(|reason| {
329                reason
330                    .as_bytes()
331                    .iter()
332                    .map(|byte| char::from(*byte))
333                    .collect::<String>()
334            })
335            .unwrap_or_else(|| status.canonical_reason().unwrap_or("").into());
336        let encoding = response
337            .headers()
338            .get_all("content-encoding")
339            .iter()
340            .map(|value| value.to_str())
341            .collect::<Result<Vec<_>, _>>()?
342            .join(",");
343        let stream = response
344            .into_body()
345            .into_data_stream()
346            .map(|result| result.map_err(io::Error::other));
347        let mut reader: Reader = Box::pin(StreamReader::new(stream));
348        if method != Method::HEAD && !matches!(status.as_u16(), 204 | 205 | 304) {
349            for encoding in encoding.split(',').rev().map(str::trim) {
350                reader = match encoding.to_ascii_lowercase().as_str() {
351                    "" | "identity" => reader,
352                    "gzip" => {
353                        let mut decoder = async_compression::tokio::bufread::GzipDecoder::new(
354                            BufReader::new(reader),
355                        );
356                        decoder.multiple_members(true);
357                        Box::pin(decoder)
358                    }
359                    "br" => Box::pin(async_compression::tokio::bufread::BrotliDecoder::new(
360                        BufReader::new(reader),
361                    )),
362                    "deflate" => Box::pin(async_compression::tokio::bufread::ZlibDecoder::new(
363                        BufReader::new(reader),
364                    )),
365                    _ => return Err("not yet implemented: response content encoding".into()),
366                };
367            }
368        }
369        *resource.body.lock().await = Some(reader);
370        return Ok((
371            status.as_u16(),
372            status_text,
373            headers,
374            url.into(),
375            hop != 0,
376            abandoned,
377        ));
378    }
379    unreachable!()
380}
381
382pub fn init(runtime: tokio::runtime::Handle, caps: Arc<Capabilities>, base: String) -> Extension {
383    let connector = HttpsConnectorBuilder::new()
384        .with_webpki_roots()
385        .https_or_http()
386        .enable_http1()
387        .enable_http2()
388        .build();
389    let client = Client::builder(TokioExecutor::new()).build(connector);
390    let state = Arc::new(Fetch {
391        runtime,
392        client,
393        resources: Arc::new(Mutex::new(HashMap::new())),
394        next: AtomicU64::new(1),
395        caps,
396    });
397    let create = state.clone();
398    let send = state.clone();
399    let read = state.clone();
400    let collect = state.clone();
401    let write = state.clone();
402    Extension {
403        name: "ext:fetch",
404        dependencies: &[
405            "ext:encoding",
406            "ext:http_types",
407            "ext:timers",
408            "ext:url",
409            "ext:web",
410            "ext:webstreams",
411        ],
412        ops: vec![
413            Op::new("op_fetch_base", move |(): ()| {
414                Ok::<_, Infallible>(base.clone())
415            }),
416            Op::new("op_fetch_create", move |_: ()| {
417                let id = create
418                    .next
419                    .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| {
420                        (id <= MAX_ID).then_some(id + 1)
421                    })
422                    .map_err(|_| io::Error::other("Fetch IDs exhausted"))?;
423                create.resources.lock().unwrap().insert(
424                    id,
425                    Arc::new(Resource {
426                        canceled: CancellationToken::new(),
427                        body: AsyncMutex::new(None),
428                        upload: Mutex::new(None),
429                        upload_closed: CancellationToken::new(),
430                    }),
431                );
432                Ok::<_, io::Error>(id)
433            }),
434            Op::asynchronous("op_fetch_send", move |args: SendArgs| {
435                let pending = send.send(args);
436                async move { pending.await.map_err(io::Error::other) }
437            }),
438            Op::asynchronous("op_fetch_read", move |id: u64| {
439                let resource = read.resource(id);
440                let resources = read.resources.clone();
441                let pending = read.run(async move {
442                    let resource = resource?;
443                    tokio::select! {
444                        _ = resource.canceled.cancelled() => Err("Fetch aborted".into()),
445                        result = async {
446                            let mut body = resource.body.lock().await;
447                            let mut bytes = vec![0; 64 * 1024];
448                            let count = body.as_mut().ok_or("Response body is unavailable")?.read(&mut bytes).await?;
449                            bytes.truncate(count);
450                            if count == 0 { body.take(); resources.lock().unwrap().remove(&id); }
451                            Ok::<_, Error>(bytes)
452                        } => result,
453                    }
454                });
455                async move {
456                    Ok::<_, io::Error>(match pending.await.map_err(io::Error::other)? {
457                        bytes if bytes.is_empty() => None,
458                        bytes => Some(ToJsBuffer::from(bytes)),
459                    })
460                }
461            }),
462            Op::asynchronous("op_fetch_read_all", move |id: u64| {
463                let resource = collect.resource(id);
464                let resources = collect.resources.clone();
465                let pending = collect.run(async move {
466                    let resource = resource?;
467                    let result = tokio::select! {
468                        biased;
469                        _ = resource.canceled.cancelled() => Err("Fetch aborted".into()),
470                        result = async {
471                            let mut reader = resource.body.lock().await.take()
472                                .ok_or("Response body is unavailable")?;
473                            let mut bytes = Vec::new();
474                            reader.read_to_end(&mut bytes).await?;
475                            Ok::<_, Error>(bytes)
476                        } => result,
477                    };
478                    resources.lock().unwrap().remove(&id);
479                    result
480                });
481                async move {
482                    pending
483                        .await
484                        .map(ToJsBuffer::from)
485                        .map_err(io::Error::other)
486                }
487            }),
488            Op::asynchronous(
489                "op_fetch_write",
490                move |(id, bytes): (u64, Option<JsBuffer>)| {
491                    let resource = write.resource(id);
492                    let bytes = bytes.map(|bytes| Bytes::copy_from_slice(&bytes));
493                    let pending = write.run(async move {
494                        let Ok(resource) = resource else {
495                            return Ok(false);
496                        };
497                        if let Some(bytes) = bytes {
498                            let Some(upload) = resource.upload.lock().unwrap().clone() else {
499                                return Ok(false);
500                            };
501                            tokio::select! {
502                                _ = resource.canceled.cancelled() => Err("Fetch aborted".into()),
503                                _ = resource.upload_closed.cancelled() => Ok(false),
504                                result = upload.send(Ok(Frame::data(bytes))) => Ok(result.is_ok()),
505                            }
506                        } else {
507                            Ok(resource.upload.lock().unwrap().take().is_some())
508                        }
509                    });
510                    async move { pending.await.map_err(io::Error::other) }
511                },
512            ),
513            Op::new("op_fetch_cancel", move |id: u64| {
514                state.cancel(id);
515                Ok::<_, io::Error>(())
516            }),
517        ],
518        modules: &[
519            (
520                "ext:fetch/main.js",
521                include_str!(concat!(env!("OUT_DIR"), "/main.js")),
522            ),
523            (
524                "ext:fetch/fetch.js",
525                include_str!(concat!(env!("OUT_DIR"), "/fetch.js")),
526            ),
527            (
528                "ext:fetch/eventsource.js",
529                include_str!(concat!(env!("OUT_DIR"), "/eventsource.js")),
530            ),
531        ],
532        entry_point: Some("ext:fetch/main.js"),
533    }
534}