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
36.5 KiB968 linesraw
1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::io;
4use std::sync::Arc;
5use std::sync::Mutex;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::Duration;
8
9use avril_caps::Capabilities;
10use avril_core::serde_v8::{JsBuffer, ToJsBuffer};
11use avril_core::{Extension, Op};
12use base64::Engine;
13use base64::engine::general_purpose::STANDARD as BASE64;
14use bytes::Bytes;
15use fastwebsockets::{Frame, OpCode, WebSocketError, WebSocketWrite};
16use futures::channel::oneshot;
17use http_body_util::Empty;
18use hyper::body::Incoming;
19use hyper::header::{self, HeaderValue};
20use hyper::{Method, Request, StatusCode};
21use hyper_util::rt::TokioIo;
22use rustls::RootCertStore;
23use sha1::{Digest, Sha1};
24use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf};
25use tokio::net::TcpStream;
26use tokio::sync::{Mutex as AsyncMutex, mpsc};
27use tokio_rustls::{TlsConnector, rustls::pki_types::ServerName};
28use tokio_util::sync::CancellationToken;
29use tokio_util::task::AbortOnDropHandle;
30use url::{Host, Url};
31
32mod reader;
33
34type Error = Box<dyn std::error::Error + Send + Sync>;
35/// Undelivered events; the guest pump drains this promptly, stalling the read
36/// loop (and with it the peer) when it falls behind.
37const EVENT_QUEUE: usize = 64;
38/// Frames read but not yet handled; bounds what a stalled guest can buffer.
39const READ_QUEUE: usize = 16;
40/// RFC 6455 §7.1.5 recommends waiting a reasonable time for the peer's close frame.
41const CLOSE_TIMEOUT: Duration = Duration::from_secs(30);
42const TCP_CLOSE_TIMEOUT: Duration = Duration::from_secs(2);
43const MAX_ID: u64 = 9_007_199_254_740_991;
44
45trait Duplex: AsyncRead + AsyncWrite {}
46impl<T: AsyncRead + AsyncWrite> Duplex for T {}
47type Socket = Box<dyn Duplex + Send + Unpin>;
48type Negotiated = TokioIo<hyper::upgrade::Upgraded>;
49/// Reading runs in its own task: decoding consumes frame bytes across
50/// await points, so a dropped read desynchronizes the stream. It is reached
51/// through a channel instead, whose receive is cancel-safe.
52type Reader = reader::Reader<ReadHalf<Negotiated>>;
53type Writer = WebSocketWrite<WriteHalf<Negotiated>>;
54
55/// A received frame, owned so it can cross a task boundary.
56struct Received {
57    opcode: OpCode,
58    payload: Vec<u8>,
59}
60
61enum FrameKind {
62    Text,
63    Binary,
64}
65
66enum Command {
67    Send(FrameKind, Vec<u8>, oneshot::Sender<bool>),
68    Close(Option<Vec<u8>>),
69}
70
71struct Resource {
72    /// Aborts a connection that was not yet established, per close() during CONNECTING.
73    canceled: CancellationToken,
74    commands: mpsc::UnboundedSender<Command>,
75    events: AsyncMutex<mpsc::Receiver<Event>>,
76}
77
78/// One event per pump turn, shaped for `serde_v8` as a plain object.
79#[derive(serde::Serialize)]
80#[serde(tag = "kind", rename_all = "lowercase")]
81enum Event {
82    Open {
83        protocol: String,
84    },
85    Text {
86        data: String,
87    },
88    Binary {
89        data: ToJsBuffer,
90    },
91    Error {
92        message: String,
93    },
94    Closing,
95    Close {
96        clean: bool,
97        code: u16,
98        reason: String,
99    },
100}
101
102struct CreatedSocket {
103    id: u64,
104    resource: Arc<Resource>,
105    commands: mpsc::UnboundedReceiver<Command>,
106    events: mpsc::Sender<Event>,
107}
108
109pub struct Sockets {
110    runtime: tokio::runtime::Handle,
111    tls: TlsConnector,
112    resources: Arc<Mutex<HashMap<u64, Arc<Resource>>>>,
113    next: AtomicU64,
114    caps: Arc<Capabilities>,
115}
116
117impl Sockets {
118    fn create(&self) -> io::Result<CreatedSocket> {
119        let id = self
120            .next
121            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| {
122                (id <= MAX_ID).then_some(id + 1)
123            })
124            .map_err(|_| io::Error::other("WebSocket IDs exhausted"))?;
125        let (commands, receiving) = mpsc::unbounded_channel();
126        let (events, event_queue) = mpsc::channel(EVENT_QUEUE);
127        let resource = Arc::new(Resource {
128            canceled: CancellationToken::new(),
129            commands,
130            events: AsyncMutex::new(event_queue),
131        });
132        self.resources.lock().unwrap().insert(id, resource.clone());
133        Ok(CreatedSocket {
134            id,
135            resource,
136            commands: receiving,
137            events,
138        })
139    }
140
141    pub fn accept(&self, upgrade: avril_http::Upgrade, protocol: String) -> io::Result<u64> {
142        let CreatedSocket { id, resource, commands, events } = self.create()?;
143        self.runtime.spawn(async move {
144            let server_abort = upgrade.canceled.clone();
145            let accepted = tokio::select! {
146                _ = resource.canceled.cancelled() => Err("WebSocket upgrade canceled".into()),
147                result = upgrade.accept() => result,
148            };
149            let (stream, _lease) = match accepted {
150                Ok(accepted) => accepted,
151                Err(error) => {
152                    fail(&events, error.to_string()).await;
153                    return;
154                }
155            };
156            let (reader, writer) = tokio::io::split(TokioIo::new(stream));
157            let (_, writer) = fastwebsockets::after_handshake_split(
158                tokio::io::empty(),
159                writer,
160                fastwebsockets::Role::Server,
161            );
162            connected(
163                (reader::Reader::new(reader, true), writer, protocol),
164                resource.canceled.clone(),
165                commands,
166                events,
167                Some(server_abort),
168            )
169            .await;
170        });
171        Ok(id)
172    }
173
174    fn resource(&self, id: u64) -> Result<Arc<Resource>, Error> {
175        self.resources
176            .lock()
177            .unwrap()
178            .get(&id)
179            .cloned()
180            .ok_or_else(|| "WebSocket is closed".into())
181    }
182}
183
184fn accept_key(key: &str) -> String {
185    let mut hasher = Sha1::new();
186    hasher.update(key.as_bytes());
187    hasher.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
188    BASE64.encode(hasher.finalize())
189}
190
191async fn attempt(
192    tls: &TlsConnector,
193    url: &Url,
194    protocols: &[String],
195    user_agent: &str,
196) -> Result<(Reader, Writer, String), Error> {
197    let user_agent = user_agent
198        .chars()
199        .map(|c| u8::try_from(c as u32))
200        .collect::<Result<Vec<_>, _>>()?;
201    let user_agent = HeaderValue::from_bytes(&user_agent)?;
202    // hosts serialize with brackets for IPv6, which neither TcpStream nor SNI accept.
203    let (host, server_name) = match url.host() {
204        Some(Host::Domain(domain)) => (
205            Cow::Borrowed(domain),
206            ServerName::try_from(domain.to_owned())?,
207        ),
208        Some(Host::Ipv4(ip)) => (Cow::Owned(ip.to_string()), ServerName::IpAddress(ip.into())),
209        Some(Host::Ipv6(ip)) => (Cow::Owned(ip.to_string()), ServerName::IpAddress(ip.into())),
210        None => return Err("Missing WebSocket host".into()),
211    };
212    let port = url
213        .port_or_known_default()
214        .ok_or("Missing WebSocket port")?;
215    let socket: Socket = if url.scheme() == "wss" {
216        let stream = TcpStream::connect((host.as_ref(), port)).await?;
217        Box::new(tls.connect(server_name, stream).await?)
218    } else {
219        Box::new(TcpStream::connect((host.as_ref(), port)).await?)
220    };
221    // The request target is origin-form, as the fetch-integrated spec requires.
222    let target = match url.query() {
223        Some(query) => format!("{}?{query}", url.path()),
224        None => url.path().to_owned(),
225    };
226    let host_header = match url.port() {
227        Some(port) => format!("{}:{port}", url.host().ok_or("Missing WebSocket host")?),
228        None => url.host().ok_or("Missing WebSocket host")?.to_string(),
229    };
230    let key = fastwebsockets::handshake::generate_key();
231    let mut request = Request::builder()
232        .method(Method::GET)
233        .uri(target)
234        .header(header::HOST, host_header)
235        .header(header::UPGRADE, "websocket")
236        .header(header::CONNECTION, "Upgrade")
237        .header("sec-websocket-key", key.as_str())
238        .header("sec-websocket-version", "13")
239        .header(header::USER_AGENT, user_agent);
240    if !protocols.is_empty() {
241        request = request.header("sec-websocket-protocol", protocols.join(", "));
242    }
243    let request = request.body(Empty::<Bytes>::new())?;
244    let (mut sender, connection) =
245        hyper::client::conn::http1::handshake(TokioIo::new(socket)).await?;
246    // An abandoned handshake must not leave Hyper holding the transport.
247    let _connection = AbortOnDropHandle::new(tokio::spawn(connection.with_upgrades()));
248    let mut response = sender.send_request(request).await?;
249    let protocol = validate(&response, &key, protocols)?;
250    let upgraded = hyper::upgrade::on(&mut response).await?;
251    let (reader, writer) = tokio::io::split(TokioIo::new(upgraded));
252    let (_, writer) = fastwebsockets::after_handshake_split(
253        tokio::io::empty(),
254        writer,
255        fastwebsockets::Role::Client,
256    );
257    Ok((reader::Reader::new(reader, false), writer, protocol))
258}
259
260fn validate(
261    response: &hyper::Response<Incoming>,
262    key: &str,
263    protocols: &[String],
264) -> Result<String, Error> {
265    let headers = response.headers();
266    let contains_token = |name: &str, token: &str| {
267        headers.get_all(name).iter().any(|value| {
268            value.to_str().is_ok_and(|value| {
269                value
270                    .split(',')
271                    .any(|value| value.trim().eq_ignore_ascii_case(token))
272            })
273        })
274    };
275    if response.status() != StatusCode::SWITCHING_PROTOCOLS
276        || !contains_token("upgrade", "websocket")
277        || !contains_token("connection", "upgrade")
278    {
279        return Err("The WebSocket handshake response is invalid".into());
280    }
281    let accepted: Vec<&HeaderValue> = headers.get_all("sec-websocket-accept").iter().collect();
282    if accepted.len() != 1 || accepted[0].to_str().ok() != Some(accept_key(key).as_str()) {
283        return Err("The WebSocket handshake response is invalid".into());
284    }
285    // No extensions are offered, so any response extension is unsupported.
286    if headers.contains_key("sec-websocket-extensions") {
287        return Err("The WebSocket server selected an unsupported extension".into());
288    }
289    let chosen: Vec<&HeaderValue> = headers.get_all("sec-websocket-protocol").iter().collect();
290    if protocols.is_empty() {
291        if !chosen.is_empty() {
292            return Err("The WebSocket server selected an unexpected subprotocol".into());
293        }
294        return Ok(String::new());
295    }
296    if chosen.len() != 1 {
297        return Err("The WebSocket server did not select one subprotocol".into());
298    }
299    let value = chosen[0]
300        .to_str()
301        .map_err(|_| "The WebSocket server selected an invalid subprotocol")?;
302    if value.is_empty() || !protocols.iter().any(|protocol| protocol == value) {
303        return Err("The WebSocket server selected an invalid subprotocol".into());
304    }
305    Ok(value.to_owned())
306}
307
308/// Validate a close frame payload, returning its code and reason or the failure code.
309fn close_payload(payload: &[u8]) -> Result<Option<(u16, String)>, u16> {
310    match payload.len() {
311        0 => Ok(None),
312        1 => Err(1002),
313        _ => {
314            let code = u16::from_be_bytes([payload[0], payload[1]]);
315            let reason = match std::str::from_utf8(&payload[2..]) {
316                Ok(reason) => reason.to_owned(),
317                Err(_) => return Err(1007),
318            };
319            if matches!(code, 1000..=1003 | 1007..=1014 | 3000..=4999) {
320                Ok(Some((code, reason)))
321            } else {
322                Err(1002)
323            }
324        }
325    }
326}
327
328/// The close frame code for a failed connection, per RFC 6455 §7.1.7.
329fn failure_code(error: &WebSocketError) -> u16 {
330    match error {
331        WebSocketError::InvalidUTF8 => 1007,
332        WebSocketError::FrameTooLarge => 1009,
333        WebSocketError::IoError(_) | WebSocketError::UnexpectedEOF => 1006,
334        _ => 1002,
335    }
336}
337
338/// Forward frames to the event loop, which owns the write half.
339async fn read_frames(
340    mut reader: reader::Reader<impl AsyncRead + Unpin>,
341    received: mpsc::Sender<Result<Received, WebSocketError>>,
342    transport_closed: oneshot::Sender<()>,
343    shutdown: CancellationToken,
344) {
345    loop {
346        let frame = tokio::select! {
347            _ = shutdown.cancelled() => return,
348            frame = reader.read() => frame,
349        };
350        match frame {
351            Ok(frame) => {
352                let close = frame.opcode == OpCode::Close;
353                if received.send(Ok(frame)).await.is_err() {
354                    return;
355                }
356                if close {
357                    // After Close, only transport termination matters; do not assemble more messages.
358                    let mut discard = tokio::io::sink();
359                    tokio::select! {
360                        _ = shutdown.cancelled() => return,
361                        _ = tokio::io::copy(&mut reader.stream, &mut discard) => {
362                            let _ = transport_closed.send(());
363                        }
364                    }
365                    return;
366                }
367            }
368            Err(error) => {
369                let _ = received.send(Err(error)).await;
370                return;
371            }
372        }
373    }
374}
375
376async fn write(
377    writer: &mut WebSocketWrite<impl AsyncWrite + Unpin>,
378    frame: Frame<'_>,
379) -> Result<(), WebSocketError> {
380    writer.write_frame(frame).await?;
381    // TLS buffers writes until flushed.
382    writer.flush().await
383}
384
385async fn push(events: &mpsc::Sender<Event>, event: Event) {
386    // A dropped receiver means the connection is already gone.
387    let _ = events.send(event).await;
388}
389
390async fn closed(events: &mpsc::Sender<Event>, clean: bool, code: u16, reason: String) {
391    push(events, Event::Close { clean, code, reason }).await;
392}
393
394/// Fail the connection without an established stream.
395async fn fail(events: &mpsc::Sender<Event>, message: String) {
396    push(events, Event::Error { message }).await;
397    closed(events, false, 1006, String::new()).await;
398}
399
400/// Fail an established connection: send the close frame for the failure, then error and close.
401async fn abort(
402    writer: &mut WebSocketWrite<impl AsyncWrite + Unpin>,
403    events: &mpsc::Sender<Event>,
404    message: String,
405    code: u16,
406    wrote_close: bool,
407) {
408    // An abruptly closed transport cannot carry a close frame (code 1006).
409    if !wrote_close && code != 1006 {
410        let _ = tokio::time::timeout(CLOSE_TIMEOUT, write(writer, Frame::close(code, b""))).await;
411    }
412    push(events, Event::Error { message }).await;
413    closed(events, false, 1006, String::new()).await;
414}
415
416async fn connect(
417    tls: TlsConnector,
418    canceled: CancellationToken,
419    commands: mpsc::UnboundedReceiver<Command>,
420    events: mpsc::Sender<Event>,
421    url: Url,
422    protocols: Vec<String>,
423    user_agent: String,
424) {
425    let established = tokio::select! {
426        _ = canceled.cancelled() => {
427            Err("The WebSocket connection was closed while connecting".into())
428        }
429        result = attempt(&tls, &url, &protocols, &user_agent) => match result {
430            Ok(established) => Ok(established),
431            Err(error) => Err(format!("failed to connect to WebSocket: {error}")),
432        },
433    };
434    let established = match established {
435        Ok(established) => established,
436        Err(message) => {
437            fail(&events, message).await;
438            return;
439        }
440    };
441    if canceled.is_cancelled() {
442        fail(
443            &events,
444            "The WebSocket connection was closed while connecting".into(),
445        )
446        .await;
447        return;
448    }
449    connected(established, canceled, commands, events, None).await;
450}
451
452async fn connected(
453    (reader, mut writer, protocol): (Reader, Writer, String),
454    canceled: CancellationToken,
455    commands: mpsc::UnboundedReceiver<Command>,
456    events: mpsc::Sender<Event>,
457    server_abort: Option<CancellationToken>,
458) {
459    push(&events, Event::Open { protocol }).await;
460    let (received, frames) = mpsc::channel(READ_QUEUE);
461    let shutdown = CancellationToken::new();
462    let (ended, transport_closed) = oneshot::channel();
463    let reading = tokio::spawn(read_frames(reader, received, ended, shutdown.clone()));
464    tokio::select! {
465        // close() during CONNECTING can land after `open` was queued. Keep
466        // cancellation outside the loop so it also interrupts blocked writes.
467        _ = canceled.cancelled() => {
468            fail(&events, "The WebSocket connection was closed while connecting".into()).await;
469        }
470        _ = async {
471            if let Some(token) = &server_abort { token.cancelled().await; }
472            else { std::future::pending::<()>().await; }
473        } => { closed(&events, false, 1006, String::new()).await; }
474        _ = events.closed() => {}
475        _ = stream_loop(&mut writer, commands, frames, transport_closed, &events, server_abort.is_some()) => {}
476    }
477    // Drops the read half, closing the transport with the connection.
478    shutdown.cancel();
479    let _ = reading.await;
480}
481
482async fn stream_loop(
483    writer: &mut WebSocketWrite<impl AsyncWrite + Unpin>,
484    mut commands: mpsc::UnboundedReceiver<Command>,
485    mut frames: mpsc::Receiver<Result<Received, WebSocketError>>,
486    transport_closed: oneshot::Receiver<()>,
487    events: &mpsc::Sender<Event>,
488    server: bool,
489) {
490    let mut wrote_close = false;
491    let mut timer = Box::pin(tokio::time::sleep(CLOSE_TIMEOUT));
492    loop {
493        tokio::select! {
494            _ = timer.as_mut(), if wrote_close => {
495                // The peer never completed the closing handshake.
496                closed(events, false, 1006, String::new()).await;
497                return;
498            }
499            command = commands.recv() => match command {
500                Some(Command::Send(kind, payload, ack)) => {
501                    let result = if wrote_close {
502                        Ok(())
503                    } else {
504                        let frame = match kind {
505                            FrameKind::Text => Frame::text(payload.into()),
506                            FrameKind::Binary => Frame::binary(payload.into()),
507                        };
508                        write(writer, frame).await
509                    };
510                    let _ = ack.send(result.is_ok() && !wrote_close);
511                    if let Err(error) = result {
512                        abort(writer, events, error.to_string(), 1006, wrote_close).await;
513                        return;
514                    }
515                }
516                Some(Command::Close(payload)) => {
517                    if wrote_close {
518                        continue;
519                    }
520                    let frame = match payload {
521                        Some(payload) => Frame::close_raw(payload.into()),
522                        None => Frame::close_raw(Vec::new().into()),
523                    };
524                    let deadline = tokio::time::Instant::now() + CLOSE_TIMEOUT;
525                    match tokio::time::timeout_at(deadline, write(writer, frame)).await {
526                        Ok(Ok(())) => {
527                            wrote_close = true;
528                            timer.as_mut().reset(deadline);
529                        }
530                        Ok(Err(error)) => {
531                            abort(writer, events, error.to_string(), 1006, wrote_close).await;
532                            return;
533                        }
534                        Err(_) => {
535                            closed(events, false, 1006, String::new()).await;
536                            return;
537                        }
538                    }
539                }
540                None => {
541                    closed(events, false, 1006, String::new()).await;
542                    return;
543                }
544            },
545            frame = frames.recv() => match frame {
546                Some(Err(error)) => {
547                    if wrote_close {
548                        closed(events, false, 1006, String::new()).await;
549                    } else {
550                        abort(writer, events, error.to_string(), failure_code(&error), false).await;
551                    }
552                    return;
553                }
554                None => {
555                    closed(events, false, 1006, String::new()).await;
556                    return;
557                }
558                Some(Ok(frame)) => match frame.opcode {
559                    OpCode::Ping if !wrote_close => {
560                        if let Err(error) = write(writer, Frame::pong(frame.payload.into())).await {
561                            abort(writer, events, error.to_string(), 1006, wrote_close).await;
562                            return;
563                        }
564                    }
565                    OpCode::Close => match close_payload(&frame.payload) {
566                        Ok(payload) => {
567                            push(events, Event::Closing).await;
568                            let (code, reason) = match payload {
569                                Some((code, reason)) => (code, reason),
570                                None => (1005, String::new()),
571                            };
572                            if !wrote_close {
573                                let echo = Frame::close_raw(frame.payload.into());
574                                let result = tokio::time::timeout(CLOSE_TIMEOUT, write(writer, echo)).await;
575                                let message = match result {
576                                    Ok(Ok(())) => None,
577                                    Ok(Err(error)) => Some(error.to_string()),
578                                    Err(_) => Some("The WebSocket close reply timed out".into()),
579                                };
580                                if let Some(message) = message {
581                                    push(events, Event::Error { message }).await;
582                                    closed(events, false, code, reason).await;
583                                    return;
584                                }
585                            }
586                            // The handshake is complete. Closing our own transport after
587                            // the grace period is still a clean closure (RFC 6455 §7.1.1).
588                            if !server { let _ = tokio::time::timeout(TCP_CLOSE_TIMEOUT, transport_closed).await; }
589                            closed(events, true, code, reason).await;
590                            return;
591                        }
592                        Err(code) => {
593                            abort(
594                                writer,
595                                events,
596                                "The WebSocket connection received an invalid close frame".into(),
597                                code,
598                                wrote_close,
599                            )
600                            .await;
601                            return;
602                        }
603                    },
604                    OpCode::Text => match String::from_utf8(frame.payload) {
605                        Ok(data) => {
606                            push(events, Event::Text { data }).await;
607                        }
608                        Err(_) => {
609                            abort(
610                                writer,
611                                events,
612                                "The WebSocket connection received invalid UTF-8 in a text message".into(),
613                                1007,
614                                wrote_close,
615                            )
616                            .await;
617                            return;
618                        }
619                    },
620                    OpCode::Binary => {
621                        let data = ToJsBuffer::from(frame.payload);
622                        push(events, Event::Binary { data }).await;
623                    }
624                    _ => {}
625                },
626            },
627        }
628    }
629}
630
631pub fn init(
632    runtime: tokio::runtime::Handle,
633    caps: Arc<Capabilities>,
634) -> io::Result<(Extension, Arc<Sockets>)> {
635    let mut roots = RootCertStore::empty();
636    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
637    let config = rustls::ClientConfig::builder_with_provider(Arc::new(
638        rustls::crypto::ring::default_provider(),
639    ))
640    .with_safe_default_protocol_versions()
641    .map_err(io::Error::other)?
642    .with_root_certificates(roots)
643    .with_no_client_auth();
644    let tls = TlsConnector::from(Arc::new(config));
645    let state = Arc::new(Sockets {
646        runtime,
647        tls,
648        resources: Arc::new(Mutex::new(HashMap::new())),
649        next: AtomicU64::new(1),
650        caps,
651    });
652    let create = state.clone();
653    let send = state.clone();
654    let close = state.clone();
655    let fail = state.clone();
656    let retire = state.clone();
657    let wait = state.clone();
658    Ok((
659        Extension {
660            name: "ext:websocket",
661            dependencies: &["ext:blob", "ext:encoding", "ext:url", "ext:web"],
662            ops: vec![
663                Op::new(
664                    "op_ws_create",
665                    move |(url, protocols, user_agent): (String, Vec<String>, String)| {
666                        let url = Url::parse(&url).map_err(io::Error::other)?;
667                        if !matches!(url.scheme(), "ws" | "wss") {
668                            return Err(io::Error::other("Invalid WebSocket scheme"));
669                        }
670                        let host = url
671                            .host_str()
672                            .ok_or_else(|| io::Error::other("Missing WebSocket host"))?;
673                        let port = url
674                            .port_or_known_default()
675                            .ok_or_else(|| io::Error::other("Missing WebSocket port"))?;
676                        create
677                            .caps
678                            .check_net(host, port)
679                            .map_err(io::Error::other)?;
680                        let CreatedSocket {
681                            id,
682                            resource,
683                            commands: receiving,
684                            events,
685                        } = create.create()?;
686                        create.runtime.spawn(connect(
687                            create.tls.clone(),
688                            resource.canceled.clone(),
689                            receiving,
690                            events,
691                            url,
692                            protocols,
693                            user_agent,
694                        ));
695                        Ok::<_, io::Error>(id)
696                    },
697                ),
698                // The command is enqueued at dispatch so queue order equals call order;
699                // the future only awaits the write acknowledgement.
700                Op::asynchronous_unreferenced(
701                    "op_ws_send",
702                    move |(id, text, bytes): (u64, bool, Option<JsBuffer>)| {
703                        let (ack, receive) = oneshot::channel();
704                        if let Ok(resource) = send.resource(id) {
705                            let kind = if text {
706                                FrameKind::Text
707                            } else {
708                                FrameKind::Binary
709                            };
710                            let payload = bytes.map(|bytes| bytes.to_vec()).unwrap_or_default();
711                            let _ = resource.commands.send(Command::Send(kind, payload, ack));
712                        } else {
713                            drop(ack);
714                        }
715                        async move { Ok::<_, io::Error>(receive.await.unwrap_or(false)) }
716                    },
717                ),
718                Op::new(
719                    "op_ws_close",
720                    move |(id, code, reason): (u64, Option<u16>, Option<String>)| {
721                        let payload = match (code, reason) {
722                            (Some(code), Some(reason)) => {
723                                let mut payload = code.to_be_bytes().to_vec();
724                                payload.extend_from_slice(reason.as_bytes());
725                                Some(payload)
726                            }
727                            (Some(code), None) => Some(code.to_be_bytes().to_vec()),
728                            // A reason without a code is not a valid close payload; use 1000.
729                            (None, Some(reason)) => {
730                                let mut payload = 1000u16.to_be_bytes().to_vec();
731                                payload.extend_from_slice(reason.as_bytes());
732                                Some(payload)
733                            }
734                            (None, None) => None,
735                        };
736                        if let Ok(resource) = close.resource(id) {
737                            let _ = resource.commands.send(Command::Close(payload));
738                        }
739                        Ok::<_, io::Error>(())
740                    },
741                ),
742                Op::new("op_ws_fail", move |id: u64| {
743                    if let Ok(resource) = fail.resource(id) {
744                        resource.canceled.cancel();
745                    }
746                    Ok::<_, io::Error>(())
747                }),
748                Op::new("op_ws_retire", move |id: u64| {
749                    if let Some(resource) = retire.resources.lock().unwrap().remove(&id) {
750                        resource.canceled.cancel();
751                    }
752                    Ok::<_, io::Error>(())
753                }),
754                Op::asynchronous("op_ws_wait", move |id: u64| {
755                    let resource = wait.resource(id);
756                    async move {
757                        let Ok(resource) = resource else {
758                            return Err(io::Error::other("WebSocket is closed"));
759                        };
760                        let mut events = resource.events.lock().await;
761                        events
762                            .recv()
763                            .await
764                            .ok_or_else(|| io::Error::other("WebSocket task ended"))
765                    }
766                }),
767            ],
768            modules: &[
769                (
770                    "ext:websocket/main.js",
771                    include_str!(concat!(env!("OUT_DIR"), "/main.js")),
772                ),
773                (
774                    "ext:websocket/websocket.js",
775                    include_str!(concat!(env!("OUT_DIR"), "/websocket.js")),
776                ),
777            ],
778            entry_point: Some("ext:websocket/main.js"),
779        },
780        state,
781    ))
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787
788    enum Input {
789        Command(Command),
790        Frame(Result<Received, WebSocketError>),
791    }
792
793    async fn events_with_blocked_transport(input: Input) -> Vec<Event> {
794        let (socket, _peer) = tokio::io::duplex(1);
795        let ws = fastwebsockets::WebSocket::after_handshake(socket, fastwebsockets::Role::Client);
796        let (_, mut writer) = ws.split(tokio::io::split);
797        let (commands, queued) = mpsc::unbounded_channel();
798        let (frames, received) = mpsc::channel(1);
799        let (events, mut emitted) = mpsc::channel(2);
800        let (_ended, transport_closed) = oneshot::channel();
801        match input {
802            Input::Command(command) => commands.send(command).unwrap(),
803            Input::Frame(frame) => frames.send(frame).await.unwrap(),
804        }
805        let started = tokio::time::Instant::now();
806        let connection = tokio::spawn(async move {
807            stream_loop(
808                &mut writer,
809                queued,
810                received,
811                transport_closed,
812                &events,
813                false,
814            )
815            .await;
816        });
817        let mut result = Vec::new();
818        while let Some(event) = emitted.recv().await {
819            result.push(event);
820        }
821        connection.await.unwrap();
822        assert_eq!(started.elapsed(), Duration::from_secs(30));
823        result
824    }
825
826    #[tokio::test(start_paused = true)]
827    async fn client_close_write_cannot_stall_shutdown() {
828        let events = events_with_blocked_transport(Input::Command(Command::Close(None))).await;
829        assert!(matches!(
830            events.as_slice(),
831            [Event::Close { clean: false, code: 1006, reason }] if reason.is_empty()
832        ));
833    }
834
835    #[tokio::test(start_paused = true)]
836    async fn peer_close_reply_cannot_stall_shutdown() {
837        let events = events_with_blocked_transport(Input::Frame(Ok(Received {
838            opcode: OpCode::Close,
839            payload: [3001u16.to_be_bytes().as_slice(), b"bye"].concat(),
840        })))
841        .await;
842        assert!(matches!(
843            events.as_slice(),
844            [Event::Closing, Event::Error { .. }, Event::Close { clean: false, code: 3001, reason }]
845                if reason == "bye"
846        ));
847    }
848
849    #[tokio::test(start_paused = true)]
850    async fn failure_close_write_cannot_stall_shutdown() {
851        let events =
852            events_with_blocked_transport(Input::Frame(Err(WebSocketError::InvalidValue))).await;
853        assert!(matches!(
854            events.as_slice(),
855            [Event::Error { .. }, Event::Close { clean: false, code: 1006, reason }]
856                if reason.is_empty()
857        ));
858    }
859
860    struct Connection {
861        peer: tokio::io::DuplexStream,
862        events: mpsc::Receiver<Event>,
863        task: tokio::task::JoinHandle<()>,
864        _commands: mpsc::UnboundedSender<Command>,
865    }
866
867    async fn completed_handshake(client_initiated: bool) -> Connection {
868        use tokio::io::{AsyncReadExt, AsyncWriteExt};
869
870        let (socket, mut peer) = tokio::io::duplex(1024);
871        let (reader, writer) = tokio::io::split(socket);
872        let (_, mut writer) = fastwebsockets::after_handshake_split(
873            tokio::io::empty(),
874            writer,
875            fastwebsockets::Role::Client,
876        );
877        let (commands, queued) = mpsc::unbounded_channel();
878        let (received, frames) = mpsc::channel(READ_QUEUE);
879        let (ended, transport_closed) = oneshot::channel();
880        let (events, mut emitted) = mpsc::channel(EVENT_QUEUE);
881        let shutdown = CancellationToken::new();
882        let reading = tokio::spawn(read_frames(
883            reader::Reader::new(reader, false),
884            received,
885            ended,
886            shutdown.clone(),
887        ));
888        let task = tokio::spawn(async move {
889            stream_loop(
890                &mut writer,
891                queued,
892                frames,
893                transport_closed,
894                &events,
895                false,
896            )
897            .await;
898            shutdown.cancel();
899            reading.await.unwrap();
900        });
901        let payload = [3001u16.to_be_bytes().as_slice(), b"bye"].concat();
902        let server_close = [b"\x88\x05".as_slice(), &payload].concat();
903        if client_initiated {
904            commands
905                .send(Command::Close(Some(payload.clone())))
906                .unwrap();
907        } else {
908            peer.write_all(&server_close).await.unwrap();
909        }
910        let mut reply = [0; 11];
911        peer.read_exact(&mut reply).await.unwrap();
912        assert_eq!(&reply[..2], b"\x88\x85");
913        for index in 0..5 {
914            assert_eq!(reply[6 + index] ^ reply[2 + index % 4], payload[index]);
915        }
916        if client_initiated {
917            peer.write_all(&server_close).await.unwrap();
918        }
919        assert!(matches!(emitted.recv().await, Some(Event::Closing)));
920        Connection {
921            peer,
922            events: emitted,
923            task,
924            _commands: commands,
925        }
926    }
927
928    async fn assert_waits_for_eof(client_initiated: bool) {
929        use tokio::io::AsyncWriteExt;
930
931        let mut connection = completed_handshake(client_initiated).await;
932        tokio::time::advance(Duration::from_secs(1)).await;
933        assert!(matches!(
934            connection.events.try_recv(),
935            Err(mpsc::error::TryRecvError::Empty)
936        ));
937        connection.peer.shutdown().await.unwrap();
938        assert!(matches!(connection.events.recv().await,
939            Some(Event::Close { clean: true, code: 3001, reason }) if reason == "bye"));
940        connection.task.await.unwrap();
941        assert!(connection.events.recv().await.is_none());
942    }
943
944    #[tokio::test(start_paused = true)]
945    async fn client_close_waits_for_transport_eof() {
946        assert_waits_for_eof(true).await;
947    }
948
949    #[tokio::test(start_paused = true)]
950    async fn peer_close_waits_for_transport_eof() {
951        assert_waits_for_eof(false).await;
952    }
953
954    #[tokio::test(start_paused = true)]
955    async fn tcp_close_timeout_still_reports_clean_close() {
956        use tokio::io::AsyncReadExt;
957
958        let mut connection = completed_handshake(false).await;
959        let started = tokio::time::Instant::now();
960        assert!(matches!(connection.events.recv().await,
961            Some(Event::Close { clean: true, code: 3001, reason }) if reason == "bye"));
962        assert_eq!(started.elapsed(), Duration::from_secs(2));
963        connection.task.await.unwrap();
964        let mut byte = [0];
965        assert_eq!(connection.peer.read(&mut byte).await.unwrap(), 0);
966        assert!(connection.events.recv().await.is_none());
967    }
968}