char/avril

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

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

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
28.3 KiB767 linesraw
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,
10    child: Option<Child>,
11    address: String,
12}
13
14impl Hosted {
15    fn start(source: &str, options: &[&str]) -> Self {
16        let dir = tempfile::tempdir().unwrap();
17        let path = dir.path().join("main.js");
18        std::fs::write(&path, source).unwrap();
19        let 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();
28        let mut hosted = Self {
29            _dir: dir,
30            child: Some(child),
31            address: String::new(),
32        };
33        let child = hosted.child.as_mut().unwrap();
34        let mut stderr = BufReader::new(child.stderr.take().unwrap());
35        let 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)]
47    fn stop(self) {
48        self.stop_with(libc::SIGINT);
49    }
50
51    #[cfg(unix)]
52    fn stop_with(mut self, signal: i32) {
53        let child = self.child.take().unwrap();
54        assert_eq!(unsafe { libc::kill(child.id() as i32, signal) }, 0);
55        let output = common::wait(child);
56        assert!(
57            output.status.success(),
58            "{}",
59            String::from_utf8_lossy(&output.stderr)
60        );
61    }
62}
63
64impl Drop for Hosted {
65    fn drop(&mut self) {
66        if let Some(mut child) = self.child.take() {
67            let _ = child.kill();
68            let _ = child.wait();
69        }
70    }
71}
72
73fn headers(reader: &mut impl BufRead) -> String {
74    let mut result = String::new();
75    loop {
76        let mut line = String::new();
77        assert_ne!(reader.read_line(&mut line).unwrap(), 0, "unexpected EOF");
78        result.push_str(&line);
79        if line == "\r\n" {
80            return result;
81        }
82    }
83}
84
85fn masked_frame(socket: &mut impl Write, fin: bool, opcode: u8, payload: &[u8]) {
86    assert!(payload.len() <= 125);
87    let mask = [1, 7, 3, 9];
88    let mut frame = vec![
89        (if fin { 0x80 } else { 0 }) | opcode,
90        0x80 | payload.len() as u8,
91    ];
92    frame.extend_from_slice(&mask);
93    for (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>) {
100    let mut header = [0; 2];
101    socket.read_exact(&mut header).unwrap();
102    assert_eq!(header[1] & 0x80, 0, "server frames must not be masked");
103    assert_ne!(header[0] & 0x80, 0);
104    let 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() {
112    let dir = tempfile::tempdir().unwrap();
113    let path = dir.path().join("file.txt");
114    std::fs::write(&path, b"file body").unwrap();
115    let 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    );
119    let grant = format!("--allow-read={}", path.display());
120    for allowed in [false, true] {
121        let mut options = vec!["--port=0"];
122        if allowed {
123            options.push(&grant);
124        }
125        let hosted = Hosted::start(&source, &options);
126        let url = url::Url::parse(&hosted.address).unwrap();
127        let mut socket =
128            TcpStream::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();
135        let mut reader = BufReader::new(socket);
136        let head = headers(&mut reader);
137        let mut body = String::new();
138        reader.read_to_string(&mut body).unwrap();
139        if allowed {
140            assert!(head.starts_with("HTTP/1.1 200"), "{head}");
141            assert_eq!(body, "file body");
142        } else {
143            assert!(head.starts_with("HTTP/1.1 500"), "{head}");
144            assert_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() {
153    let hosted = Hosted::start(
154        r#"
155        import {serve} from 'avril:std';
156        await new Promise(resolve=>setTimeout(resolve,10));
157        setInterval(()=>{},1000);
158        export default {
159            prefix:'host:',
160            async fetch(request,info) {
161                try {serve({port:0},()=>new Response()); throw Error('programmatic listener escaped');}
162                catch(e) {if(!e.message.includes('PermissionDenied')) throw e;}
163                try {await fetch(request.url); throw Error('outbound fetch escaped');}
164                catch(e) {if(!e.message.includes('PermissionDenied')) throw e;}
165                try {new WebSocket(request.url); throw Error('outbound WebSocket escaped');}
166                catch(e) {if(!e.message.includes('PermissionDenied')) throw e;}
167                if(info.remoteAddr.transport!=='tcp') throw Error('peer');
168                return new Response(this.prefix+await request.text(),{status:202});
169            }
170        };
171    "#,
172        &["--port=0"],
173    );
174    let url = url::Url::parse(&hosted.address).unwrap();
175    let 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();
180    let mut reader = BufReader::new(socket);
181    assert!(headers(&mut reader).starts_with("HTTP/1.1 202"));
182    let mut body = String::new();
183    reader.read_to_string(&mut body).unwrap();
184    assert_eq!(body, "host:test");
185    hosted.stop();
186}
187
188#[test]
189fn cli_rejects_invalid_default_exports_and_listener_options() {
190    let dir = tempfile::tempdir().unwrap();
191    let path = dir.path().join("main.js");
192    for source in [
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();
200        let 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();
207        let output = common::wait(child);
208        assert!(!output.status.success());
209        let error = String::from_utf8_lossy(&output.stderr);
210        assert!(
211            error.contains("default export object with a fetch function"),
212            "{error}"
213        );
214        assert!(!error.contains("Listening on"));
215    }
216    for options in [
217        vec!["--unix=/tmp/unused-avril.sock", "--port=0"],
218        vec!["--port=65536"],
219        vec!["--port=-1"],
220    ] {
221        let output = Command::new(env!("CARGO_BIN_EXE_avril"))
222            .arg("serve")
223            .args(options)
224            .arg(&path)
225            .output()
226            .unwrap();
227        assert!(!output.status.success());
228        assert!(!String::from_utf8_lossy(&output.stderr).contains("Listening on"));
229    }
230    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
231        .args(["serve", "--help"])
232        .output()
233        .unwrap();
234    let help = String::from_utf8_lossy(&output.stdout);
235    assert!(
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() {
245    let dir = tempfile::tempdir().unwrap();
246    let path = dir.path().join("server.sock");
247    let option = format!("--unix={}", path.display());
248    let hosted = Hosted::start(
249        r#"
250        export default {fetch(request,info){
251            if(info.remoteAddr.transport!=='unix'||info.remoteAddr.path!==null) throw Error('peer');
252            return new Response(request.url);
253        }};
254    "#,
255        &[&option],
256    );
257    assert_eq!(hosted.address, format!("unix:{}", path.display()));
258    let 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();
265    let mut reader = BufReader::new(socket);
266    assert!(headers(&mut reader).starts_with("HTTP/1.1 200"));
267    let mut body = String::new();
268    reader.read_to_string(&mut body).unwrap();
269    assert_eq!(body, "http://unix.test/unix?q=1");
270    hosted.stop();
271    assert!(!path.exists());
272}
273
274#[test]
275#[cfg(unix)]
276fn programmatic_unix_listener_requires_only_write_and_preserves_replacements() {
277    let dir = tempfile::tempdir().unwrap();
278    let path = dir.path().join("server.sock");
279    let label = serde_json::to_string(path.to_str().unwrap()).unwrap();
280    let (_source, mut child) = common::spawn(&format!(
281        r#"
282        import {{serve,pledge}} from 'avril:std';
283        pledge({{net:false,write:{{allow:[{label}]}}}});
284        const server=serve({{path:{label}}},()=>{{server.shutdown();return new Response('unix');}});
285        if(server.addr.transport!=='unix'||server.addr.path!=={label}) throw Error('address');
286        console.log('ready'); await server.finished;
287    "#
288    ));
289    let mut stdout = BufReader::new(child.stdout.take().unwrap());
290    let mut line = String::new();
291    stdout.read_line(&mut line).unwrap();
292    assert_eq!(line, "ready\n");
293    child.stdout = Some(stdout.into_inner());
294    let 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();
303    let mut reader = BufReader::new(socket);
304    assert!(headers(&mut reader).starts_with("HTTP/1.1 200"));
305    let mut body = String::new();
306    reader.read_to_string(&mut body).unwrap();
307    assert_eq!(body, "unix");
308    let output = common::wait(child);
309    assert!(
310        output.status.success(),
311        "{}",
312        String::from_utf8_lossy(&output.stderr)
313    );
314    assert_eq!(std::fs::read_to_string(&path).unwrap(), "replacement");
315    let output = common::run(
316        &format!(
317            r#"
318        import {{serve,pledge}} from 'avril:std';
319        try {{serve({{path:{label}}},()=>new Response());throw Error('replaced existing entry');}}
320        catch(e) {{if(e.message==='replaced existing entry')throw e;}}
321        pledge({{write:false}});
322        try {{serve({{path:{label}}},()=>new Response());throw Error('no write authority');}}
323        catch(e) {{if(!e.message.includes('PermissionDenied'))throw e;}}
324        console.log('denied');
325    "#
326        ),
327        &[],
328    );
329    assert_eq!(output.stdout, b"denied\n");
330    assert_eq!(std::fs::read_to_string(&path).unwrap(), "replacement");
331}
332
333#[test]
334fn websockets_upgrade_echo_text_binary_and_complete_clean_close() {
335    let output = common::run(
336        r#"
337        import {serve,upgradeWebSocket} from 'avril:std';
338        const server=serve({port:0},request=>{
339            const {socket,response}=upgradeWebSocket(request,{protocol:'chat'});
340            if(!(socket instanceof WebSocket)||response.status!==101)throw Error('upgrade objects');
341            socket.onmessage=e=>socket.send(e.data);
342            return response;
343        });
344        const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`,'chat');
345        client.binaryType='arraybuffer';
346        const messages=[];
347        client.onopen=()=>{if(client.protocol!=='chat')throw Error('protocol');client.send('echo');client.send(new Uint8Array([4,2]));};
348        client.onmessage=e=>{messages.push(typeof e.data==='string'?e.data:new Uint8Array(e.data).join());if(messages.length===2)client.close(3001,'done');};
349        const close=await new Promise(resolve=>client.onclose=resolve);
350        if(messages.join('|')!=='echo|4,2'||!close.wasClean||close.code!==3001||close.reason!=='done')throw Error('echo or close');
351        await server.shutdown();console.log('upgraded');
352    "#,
353        &[],
354    );
355    assert_eq!(output.stdout, b"upgraded\n");
356}
357
358#[test]
359fn abandoning_an_upgrade_releases_the_socket_and_server() {
360    let output = common::run(
361        r#"
362        import {serve,upgradeWebSocket} from 'avril:std';
363        let closed;
364        const gone=new Promise(resolve=>closed=resolve);
365        const server=serve({port:0},request=>{
366            const {socket}=upgradeWebSocket(request);
367            socket.onclose=closed;
368            return new Response('not upgraded');
369        });
370        const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`);
371        const clientClosed=new Promise(resolve=>client.onclose=resolve);
372        await gone; await clientClosed; await server.shutdown(); console.log('abandoned');
373    "#,
374        &[],
375    );
376    assert_eq!(output.stdout, b"abandoned\n");
377}
378
379#[test]
380fn forced_shutdown_closes_upgraded_sockets_even_after_graceful_shutdown() {
381    let output = common::run(
382        r#"
383        import {serve,upgradeWebSocket} from 'avril:std';
384        const controller=new AbortController();let serverClosed;
385        const gone=new Promise(resolve=>serverClosed=resolve);
386        const server=serve({port:0,signal:controller.signal},request=>{
387            const {socket,response}=upgradeWebSocket(request);socket.onclose=serverClosed;return response;
388        });
389        const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`);
390        await new Promise(resolve=>client.onopen=resolve);
391        const closed=new Promise(resolve=>client.onclose=resolve);
392        let finished=false;server.shutdown().then(()=>finished=true);
393        await new Promise(resolve=>setTimeout(resolve,10));if(finished)throw Error('lost active WebSocket');
394        controller.abort(); await gone; await closed; await server.finished;console.log('forced');
395    "#,
396        &[],
397    );
398    assert_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();
403    let mut reader = BufReader::new(socket);
404    let response = headers(&mut reader);
405    assert!(response.starts_with("HTTP/1.1 101"), "{response}");
406    assert!(
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() {
417    let hosted = Hosted::start(
418        r#"
419        import {upgradeWebSocket} from 'avril:std';
420        export default {fetch(request){const {socket,response}=upgradeWebSocket(request);socket.onmessage=e=>socket.send(e.data);return response;}};
421    "#,
422        &["--port=0"],
423    );
424    let url = url::Url::parse(&hosted.address).unwrap();
425    let socket = TcpStream::connect((url.host_str().unwrap(), url.port().unwrap())).unwrap();
426    socket
427        .set_read_timeout(Some(Duration::from_secs(3)))
428        .unwrap();
429    let mut reader = handshake(socket);
430    masked_frame(reader.get_mut(), false, 1, b"frag");
431    masked_frame(reader.get_mut(), true, 9, b"ping");
432    assert_eq!(server_frame(&mut reader), (10, b"ping".to_vec()));
433    masked_frame(reader.get_mut(), true, 0, b"mented");
434    assert_eq!(server_frame(&mut reader), (1, b"fragmented".to_vec()));
435    reader.get_mut().write_all(b"\x81\x01x").unwrap();
436    let (opcode, payload) = server_frame(&mut reader);
437    assert_eq!(opcode, 8);
438    assert_eq!(u16::from_be_bytes([payload[0], payload[1]]), 1002);
439    drop(reader);
440    hosted.stop();
441}
442
443#[test]
444#[cfg(unix)]
445fn websocket_upgrades_work_over_unix_sockets() {
446    let dir = tempfile::tempdir().unwrap();
447    let path = dir.path().join("websocket.sock");
448    let option = format!("--unix={}", path.display());
449    let hosted = Hosted::start(
450        r#"
451        import {upgradeWebSocket} from 'avril:std';
452        export default {fetch(request){const {socket,response}=upgradeWebSocket(request);socket.onmessage=e=>socket.send(e.data);return response;}};
453    "#,
454        &[&option],
455    );
456    let socket = std::os::unix::net::UnixStream::connect(&path).unwrap();
457    socket
458        .set_read_timeout(Some(Duration::from_secs(3)))
459        .unwrap();
460    let mut reader = handshake(socket);
461    masked_frame(reader.get_mut(), true, 1, b"unix");
462    assert_eq!(server_frame(&mut reader), (1, b"unix".to_vec()));
463    masked_frame(reader.get_mut(), true, 8, &1000u16.to_be_bytes());
464    assert_eq!(
465        server_frame(&mut reader),
466        (8, 1000u16.to_be_bytes().to_vec())
467    );
468    drop(reader);
469    hosted.stop();
470    assert!(!path.exists());
471}
472
473#[test]
474#[cfg(unix)]
475fn malformed_websocket_handshakes_are_rejected_without_terminating_the_server() {
476    let hosted = Hosted::start(
477        r#"
478        import {upgradeWebSocket} from 'avril:std';
479        export default {fetch(request){
480            try { return upgradeWebSocket(request,new URL(request.url).pathname==='/protocol'?{protocol:'missing'}:{}).response; }
481            catch(error){ return new Response(error.message,{status:400}); }
482        }};
483    "#,
484        &["--port=0"],
485    );
486    let url = url::Url::parse(&hosted.address).unwrap();
487    for (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    ] {
509        let mut socket =
510            TcpStream::connect((url.host_str().unwrap(), url.port().unwrap())).unwrap();
511        socket
512            .set_read_timeout(Some(Duration::from_secs(3)))
513            .unwrap();
514        write!(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();
515        let mut reader = BufReader::new(socket);
516        let response = headers(&mut reader);
517        assert!(response.starts_with("HTTP/1.1 400"), "{response}");
518        let mut error = String::new();
519        reader.read_to_string(&mut error).unwrap();
520        assert!(error.contains("WebSocket"), "{error}");
521    }
522    hosted.stop();
523}
524
525#[test]
526fn constructed_requests_clones_and_responses_from_other_requests_cannot_upgrade() {
527    let output = common::run(
528        r#"
529        import {serve,upgradeWebSocket} from 'avril:std';
530        try {upgradeWebSocket(new Request('http://example.test'));throw Error('constructed request escaped');}
531        catch(error){if(!(error instanceof TypeError))throw error;}
532        let saved;
533        const server=serve({port:0},request=>{
534            if(new URL(request.url).pathname==='/reuse') return saved;
535            try {upgradeWebSocket(request.clone());throw Error('cloned request escaped');}
536            catch(error){if(!(error instanceof TypeError))throw error;}
537            saved=upgradeWebSocket(request).response;
538            try {upgradeWebSocket(request);throw Error('duplicate upgrade');}
539            catch(error){if(error.message==='duplicate upgrade')throw error;}
540            return new Response('abandoned');
541        });
542        const client=new WebSocket(`ws://127.0.0.1:${server.addr.port}`);
543        await new Promise(resolve=>client.onclose=resolve);
544        const response=await fetch(`http://127.0.0.1:${server.addr.port}/reuse`);
545        if(response.status!==500)throw Error('unrelated request upgraded');
546        await response.text();await server.shutdown();console.log('guarded');
547    "#,
548        &[],
549    );
550    assert_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() {
556    let dir = tempfile::tempdir().unwrap();
557    let path = dir.path().join("worker.js");
558    std::fs::write(
559        &path,
560        r#"
561        import {serve} from 'avril:std';
562        try {serve({port:0},()=>new Response());postMessage('escaped');}
563        catch(error){postMessage(error.message);}
564        close();
565    "#,
566    )
567    .unwrap();
568    let worker = url::Url::from_file_path(&path).unwrap();
569    let grant = format!("--allow-js-exec={}", path.display());
570    let hosted = Hosted::start(
571        &format!(
572            r#"
573        const std=await import('avril:std');
574        if('startCLI' in std)throw Error('bootstrap exported');
575        try {{await import('ext:http/main.js');throw Error('private module escaped');}}
576        catch(error){{if(!error.message.includes('private'))throw error;}}
577        const worker=new Worker('{worker}',{{type:'module'}});
578        const result=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data));
579        worker.terminate();
580        export default {{fetch(){{return new Response(result);}}}};
581    "#
582        ),
583        &["--port=0", &grant],
584    );
585    let url = url::Url::parse(&hosted.address).unwrap();
586    let 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();
593    let mut reader = BufReader::new(socket);
594    assert!(headers(&mut reader).starts_with("HTTP/1.1 200"));
595    let mut body = String::new();
596    reader.read_to_string(&mut body).unwrap();
597    assert!(body.contains("PermissionDenied"), "{body}");
598    hosted.stop();
599}
600
601#[test]
602fn aborting_during_option_conversion_prevents_binding() {
603    let output = common::run(
604        r#"
605        import {serve,pledge} from 'avril:std';
606        pledge({net:false});
607        const controller=new AbortController();
608        try {
609            serve({port:0,signal:controller.signal,get path(){controller.abort('late');return undefined;}},()=>new Response());
610            throw Error('aborted listener bound');
611        } catch(error){if(error!=='late')throw error;}
612        console.log('aborted');
613    "#,
614        &[],
615    );
616    assert_eq!(output.stdout, b"aborted\n");
617}
618
619#[test]
620#[cfg(unix)]
621fn sigterm_cleans_up_cli_unix_listeners() {
622    let dir = tempfile::tempdir().unwrap();
623    let path = dir.path().join("terminate.sock");
624    let option = format!("--unix={}", path.display());
625    let hosted = Hosted::start(
626        "export default {fetch(){return new Response('ok');}};",
627        &[&option],
628    );
629    assert!(path.exists());
630    hosted.stop_with(libc::SIGTERM);
631    assert!(!path.exists());
632}
633
634#[test]
635#[cfg(unix)]
636fn a_second_shutdown_signal_interrupts_cpu_bound_handlers() {
637    let dir = tempfile::tempdir().unwrap();
638    let path = dir.path().join("busy.sock");
639    let option = format!("--unix={}", path.display());
640    let mut hosted = Hosted::start(
641        "export default {fetch(){console.log('busy');while(true){}}};",
642        &[&option],
643    );
644    let 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();
648    let child = hosted.child.as_mut().unwrap();
649    let mut stdout = BufReader::new(child.stdout.take().unwrap());
650    let mut line = String::new();
651    stdout.read_line(&mut line).unwrap();
652    assert_eq!(line, "busy\n");
653    child.stdout = Some(stdout.into_inner());
654    assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0);
655    let deadline = std::time::Instant::now() + Duration::from_secs(2);
656    while path.exists() && std::time::Instant::now() < deadline {
657        std::thread::sleep(Duration::from_millis(5));
658    }
659    assert!(!path.exists());
660    assert_eq!(unsafe { libc::kill(child.id() as i32, libc::SIGINT) }, 0);
661    let output = common::wait(hosted.child.take().unwrap());
662    assert!(
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() {
671    use std::os::unix::process::CommandExt;
672    use std::time::Instant;
673
674    let dir = tempfile::tempdir().unwrap();
675    let 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();
681    let 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());
687    unsafe {
688        command.pre_exec(|| {
689            let limit = libc::rlimit { rlim_cur: 64, rlim_max: 64 };
690            if libc::setrlimit(libc::RLIMIT_NOFILE, &limit) != 0 {
691                return Err(std::io::Error::last_os_error());
692            }
693            Ok(())
694        });
695    }
696    let child = command.spawn().unwrap();
697    let mut hosted = Hosted {
698        _dir: dir,
699        child: Some(child),
700        address: String::new(),
701    };
702    let child = hosted.child.as_mut().unwrap();
703    let mut stdout = BufReader::new(child.stdout.take().unwrap());
704    let mut line = String::new();
705    stdout.read_line(&mut line).unwrap();
706    let port: u16 = line.trim().parse().expect(&line);
707    child.stdout = Some(stdout.into_inner());
708    let fd_path = format!("/proc/{}/fd", child.id());
709    let mut sockets = Vec::new();
710    for _ in 0..80 {
711        sockets.push(
712            TcpStream::connect_timeout(&([127, 0, 0, 1], port).into(), Duration::from_secs(2))
713                .unwrap(),
714        );
715    }
716    let deadline = Instant::now() + Duration::from_secs(2);
717    while std::fs::read_dir(&fd_path).unwrap().count() < 64 {
718        assert!(
719            Instant::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));
726    drop(sockets);
727    let 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();
734    let mut response = String::new();
735    socket.read_to_string(&mut response).unwrap();
736    assert!(response.starts_with("HTTP/1.1 200"), "{response}");
737    assert!(response.ends_with("recovered"), "{response}");
738    let output = common::wait(hosted.child.take().unwrap());
739    assert!(
740        output.status.success(),
741        "{}",
742        String::from_utf8_lossy(&output.stderr)
743    );
744}
745
746#[test]
747fn programmatic_ipv6_loopback_address_can_be_reused() {
748    let Ok(_probe) = std::net::TcpListener::bind("[::1]:0") else {
749        return;
750    };
751    let output = common::run(
752        r#"
753        import {serve,pledge} from 'avril:std';
754        pledge({net:{allow:['[::1]']}});
755        let server=serve({hostname:'::1',port:0},()=>new Response('ipv6'));
756        if(server.addr.hostname!=='::1')throw Error('hostname');
757        const addr=server.addr;
758        await server.shutdown();
759        server=serve(addr,()=>new Response('reused'));
760        const response=await fetch(`http://[${addr.hostname}]:${addr.port}/`);
761        if(await response.text()!=='reused')throw Error('response');
762        await server.shutdown();console.log('ipv6');
763    "#,
764        &[],
765    );
766    assert_eq!(output.stdout, b"ipv6\n");
767}