char/avril

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

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

Charlotte Somport websocket extension to typescriptec3de1c

main
49.2 KiB1378 linesraw
1mod common;
2
3use std::io::{Read, Write};
4use std::net::TcpListener;
5use std::time::{Duration, Instant};
6
7// The test server is shared with no other test target, so it is declared here.
8#[path = "common/websocket.rs"]
9mod ws_server;
10
11use ws_server::*;
12
13#[test]
14fn reflected_construction_live_handlers_and_close_event_conversion() {
15    let (url, thread) = server(1, |session| session.echo());
16    let source = r#"
17        const order = [], poison = function() {throw Error('guest base constructor');};
18        class ChildSocket extends WebSocket {}
19        class ChildClose extends CloseEvent {}
20        Object.setPrototypeOf(WebSocket, poison);
21        Object.setPrototypeOf(CloseEvent, poison);
22        if (WebSocket.length !== 1 || CloseEvent.length !== 1 ||
23            WebSocket.prototype.send.length !== 1 || WebSocket.prototype.close.length !== 0) throw Error('arity');
24        const close = new ChildClose({toString() {order.push('type'); return 'close';}}, {
25            get code() {order.push('code'); return {valueOf() {order.push('code:number'); return 65537;}};},
26            get reason() {order.push('reason'); return {toString() {order.push('reason:string'); return '\ud800';}};},
27            get wasClean() {order.push('clean'); return true;}
28        });
29        if (order.join('|') !== 'type|code|reason|clean|code:number|reason:string' ||
30            close.code !== 1 || close.reason !== '\ufffd' || !close.wasClean ||
31            Object.getPrototypeOf(close) !== ChildClose.prototype) throw Error('CloseEvent conversion');
32        if (Object.getOwnPropertyNames(close).join() !== 'isTrusted') throw Error('emitted event fields');
33        const socket = new ChildSocket('URL');
34        if (Object.getPrototypeOf(socket) !== ChildSocket.prototype || Object.getOwnPropertyNames(socket).length) throw Error('socket shape');
35        const calls = [];
36        socket.onmessage = function(event) {
37            if (this !== socket) throw Error('handler receiver');
38            calls.push('original'); return false;
39        };
40        socket.addEventListener('message', () => calls.push('listener'));
41        const first = new MessageEvent('message', {cancelable:true});
42        if (socket.dispatchEvent(first) || !first.defaultPrevented) throw Error('handler cancellation');
43        socket.onmessage = function() {calls.push('replacement');};
44        socket.dispatchEvent(new MessageEvent('message'));
45        if (calls.join('|') !== 'original|listener|replacement|listener') throw Error('live handler order');
46        socket.onmessage = null;
47        const completed = new Promise((resolve, reject) => {
48            socket.onopen = () => socket.send('echo');
49            socket.onmessage = event => {
50                if (event.data !== 'echo') reject(Error('echo'));
51                socket.close(1000, 'done');
52            };
53            socket.onerror = () => reject(Error('socket error'));
54            socket.onclose = event => {
55                if (!event.wasClean || event.code !== 1000 || event.reason !== 'done') reject(Error('close'));
56                else resolve();
57            };
58        });
59        await completed;
60        console.log('reflected sockets and events');
61    "#
62    .replace("URL", &url);
63    let output = common::run(&source, &[]);
64    assert_eq!(output.stdout, b"reflected sockets and events\n");
65    assert!(
66        output.stderr.is_empty(),
67        "{}",
68        String::from_utf8_lossy(&output.stderr)
69    );
70    thread.join().unwrap();
71}
72
73#[test]
74fn handshakes_capture_the_isolate_user_agent_at_construction() {
75    let (url, thread) = server(5, |mut session| {
76        let ua = session
77            .headers
78            .lines()
79            .find_map(|line| line.strip_prefix("user-agent:"))
80            .expect("Missing User-Agent")
81            .trim()
82            .to_owned();
83        session.accept(None)?;
84        session.write(TEXT, ua.as_bytes())?;
85        let close = session.read()?;
86        assert_eq!(close.opcode, CLOSE);
87        session.write(CLOSE, &close.payload)
88    });
89    let source = r#"
90        const original = navigator.userAgent;
91        function handshake() {
92            return new Promise((resolve, reject) => {
93                const socket = new WebSocket('URL');
94                let received;
95                socket.onmessage = event => {received = event.data; socket.close();};
96                socket.onerror = () => reject(Error('Handshake failed'));
97                socket.onclose = event => {
98                    if (!event.wasClean) reject(Error('Unclean close'));
99                    else resolve(received);
100                };
101            });
102        }
103        if (await handshake() !== original) throw Error('Initial default');
104        navigator.userAgent = 'custom/1.0';
105        const pending = handshake();
106        navigator.userAgent = 'changed/2.0';
107        if (await pending !== 'custom/1.0') throw Error('In-flight UA changed');
108        if (await handshake() !== 'changed/2.0') throw Error('Default not updated');
109        navigator.userAgent = '';
110        if (await handshake() !== '') throw Error('Empty UA lost');
111        navigator.userAgent = 'hardened/1.0';
112        Object.defineProperty(navigator, 'userAgent', {get() {throw Error('Guest getter');}});
113        if (await handshake() !== 'hardened/1.0') throw Error('Internal default lost');
114        console.log('WebSocket UA');
115    "#
116    .replace("URL", &url);
117    assert_eq!(common::run(&source, &[]).stdout, b"WebSocket UA\n");
118    thread.join().unwrap();
119}
120
121#[test]
122fn invalid_user_agents_fail_before_opening_a_websocket_transport() {
123    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
124    let url = format!("ws://{}", listener.local_addr().unwrap());
125    let source = r#"
126        for (const invalid of ['bad\r\nInjected: yes', 'bad\0value', '\u0100']) {
127            navigator.userAgent = invalid;
128            await new Promise((resolve, reject) => {
129                const socket = new WebSocket('URL');
130                let errored = false;
131                socket.onopen = () => {socket.close(); reject(Error('Invalid UA connected'));};
132                socket.onerror = () => {errored = true;};
133                socket.onclose = event => {
134                    if (!errored || event.wasClean || event.code !== 1006) reject(Error('Failure events'));
135                    else resolve();
136                };
137            });
138        }
139        console.log('invalid WebSocket UA');
140    "#
141    .replace("URL", &url);
142    assert_eq!(common::run(&source, &[]).stdout, b"invalid WebSocket UA\n");
143    listener.set_nonblocking(true).unwrap();
144    assert_eq!(
145        listener.accept().unwrap_err().kind(),
146        std::io::ErrorKind::WouldBlock
147    );
148}
149
150#[test]
151fn echo_text_binary_and_binary_types() {
152    let (url, thread) = server(1, |session| session.echo());
153    let out = common::run(
154        &connect(
155            &url,
156            r#"
157            ws.binaryType = 'arraybuffer';
158            const log = [];
159            ws.onopen = () => {
160                if (ws.url !== 'WSURL/' || ws.readyState !== 1) throw Error('Attributes');
161                ws.send('hello');
162                ws.send(new Uint8Array([1, 2, 255]));
163            };
164            ws.onmessage = (event) => {
165                if (typeof event.data === 'string') {
166                    if (event.data !== 'hello') throw Error('Text');
167                    log.push('text');
168                } else if (event.data instanceof ArrayBuffer) {
169                    if ([...new Uint8Array(event.data)].join(',') !== '1,2,255') throw Error('Binary');
170                    log.push('binary');
171                    ws.binaryType = 'blob';
172                    ws.send(new Blob([new Uint8Array([9, 8]), new Uint8Array([7])], { type: 'application/x-test' }));
173                } else {
174                    if (!(event.data instanceof Blob) || event.data.size !== 3 || event.data.type !== '') {
175                        throw Error('Blob message');
176                    }
177                    log.push('blob');
178                    ws.close(1000, 'done');
179                }
180            };
181            ws.onerror = () => log.push('error');
182            ws.onclose = (event) => {
183                if (log.join('|') !== 'text|binary|blob') throw Error('Order');
184                if (!event.wasClean || event.code !== 1000 || event.reason !== 'done') throw Error('Close');
185                console.log('echo ok', event instanceof CloseEvent);
186            };
187        "#)
188        .replace("WSURL", &url),
189        &[],
190    );
191    assert_eq!(out.stdout, b"echo ok true\n");
192    thread.join().unwrap();
193}
194
195#[test]
196fn http_urls_map_to_websocket_scheme() {
197    let (url, thread) = server(1, |session| session.echo());
198    let http = url.replacen("ws://", "http://", 1);
199    let out = common::run(
200        &format!(
201            r#"
202            const ws = new WebSocket('{http}');
203            ws.onopen = () => {{
204                if (ws.url !== '{url}/') throw Error('Mapped URL');
205                console.log('mapped');
206                ws.close();
207            }};
208            ws.onclose = (event) => {{
209                if (!event.wasClean || event.code !== 1005) throw Error('Close code');
210                console.log('closed');
211            }};
212        "#
213        ),
214        &[],
215    );
216    assert_eq!(out.stdout, b"mapped\nclosed\n");
217    thread.join().unwrap();
218}
219
220#[test]
221fn subprotocol_negotiation() {
222    let (url, thread) = server(3, |mut session| {
223        match session.path.as_str() {
224            "/chosen" => {
225                assert_eq!(session.protocol, "chat, v2");
226                session.accept(Some("v2"))?;
227                let close = session.read()?;
228                assert_eq!(close.opcode, CLOSE);
229                session.write(CLOSE, &close.payload)?;
230            }
231            "/unacknowledged" => {
232                session.accept(None)?;
233                assert_eq!(
234                    session.read().err().unwrap().kind(),
235                    std::io::ErrorKind::UnexpectedEof
236                );
237            }
238            _ => {
239                session.accept(Some("bogus"))?;
240                assert_eq!(
241                    session.read().err().unwrap().kind(),
242                    std::io::ErrorKind::UnexpectedEof
243                );
244            }
245        }
246        Ok(())
247    });
248    let out = common::run(
249        &format!(
250            r#"
251            const log = [];
252            await new Promise((resolve) => {{
253                const ws = new WebSocket('{url}/chosen', ['chat', 'v2']);
254                ws.onopen = () => {{
255                    if (ws.protocol !== 'v2') throw Error('Selected protocol');
256                    log.push('chosen');
257                    ws.close(1000);
258                }};
259                ws.onclose = () => resolve();
260            }});
261            await new Promise((resolve) => {{
262                const ws = new WebSocket('{url}/unacknowledged', ['chat']);
263                ws.onerror = () => log.push('error');
264                ws.onclose = (event) => {{
265                    if (event.wasClean || event.code !== 1006) throw Error('Failed close');
266                    if (ws.readyState !== 3) throw Error('Ready state');
267                    log.push('close');
268                    resolve();
269                }};
270            }});
271            await new Promise((resolve) => {{
272                const ws = new WebSocket('{url}/bogus', ['chat']);
273                ws.onclose = (event) => {{
274                    if (event.wasClean || event.code !== 1006) throw Error('Unexpected protocol');
275                    log.push('close');
276                    resolve();
277                }};
278            }});
279            console.log(log.join('|'));
280        "#
281        ),
282        &[],
283    );
284    assert_eq!(out.stdout, b"chosen|error|close|close\n");
285    thread.join().unwrap();
286}
287
288#[test]
289fn client_close_codes_and_reasons() {
290    let (url, thread) = server(3, |mut session| {
291        session.accept(None)?;
292        let close = session.read()?;
293        assert_eq!(close.opcode, CLOSE);
294        match session.path.as_str() {
295            "/code" => {
296                assert_eq!(close.code(), 1000);
297                assert_eq!(close.reason(), "bye");
298            }
299            "/bare" => assert!(close.payload.is_empty()),
300            _ => {
301                assert_eq!(close.code(), 3001);
302                assert_eq!(close.reason(), "later");
303            }
304        }
305        session.write(CLOSE, &close.payload)?;
306        Ok(())
307    });
308    let out = common::run(
309        &format!(
310            r#"
311            const events = [];
312            await new Promise((resolve) => {{
313                const ws = new WebSocket('{url}/code');
314                ws.onopen = () => ws.close(1000, 'bye');
315                ws.onclose = (event) => {{
316                    if (!event.wasClean || event.code !== 1000 || event.reason !== 'bye') throw Error('Echoed close');
317                    if (ws.readyState !== 3 || ws.bufferedAmount !== 0) throw Error('State');
318                    events.push(1000);
319                    resolve();
320                }};
321            }});
322            await new Promise((resolve) => {{
323                const ws = new WebSocket('{url}/bare');
324                ws.onopen = () => ws.close();
325                ws.onclose = (event) => {{
326                    if (!event.wasClean || event.code !== 1005 || event.reason !== '') throw Error('Bare close');
327                    events.push(1005);
328                    resolve();
329                }};
330            }});
331            await new Promise((resolve) => {{
332                const ws = new WebSocket('{url}/later');
333                ws.onopen = () => ws.close(3001, 'later');
334                ws.onclose = (event) => {{
335                    if (event.code !== 3001 || event.reason !== 'later') throw Error('Later close');
336                    events.push(3001);
337                    resolve();
338                }};
339            }});
340            console.log(events.join(','));
341        "#
342        ),
343        &[],
344    );
345    assert_eq!(out.stdout, b"1000,1005,3001\n");
346    thread.join().unwrap();
347}
348
349#[test]
350fn server_initiated_close() {
351    let (url, thread) = server(1, |mut session| {
352        session.accept(None)?;
353        session.write(CLOSE, &Frame::close(3001, "done").payload)?;
354        let close = session.read()?;
355        assert_eq!(close.code(), 3001);
356        assert_eq!(close.reason(), "done");
357        Ok(())
358    });
359    let out = common::run(
360        &connect(
361            &url,
362            r#"
363            ws.onopen = () => {
364                if (ws.readyState !== 1) throw Error('Ready state');
365            };
366            ws.onclose = (event) => {
367                if (!event.wasClean || event.code !== 3001 || event.reason !== 'done') throw Error('Server close');
368                if (ws.readyState !== 3) throw Error('Ready state');
369                console.log('server closed');
370            };
371        "#,
372        ),
373        &[],
374    );
375    assert_eq!(out.stdout, b"server closed\n");
376    thread.join().unwrap();
377}
378
379#[test]
380fn handshake_failure_fires_error_then_close() {
381    let (url, thread) = server(1, |mut session| session.reject());
382    let out = common::run(
383        &connect(
384            &url,
385            r#"
386            const order = [];
387            ws.onerror = (event) => order.push('error');
388            ws.onclose = (event) => {
389                if (order.join('|') !== 'error') throw Error('Error before close');
390                if (event.wasClean || event.code !== 1006) throw Error('Failed close');
391                if (ws.readyState !== 3) throw Error('Ready state');
392                console.log('handshake failed');
393            };
394        "#,
395        ),
396        &[],
397    );
398    assert_eq!(out.stdout, b"handshake failed\n");
399    thread.join().unwrap();
400}
401
402#[test]
403fn refused_connection_fails() {
404    let out = common::run(
405        r#"
406        let errors = 0;
407        await new Promise((resolve) => {
408            const ws = new WebSocket('ws://127.0.0.1:1/');
409            ws.onerror = () => errors++;
410            ws.onclose = (event) => {
411                if (errors !== 1 || event.wasClean || event.code !== 1006) throw Error('Refused close');
412                console.log('refused');
413                resolve();
414            };
415        });
416    "#,
417        &[],
418    );
419    assert_eq!(out.stdout, b"refused\n");
420}
421
422#[test]
423fn permission_denied() {
424    let out = common::run(
425        r#"
426        import { pledge } from 'avril:std';
427        pledge({ net: false });
428        try {
429            new WebSocket('ws://127.0.0.1:1/');
430            throw Error('Permission ignored');
431        } catch (error) {
432            if (!error.message.includes('PermissionDenied')) throw error;
433        }
434        console.log('denied');
435    "#,
436        &[],
437    );
438    assert_eq!(out.stdout, b"denied\n");
439}
440
441#[test]
442fn constructor_validation() {
443    let out = common::run(
444        r#"
445        const syntax = (operation) => {
446            try {
447                operation();
448                throw Error('Accepted invalid URL');
449            } catch (error) {
450                if (!(error instanceof DOMException) || error.name !== 'SyntaxError') throw error;
451            }
452        };
453        syntax(() => new WebSocket('ftp://example.com'));
454        syntax(() => new WebSocket('/relative'));
455        syntax(() => new WebSocket('ws://example.com/#fragment'));
456        syntax(() => new WebSocket('ws://example.com/#'));
457        syntax(() => new WebSocket('ws://example.com', 'chat\n'));
458        syntax(() => new WebSocket('ws://example.com', 'a b'));
459        syntax(() => new WebSocket('ws://example.com', ['a', 'a']));
460        syntax(() => new WebSocket('ws://example.com', ['a,']));
461        syntax(() => new WebSocket('ws://example.com', {}));
462        const numeric = new WebSocket('ws://127.0.0.1:1/x', 5);
463        if (numeric.protocol !== '') throw Error('Numeric protocols');
464        numeric.close();
465        numeric.onmessage = () => false;
466        const canceled = new MessageEvent('message', { cancelable: true });
467        if (numeric.dispatchEvent(canceled) || !canceled.defaultPrevented) {
468            throw Error('Event handler return value');
469        }
470        const invalid = (operation, name) => {
471            try {
472                operation();
473                throw Error('Accepted invalid argument');
474            } catch (error) {
475                if (error.name !== name) throw error;
476            }
477        };
478        const ws = new WebSocket('ws://127.0.0.1:1/x');
479        for (const code of [0, 999, 1001, 2999, 5000]) {
480            invalid(() => ws.close(code), 'InvalidAccessError');
481        }
482        invalid(() => ws.close(1000, 'x'.repeat(124)), 'SyntaxError');
483        invalid(() => ws.send('too early'), 'InvalidStateError');
484        invalid(() => ws.send(), 'TypeError');
485        invalid(() => ws.send(Symbol()), 'TypeError');
486        invalid(() => new WebSocket(Symbol()), 'TypeError');
487        const marker = {};
488        try {
489            new WebSocket({ toString() { throw marker; } });
490            throw Error('Swallowed coercion failure');
491        } catch (error) {
492            if (error !== marker) throw error;
493        }
494        try {
495            new WebSocket('invalid URL', [{ toString() { throw marker; } }]);
496            throw Error('Protocol conversion skipped');
497        } catch (error) {
498            if (error !== marker) throw error;
499        }
500        invalid(() => ws.close(0, Symbol()), 'TypeError');
501        invalid(() => ws.send(new ArrayBuffer(1, { maxByteLength: 2 })), 'TypeError');
502        ws.binaryType = 'nodebuffer';
503        if (ws.binaryType !== 'blob') throw Error('binaryType setter');
504        ws.binaryType = 'arraybuffer';
505        if (ws.binaryType !== 'arraybuffer') throw Error('binaryType setter');
506        if (new CloseEvent('close', { code: -1 }).code !== 65535) throw Error('CloseEvent code');
507        if (new CloseEvent('close', { code: 65537 }).code !== 1) throw Error('CloseEvent code wrap');
508        if (ws.url !== 'ws://127.0.0.1:1/x' || ws.readyState !== 0 || ws.bufferedAmount !== 0) {
509            throw Error('Initial attributes');
510        }
511        if (ws.extensions !== '' || ws.protocol !== '') throw Error('Initial protocol');
512        if (WebSocket.CONNECTING !== 0 || ws.OPEN !== 1 || WebSocket.CLOSING !== 2 || ws.CLOSED !== 3) {
513            throw Error('Ready state constants');
514        }
515        if (ws.onopen !== null || typeof ws.addEventListener !== 'function') throw Error('Event handlers');
516        ws.close();
517        console.log('validated', ws.readyState);
518    "#,
519        &[],
520    );
521    assert_eq!(out.stdout, b"validated 2\n");
522}
523
524#[test]
525fn constructor_checks_url_before_validating_protocols() {
526    let out = common::run(
527        r#"
528        for (const [url, message] of [
529            ['invalid URL', 'URL is invalid'],
530            ['ftp://x', 'scheme'],
531            ['ws://x/#', 'fragment'],
532        ]) {
533            for (const protocols of ['a b', ['chat', 'chat']]) {
534                try {
535                    new WebSocket(url, protocols);
536                    throw Error('Accepted invalid constructor arguments');
537                } catch (error) {
538                    if (error.name !== 'SyntaxError' || !error.message.includes(message)) throw error;
539                }
540            }
541        }
542        const marker = {};
543        try {
544            new WebSocket('ftp://x', {
545                get [Symbol.iterator]() { throw marker; }
546            });
547            throw Error('Protocol conversion skipped');
548        } catch (error) {
549            if (error !== marker) throw error;
550        }
551        console.log('constructor order checked');
552    "#,
553        &[],
554    );
555    assert_eq!(out.stdout, b"constructor order checked\n");
556}
557
558#[test]
559fn buffer_sources_send_detached_as_empty_and_reject_shared() {
560    let (url, thread) = server(1, |mut session| {
561        session.accept(None)?;
562        for expected in [vec![], vec![], vec![], vec![1, 2]] {
563            let frame = session.read()?;
564            assert_eq!(frame.opcode, BINARY);
565            assert_eq!(frame.payload, expected);
566        }
567        let close = session.read()?;
568        assert_eq!(close.opcode, CLOSE);
569        session.write(CLOSE, &close.payload)?;
570        Ok(())
571    });
572    let out = common::run(
573        &connect(
574            &url,
575            r#"
576            ws.onopen = () => {
577                const shared = new SharedArrayBuffer(3);
578                const resizable = new ArrayBuffer(3, { maxByteLength: 4 });
579                for (const data of [
580                    shared, new Uint8Array(shared), new DataView(shared),
581                    resizable, new Uint8Array(resizable), new DataView(resizable),
582                ]) {
583                    try {
584                        ws.send(data);
585                        throw Error('Accepted shared or resizable data');
586                    } catch (error) {
587                        if (!(error instanceof TypeError)) throw error;
588                    }
589                }
590                const detached = new ArrayBuffer(3);
591                const typed = new Uint8Array(detached);
592                const view = new DataView(detached);
593                detached.transfer();
594                ws.send(detached);
595                ws.send(typed);
596                ws.send(view);
597                ws.send(new Uint8Array([0, 1, 2, 3]).subarray(1, 3));
598                if (ws.bufferedAmount !== 2) throw Error('Buffered amount');
599                ws.close(1000);
600            };
601            ws.onclose = event => {
602                if (!event.wasClean || event.code !== 1000) throw Error('Close');
603                console.log('buffer sources checked');
604            };
605        "#,
606        ),
607        &[],
608    );
609    assert_eq!(out.stdout, b"buffer sources checked\n");
610    thread.join().unwrap();
611}
612
613#[test]
614fn send_converts_other_arguments_to_text() {
615    let (url, thread) = server(1, |mut session| {
616        session.accept(None)?;
617        for expected in [
618            &b"5"[..],
619            &b"[object Object]"[..],
620            &b"1,2"[..],
621            &b"null"[..],
622        ] {
623            let frame = session.read()?;
624            assert_eq!(frame.opcode, TEXT, "must be sent as a text frame");
625            assert_eq!(frame.payload, expected);
626        }
627        let frame = session.read()?;
628        assert_eq!(frame.opcode, BINARY);
629        assert_eq!(frame.payload, [7]);
630        let close = session.read()?;
631        session.write(CLOSE, &close.payload)?;
632        Ok(())
633    });
634    let out = common::run(
635        &connect(
636            &url,
637            r#"
638            ws.onopen = () => {
639                ws.send(5);
640                ws.send({});
641                ws.send([1, 2]);
642                ws.send(null);
643                ws.send(new Uint8Array([7]));
644                ws.close(1000);
645            };
646            ws.onclose = (event) => {
647                if (!event.wasClean || event.code !== 1000) throw Error('Close');
648                console.log('converted');
649            };
650        "#,
651        ),
652        &[],
653    );
654    assert_eq!(out.stdout, b"converted\n");
655    thread.join().unwrap();
656}
657
658#[test]
659fn fragmentation_and_ping() {
660    let (url, thread) = server(1, |mut session| {
661        session.accept(None)?;
662        write_frame(&mut session.stream, false, TEXT, b"hel")?;
663        write_frame(&mut session.stream, true, CONTINUATION, b"lo")?;
664        session.write(PING, b"keepalive")?;
665        let deadline = Instant::now() + Duration::from_secs(5);
666        let mut pong = None;
667        let mut reply = None;
668        while (pong.is_none() || reply.is_none()) && Instant::now() < deadline {
669            let frame = match session.read() {
670                Ok(frame) => frame,
671                Err(_) => break,
672            };
673            if frame.opcode == PONG {
674                pong = Some(frame.payload);
675            } else if frame.opcode == TEXT {
676                reply = Some(frame.payload);
677            }
678        }
679        assert_eq!(pong.as_deref(), Some(&b"keepalive"[..]));
680        // The fragmented message must arrive as a single text message.
681        assert_eq!(reply.as_deref(), Some(&b"reply"[..]));
682        session.write(CLOSE, &Frame::close(1000, "").payload)?;
683        Ok(())
684    });
685    let out = common::run(
686        &connect(
687            &url,
688            r#"
689            ws.onmessage = (event) => {
690                if (event.data !== 'hello') throw Error('Fragmented message');
691                console.log('fragmented', event.origin);
692                ws.send('reply');
693            };
694        "#,
695        ),
696        &[],
697    );
698    let expected = format!("fragmented {}\n", url);
699    assert_eq!(out.stdout, expected.as_bytes());
700    thread.join().unwrap();
701}
702
703#[test]
704fn invalid_utf8_text_fails() {
705    let (url, thread) = server(1, |mut session| {
706        session.accept(None)?;
707        // A text frame carrying invalid UTF-8.
708        session.stream.write_all(&[0x81, 0x02, 0xff, 0xfe])?;
709        let close = session.read()?;
710        assert_eq!(close.opcode, CLOSE);
711        assert_eq!(close.code(), 1007);
712        Ok(())
713    });
714    let out = common::run(
715        &connect(
716            &url,
717            r#"
718            const order = [];
719            ws.onmessage = () => order.push('message');
720            ws.onerror = (event) => {
721                if (!event.message.includes('UTF-8')) throw Error('Missing failure reason');
722                order.push('error');
723            };
724            ws.onclose = (event) => {
725                if (order.join('|') !== 'error') throw Error('Error before close');
726                if (event.wasClean || event.code !== 1006) throw Error('Failed close');
727                console.log('utf8 failed');
728            };
729        "#,
730        ),
731        &[],
732    );
733    assert_eq!(out.stdout, b"utf8 failed\n");
734    thread.join().unwrap();
735}
736
737#[test]
738fn buffered_amount_only_grows_after_close() {
739    let (url, thread) = server(1, |mut session| {
740        session.accept(None)?;
741        session.write(CLOSE, &Frame::close(1000, "").payload)?;
742        assert_eq!(session.read()?.opcode, CLOSE);
743        Ok(())
744    });
745    let out = common::run(
746        &connect(
747            &url,
748            r#"
749            ws.onclose = () => {
750                ws.send('a');
751                ws.send('bb');
752                ws.send(new Uint8Array(3));
753                if (ws.bufferedAmount !== 6) throw Error('Buffered amount');
754                console.log('buffered', ws.bufferedAmount);
755            };
756        "#,
757        ),
758        &[],
759    );
760    assert_eq!(out.stdout, b"buffered 6\n");
761    thread.join().unwrap();
762}
763
764#[test]
765fn socket_keeps_process_alive_until_closed() {
766    let (close, closing) = std::sync::mpsc::channel();
767    let (url, thread) = server(1, move |mut session| {
768        session.accept(None)?;
769        closing.recv_timeout(Duration::from_secs(5)).unwrap();
770        session.write(CLOSE, &Frame::close(1000, "").payload)?;
771        assert_eq!(session.read()?.opcode, CLOSE);
772        Ok(())
773    });
774    let (_dir, mut child) = common::spawn(&connect(
775        &url,
776        r#"
777        ws.onopen = () => console.log('open');
778        ws.onclose = () => console.log('closed');
779    "#,
780    ));
781    let stdout = child.stdout.take().unwrap();
782    let (lines, received) = std::sync::mpsc::channel();
783    let output = std::thread::spawn(move || {
784        use std::io::BufRead;
785        for line in std::io::BufReader::new(stdout).lines() {
786            lines.send(line.unwrap()).unwrap();
787        }
788    });
789    assert_eq!(
790        received.recv_timeout(Duration::from_secs(5)).unwrap(),
791        "open"
792    );
793    std::thread::sleep(Duration::from_millis(100));
794    assert!(
795        child.try_wait().unwrap().is_none(),
796        "an open socket must keep the process alive"
797    );
798    close.send(()).unwrap();
799    let out = common::wait(child);
800    assert!(
801        out.status.success(),
802        "{}",
803        String::from_utf8_lossy(&out.stderr)
804    );
805    assert_eq!(
806        received.recv_timeout(Duration::from_secs(5)).unwrap(),
807        "closed"
808    );
809    output.join().unwrap();
810    thread.join().unwrap();
811}
812
813#[test]
814fn ipv6_loopback() {
815    let Ok((url, thread)) =
816        TcpListener::bind("[::1]:0").map(|_| server_on("::1", 1, |session| session.echo()))
817    else {
818        // IPv6 loopback is unavailable; nothing to test.
819        return;
820    };
821    let out = common::run(
822        &connect(
823            &url,
824            r#"
825            ws.onopen = () => {
826                if (ws.url !== 'WSURL/') throw Error('URL');
827                console.log('open', ws.readyState);
828                ws.send('hello');
829            };
830            ws.onmessage = (event) => {
831                if (event.data !== 'hello') throw Error('Echo');
832                if (event.origin !== 'WSURL') throw Error('Origin');
833                ws.close(1000);
834            };
835            ws.onclose = (event) => {
836                if (!event.wasClean || event.code !== 1000) throw Error('Close');
837                console.log('closed');
838            };
839        "#,
840        )
841        .replace("WSURL", &url),
842        &[],
843    );
844    assert_eq!(out.stdout, b"open 1\nclosed\n");
845    thread.join().unwrap();
846}
847
848#[test]
849fn tls_handshake_against_plain_server_fails() {
850    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
851    let url = format!("wss://{}", listener.local_addr().unwrap());
852    let thread = std::thread::spawn(move || {
853        for _ in 0..1 {
854            let mut stream = incoming(&listener);
855            // Accept the TCP connection but never speak TLS; the client must fail.
856            let mut buffer = [0; 64];
857            let _ = stream.read(&mut buffer);
858        }
859    });
860    let out = common::run(
861        &connect(
862            &url,
863            r#"
864            const order = [];
865            ws.onerror = () => order.push('error');
866            ws.onclose = (event) => {
867                if (order.join('|') !== 'error') throw Error('Error before close');
868                if (event.wasClean || event.code !== 1006) throw Error('Failed close');
869                console.log('tls failed');
870            };
871        "#,
872        ),
873        &[],
874    );
875    assert_eq!(out.stdout, b"tls failed\n");
876    thread.join().unwrap();
877}
878
879#[test]
880fn close_during_connecting_fails_the_connection() {
881    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
882    let url = format!("ws://{}", listener.local_addr().unwrap());
883    let out = common::run(
884        &connect(
885            &url,
886            r#"
887            const order = [];
888            ws.close(1000, 'never opened');
889            if (ws.readyState !== 2) throw Error('Ready state after close');
890            ws.onopen = () => order.push('open');
891            ws.onerror = () => order.push('error');
892            ws.onclose = (event) => {
893                order.push('close');
894                if (order.join('|') !== 'error|close') throw Error('Order: ' + order.join('|'));
895                if (event.wasClean || event.code !== 1006) throw Error('Failed close');
896                if (ws.readyState !== 3) throw Error('Ready state');
897                console.log('closed while connecting');
898            };
899        "#,
900        ),
901        &[],
902    );
903    assert_eq!(out.stdout, b"closed while connecting\n");
904}
905
906#[test]
907fn sends_are_ordered() {
908    let (url, thread) = server(1, |mut session| {
909        session.accept(None)?;
910        let mut received = Vec::new();
911        loop {
912            let frame = session.read()?;
913            if frame.opcode == CLOSE {
914                session.write(CLOSE, &frame.payload)?;
915                break;
916            }
917            assert_eq!(frame.opcode, TEXT);
918            session.write(TEXT, &frame.payload)?;
919            received.push(String::from_utf8(frame.payload).unwrap());
920        }
921        assert_eq!(received.len(), 50);
922        for (index, message) in received.iter().enumerate() {
923            assert_eq!(message, &format!("message {index}"));
924        }
925        Ok(())
926    });
927    let out = common::run(
928        &connect(
929            &url,
930            r#"
931            const replies = [];
932            ws.onopen = () => {
933                for (let i = 0; i < 50; i++) ws.send('message ' + i);
934            };
935            ws.onmessage = (event) => {
936                replies.push(event.data);
937                if (replies.length < 50) return;
938                for (let i = 0; i < replies.length; i++) {
939                    if (replies[i] !== 'message ' + i) throw Error('Out of order');
940                }
941                console.log('ordered', replies.length);
942                ws.close(1000);
943            };
944        "#,
945        ),
946        &[],
947    );
948    assert_eq!(out.stdout, b"ordered 50\n");
949    thread.join().unwrap();
950}
951
952#[test]
953fn handshake_response_validation_and_failure_events() {
954    let (url, thread) = server(4, |mut session| {
955        let key = accept(&session.key());
956        let response = match session.path.as_str() {
957            "/tokens" => format!("HTTP/1.1 101 Switching Protocols\r\nUpgrade: example, WebSocket\r\nConnection: keep-alive, Upgrade\r\nSec-WebSocket-Accept: {key}\r\n\r\n"),
958            "/duplicate" => format!("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\nSec-WebSocket-Accept: wrong\r\n\r\n"),
959            "/extension" => format!("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {key}\r\nSec-WebSocket-Extensions: permessage-deflate\r\n\r\n"),
960            _ => "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: wrong\r\n\r\n".to_owned(),
961        };
962        session.stream.write_all(response.as_bytes())?;
963        if session.path == "/tokens" {
964            let close = session.read()?;
965            assert_eq!(close.opcode, CLOSE);
966            session.write(CLOSE, &close.payload)?;
967        } else {
968            assert_eq!(
969                session.read().err().unwrap().kind(),
970                std::io::ErrorKind::UnexpectedEof
971            );
972        }
973        Ok(())
974    });
975    let out = common::run(
976        &format!(
977            r#"
978        for (const path of ['tokens', 'duplicate', 'extension', 'accept']) {{
979            await new Promise(resolve => {{
980                const ws = new WebSocket('{url}/' + path);
981                let errors = 0, microtaskRan = false;
982                ws.onopen = () => {{
983                    if (path !== 'tokens') throw Error('Accepted bad handshake');
984                    ws.close(1000);
985                }};
986                ws.onerror = () => {{
987                    if (ws.readyState !== WebSocket.CLOSED) throw Error('Error readyState');
988                    errors++;
989                    queueMicrotask(() => microtaskRan = true);
990                    ws.send('discarded');
991                    if (ws.bufferedAmount !== 9) throw Error('Failed socket buffering');
992                }};
993                ws.onclose = event => {{
994                    if (microtaskRan) throw Error('Error and close must share a turn');
995                    if (path === 'tokens') {{
996                        if (!event.wasClean || event.code !== 1000 || errors) throw Error('Valid tokens');
997                    }} else if (event.wasClean || event.code !== 1006 || errors !== 1) {{
998                        throw Error('Failed handshake');
999                    }}
1000                    resolve();
1001                }};
1002            }});
1003        }}
1004        console.log('handshakes checked');
1005    "#
1006        ),
1007        &[],
1008    );
1009    assert_eq!(out.stdout, b"handshakes checked\n");
1010    thread.join().unwrap();
1011}
1012
1013#[test]
1014fn close_reason_preserves_bom_and_registered_code() {
1015    let (url, thread) = server(1, |mut session| {
1016        session.accept(None)?;
1017        let payload = Frame::close(1014, "\u{feff}gateway").payload;
1018        session.write(CLOSE, &payload)?;
1019        let reply = session.read()?;
1020        assert_eq!(reply.opcode, CLOSE);
1021        assert_eq!(reply.payload, payload);
1022        Ok(())
1023    });
1024    let out = common::run(
1025        &connect(
1026            &url,
1027            r#"
1028        ws.onclose = event => {
1029            if (!event.wasClean || event.code !== 1014 || event.reason !== '\ufeffgateway') {
1030                throw Error('Close payload changed');
1031            }
1032            console.log('close payload preserved');
1033        };
1034    "#,
1035        ),
1036        &[],
1037    );
1038    assert_eq!(out.stdout, b"close payload preserved\n");
1039    thread.join().unwrap();
1040}
1041
1042#[test]
1043fn invalid_control_frames_fail() {
1044    let (url, thread) = server(5, |mut session| {
1045        session.accept(None)?;
1046        let (opcode, payload, code) = match session.path.as_str() {
1047            "/pong" => (PONG, vec![0; 126], 1002),
1048            "/long-close" => (CLOSE, Frame::close(1000, &"x".repeat(124)).payload, 1002),
1049            "/short-close" => (CLOSE, vec![0], 1002),
1050            "/code" => (CLOSE, Frame::close(1005, "").payload, 1002),
1051            _ => (CLOSE, vec![3, 232, 255], 1007),
1052        };
1053        session.write(opcode, &payload)?;
1054        let close = session.read()?;
1055        assert_eq!(close.opcode, CLOSE);
1056        assert_eq!(close.code(), code);
1057        Ok(())
1058    });
1059    let out = common::run(
1060        &format!(
1061            r#"
1062        for (const path of ['pong', 'long-close', 'short-close', 'code', 'utf8']) {{
1063            await new Promise(resolve => {{
1064                const ws = new WebSocket('{url}/' + path);
1065                let errors = 0;
1066                ws.onerror = () => errors++;
1067                ws.onmessage = () => {{ throw Error('Control frame delivered'); }};
1068                ws.onclose = event => {{
1069                    if (event.wasClean || event.code !== 1006 || errors !== 1) throw Error('Invalid control frame');
1070                    resolve();
1071                }};
1072            }});
1073        }}
1074        console.log('invalid controls rejected');
1075    "#
1076        ),
1077        &[],
1078    );
1079    assert_eq!(out.stdout, b"invalid controls rejected\n");
1080    thread.join().unwrap();
1081}
1082
1083#[test]
1084fn call_time_primordials_and_protocol_iterator() {
1085    let (url, thread) = server(1, |mut session| {
1086        assert_eq!(session.path, "/CaseSensitive");
1087        assert_eq!(session.protocol, "chat");
1088        session.accept(Some("chat"))?;
1089        let text = session.read()?;
1090        assert_eq!(text.opcode, TEXT);
1091        assert_eq!(text.payload, b"hello");
1092        session.write(TEXT, &text.payload)?;
1093        let close = session.read()?;
1094        assert_eq!(close.code(), 1000);
1095        session.write(CLOSE, &close.payload)?;
1096        Ok(())
1097    });
1098    let out = common::run(
1099        &format!(
1100            r#"
1101        const poison = () => {{ throw Error('Guest replacement invoked'); }};
1102        Math.floor = poison;
1103        Promise.prototype.then = poison;
1104        Object.defineProperty(Promise.prototype, 'constructor', {{ get: poison }});
1105        Object.setPrototypeOf(WebSocket, poison);
1106        Object.setPrototypeOf(CloseEvent, poison);
1107        for (const name of ['href', 'protocol', 'origin', 'hash']) {{
1108            Object.defineProperty(URL.prototype, name, {{ get: poison }});
1109        }}
1110        Object.defineProperty(Object.getPrototypeOf(Uint8Array.prototype), 'length', {{ get: poison }});
1111        let gets = 0;
1112        const protocols = {{
1113            get [Symbol.iterator]() {{
1114                if (++gets !== 1) throw Error('Iterator read twice');
1115                return function* () {{ yield 'chat'; }};
1116            }}
1117        }};
1118        const ws = new WebSocket('{url}/CaseSensitive', protocols);
1119        ws.onopen = () => {{
1120            if (ws.url !== '{url}/CaseSensitive' || gets !== 1) throw Error('Constructor');
1121            ws.send('hello');
1122            if (ws.bufferedAmount !== 5) throw Error('Buffered amount');
1123        }};
1124        ws.onmessage = event => {{
1125            if (event.data !== 'hello' || event.origin !== '{url}') throw Error('Message');
1126            ws.close(1000.4);
1127        }};
1128        ws.onclose = event => {{
1129            if (!event.wasClean || event.code !== 1000) throw Error('Close');
1130            console.log('primordials safe');
1131        }};
1132    "#
1133        ),
1134        &[],
1135    );
1136    assert_eq!(out.stdout, b"primordials safe\n");
1137    thread.join().unwrap();
1138}
1139
1140#[test]
1141fn canceling_a_pending_handshake_drops_the_transport() {
1142    let (ready, handshake) = std::sync::mpsc::channel();
1143    let (closed, disconnected) = std::sync::mpsc::channel();
1144    let (url, thread) = server(1, move |mut session| {
1145        ready.send(()).unwrap();
1146        let mut byte = [0];
1147        assert_eq!(
1148            session.stream.read(&mut byte)?,
1149            0,
1150            "canceled handshake retained TCP socket"
1151        );
1152        closed.send(()).unwrap();
1153        Ok(())
1154    });
1155    let (_dir, mut child) = common::spawn(&connect(
1156        &url,
1157        r#"
1158        import { stdin } from "avril:std";
1159        const reader = stdin.getReader();
1160        ws.onopen = () => { throw Error('Pending handshake opened'); };
1161        ws.onclose = event => {
1162            if (event.wasClean || event.code !== 1006) throw Error('Failed close');
1163            console.log('canceled');
1164        };
1165        await reader.read();
1166        ws.close();
1167        await reader.read();
1168        reader.releaseLock();
1169    "#,
1170    ));
1171    handshake.recv_timeout(Duration::from_secs(5)).unwrap();
1172    let mut stdin = child.stdin.take().unwrap();
1173    stdin.write_all(b"a").unwrap();
1174    disconnected.recv_timeout(Duration::from_secs(5)).unwrap();
1175    stdin.write_all(b"b").unwrap();
1176    drop(stdin);
1177    let out = common::wait(child);
1178    assert!(
1179        out.status.success(),
1180        "{}",
1181        String::from_utf8_lossy(&out.stderr)
1182    );
1183    assert_eq!(out.stdout, b"canceled\n");
1184    thread.join().unwrap();
1185}
1186
1187#[test]
1188fn malformed_wire_frames_fail_with_protocol_or_utf8_close() {
1189    let cases: &[(&str, &[u8], u16)] = &[
1190        ("masked-server-text", b"\x81\x82\0\0\0\0hi", 1002),
1191        (
1192            "overlapping-text",
1193            b"\x01\x03old\x01\x03new\x80\x03end",
1194            1002,
1195        ),
1196        ("interleaved-binary", b"\x02\x01a\x01\x01b\x80\x01c", 1002),
1197        ("interleaved-complete-message", b"\x01\x01a\x82\x01b", 1002),
1198        (
1199            "incomplete-fragmented-utf8",
1200            b"\x01\x03hi\xe2\x80\x00",
1201            1007,
1202        ),
1203        ("invalid-fragmented-utf8", b"\x01\x01\xc2\x80\x01x", 1007),
1204        ("nonminimal-16-bit-length", b"\x82\x7e\x00\x01x", 1002),
1205        (
1206            "nonminimal-64-bit-length",
1207            b"\x82\x7f\0\0\0\0\0\0\0\x01x",
1208            1002,
1209        ),
1210        (
1211            "negative-64-bit-length",
1212            b"\x82\x7f\x80\0\0\0\0\0\0\0",
1213            1002,
1214        ),
1215        ("unexpected-continuation", b"\x80\x00", 1002),
1216        ("reserved-bits", b"\xc1\x00", 1002),
1217        ("reserved-opcode", b"\x83\x00", 1002),
1218        ("fragmented-control", b"\x09\x00", 1002),
1219    ];
1220    for &(name, wire, code) in cases {
1221        let (url, thread) = server(1, move |mut session| {
1222            session.accept(None)?;
1223            session.stream.write_all(wire)?;
1224            let close = session.read()?;
1225            assert_eq!(close.opcode, CLOSE, "{name}");
1226            assert_eq!(close.code(), code, "{name}");
1227            Ok(())
1228        });
1229        let out = common::run(
1230            &connect(
1231                &url,
1232                r#"
1233                let errors = 0;
1234                ws.onmessage = () => { throw Error('Malformed message delivered'); };
1235                ws.onerror = () => errors++;
1236                ws.onclose = event => {
1237                    if (event.wasClean || event.code !== 1006 || errors !== 1) {
1238                        throw Error('Malformed frame did not fail the connection');
1239                    }
1240                    console.log('framing failed');
1241                };
1242            "#,
1243            ),
1244            &[],
1245        );
1246        assert_eq!(out.stdout, b"framing failed\n", "{name}");
1247        thread.join().unwrap();
1248    }
1249}
1250
1251#[test]
1252fn fragmented_utf8_survives_control_frames_and_resets_between_messages() {
1253    let (url, thread) = server(1, |mut session| {
1254        session.accept(None)?;
1255        write_frame(&mut session.stream, false, TEXT, b"a\xf0")?;
1256        write_frame(&mut session.stream, false, CONTINUATION, b"\x9f")?;
1257        session.write(PING, b"between fragments")?;
1258        write_frame(&mut session.stream, true, CONTINUATION, b"\x92\xa9")?;
1259        session.write(TEXT, b"next")?;
1260        let mut pong = false;
1261        loop {
1262            let frame = session.read()?;
1263            match frame.opcode {
1264                PONG => {
1265                    assert_eq!(frame.payload, b"between fragments");
1266                    pong = true;
1267                }
1268                CLOSE => {
1269                    assert!(pong, "control frames must not interrupt message assembly");
1270                    session.write(CLOSE, &frame.payload)?;
1271                    break;
1272                }
1273                _ => panic!("unexpected client frame"),
1274            }
1275        }
1276        Ok(())
1277    });
1278    let out = common::run(
1279        &connect(
1280            &url,
1281            r#"
1282        const messages = [];
1283        ws.onmessage = event => {
1284            messages.push(event.data);
1285            if (messages.length === 2) {
1286                if (messages[0] !== 'a\u{1f4a9}' || messages[1] !== 'next') throw Error('Fragmented UTF-8');
1287                ws.close(1000);
1288            }
1289        };
1290        ws.onclose = event => {
1291            if (!event.wasClean || event.code !== 1000) throw Error('Close');
1292            console.log('fragmented utf8 checked');
1293        };
1294    "#,
1295        ),
1296        &[],
1297    );
1298    assert_eq!(out.stdout, b"fragmented utf8 checked\n");
1299    thread.join().unwrap();
1300}
1301
1302#[test]
1303fn message_size_limit_applies_before_reading_excess_payload() {
1304    for fragmented in [false, true] {
1305        let (url, thread) = server(1, move |mut session| {
1306            session.accept(None)?;
1307            if fragmented {
1308                let fragment = vec![0; 8 * 1024 * 1024];
1309                write_frame(&mut session.stream, false, BINARY, &fragment)?;
1310                write_frame(&mut session.stream, false, CONTINUATION, &fragment)?;
1311                // No payload follows: the aggregate length alone must trigger failure.
1312                session.stream.write_all(b"\x80\x01")?;
1313            } else {
1314                session.stream.write_all(b"\x82\x7f")?;
1315                session
1316                    .stream
1317                    .write_all(&(16 * 1024 * 1024 + 1u64).to_be_bytes())?;
1318            }
1319            let close = session.read()?;
1320            assert_eq!(close.opcode, CLOSE);
1321            assert_eq!(close.code(), 1009);
1322            Ok(())
1323        });
1324        let out = common::run(
1325            &connect(
1326                &url,
1327                r#"
1328            let errors = 0;
1329            ws.onerror = () => errors++;
1330            ws.onmessage = () => { throw Error('Oversized message delivered'); };
1331            ws.onclose = event => {
1332                if (event.wasClean || event.code !== 1006 || errors !== 1) throw Error('Oversized message');
1333                console.log('size limit checked');
1334            };
1335        "#,
1336            ),
1337            &[],
1338        );
1339        assert_eq!(out.stdout, b"size limit checked\n");
1340        thread.join().unwrap();
1341    }
1342}
1343
1344#[test]
1345fn fragmented_message_at_size_limit_is_accepted() {
1346    let (url, thread) = server(1, |mut session| {
1347        session.accept(None)?;
1348        let fragment = vec![0x5a; 8 * 1024 * 1024];
1349        write_frame(&mut session.stream, false, BINARY, &fragment)?;
1350        write_frame(&mut session.stream, true, CONTINUATION, &fragment)?;
1351        let close = session.read()?;
1352        assert_eq!(close.opcode, CLOSE);
1353        session.write(CLOSE, &close.payload)?;
1354        Ok(())
1355    });
1356    let out = common::run(
1357        &connect(
1358            &url,
1359            r#"
1360        ws.binaryType = 'arraybuffer';
1361        let messages = 0;
1362        ws.onmessage = event => {
1363            const bytes = new Uint8Array(event.data);
1364            if (++messages !== 1 || bytes.length !== 16 * 1024 * 1024 ||
1365                bytes[0] !== 0x5a || bytes[bytes.length - 1] !== 0x5a) throw Error('Size limit boundary');
1366            ws.close(1000);
1367        };
1368        ws.onclose = event => {
1369            if (!event.wasClean || event.code !== 1000 || messages !== 1) throw Error('Close');
1370            console.log('size boundary checked');
1371        };
1372    "#,
1373        ),
1374        &[],
1375    );
1376    assert_eq!(out.stdout, b"size boundary checked\n");
1377    thread.join().unwrap();
1378}