char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f2c35fd
main
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::{ 16Method , 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 =( 36u64 , 37String , 38String , 39Vec <( String , String )>, 40Option < JsBuffer >, 41bool , 42String , 43); 44type Upload = mpsc:: Sender < Result < Frame < Bytes >, Error >>; 45 46const MAX_ID : u64 =9_007_199_254_740_991 ; 47 48enum Redirect { 49Follow , 50Error , 51Manual , 52} 53 54impl Redirect { 55fn parse ( mode : & str ) ->Result < Self , Error > { 56match 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 { 66Empty , 67Bytes ( Bytes ), 68Upload ( mpsc:: Receiver < Result < Frame < Bytes >, Error >>), 69} 70 71struct PendingRequest { 72url : String , 73method : String , 74headers : Vec <( String , String )>, 75body : RequestBody , 76redirect : String , 77} 78 79struct Resource { 80canceled : CancellationToken , 81body : AsyncMutex < Option < Reader >>, 82upload : Mutex < Option < Upload >>, 83/// Releases writes blocked on a request body that will no longer be sent. 84upload_closed : CancellationToken , 85} 86 87impl Resource { 88fn close_upload ( & self ) { 89self . upload . lock (). unwrap (). take (); 90self . upload_closed . cancel (); 91} 92} 93 94struct Fetch { 95runtime : tokio:: runtime:: Handle , 96client : Client < HttpsConnector < HttpConnector >, Body >, 97resources : Arc < Mutex < HashMap < u64 , Arc < Resource >>>>, 98next : AtomicU64 , 99caps : Arc < Capabilities >, 100} 101 102impl Fetch { 103fn resource ( & self , id : u64 ) ->Result < Arc < Resource >, Error > { 104self . resources 105. lock () 106. unwrap () 107. get ( & id) 108. cloned () 109. ok_or_else ( ||"Fetch resource is closed" . into ()) 110} 111 112fn run < T : Send +' static , F : Future < Output =Result < T , Error >> +Send +' static >( 113& self , 114future : F , 115) ->impl Future < Output =Result < T , Error >> +use <T , F >{ 116let handle =self . runtime . spawn ( future); 117async 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 125fn cancel ( & self , id : u64 ) { 126if let Some ( resource) =self . resources . lock (). unwrap (). remove ( & id) { 127 resource. canceled . cancel (); 128 resource. close_upload (); 129} 130} 131 132fn send ( 133& self , 134( id, url, method, headers, bytes, streaming, redirect): SendArgs , 135) ->impl Future < Output =Result < Metadata , Error >> +use <>{ 136let resource =self . resource ( id); 137let client =self . client . clone (); 138let caps =self . caps . clone (); 139// Install the upload channel before returning the promise, so JS can submit its first chunk. 140let body =if streaming{ 141let ( send, receive) = mpsc:: channel ( 1 ); 142if let Ok ( resource) =& resource{ 143* resource. upload . lock (). unwrap () =Some ( send); 144} 145RequestBody :: Upload ( receive) 146} else { 147 bytes 148. map ( |bytes|Bytes :: copy_from_slice ( & bytes)) 149. map ( RequestBody :: Bytes ) 150. unwrap_or ( RequestBody :: Empty ) 151}; 152let request =PendingRequest { 153 url, 154 method, 155 headers, 156 body, 157 redirect, 158}; 159self . run ( async move { 160let resource = resource?; 161let canceled = resource. canceled . clone (); 162let 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 ( 172client : Client < HttpsConnector < HttpConnector >, Body >, 173caps : Arc < Capabilities >, 174resource : & Resource , 175request : PendingRequest , 176) ->Result < Metadata , Error > { 177let PendingRequest { 178 url, 179 method, 180 headers, 181 body, 182 redirect, 183} = request; 184let url = url:: Url :: parse ( & url) ?; 185let method =Method :: from_bytes ( method. as_bytes ()) ?; 186if [ "CONNECT" , "TRACE" , "TRACK" ] 187. iter () 188. any ( |name| method. as_str (). eq_ignore_ascii_case ( name)) 189{ 190return Err ( "Forbidden HTTP method" . into ()); 191} 192let redirect =Redirect :: parse ( & redirect) ?; 193if !matches! ( body, RequestBody :: Empty ) &&( method ==Method :: GET || method ==Method :: HEAD ) { 194return Err ( "GET/HEAD requests cannot have a body" . into ()); 195} 196let mut list =HeaderMap :: new (); 197for ( name, value) in headers{ 198let name = name. to_ascii_lowercase (); 199if [ 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{ 213return Err ( format! ( "Forbidden request header: {name}" ). into ()); 214} 215let value = value 216. chars () 217. map ( |c| u8:: try_from ( cas u32 )) 218. collect ::< Result < Vec < _ >, _ >>() ?; 219 list. append ( 220HeaderName :: from_bytes ( name. as_bytes ()) ?, 221HeaderValue :: from_bytes ( & value) ?, 222); 223} 224if !list. contains_key ( "accept" ) { 225 list. insert ( "accept" , HeaderValue :: from_static ( "*/*" )); 226} 227if !list. contains_key ( "accept-encoding" ) { 228 list. insert ( 229"accept-encoding" , 230HeaderValue :: from_static ( "gzip, br, deflate" ), 231); 232} 233let streaming =matches! ( body, RequestBody :: Upload ( _)); 234let ( mut url, mut method, mut body) =( url, method, body); 235let mut abandoned =false ; 236for hopin 0 ..=20 { 237if ![ "http" , "https" ]. contains ( & url. scheme ()) { 238return Err ( "not yet implemented: non-HTTP fetch" . into ()); 239} 240if !url. username (). is_empty () || url. password (). is_some () { 241return 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) ?; 249let request_body =match std:: mem:: replace ( & mut body, RequestBody :: Empty ) { 250RequestBody :: Upload ( receive) =>{ 251let stream = futures:: stream:: unfold ( receive, |mut receive|async move { 252 receive. recv (). await . map ( |frame|( frame, receive)) 253}); 254StreamBody :: new ( stream). boxed_unsync () 255} 256RequestBody :: Bytes ( bytes) =>{ 257let payload =Full :: new ( bytes. clone ()) 258. map_err ( |never|match never{}) 259. boxed_unsync (); 260 body =RequestBody :: Bytes ( bytes); 261 payload 262} 263RequestBody :: Empty =>Full :: new ( Bytes :: new ()) 264. map_err ( |never|match never{}) 265. boxed_unsync (), 266}; 267let mut request =Request :: builder () 268. method ( method. clone ()) 269. uri ( url. as_str ()) 270. body ( request_body) ?; 271* request. headers_mut () = list. clone (); 272let response = client. request ( request). await ?; 273let status = response. status (); 274if matches! ( status. as_u16 (), 301 |302 |303 |307 |308 ) 275 && !matches! ( redirect, Redirect :: Manual ) 276 &&let Some ( location) = response. headers (). get ( "location" ) 277{ 278if matches! ( redirect, Redirect :: Error ) { 279return Err ( "Redirect encountered in redirect:error mode" . into ()); 280} 281if hop ==20 { 282return Err ( "Too many redirects" . into ()); 283} 284let location =String :: from_utf8_lossy ( location. as_bytes ()); 285let next = url. join ( & location) ?; 286if url. origin () != next. origin () { 287for namein [ "authorization" , "proxy-authorization" , "cookie" , "host" ] { 288 list. remove ( name); 289} 290} 291if (( 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 ; 296for namein [ 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 { 308return Err ( 309"not yet implemented: replaying streaming request bodies on redirects" . into (), 310); 311} 312 url = next; 313continue ; 314} 315let 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 (); 325let 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 ()); 336let encoding = response 337. headers () 338. get_all ( "content-encoding" ) 339. iter () 340. map ( |value| value. to_str ()) 341. collect ::< Result < Vec < _ >, _ >>() ? 342. join ( "," ); 343let stream = response 344. into_body () 345. into_data_stream () 346. map ( |result| result. map_err ( io:: Error :: other)); 347let mut reader: Reader =Box :: pin ( StreamReader :: new ( stream)); 348if method !=Method :: HEAD && !matches! ( status. as_u16 (), 204 |205 |304 ) { 349for encodingin encoding. split ( ',' ). rev (). map ( str:: trim) { 350 reader =match encoding. to_ascii_lowercase (). as_str () { 351"" |"identity" => reader, 352"gzip" =>{ 353let mut decoder = async_compression:: tokio:: bufread:: GzipDecoder :: new ( 354BufReader :: new ( reader), 355); 356 decoder. multiple_members ( true ); 357Box :: pin ( decoder) 358} 359"br" =>Box :: pin ( async_compression:: tokio:: bufread:: BrotliDecoder :: new ( 360BufReader :: new ( reader), 361)), 362"deflate" =>Box :: pin ( async_compression:: tokio:: bufread:: ZlibDecoder :: new ( 363BufReader :: new ( reader), 364)), 365 _ =>return Err ( "not yet implemented: response content encoding" . into ()), 366}; 367} 368} 369* resource. body . lock (). await =Some ( reader); 370return Ok (( 371 status. as_u16 (), 372 status_text, 373 headers, 374 url. into (), 375 hop !=0 , 376 abandoned, 377)); 378} 379unreachable! () 380} 381 382pub fn init ( runtime : tokio:: runtime:: Handle , caps : Arc < Capabilities >, base : String ) ->Extension { 383let connector =HttpsConnectorBuilder :: new () 384. with_webpki_roots () 385. https_or_http () 386. enable_http1 () 387. enable_http2 () 388. build (); 389let client =Client :: builder ( TokioExecutor :: new ()). build ( connector); 390let state =Arc :: new ( Fetch { 391 runtime, 392 client, 393resources : Arc :: new ( Mutex :: new ( HashMap :: new ())), 394next : AtomicU64 :: new ( 1 ), 395 caps, 396}); 397let create = state. clone (); 398let send = state. clone (); 399let read = state. clone (); 400let collect = state. clone (); 401let write = state. clone (); 402Extension { 403name : "ext:fetch" , 404dependencies : & [ 405"ext:encoding" , 406"ext:http_types" , 407"ext:timers" , 408"ext:url" , 409"ext:web" , 410"ext:webstreams" , 411], 412ops : vec! [ 413Op :: new ( "op_fetch_base" , move |(): () |{ 414Ok ::< _ , Infallible >( base. clone ()) 415}), 416Op :: new ( "op_fetch_create" , move |_: () |{ 417let 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, 425Arc :: new ( Resource { 426canceled : CancellationToken :: new (), 427body : AsyncMutex :: new ( None ), 428upload : Mutex :: new ( None ), 429upload_closed : CancellationToken :: new (), 430}), 431); 432Ok ::< _ , io:: Error >( id) 433}), 434Op :: asynchronous ( "op_fetch_send" , move |args : SendArgs |{ 435let pending = send. send ( args); 436async move { pending. await . map_err ( io:: Error :: other) } 437}), 438Op :: asynchronous ( "op_fetch_read" , move |id : u64 |{ 439let resource = read. resource ( id); 440let resources = read. resources . clone (); 441let pending = read. run ( async move { 442let resource = resource?; 443 tokio:: select!{ 444 _ = resource. canceled . cancelled () =>Err ( "Fetch aborted" . into ()), 445 result =async { 446let mut body = resource. body . lock (). await ; 447let mut bytes =vec! [ 0 ; 64 * 1024 ]; 448let count = body. as_mut (). ok_or ( "Response body is unavailable" ) ?. read ( & mut bytes). await ?; 449 bytes. truncate ( count); 450if count ==0 { body. take (); resources. lock (). unwrap (). remove ( & id); } 451Ok ::< _ , Error >( bytes) 452} => result, 453} 454}); 455async move { 456Ok ::< _ , io:: Error >( match pending. await . map_err ( io:: Error :: other) ?{ 457 bytesif bytes. is_empty () =>None , 458 bytes =>Some ( ToJsBuffer :: from ( bytes)), 459}) 460} 461}), 462Op :: asynchronous ( "op_fetch_read_all" , move |id : u64 |{ 463let resource = collect. resource ( id); 464let resources = collect. resources . clone (); 465let pending = collect. run ( async move { 466let resource = resource?; 467let result = tokio:: select!{ 468 biased; 469 _ = resource. canceled . cancelled () =>Err ( "Fetch aborted" . into ()), 470 result =async { 471let mut reader = resource. body . lock (). await . take () 472. ok_or ( "Response body is unavailable" ) ?; 473let mut bytes =Vec :: new (); 474 reader. read_to_end ( & mut bytes). await ?; 475Ok ::< _ , Error >( bytes) 476} => result, 477}; 478 resources. lock (). unwrap (). remove ( & id); 479 result 480}); 481async move { 482 pending 483. await 484. map ( ToJsBuffer :: from) 485. map_err ( io:: Error :: other) 486} 487}), 488Op :: asynchronous ( 489"op_fetch_write" , 490move |( id, bytes): ( u64 , Option < JsBuffer >) |{ 491let resource = write. resource ( id); 492let bytes = bytes. map ( |bytes|Bytes :: copy_from_slice ( & bytes)); 493let pending = write. run ( async move { 494let Ok ( resource) = resourceelse { 495return Ok ( false ); 496}; 497if let Some ( bytes) = bytes{ 498let Some ( upload) = resource. upload . lock (). unwrap (). clone () else { 499return 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 { 507Ok ( resource. upload . lock (). unwrap (). take (). is_some ()) 508} 509}); 510async move { pending. await . map_err ( io:: Error :: other) } 511}, 512), 513Op :: new ( "op_fetch_cancel" , move |id : u64 |{ 514 state. cancel ( id); 515Ok ::< _ , io:: Error >(()) 516}), 517], 518modules : & [ 519( 520"ext:fetch/main.js" , 521include_str! ( concat! ( env! ( "OUT_DIR" ), "/main.js" )), 522), 523( 524"ext:fetch/fetch.js" , 525include_str! ( concat! ( env! ( "OUT_DIR" ), "/fetch.js" )), 526), 527( 528"ext:fetch/eventsource.js" , 529include_str! ( concat! ( env! ( "OUT_DIR" ), "/eventsource.js" )), 530), 531], 532entry_point : Some ( "ext:fetch/main.js" ), 533} 534}