Skip to main content

web_transport_node/
lib.rs

1use napi::bindgen_prelude::*;
2use napi_derive::napi;
3
4use tokio::sync::Mutex;
5
6fn session_error_to_close_info(err: &web_transport_quinn::SessionError) -> NapiCloseInfo {
7    match err {
8        web_transport_quinn::SessionError::WebTransportError(
9            web_transport_quinn::WebTransportError::Closed(code, reason),
10        ) => NapiCloseInfo {
11            close_code: *code,
12            reason: reason.clone(),
13        },
14        other => NapiCloseInfo {
15            close_code: 0,
16            reason: other.to_string(),
17        },
18    }
19}
20
21/// A WebTransport client that can connect to servers.
22#[napi]
23pub struct NapiClient {
24    inner: web_transport_quinn::Client,
25}
26
27#[napi]
28impl NapiClient {
29    /// Create a client that validates server certificates against system root CAs.
30    #[napi(factory)]
31    pub fn with_system_roots() -> Result<Self> {
32        napi::bindgen_prelude::within_runtime_if_available(|| {
33            let client = web_transport_quinn::ClientBuilder::new()
34                .with_system_roots()
35                .map_err(|e| Error::from_reason(e.to_string()))?;
36            Ok(Self { inner: client })
37        })
38    }
39
40    /// Create a client that skips certificate verification entirely.
41    /// WARNING: Only use for testing with self-signed certificates.
42    #[napi(factory)]
43    pub fn disable_verify() -> Result<Self> {
44        napi::bindgen_prelude::within_runtime_if_available(|| {
45            let client = web_transport_quinn::ClientBuilder::new()
46                .dangerous()
47                .with_no_certificate_verification()
48                .map_err(|e| Error::from_reason(e.to_string()))?;
49            Ok(Self { inner: client })
50        })
51    }
52
53    /// Create a client that validates server certificates by SHA-256 hash.
54    #[napi(factory)]
55    pub fn with_certificate_hashes(hashes: Vec<Buffer>) -> Result<Self> {
56        napi::bindgen_prelude::within_runtime_if_available(|| {
57            let hashes: Vec<Vec<u8>> = hashes.into_iter().map(|b| b.to_vec()).collect();
58            let client = web_transport_quinn::ClientBuilder::new()
59                .with_server_certificate_hashes(hashes)
60                .map_err(|e| Error::from_reason(e.to_string()))?;
61            Ok(Self { inner: client })
62        })
63    }
64
65    /// Connect to a WebTransport server at the given URL.
66    #[napi]
67    pub async fn connect(&self, url_str: String) -> Result<NapiSession> {
68        let url: url::Url = url_str
69            .parse()
70            .map_err(|e: url::ParseError| Error::from_reason(e.to_string()))?;
71        let session = self
72            .inner
73            .connect(url)
74            .await
75            .map_err(|e| Error::from_reason(e.to_string()))?;
76        Ok(NapiSession {
77            inner: session.clone(),
78            closed: Mutex::new(None),
79        })
80    }
81}
82
83/// A WebTransport server that accepts incoming sessions.
84#[napi]
85pub struct NapiServer {
86    inner: Mutex<web_transport_quinn::Server>,
87}
88
89#[napi]
90impl NapiServer {
91    /// Create a server bound to the given address with the given TLS certificate.
92    #[napi(factory)]
93    pub fn bind(addr: String, cert_pem: Buffer, key_pem: Buffer) -> Result<Self> {
94        napi::bindgen_prelude::within_runtime_if_available(|| {
95            let certs = rustls_pemfile::certs(&mut &cert_pem[..])
96                .collect::<std::result::Result<Vec<_>, _>>()
97                .map_err(|e| Error::from_reason(format!("invalid certificate PEM: {e}")))?;
98            let key = rustls_pemfile::private_key(&mut &key_pem[..])
99                .map_err(|e| Error::from_reason(format!("invalid private key PEM: {e}")))?
100                .ok_or_else(|| Error::from_reason("no private key found in PEM"))?;
101
102            let addr: std::net::SocketAddr = addr
103                .parse()
104                .map_err(|e: std::net::AddrParseError| Error::from_reason(e.to_string()))?;
105
106            let server = web_transport_quinn::ServerBuilder::new()
107                .with_addr(addr)
108                .with_certificate(certs, key)
109                .map_err(|e| Error::from_reason(e.to_string()))?;
110
111            Ok(Self {
112                inner: Mutex::new(server),
113            })
114        })
115    }
116
117    /// Accept the next incoming WebTransport session request.
118    #[napi]
119    pub async fn accept(&self) -> Result<Option<NapiRequest>> {
120        let mut server = self.inner.lock().await;
121        match server.accept().await {
122            Some(request) => Ok(Some(NapiRequest {
123                inner: Mutex::new(Some(request)),
124            })),
125            None => Ok(None),
126        }
127    }
128}
129
130/// A pending WebTransport session request from a client.
131#[napi]
132pub struct NapiRequest {
133    // Option so we can take it in ok()/reject() which consume the Request.
134    inner: Mutex<Option<web_transport_quinn::Request>>,
135}
136
137#[napi]
138impl NapiRequest {
139    /// Get the URL of the CONNECT request.
140    #[napi(getter)]
141    pub async fn url(&self) -> Result<String> {
142        let guard = self.inner.lock().await;
143        let request = guard
144            .as_ref()
145            .ok_or_else(|| Error::from_reason("request already consumed"))?;
146        Ok(request.url.to_string())
147    }
148
149    /// Accept the session with 200 OK.
150    #[napi]
151    pub async fn ok(&self) -> Result<NapiSession> {
152        let request = self
153            .inner
154            .lock()
155            .await
156            .take()
157            .ok_or_else(|| Error::from_reason("request already consumed"))?;
158        let session = request
159            .ok()
160            .await
161            .map_err(|e| Error::from_reason(e.to_string()))?;
162        Ok(NapiSession {
163            inner: session.clone(),
164            closed: Mutex::new(None),
165        })
166    }
167
168    /// Reject the session with the given HTTP status code.
169    #[napi]
170    pub async fn reject(&self, status: u16) -> Result<()> {
171        let request = self
172            .inner
173            .lock()
174            .await
175            .take()
176            .ok_or_else(|| Error::from_reason("request already consumed"))?;
177        let status = http::StatusCode::from_u16(status)
178            .map_err(|e| Error::from_reason(format!("invalid status code: {e}")))?;
179        request
180            .reject(status)
181            .await
182            .map_err(|e| Error::from_reason(e.to_string()))?;
183        Ok(())
184    }
185}
186
187/// An established WebTransport session.
188#[napi]
189pub struct NapiSession {
190    inner: web_transport_quinn::Session,
191    // Cache the closed future result so multiple callers can await it.
192    closed: Mutex<Option<NapiCloseInfo>>,
193}
194
195/// Info about why a session was closed, matching W3C WebTransportCloseInfo.
196#[derive(Clone)]
197#[napi(object)]
198pub struct NapiCloseInfo {
199    pub close_code: u32,
200    pub reason: String,
201}
202
203/// A bidirectional stream pair.
204#[napi]
205pub struct NapiBiStream {
206    send: Option<NapiSendStream>,
207    recv: Option<NapiRecvStream>,
208}
209
210#[napi]
211impl NapiBiStream {
212    /// Take the send stream. Can only be called once.
213    #[napi]
214    pub fn take_send(&mut self) -> Result<NapiSendStream> {
215        self.send
216            .take()
217            .ok_or_else(|| Error::from_reason("send stream already taken"))
218    }
219
220    /// Take the recv stream. Can only be called once.
221    #[napi]
222    pub fn take_recv(&mut self) -> Result<NapiRecvStream> {
223        self.recv
224            .take()
225            .ok_or_else(|| Error::from_reason("recv stream already taken"))
226    }
227}
228
229#[napi]
230impl NapiSession {
231    /// Accept an incoming unidirectional stream.
232    #[napi]
233    pub async fn accept_uni(&self) -> Result<NapiRecvStream> {
234        let recv = self
235            .inner
236            .accept_uni()
237            .await
238            .map_err(|e| Error::from_reason(e.to_string()))?;
239        Ok(NapiRecvStream {
240            inner: Mutex::new(recv),
241        })
242    }
243
244    /// Accept an incoming bidirectional stream.
245    #[napi]
246    pub async fn accept_bi(&self) -> Result<NapiBiStream> {
247        let (send, recv) = self
248            .inner
249            .accept_bi()
250            .await
251            .map_err(|e| Error::from_reason(e.to_string()))?;
252        Ok(NapiBiStream {
253            send: Some(NapiSendStream {
254                inner: Mutex::new(send),
255            }),
256            recv: Some(NapiRecvStream {
257                inner: Mutex::new(recv),
258            }),
259        })
260    }
261
262    /// Open a new unidirectional stream.
263    #[napi]
264    pub async fn open_uni(&self) -> Result<NapiSendStream> {
265        let send = self
266            .inner
267            .open_uni()
268            .await
269            .map_err(|e| Error::from_reason(e.to_string()))?;
270        Ok(NapiSendStream {
271            inner: Mutex::new(send),
272        })
273    }
274
275    /// Open a new bidirectional stream.
276    #[napi]
277    pub async fn open_bi(&self) -> Result<NapiBiStream> {
278        let (send, recv) = self
279            .inner
280            .open_bi()
281            .await
282            .map_err(|e| Error::from_reason(e.to_string()))?;
283        Ok(NapiBiStream {
284            send: Some(NapiSendStream {
285                inner: Mutex::new(send),
286            }),
287            recv: Some(NapiRecvStream {
288                inner: Mutex::new(recv),
289            }),
290        })
291    }
292
293    /// Send a datagram.
294    #[napi]
295    pub fn send_datagram(&self, data: Buffer) -> Result<()> {
296        within_runtime_if_available(|| {
297            self.inner
298                .send_datagram(bytes::Bytes::from(data.to_vec()))
299                .map_err(|e| Error::from_reason(e.to_string()))
300        })
301    }
302
303    /// Receive a datagram.
304    #[napi]
305    pub async fn recv_datagram(&self) -> Result<Buffer> {
306        let data = self
307            .inner
308            .read_datagram()
309            .await
310            .map_err(|e| Error::from_reason(e.to_string()))?;
311        Ok(Buffer::from(data.to_vec()))
312    }
313
314    /// Get the maximum datagram size.
315    #[napi]
316    pub fn max_datagram_size(&self) -> u32 {
317        within_runtime_if_available(|| self.inner.max_datagram_size() as u32)
318    }
319
320    /// Close the session with a code and reason.
321    #[napi]
322    pub fn close(&self, code: u32, reason: String) {
323        within_runtime_if_available(|| {
324            self.inner.close(code, reason.as_bytes());
325        });
326    }
327
328    /// Wait for the session to close, returning close info matching W3C WebTransportCloseInfo.
329    #[napi]
330    pub async fn closed(&self) -> Result<NapiCloseInfo> {
331        // Check if we already have a cached result.
332        {
333            let cached = self.closed.lock().await;
334            if let Some(info) = cached.as_ref() {
335                return Ok(info.clone());
336            }
337        }
338
339        let err = self.inner.closed().await;
340        let info = session_error_to_close_info(&err);
341
342        // Cache the result.
343        {
344            let mut cached = self.closed.lock().await;
345            *cached = Some(info.clone());
346        }
347
348        Ok(info)
349    }
350}
351
352/// A send stream for writing data.
353#[napi]
354pub struct NapiSendStream {
355    inner: Mutex<web_transport_quinn::SendStream>,
356}
357
358#[napi]
359impl NapiSendStream {
360    /// Write data to the stream.
361    #[napi]
362    pub async fn write(&self, data: Buffer) -> Result<()> {
363        let mut stream = self.inner.lock().await;
364        stream
365            .write_all(&data)
366            .await
367            .map_err(|e| Error::from_reason(e.to_string()))
368    }
369
370    /// Signal that no more data will be written.
371    #[napi]
372    pub async fn finish(&self) -> Result<()> {
373        let mut stream = self.inner.lock().await;
374        stream
375            .finish()
376            .map_err(|e| Error::from_reason(e.to_string()))
377    }
378
379    /// Abruptly reset the stream with an error code.
380    #[napi]
381    pub async fn reset(&self, code: u32) -> Result<()> {
382        let mut stream = self.inner.lock().await;
383        stream
384            .reset(code)
385            .map_err(|e| Error::from_reason(e.to_string()))
386    }
387
388    /// Set the priority of the stream.
389    #[napi]
390    pub async fn set_priority(&self, priority: i32) -> Result<()> {
391        let stream = self.inner.lock().await;
392        stream
393            .set_priority(priority)
394            .map_err(|e| Error::from_reason(e.to_string()))
395    }
396}
397
398/// A receive stream for reading data.
399#[napi]
400pub struct NapiRecvStream {
401    inner: Mutex<web_transport_quinn::RecvStream>,
402}
403
404#[napi]
405impl NapiRecvStream {
406    /// Read up to `max_size` bytes from the stream. Returns null on FIN.
407    #[napi]
408    pub async fn read(&self, max_size: u32) -> Result<Option<Buffer>> {
409        let mut stream = self.inner.lock().await;
410        let Some(chunk) = stream
411            .read_chunk(max_size as usize, true)
412            .await
413            .map_err(|e| Error::from_reason(e.to_string()))?
414        else {
415            return Ok(None);
416        };
417
418        Ok(Some(Buffer::from(chunk.bytes.to_vec())))
419    }
420
421    /// Tell the peer to stop sending with the given error code.
422    #[napi]
423    pub async fn stop(&self, code: u32) -> Result<()> {
424        let mut stream = self.inner.lock().await;
425        stream
426            .stop(code)
427            .map_err(|e| Error::from_reason(e.to_string()))
428    }
429}