char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1mod common; 2 3use std:: io::{ BufRead , BufReader , Read , Write }; 4use std:: net:: TcpStream ; 5use std:: process::{ Child , Command , Stdio }; 6use std:: time:: Duration ; 7 8struct Hosted { 9_dir : tempfile:: TempDir , 10child : Option < Child >, 11address : String , 12} 13 14impl Hosted { 15fn start ( source : & str , options : & [ & str ]) ->Self { 16let dir = tempfile:: tempdir (). unwrap (); 17let path = dir. path (). join ( "main.js" ); 18 std:: fs:: write ( & path, source). unwrap (); 19let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 20. args ([ "serve" , "-0" ]) 21. args ( options) 22. arg ( path) 23. stdin ( Stdio :: null ()) 24. stdout ( Stdio :: piped ()) 25. stderr ( Stdio :: piped ()) 26. spawn () 27. unwrap (); 28let mut hosted =Self { 29_dir : dir, 30child : Some ( child), 31address : String :: new (), 32}; 33let child = hosted. child . as_mut (). unwrap (); 34let mut stderr =BufReader :: new ( child. stderr . take (). unwrap ()); 35let mut line =String :: new (); 36 stderr. read_line ( & mut line). unwrap (); 37 hosted. address = line 38. trim () 39. strip_prefix ( "Listening on " ) 40. expect ( & line) 41. to_owned (); 42 child. stderr =Some ( stderr. into_inner ()); 43 hosted 44} 45 46# [ cfg ( unix )] 47fn stop ( self ) { 48self . stop_with ( libc:: SIGINT ); 49} 50 51# [ cfg ( unix )] 52fn stop_with ( mut self , signal : i32 ) { 53let child =self . child . take (). unwrap (); 54assert_eq! ( unsafe { libc:: kill ( child. id () as i32 , signal) }, 0 ); 55let output = common:: wait ( child); 56assert! ( 57 output. status . success (), 58"{}" , 59String :: from_utf8_lossy ( & output. stderr ) 60); 61} 62} 63 64impl Drop for Hosted { 65fn drop ( & mut self ) { 66if let Some ( mut child) =self . child . take () { 67let _ = child. kill (); 68let _ = child. wait (); 69} 70} 71} 72 73fn headers ( reader : & mut impl BufRead ) ->String { 74let mut result =String :: new (); 75loop { 76let mut line =String :: new (); 77assert_ne! ( reader. read_line ( & mut line). unwrap (), 0 , "unexpected EOF" ); 78 result. push_str ( & line); 79if line =="\r\n" { 80return result; 81} 82} 83} 84 85fn masked_frame ( socket : & mut impl Write , fin : bool , opcode : u8 , payload : & [ u8 ]) { 86assert! ( payload. len () <=125 ); 87let mask =[ 1 , 7 , 3 , 9 ]; 88let mut frame =vec! [ 89( if fin{ 0x80 } else { 0 }) | opcode, 900x80 | payload. len () as u8 , 91]; 92 frame. extend_from_slice ( & mask); 93for ( index, byte) in payload. iter (). enumerate () { 94 frame. push ( byte ^ mask[ index %4 ]); 95} 96 socket. write_all ( & frame). unwrap (); 97} 98 99fn server_frame ( socket : & mut impl Read ) ->( u8 , Vec < u8 >) { 100let mut header =[ 0 ; 2 ]; 101 socket. read_exact ( & mut header). unwrap (); 102assert_eq! ( header[ 1 ] & 0x80 , 0 , "server frames must not be masked" ); 103assert_ne! ( header[ 0 ] & 0x80 , 0 ); 104let mut bytes =vec! [ 0 ; ( header[ 1 ] & 0x7f ) as usize ]; 105 socket. read_exact ( & mut bytes). unwrap (); 106( header[ 0 ] & 0x0f , bytes) 107} 108 109# [ test ] 110# [ cfg ( unix )] 111fn cli_file_responses_require_guest_read_authority () { 112let dir = tempfile:: tempdir (). unwrap (); 113let path = dir. path (). join ( "file.txt" ); 114 std:: fs:: write ( & path, b"file body" ). unwrap (); 115let source =format! ( 116"import {{serveFile}} from 'avril:std'; export default {{fetch(request) {{return serveFile(request, {});}}}};" , 117 serde_json:: to_string ( path. to_str (). unwrap ()). unwrap () 118); 119let grant =format! ( "--allow-read={}" , path. display ()); 120for allowedin [ false , true ] { 121let mut options =vec! [ "--port=0" ]; 122if allowed{ 123 options. push ( & grant); 124} 125let hosted =Hosted :: start ( & source, & options); 126let url = url:: Url :: parse ( & hosted. address ). unwrap (); 127let mut socket = 128TcpStream :: connect (( url. host_str (). unwrap (), url. port (). unwrap ())). unwrap (); 129 socket 130. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 131. unwrap (); 132 socket 133. write_all ( b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" ) 134. unwrap (); 135let mut reader =BufReader :: new ( socket); 136let head =headers ( & mut reader); 137let mut body =String :: new (); 138 reader. read_to_string ( & mut body). unwrap (); 139if allowed{ 140assert! ( head. starts_with ( "HTTP/1.1 200" ), "{head}" ); 141assert_eq! ( body, "file body" ); 142} else { 143assert! ( head. starts_with ( "HTTP/1.1 500" ), "{head}" ); 144assert_eq! ( body, "Internal Server Error" ); 145} 146 hosted. stop (); 147} 148} 149 150# [ test ] 151# [ cfg ( unix )] 152fn cli_serves_without_net_authority_preserves_this_and_waits_for_top_level_await () { 153let hosted =Hosted :: start ( 154r#" 155import {serve} from 'avril:std'; 156await new Promise(resolve=>setTimeout(resolve,10)); 157setInterval(()=>{},1000); 158export default { 159prefix:'host:', 160async fetch(request,info) { 161try {serve({port:0},()=>new Response()); throw Error('programmatic listener escaped');} 162catch(e) {if(!e.message.includes('PermissionDenied')) throw e;} 163try {await fetch(request.url); throw Error('outbound fetch escaped');} 164catch(e) {if(!e.message.includes('PermissionDenied')) throw e;} 165try {new WebSocket(request.url); throw Error('outbound WebSocket escaped');} 166catch(e) {if(!e.message.includes('PermissionDenied')) throw e;} 167if(info.remoteAddr.transport!=='tcp') throw Error('peer'); 168return new Response(this.prefix+await request.text(),{status:202}); 169} 170}; 171"# , 172& [ "--port=0" ], 173); 174let url = url:: Url :: parse ( & hosted. address ). unwrap (); 175let mut socket =TcpStream :: connect (( url. host_str (). unwrap (), url. port (). unwrap ())). unwrap (); 176 socket 177. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 178. unwrap (); 179 socket. write_all ( b"POST / HTTP/1.1\r\nHost: example.test\r\nContent-Length: 4\r\nConnection: close\r\n\r\ntest" ). unwrap (); 180let mut reader =BufReader :: new ( socket); 181assert! ( headers ( & mut reader). starts_with ( "HTTP/1.1 202" )); 182let mut body =String :: new (); 183 reader. read_to_string ( & mut body). unwrap (); 184assert_eq! ( body, "host:test" ); 185 hosted. stop (); 186} 187 188# [ test ] 189fn cli_rejects_invalid_default_exports_and_listener_options () { 190let dir = tempfile:: tempdir (). unwrap (); 191let path = dir. path (). join ( "main.js" ); 192for sourcein [ 193"" , 194"export default () => new Response();" , 195"export default null;" , 196"export default {};" , 197"export default {fetch:1};" , 198] { 199 std:: fs:: write ( & path, source). unwrap (); 200let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 201. args ([ "serve" , "-0" , "--port=0" ]) 202. arg ( & path) 203. stdout ( Stdio :: piped ()) 204. stderr ( Stdio :: piped ()) 205. spawn () 206. unwrap (); 207let output = common:: wait ( child); 208assert! ( !output. status . success ()); 209let error =String :: from_utf8_lossy ( & output. stderr ); 210assert! ( 211 error. contains ( "default export object with a fetch function" ), 212"{error}" 213); 214assert! ( !error. contains ( "Listening on" )); 215} 216for optionsin [ 217vec! [ "--unix=/tmp/unused-avril.sock" , "--port=0" ], 218vec! [ "--port=65536" ], 219vec! [ "--port=-1" ], 220] { 221let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 222. arg ( "serve" ) 223. args ( options) 224. arg ( & path) 225. output () 226. unwrap (); 227assert! ( !output. status . success ()); 228assert! ( !String :: from_utf8_lossy ( & output. stderr ). contains ( "Listening on" )); 229} 230let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 231. args ([ "serve" , "--help" ]) 232. output () 233. unwrap (); 234let help =String :: from_utf8_lossy ( & output. stdout ); 235assert! ( 236 help. contains ( "--unix=PATH" ) 237 && help. contains ( "--hostname=HOST" ) 238 && help. contains ( "--port=PORT" ) 239); 240} 241 242# [ test ] 243# [ cfg ( unix )] 244fn cli_unix_listener_needs_no_guest_filesystem_authority_and_cleans_up () { 245let dir = tempfile:: tempdir (). unwrap (); 246let path = dir. path (). join ( "server.sock" ); 247let option =format! ( "--unix={}" , path. display ()); 248let hosted =Hosted :: start ( 249r#" 250export default {fetch(request,info){ 251if(info.remoteAddr.transport!=='unix'||info.remoteAddr.path!==null) throw Error('peer'); 252return new Response(request.url); 253}}; 254"# , 255& [ & option], 256); 257assert_eq! ( hosted. address , format! ( "unix:{}" , path. display ())); 258let mut socket = std:: os:: unix:: net:: UnixStream :: connect ( & path). unwrap (); 259 socket 260. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 261. unwrap (); 262 socket 263. write_all ( b"GET /unix?q=1 HTTP/1.1\r\nHost: unix.test\r\nConnection: close\r\n\r\n" ) 264. unwrap (); 265let mut reader =BufReader :: new ( socket); 266assert! ( headers ( & mut reader). starts_with ( "HTTP/1.1 200" )); 267let mut body =String :: new (); 268 reader. read_to_string ( & mut body). unwrap (); 269assert_eq! ( body, "http://unix.test/unix?q=1" ); 270 hosted. stop (); 271assert! ( !path. exists ()); 272} 273 274# [ test ] 275# [ cfg ( unix )] 276fn programmatic_unix_listener_requires_only_write_and_preserves_replacements () { 277let dir = tempfile:: tempdir (). unwrap (); 278let path = dir. path (). join ( "server.sock" ); 279let label = serde_json:: to_string ( path. to_str (). unwrap ()). unwrap (); 280let ( _source, mut child) = common:: spawn ( & format! ( 281r#" 282import {{serve,pledge}} from 'avril:std'; 283pledge({{net:false,write:{{allow:[{label}]}}}}); 284const server=serve({{path:{label}}},()=>{{server.shutdown();return new Response('unix');}}); 285if(server.addr.transport!=='unix'||server.addr.path!=={label}) throw Error('address'); 286console.log('ready'); await server.finished; 287"# 288)); 289let mut stdout =BufReader :: new ( child. stdout . take (). unwrap ()); 290let mut line =String :: new (); 291 stdout. read_line ( & mut line). unwrap (); 292assert_eq! ( line, "ready\n" ); 293 child. stdout =Some ( stdout. into_inner ()); 294let mut socket = std:: os:: unix:: net:: UnixStream :: connect ( & path). unwrap (); 295 socket 296. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 297. unwrap (); 298 std:: fs:: remove_file ( & path). unwrap (); 299 std:: fs:: write ( & path, "replacement" ). unwrap (); 300 socket 301. write_all ( b"GET / HTTP/1.1\r\nHost: unix.test\r\nConnection: close\r\n\r\n" ) 302. unwrap (); 303let mut reader =BufReader :: new ( socket); 304assert! ( headers ( & mut reader). starts_with ( "HTTP/1.1 200" )); 305let mut body =String :: new (); 306 reader. read_to_string ( & mut body). unwrap (); 307assert_eq! ( body, "unix" ); 308let output = common:: wait ( child); 309assert! ( 310 output. status . success (), 311"{}" , 312String :: from_utf8_lossy ( & output. stderr ) 313); 314assert_eq! ( std:: fs:: read_to_string ( & path). unwrap (), "replacement" ); 315let output = common:: run ( 316& format! ( 317r#" 318import {{serve,pledge}} from 'avril:std'; 319try {{serve({{path:{label}}},()=>new Response());throw Error('replaced existing entry');}} 320catch(e) {{if(e.message==='replaced existing entry')throw e;}} 321pledge({{write:false}}); 322try {{serve({{path:{label}}},()=>new Response());throw Error('no write authority');}} 323catch(e) {{if(!e.message.includes('PermissionDenied'))throw e;}} 324console.log('denied'); 325"# 326), 327& [], 328); 329assert_eq! ( output. stdout , b"denied\n" ); 330assert_eq! ( std:: fs:: read_to_string ( & path). unwrap (), "replacement" ); 331} 332 333# [ test ] 334fn websockets_upgrade_echo_text_binary_and_complete_clean_close () { 335let output = common:: run ( 336r#" 337import {serve,upgradeWebSocket} from 'avril:std'; 338const server=serve({port:0},request=>{ 339const {socket,response}=upgradeWebSocket(request,{protocol:'chat'}); 340if(!(socket instanceof WebSocket)||response.status!==101)throw Error('upgrade objects'); 341socket.onmessage=e=>socket.send(e.data); 342return response; 343}); 344const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`,'chat'); 345client.binaryType='arraybuffer'; 346const messages=[]; 347client.onopen=()=>{if(client.protocol!=='chat')throw Error('protocol');client.send('echo');client.send(new Uint8Array([4,2]));}; 348client.onmessage=e=>{messages.push(typeof e.data==='string'?e.data:new Uint8Array(e.data).join());if(messages.length===2)client.close(3001,'done');}; 349const close=await new Promise(resolve=>client.onclose=resolve); 350if(messages.join('|')!=='echo|4,2'||!close.wasClean||close.code!==3001||close.reason!=='done')throw Error('echo or close'); 351await server.shutdown();console.log('upgraded'); 352"# , 353& [], 354); 355assert_eq! ( output. stdout , b"upgraded\n" ); 356} 357 358# [ test ] 359fn abandoning_an_upgrade_releases_the_socket_and_server () { 360let output = common:: run ( 361r#" 362import {serve,upgradeWebSocket} from 'avril:std'; 363let closed; 364const gone=new Promise(resolve=>closed=resolve); 365const server=serve({port:0},request=>{ 366const {socket}=upgradeWebSocket(request); 367socket.onclose=closed; 368return new Response('not upgraded'); 369}); 370const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`); 371const clientClosed=new Promise(resolve=>client.onclose=resolve); 372await gone; await clientClosed; await server.shutdown(); console.log('abandoned'); 373"# , 374& [], 375); 376assert_eq! ( output. stdout , b"abandoned\n" ); 377} 378 379# [ test ] 380fn forced_shutdown_closes_upgraded_sockets_even_after_graceful_shutdown () { 381let output = common:: run ( 382r#" 383import {serve,upgradeWebSocket} from 'avril:std'; 384const controller=new AbortController();let serverClosed; 385const gone=new Promise(resolve=>serverClosed=resolve); 386const server=serve({port:0,signal:controller.signal},request=>{ 387const {socket,response}=upgradeWebSocket(request);socket.onclose=serverClosed;return response; 388}); 389const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`); 390await new Promise(resolve=>client.onopen=resolve); 391const closed=new Promise(resolve=>client.onclose=resolve); 392let finished=false;server.shutdown().then(()=>finished=true); 393await new Promise(resolve=>setTimeout(resolve,10));if(finished)throw Error('lost active WebSocket'); 394controller.abort(); await gone; await closed; await server.finished;console.log('forced'); 395"# , 396& [], 397); 398assert_eq! ( output. stdout , b"forced\n" ); 399} 400 401fn handshake < S : Read +Write >( mut socket : S ) ->BufReader < S > { 402 socket. write_all ( b"GET / HTTP/1.1\r\nHost: example.test\r\nConnection: keep-alive, Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" ). unwrap (); 403let mut reader =BufReader :: new ( socket); 404let response =headers ( & mut reader); 405assert! ( response. starts_with ( "HTTP/1.1 101" ), "{response}" ); 406assert! ( 407 response 408. to_ascii_lowercase () 409. contains ( "sec-websocket-accept: s3pplmbitxaq9kygzzhzrbk+xoo=" ) 410); 411 reader 412} 413 414# [ test ] 415# [ cfg ( unix )] 416fn cli_websocket_upgrades_require_no_guest_net_authority_and_obey_masking () { 417let hosted =Hosted :: start ( 418r#" 419import {upgradeWebSocket} from 'avril:std'; 420export default {fetch(request){const {socket,response}=upgradeWebSocket(request);socket.onmessage=e=>socket.send(e.data);return response;}}; 421"# , 422& [ "--port=0" ], 423); 424let url = url:: Url :: parse ( & hosted. address ). unwrap (); 425let socket =TcpStream :: connect (( url. host_str (). unwrap (), url. port (). unwrap ())). unwrap (); 426 socket 427. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 428. unwrap (); 429let mut reader =handshake ( socket); 430masked_frame ( reader. get_mut (), false , 1 , b"frag" ); 431masked_frame ( reader. get_mut (), true , 9 , b"ping" ); 432assert_eq! ( server_frame ( & mut reader), ( 10 , b"ping" . to_vec ())); 433masked_frame ( reader. get_mut (), true , 0 , b"mented" ); 434assert_eq! ( server_frame ( & mut reader), ( 1 , b"fragmented" . to_vec ())); 435 reader. get_mut (). write_all ( b"\x81\x01x" ). unwrap (); 436let ( opcode, payload) =server_frame ( & mut reader); 437assert_eq! ( opcode, 8 ); 438assert_eq! ( u16 :: from_be_bytes ([ payload[ 0 ], payload[ 1 ]]), 1002 ); 439drop ( reader); 440 hosted. stop (); 441} 442 443# [ test ] 444# [ cfg ( unix )] 445fn websocket_upgrades_work_over_unix_sockets () { 446let dir = tempfile:: tempdir (). unwrap (); 447let path = dir. path (). join ( "websocket.sock" ); 448let option =format! ( "--unix={}" , path. display ()); 449let hosted =Hosted :: start ( 450r#" 451import {upgradeWebSocket} from 'avril:std'; 452export default {fetch(request){const {socket,response}=upgradeWebSocket(request);socket.onmessage=e=>socket.send(e.data);return response;}}; 453"# , 454& [ & option], 455); 456let socket = std:: os:: unix:: net:: UnixStream :: connect ( & path). unwrap (); 457 socket 458. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 459. unwrap (); 460let mut reader =handshake ( socket); 461masked_frame ( reader. get_mut (), true , 1 , b"unix" ); 462assert_eq! ( server_frame ( & mut reader), ( 1 , b"unix" . to_vec ())); 463masked_frame ( reader. get_mut (), true , 8 , & 1000u16 . to_be_bytes ()); 464assert_eq! ( 465server_frame ( & mut reader), 466( 8 , 1000u16 . to_be_bytes (). to_vec ()) 467); 468drop ( reader); 469 hosted. stop (); 470assert! ( !path. exists ()); 471} 472 473# [ test ] 474# [ cfg ( unix )] 475fn malformed_websocket_handshakes_are_rejected_without_terminating_the_server () { 476let hosted =Hosted :: start ( 477r#" 478import {upgradeWebSocket} from 'avril:std'; 479export default {fetch(request){ 480try { return upgradeWebSocket(request,new URL(request.url).pathname==='/protocol'?{protocol:'missing'}:{}).response; } 481catch(error){ return new Response(error.message,{status:400}); } 482}}; 483"# , 484& [ "--port=0" ], 485); 486let url = url:: Url :: parse ( & hosted. address ). unwrap (); 487for ( method, path, version, key, extra, connection) in [ 488( "GET" , "/" , "14" , "dGhlIHNhbXBsZSBub25jZQ==" , "" , "Upgrade" ), 489( "GET" , "/" , "13" , "eA==" , "" , "Upgrade" ), 490( 491"GET" , 492"/" , 493"13" , 494"dGhlIHNhbXBsZSBub25jZQ==" , 495"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" , 496"Upgrade" , 497), 498( "POST" , "/" , "13" , "dGhlIHNhbXBsZSBub25jZQ==" , "" , "Upgrade" ), 499( "GET" , "/" , "13" , "dGhlIHNhbXBsZSBub25jZQ==" , "" , "close" ), 500( 501"GET" , 502"/protocol" , 503"13" , 504"dGhlIHNhbXBsZSBub25jZQ==" , 505"Sec-WebSocket-Protocol: chat\r\n" , 506"Upgrade" , 507), 508] { 509let mut socket = 510TcpStream :: connect (( url. host_str (). unwrap (), url. port (). unwrap ())). unwrap (); 511 socket 512. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 513. unwrap (); 514write! ( socket, "{method} {path} HTTP/1.1\r\nHost: example.test\r\nConnection: {connection}, close\r\nUpgrade: websocket\r\nSec-WebSocket-Version: {version}\r\nSec-WebSocket-Key: {key}\r\n{extra}\r\n" ). unwrap (); 515let mut reader =BufReader :: new ( socket); 516let response =headers ( & mut reader); 517assert! ( response. starts_with ( "HTTP/1.1 400" ), "{response}" ); 518let mut error =String :: new (); 519 reader. read_to_string ( & mut error). unwrap (); 520assert! ( error. contains ( "WebSocket" ), "{error}" ); 521} 522 hosted. stop (); 523} 524 525# [ test ] 526fn constructed_requests_clones_and_responses_from_other_requests_cannot_upgrade () { 527let output = common:: run ( 528r#" 529import {serve,upgradeWebSocket} from 'avril:std'; 530try {upgradeWebSocket(new Request('http://example.test'));throw Error('constructed request escaped');} 531catch(error){if(!(error instanceof TypeError))throw error;} 532let saved; 533const server=serve({port:0},request=>{ 534if(new URL(request.url).pathname==='/reuse') return saved; 535try {upgradeWebSocket(request.clone());throw Error('cloned request escaped');} 536catch(error){if(!(error instanceof TypeError))throw error;} 537saved=upgradeWebSocket(request).response; 538try {upgradeWebSocket(request);throw Error('duplicate upgrade');} 539catch(error){if(error.message==='duplicate upgrade')throw error;} 540return new Response('abandoned'); 541}); 542const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`); 543await new Promise(resolve=>client.onclose=resolve); 544const response=await fetch(`http://127.0.0.1:${server.addr.port}/reuse`); 545if(response.status!==500)throw Error('unrelated request upgraded'); 546await response.text();await server.shutdown();console.log('guarded'); 547"# , 548& [], 549); 550assert_eq! ( output. stdout , b"guarded\n" ); 551} 552 553# [ test ] 554# [ cfg ( unix )] 555fn workers_do_not_inherit_cli_listener_authority_or_private_bootstrap_access () { 556let dir = tempfile:: tempdir (). unwrap (); 557let path = dir. path (). join ( "worker.js" ); 558 std:: fs:: write ( 559& path, 560r#" 561import {serve} from 'avril:std'; 562try {serve({port:0},()=>new Response());postMessage('escaped');} 563catch(error){postMessage(error.message);} 564close(); 565"# , 566) 567. unwrap (); 568let worker = url:: Url :: from_file_path ( & path). unwrap (); 569let grant =format! ( "--allow-js-exec={}" , path. display ()); 570let hosted =Hosted :: start ( 571& format! ( 572r#" 573const std=await import('avril:std'); 574if('startCLI' in std)throw Error('bootstrap exported'); 575try {{await import('ext:http/main.js');throw Error('private module escaped');}} 576catch(error){{if(!error.message.includes('private'))throw error;}} 577const worker=new Worker('{worker}',{{type:'module'}}); 578const result=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data)); 579worker.terminate(); 580export default {{fetch(){{return new Response(result);}}}}; 581"# 582), 583& [ "--port=0" , & grant], 584); 585let url = url:: Url :: parse ( & hosted. address ). unwrap (); 586let mut socket =TcpStream :: connect (( url. host_str (). unwrap (), url. port (). unwrap ())). unwrap (); 587 socket 588. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 589. unwrap (); 590 socket 591. write_all ( b"GET / HTTP/1.1\r\nHost: example.test\r\nConnection: close\r\n\r\n" ) 592. unwrap (); 593let mut reader =BufReader :: new ( socket); 594assert! ( headers ( & mut reader). starts_with ( "HTTP/1.1 200" )); 595let mut body =String :: new (); 596 reader. read_to_string ( & mut body). unwrap (); 597assert! ( body. contains ( "PermissionDenied" ), "{body}" ); 598 hosted. stop (); 599} 600 601# [ test ] 602fn aborting_during_option_conversion_prevents_binding () { 603let output = common:: run ( 604r#" 605import {serve,pledge} from 'avril:std'; 606pledge({net:false}); 607const controller=new AbortController(); 608try { 609serve({port:0,signal:controller.signal,get path(){controller.abort('late');return undefined;}},()=>new Response()); 610throw Error('aborted listener bound'); 611} catch(error){if(error!=='late')throw error;} 612console.log('aborted'); 613"# , 614& [], 615); 616assert_eq! ( output. stdout , b"aborted\n" ); 617} 618 619# [ test ] 620# [ cfg ( unix )] 621fn sigterm_cleans_up_cli_unix_listeners () { 622let dir = tempfile:: tempdir (). unwrap (); 623let path = dir. path (). join ( "terminate.sock" ); 624let option =format! ( "--unix={}" , path. display ()); 625let hosted =Hosted :: start ( 626"export default {fetch(){return new Response('ok');}};" , 627& [ & option], 628); 629assert! ( path. exists ()); 630 hosted. stop_with ( libc:: SIGTERM ); 631assert! ( !path. exists ()); 632} 633 634# [ test ] 635# [ cfg ( unix )] 636fn a_second_shutdown_signal_interrupts_cpu_bound_handlers () { 637let dir = tempfile:: tempdir (). unwrap (); 638let path = dir. path (). join ( "busy.sock" ); 639let option =format! ( "--unix={}" , path. display ()); 640let mut hosted =Hosted :: start ( 641"export default {fetch(){console.log('busy');while(true){}}};" , 642& [ & option], 643); 644let mut socket = std:: os:: unix:: net:: UnixStream :: connect ( & path). unwrap (); 645 socket 646. write_all ( b"GET / HTTP/1.1\r\nHost: example.test\r\n\r\n" ) 647. unwrap (); 648let child = hosted. child . as_mut (). unwrap (); 649let mut stdout =BufReader :: new ( child. stdout . take (). unwrap ()); 650let mut line =String :: new (); 651 stdout. read_line ( & mut line). unwrap (); 652assert_eq! ( line, "busy\n" ); 653 child. stdout =Some ( stdout. into_inner ()); 654assert_eq! ( unsafe { libc:: kill ( child. id () as i32 , libc:: SIGINT ) }, 0 ); 655let deadline = std:: time:: Instant :: now () +Duration :: from_secs ( 2 ); 656while path. exists () && std:: time:: Instant :: now () < deadline{ 657 std:: thread:: sleep ( Duration :: from_millis ( 5 )); 658} 659assert! ( !path. exists ()); 660assert_eq! ( unsafe { libc:: kill ( child. id () as i32 , libc:: SIGINT ) }, 0 ); 661let output = common:: wait ( hosted. child . take (). unwrap ()); 662assert! ( 663 output. status . code (). is_some (), 664"forced shutdown needed an external kill" 665); 666} 667 668# [ test ] 669# [ cfg ( target_os = "linux" )] 670fn server_recovers_after_accept_exhausts_file_descriptors () { 671use std:: os:: unix:: process:: CommandExt ; 672use std:: time:: Instant ; 673 674let dir = tempfile:: tempdir (). unwrap (); 675let path = dir. path (). join ( "main.js" ); 676 std:: fs:: write ( 677& path, 678"import {serve} from 'avril:std'; const server=serve({port:0},()=>{server.shutdown();return new Response('recovered');}); console.log(server.addr.port); await server.finished;" , 679) 680. unwrap (); 681let mut command =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )); 682 command 683. arg ( "run" ) 684. arg ( path) 685. stdout ( Stdio :: piped ()) 686. stderr ( Stdio :: piped ()); 687unsafe { 688 command. pre_exec ( ||{ 689let limit = libc:: rlimit { rlim_cur : 64 , rlim_max : 64 }; 690if libc:: setrlimit ( libc:: RLIMIT_NOFILE , & limit) !=0 { 691return Err ( std:: io:: Error :: last_os_error ()); 692} 693Ok (()) 694}); 695} 696let child = command. spawn (). unwrap (); 697let mut hosted =Hosted { 698_dir : dir, 699child : Some ( child), 700address : String :: new (), 701}; 702let child = hosted. child . as_mut (). unwrap (); 703let mut stdout =BufReader :: new ( child. stdout . take (). unwrap ()); 704let mut line =String :: new (); 705 stdout. read_line ( & mut line). unwrap (); 706let port: u16 = line. trim (). parse (). expect ( & line); 707 child. stdout =Some ( stdout. into_inner ()); 708let fd_path =format! ( "/proc/{}/fd" , child. id ()); 709let mut sockets =Vec :: new (); 710for _in 0 ..80 { 711 sockets. push ( 712TcpStream :: connect_timeout ( & ([ 127 , 0 , 0 , 1 ], port). into (), Duration :: from_secs ( 2 )) 713. unwrap (), 714); 715} 716let deadline =Instant :: now () +Duration :: from_secs ( 2 ); 717while std:: fs:: read_dir ( & fd_path). unwrap (). count () <64 { 718assert! ( 719Instant :: now () < deadline, 720"server did not exhaust descriptors" 721); 722 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 723} 724// Leave an accept pending while the process has no free descriptors. 725 std:: thread:: sleep ( Duration :: from_millis ( 200 )); 726drop ( sockets); 727let mut socket =TcpStream :: connect (( "127.0.0.1" , port)). unwrap (); 728 socket 729. set_read_timeout ( Some ( Duration :: from_secs ( 3 ))) 730. unwrap (); 731 socket 732. write_all ( b"GET / HTTP/1.1\r\nHost: example.test\r\nConnection: close\r\n\r\n" ) 733. unwrap (); 734let mut response =String :: new (); 735 socket. read_to_string ( & mut response). unwrap (); 736assert! ( response. starts_with ( "HTTP/1.1 200" ), "{response}" ); 737assert! ( response. ends_with ( "recovered" ), "{response}" ); 738let output = common:: wait ( hosted. child . take (). unwrap ()); 739assert! ( 740 output. status . success (), 741"{}" , 742String :: from_utf8_lossy ( & output. stderr ) 743); 744} 745 746# [ test ] 747fn programmatic_ipv6_loopback_address_can_be_reused () { 748let Ok ( _probe) = std:: net:: TcpListener :: bind ( "[::1]:0" ) else { 749return ; 750}; 751let output = common:: run ( 752r#" 753import {serve,pledge} from 'avril:std'; 754pledge({net:{allow:['[::1]']}}); 755let server=serve({hostname:'::1',port:0},()=>new Response('ipv6')); 756if(server.addr.hostname!=='::1')throw Error('hostname'); 757const addr=server.addr; 758await server.shutdown(); 759server=serve(addr,()=>new Response('reused')); 760const response=await fetch(`http://[${addr.hostname}]:${addr.port}/`); 761if(await response.text()!=='reused')throw Error('response'); 762await server.shutdown();console.log('ipv6'); 763"# , 764& [], 765); 766assert_eq! ( output. stdout , b"ipv6\n" ); 767}