char/avril

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

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

Charlotte Somport net extension to typescript1f64888

main
11.4 KiB231 linesraw
1mod common;
2
3use std::io::{BufRead, BufReader};
4
5#[test]
6fn shared_views_keep_receive_aliasing_and_send_snapshots() {
7    let output = common::run(
8        r#"
9        import {listenDatagram} from 'avril:std';
10        const receiver = listenDatagram({port:0}), sender = listenDatagram({port:0});
11        const storage = new SharedArrayBuffer(12), target = new Uint8Array(storage, 3, 4);
12        const input = new Uint8Array(new SharedArrayBuffer(4)); input.set([1,2,3,4]);
13        const pending = receiver.receive(target);
14        const sending = sender.send(input, receiver.addr);
15        input.fill(9);
16        if (await sending !== 4) throw Error('send count');
17        const [message, remote] = await pending;
18        if (message.buffer !== storage || message.byteOffset !== 3 || message.length !== 4 ||
19            message.join() !== '1,2,3,4' || target.join() !== '1,2,3,4') throw Error('shared view or snapshot');
20        if (remote.transport !== 'udp' || remote.port !== sender.addr.port ||
21            Object.getPrototypeOf(remote) !== null || !Object.isFrozen(remote)) throw Error('peer address');
22        receiver.close(); sender.close();
23        console.log('shared datagrams');
24        "#,
25        &[],
26    );
27    assert_eq!(output.stdout, b"shared datagrams\n");
28}
29
30#[test]
31fn datagrams_preserve_boundaries_empty_messages_and_sender_addresses() {
32    let output = common::run(
33        r#"
34        import {listenDatagram,DatagramConn} from 'avril:std';
35        const a=listenDatagram({port:0}),b=listenDatagram({port:0});
36        if(!(a instanceof DatagramConn)||a.addr.transport!=='udp'||!Object.isFrozen(a.addr))throw Error('socket');
37        const bytes=new Uint8Array([1,2,3]),sending=a.send(bytes,b.addr);bytes.fill(0);if(await sending!==3)throw Error('send');
38        let [packet,peer]=await b.receive();if(packet.join()!=='1,2,3'||peer.port!==a.addr.port||peer.transport!=='udp')throw Error('packet');
39        await a.send(new Uint8Array(),b.addr);[packet,peer]=await b.receive();if(packet.length!==0||peer.port!==a.addr.port)throw Error('empty datagram');
40        const large=new Uint8Array(65507);large.fill(42);if(await a.send(large,b.addr)!==large.length)throw Error('large send');[packet]=await b.receive();if(packet.length!==large.length||packet.some(byte=>byte!==42))throw Error('large datagram');
41        a.setBroadcast();a.setBroadcast(false);a.close();b.close();console.log('datagrams');
42    "#,
43        &[],
44    );
45    assert_eq!(output.stdout, b"datagrams\n");
46}
47
48#[test]
49fn receive_buffers_truncate_only_the_current_datagram_and_empty_buffers_consume_it() {
50    let output = common::run(
51        r#"
52        import {listenDatagram} from 'avril:std';
53        const a=listenDatagram({port:0}),b=listenDatagram({port:0});
54        const buffer=new Uint8Array(5);await a.send(new Uint8Array([1,2,3,4]),b.addr);
55        const [packet]=await b.receive(buffer.subarray(1,3));if(packet.buffer!==buffer.buffer||packet.byteOffset!==1||packet.join()!=='1,2')throw Error('buffer');
56        await a.send(new Uint8Array([9]),b.addr);const [next]=await b.receive();if(next.join()!=='9')throw Error('truncated packet spilled');
57        await a.send(new Uint8Array([4]),b.addr);const [empty]=await b.receive(new Uint8Array());if(empty.length!==0)throw Error('empty buffer');
58        await a.send(new Uint8Array([5]),b.addr);const [last]=await b.receive();if(last.join()!=='5')throw Error('empty receive did not consume');
59        a.close();b.close();console.log('buffers');
60    "#,
61        &[],
62    );
63    assert_eq!(output.stdout, b"buffers\n");
64}
65
66#[test]
67fn closing_cancels_receive_and_iteration_break_closes_the_socket() {
68    let output = common::run(
69        r#"
70        import {listenDatagram,errors} from 'avril:std';
71        const socket=listenDatagram({port:0}),pending=socket.receive().catch(e=>e);socket.close();if(!((await pending) instanceof errors.BadResource))throw Error('cancellation');socket[Symbol.dispose]();
72        const a=listenDatagram({port:0}),b=listenDatagram({port:0});
73        const consuming=(async()=>{for await(const [packet] of b){if(packet.join()!=='7')throw Error('iterator');break;}})();
74        await a.send(new Uint8Array([7]),b.addr);await consuming;
75        if(!(await b[Symbol.asyncIterator]().next()).done)throw Error('closed iteration');a.close();console.log('closed');
76    "#,
77        &[],
78    );
79    assert_eq!(output.stdout, b"closed\n");
80}
81
82#[test]
83fn send_requires_destination_authority_not_just_bind_authority() {
84    let output = common::run(
85        r#"
86        import {listenDatagram,pledge,errors} from 'avril:std';
87        const a=listenDatagram({port:0}),b=listenDatagram({port:0});
88        pledge({net:{allow:[`127.0.0.1:${a.addr.port}`]}});
89        try{await a.send(new Uint8Array([1]),b.addr);throw Error('destination escaped');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;}
90        const received=a.receive();await a.send(new Uint8Array([8]),a.addr);if((await received)[0][0]!==8)throw Error('authorized peer');
91        a.close();b.close();console.log('scoped');
92    "#,
93        &[],
94    );
95    assert_eq!(output.stdout, b"scoped\n");
96}
97
98#[test]
99fn revocation_blocks_queued_sends_binding_and_broadcast_changes() {
100    let output = common::run(
101        r#"
102        import {listenDatagram,pledge,errors} from 'avril:std';
103        const a=listenDatagram({port:0}),send=a.send(new Uint8Array([1]),a.addr);pledge({net:false});
104        for(const operation of [()=>send,()=>a.receive(),()=>a.setBroadcast(),()=>listenDatagram({port:0})]){
105            try{await operation();throw Error('authority escaped');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;}
106        }
107        a.close();console.log('revoked');
108    "#,
109        &[],
110    );
111    assert_eq!(output.stdout, b"revoked\n");
112}
113
114#[test]
115fn receiving_rechecks_authority_after_waiting_for_a_packet() {
116    let (_directory, mut child) = common::spawn(
117        r#"
118        import {listenDatagram,pledge,errors} from 'avril:std';
119        const a=listenDatagram({port:0});console.log(a.addr.port);const reading=a.receive();await new Promise(resolve=>setTimeout(resolve,10));pledge({net:false});console.log('narrowed');
120        try{await reading;throw Error('received without authority');}catch(e){if(!(e instanceof errors.PermissionDenied))throw e;}
121        a.close();console.log('denied');
122    "#,
123    );
124    let mut stdout = BufReader::new(child.stdout.take().unwrap());
125    let mut line = String::new();
126    stdout.read_line(&mut line).unwrap();
127    let port = line.trim().parse::<u16>().unwrap();
128    line.clear();
129    stdout.read_line(&mut line).unwrap();
130    assert_eq!(line, "narrowed\n");
131    let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
132    socket.send_to(b"x", ("127.0.0.1", port)).unwrap();
133    line.clear();
134    stdout.read_line(&mut line).unwrap();
135    assert_eq!(line, "denied\n");
136    drop(stdout);
137    let output = common::wait(child);
138    assert!(
139        output.status.success(),
140        "{}",
141        String::from_utf8_lossy(&output.stderr)
142    );
143}
144
145#[test]
146fn worker_termination_releases_udp_sockets_and_workers_cannot_escalate_authority() {
147    let directory = tempfile::tempdir().unwrap();
148    let source = directory.path().join("worker.js");
149    std::fs::write(&source,"import {listenDatagram} from 'avril:std';try{const a=listenDatagram({port:0});postMessage(a.addr.port);await a.receive();}catch(e){postMessage(e.name);close();}").unwrap();
150    let url = url::Url::from_file_path(&source).unwrap();
151    let output = common::run(
152        &format!(
153            r#"
154        import {{listenDatagram,avril}} from 'avril:std';
155        const denied=new Worker('{url}',{{type:'module',[avril]:{{caps:{{net:false}}}}}});if(await new Promise(resolve=>denied.onmessage=e=>resolve(e.data))!=='PermissionDenied')throw Error('worker escaped');denied.terminate();
156        const worker=new Worker('{url}',{{type:'module'}}),port=await new Promise(resolve=>worker.onmessage=e=>resolve(e.data));worker.terminate();
157        let socket;for(let i=0;i<100&&!socket;i++){{try{{socket=listenDatagram({{port}});}}catch{{await new Promise(resolve=>setTimeout(resolve,5));}}}}
158        if(!socket)throw Error('worker leaked UDP socket');socket.close();console.log('terminated');
159    "#
160        ),
161        &[],
162    );
163    assert_eq!(output.stdout, b"terminated\n");
164}
165
166#[test]
167fn udp_uses_captured_intrinsics_and_handles_cannot_be_cloned() {
168    let output = common::run(
169        r#"
170        import {listenDatagram} from 'avril:std';
171        const chunk=new Uint8Array([9]),then=Promise.prototype.then,PromiseCtor=Promise;
172        function observe(p){Object.defineProperty(p,'constructor',{value:undefined});const result=Reflect.apply(then,p,[v=>v,e=>{throw e;}]);Object.defineProperty(result,'constructor',{value:PromiseCtor});return result;}
173        const poison=()=>{throw Error('guest hook');};Object.defineProperty(Promise.prototype,'constructor',{get:poison,configurable:true});Object.defineProperty(Promise,Symbol.species,{get:poison});Promise.prototype.then=poison;Array.prototype[Symbol.iterator]=poison;
174        const a=listenDatagram({port:0}),b=listenDatagram({port:0}),reading=b.receive();
175        await observe(a.send(chunk,b.addr));const packet=await observe(reading);if(packet[0][0]!==9)throw Error('packet');
176        try{structuredClone(a);throw Error('cloned');}catch(e){if(e.name!=='DataCloneError')throw e;}
177        a.close();b.close();console.log('safe');
178    "#,
179        &[],
180    );
181    assert_eq!(output.stdout, b"safe\n");
182}
183
184#[test]
185fn dns_selects_the_bound_address_family() {
186    let output = common::run(
187        r#"
188        import {listenDatagram} from 'avril:std';
189        const a=listenDatagram({port:0}),b=listenDatagram({port:0});
190        const reading=b.receive();await a.send(new Uint8Array([6]),{hostname:'localhost',port:b.addr.port});
191        if((await reading)[0][0]!==6)throw Error('DNS family');a.close();b.close();console.log('dns');
192    "#,
193        &[],
194    );
195    assert_eq!(output.stdout, b"dns\n");
196}
197
198#[test]
199fn ipv6_datagrams_accept_bracketed_destinations() {
200    if std::net::UdpSocket::bind("[::1]:0").is_err() {
201        return;
202    }
203    let output = common::run(
204        r#"
205        import {listenDatagram} from 'avril:std';
206        const a=listenDatagram({hostname:'::1',port:0}),b=listenDatagram({hostname:'::1',port:0});
207        const reading=b.receive();await a.send(new Uint8Array([6]),{hostname:'[::1]',port:b.addr.port});
208        const [packet,peer]=await reading;if(packet[0]!==6||peer.hostname!=='::1')throw Error('IPv6');a.close();b.close();console.log('ipv6');
209    "#,
210        &[],
211    );
212    assert_eq!(output.stdout, b"ipv6\n");
213}
214
215#[test]
216fn invalid_datagram_options_and_oversized_messages_are_rejected() {
217    let output = common::run(
218        r#"
219        import {listenDatagram,DatagramConn} from 'avril:std';
220        for(const port of [-1,65536,NaN,1.5]){try{listenDatagram({port});throw Error('invalid port');}catch(e){if(!(e instanceof RangeError))throw e;}}
221        try{new DatagramConn();throw Error('constructed');}catch(e){if(!(e instanceof TypeError))throw e;}
222        try{listenDatagram({transport:'tcp',port:0});throw Error('transport');}catch(e){if(!(e instanceof TypeError))throw e;}
223        const a=listenDatagram({port:0});
224        try{a.send(new Uint8Array(65508),a.addr);throw Error('oversized');}catch(e){if(!(e instanceof RangeError))throw e;}
225        try{await a.receive(new ArrayBuffer(1));throw Error('invalid buffer');}catch(e){if(!(e instanceof TypeError))throw e;}
226        a.close();console.log('validated');
227    "#,
228        &[],
229    );
230    assert_eq!(output.stdout, b"validated\n");
231}