1use ::std::cell::RefCell;
4use ::std::ffi::CString;
5use ::std::io::{Read, Write};
6use ::std::net::TcpStream;
7use ::std::net::ToSocketAddrs;
8use ::std::ptr::NonNull;
9use ::std::sync::atomic::{AtomicU64, Ordering};
10use ::std::sync::{Arc, Mutex};
11use ::std::time::Duration;
12use bun_core::ZBox;
13
14use bun_uws::ws_codec::apply_mask;
17
18use mozjs::conversions::unsafe_jsstr_to_string;
19use mozjs::jsapi::*;
20use mozjs::jsval::{BooleanValue, Int32Value, JSVal, NullValue, ObjectValue, StringValue, UndefinedValue};
21use mozjs::realm::AutoRealm;
22use mozjs::rooted;
23use mozjs::rust::wrappers2::{
24 CallOriginalPromiseResolve, CallOriginalPromiseThen, JS_DefineFunction, JS_DefineProperty3,
25 JS_NewPlainObject, NewArrayObject1,
26};
27
28use crate::gc_store::{gc_store_get, gc_store_insert, gc_store_remove};
29
30#[derive(Debug)]
59#[allow(dead_code)]
60enum WsMessage {
61 Text(String),
62 Binary(Vec<u8>),
63 Close,
64}
65
66struct TlsStream {
74 tcp: TcpStream,
75 tls: bao_boringssl_bridge::connection::TlsConnection,
76 pending_plain: Vec<u8>,
81 pending_off: usize,
82}
83
84impl TlsStream {
85 fn pump_inbound(&mut self) -> ::std::io::Result<Vec<u8>> {
89 loop {
90 self.flush_outgoing()?;
93 let res = self.tls.process().map_err(|e| {
94 ::std::io::Error::new(::std::io::ErrorKind::InvalidData, e.to_string())
95 })?;
96 if !res.plaintext.is_empty() {
97 let mut joined = Vec::new();
98 for chunk in res.plaintext {
99 joined.extend_from_slice(&chunk);
100 }
101 return Ok(joined);
102 }
103 let mut buf = [0u8; 16_384];
105 match self.tcp.read(&mut buf) {
106 Ok(0) => {
107 return Err(::std::io::Error::new(
108 ::std::io::ErrorKind::UnexpectedEof,
109 "tls peer closed",
110 ));
111 }
112 Ok(n) => self.tls.feed(&buf[..n]),
113 Err(ref e)
114 if e.kind() == ::std::io::ErrorKind::WouldBlock
115 || e.kind() == ::std::io::ErrorKind::TimedOut =>
116 {
117 return Err(::std::io::Error::from(::std::io::ErrorKind::WouldBlock));
118 }
119 Err(e) => return Err(e),
120 }
121 }
122 }
123
124 fn flush_outgoing(&mut self) -> ::std::io::Result<()> {
126 let outgoing = self.tls.take_outgoing();
127 if outgoing.is_empty() {
128 return Ok(());
129 }
130 self.tcp.write_all(&outgoing)
131 }
132}
133
134impl ::std::io::Read for TlsStream {
135 fn read(&mut self, buf: &mut [u8]) -> ::std::io::Result<usize> {
136 if self.pending_off >= self.pending_plain.len() {
139 self.pending_plain = self.pump_inbound()?;
140 self.pending_off = 0;
141 }
142 let avail = &self.pending_plain[self.pending_off..];
143 let n = avail.len().min(buf.len());
144 buf[..n].copy_from_slice(&avail[..n]);
145 self.pending_off += n;
146 Ok(n)
147 }
148}
149
150impl ::std::io::Write for TlsStream {
151 fn write(&mut self, buf: &[u8]) -> ::std::io::Result<usize> {
152 let written = self
153 .tls
154 .write(buf)
155 .map_err(|e| ::std::io::Error::new(::std::io::ErrorKind::InvalidData, e.to_string()))?;
156 self.flush_outgoing()?;
157 Ok(written)
158 }
159 fn flush(&mut self) -> ::std::io::Result<()> {
160 self.tcp.flush()
161 }
162}
163
164enum WsConn {
166 Plain(bun_uws::ws_client::WebSocketClient),
169 Tls {
171 stream: TlsStream,
172 decoder: bun_uws::ws_codec::FrameDecoder,
173 closed: bool,
174 },
175}
176
177impl WsConn {
178 fn connect(
182 url: &str,
183 profile: &::std::option::Option<bao_stealth::StealthProfile>,
184 ) -> ::std::result::Result<Self, String> {
185 let (scheme, rest) = if let Some(r) = url.strip_prefix("ws://") {
186 ("ws", r)
187 } else if let Some(r) = url.strip_prefix("wss://") {
188 ("wss", r)
189 } else {
190 ("ws", url)
193 };
194
195 let (host, port, path) = split_authority_and_path(rest, scheme);
196 if scheme == "wss" {
197 Self::connect_tls(&host, port, &path, profile)
198 } else {
199 let canonical = if url.starts_with("ws://") || url.starts_with("wss://") {
202 url.to_string()
203 } else {
204 format!("ws://{}", url)
205 };
206 let client = bun_uws::ws_client::WebSocketClient::connect(&canonical)
207 .map_err(|e| format!("ws connect: {}", e))?;
208 Ok(WsConn::Plain(client))
209 }
210 }
211
212 fn connect_tls(
213 host: &str,
214 port: u16,
215 path: &str,
216 profile: &::std::option::Option<bao_stealth::StealthProfile>,
217 ) -> ::std::result::Result<Self, String> {
218 let addr = format!("{}:{}", host, port);
219 let socket_addr = addr
220 .to_socket_addrs()
221 .map_err(|e| format!("invalid address: {}", e))?
222 .next()
223 .ok_or_else(|| format!("no address for {}", addr))?;
224 let mut tcp = TcpStream::connect_timeout(&socket_addr, Duration::from_secs(10))
225 .map_err(|e| format!("connect failed: {}", e))?;
226 tcp.set_nonblocking(false).ok();
227 tcp.set_read_timeout(Some(Duration::from_secs(10))).ok();
228
229 let tls_client = bao_boringssl_bridge::client::TlsClient::new()
231 .map_err(|e| format!("tls client init: {}", e))?;
232 let mut tls =
233 bao_boringssl_bridge::connection::TlsConnection::new_client(&tls_client, host)
234 .map_err(|e| format!("tls conn: {}", e))?;
235
236 let ssl_config = crate::stealth_http::stealth_profile_to_ssl_config(profile);
242 let host_c = CString::new(host).map_err(|_| format!("invalid host: {}", host))?;
243 {
244 let ssl = tls.ssl_ptr();
245 if !ssl.is_null() {
246 bun_http::configure_http_client_with_alpn(
250 unsafe { &mut *ssl },
251 host_c.as_ptr(),
252 bun_http::AlpnOffer::H1,
253 Some(&ssl_config),
254 );
255
256 let profile_salt = if profile.is_some() {
265 ssl_config.content_hash()
266 } else {
267 0
268 };
269 bao_boringssl_bridge::session_cache::offer_session(ssl, host, port, profile_salt);
270 }
271 }
272
273 loop {
281 match tls.process() {
282 Ok(res) => {
283 use bao_boringssl_bridge::connection::TlsState;
284 loop {
287 let outgoing = tls.take_outgoing();
288 if outgoing.is_empty() {
289 break;
290 }
291 if tcp.write_all(&outgoing).is_err() {
292 return Err("tls handshake write failed".to_string());
293 }
294 }
295 if res.state == TlsState::Active || res.state == TlsState::PeerClosed {
296 break;
297 }
298 let mut buf = [0u8; 16_384];
300 match tcp.read(&mut buf) {
301 Ok(n) if n > 0 => tls.feed(&buf[..n]),
302 _ => {
303 return Err("tls handshake stalled".to_string());
304 }
305 }
306 }
307 Err(e) => return Err(format!("tls handshake: {}", e)),
308 }
309 }
310
311 let mut stream = TlsStream {
312 tcp,
313 tls,
314 pending_plain: Vec::new(),
315 pending_off: 0,
316 };
317 bun_uws::ws_handshake::client_handshake(&mut stream, host, &path)
319 .map_err(|e| format!("ws handshake: {:?}", e))?;
320 Ok(WsConn::Tls {
321 stream,
322 decoder: bun_uws::ws_codec::FrameDecoder::new(),
323 closed: false,
324 })
325 }
326
327 fn send_text(&mut self, text: &str) -> ::std::result::Result<(), String> {
328 match self {
329 WsConn::Plain(c) => c.send_text(text).map_err(|e| format!("send failed: {}", e)),
330 WsConn::Tls { stream, .. } => {
331 let payload = text.as_bytes();
332 let key = bun_uws::ws_codec::gen_mask_key();
333 let mut frame = Vec::with_capacity(payload.len() + 14);
334 frame.push(0x81); push_masked_len(&mut frame, payload.len());
336 frame.extend_from_slice(&key);
337 let mut masked = payload.to_vec();
338 apply_mask(&mut masked, &key);
339 frame.extend_from_slice(&masked);
344 stream
345 .write_all(&frame)
346 .map_err(|e| format!("send failed: {}", e))
347 }
348 }
349 }
350
351 fn read_message(&mut self) -> ::std::result::Result<WsMessage, String> {
352 match self {
353 WsConn::Plain(c) => match c.recv().map_err(|e| format!("recv: {}", e))? {
354 bun_uws::ws_client::RecvOutcome::Message(opcode, payload) => match opcode {
355 bun_uws::ws_codec::Opcode::Text => Ok(WsMessage::Text(
356 String::from_utf8_lossy(&payload).into_owned(),
357 )),
358 bun_uws::ws_codec::Opcode::Binary => Ok(WsMessage::Binary(payload)),
359 _ => Ok(WsMessage::Binary(payload)),
360 },
361 bun_uws::ws_client::RecvOutcome::Closed => Ok(WsMessage::Close),
362 bun_uws::ws_client::RecvOutcome::Timeout => Err("wouldblock".to_string()),
363 },
364 WsConn::Tls {
365 stream,
366 decoder,
367 closed,
368 } => {
369 if *closed {
370 return Ok(WsMessage::Close);
371 }
372 let header = match decoder.decode_frame(stream) {
373 Ok(Some(h)) => h,
374 Ok(None) => return Err("wouldblock".to_string()),
375 Err(ref e)
376 if e.kind() == ::std::io::ErrorKind::WouldBlock
377 || e.kind() == ::std::io::ErrorKind::TimedOut =>
378 {
379 return Err("wouldblock".to_string());
380 }
381 Err(ref e) if e.kind() == ::std::io::ErrorKind::UnexpectedEof => {
382 *closed = true;
383 return Ok(WsMessage::Close);
384 }
385 Err(e) => return Err(format!("recv: {}", e)),
386 };
387 let mut payload = if header.mask {
388 let mask_key = decoder.take_mask();
389 let mut p = decoder.take_payload(&header);
390 apply_mask(&mut p, &mask_key);
391 p
392 } else {
393 decoder.take_payload(&header)
394 };
395 match header.opcode {
396 bun_uws::ws_codec::Opcode::Text => Ok(WsMessage::Text(
397 String::from_utf8_lossy(&payload).into_owned(),
398 )),
399 bun_uws::ws_codec::Opcode::Binary => Ok(WsMessage::Binary(payload)),
400 bun_uws::ws_codec::Opcode::Close => {
401 *closed = true;
402 Ok(WsMessage::Close)
403 }
404 bun_uws::ws_codec::Opcode::Ping => {
405 let key = bun_uws::ws_codec::gen_mask_key();
407 let mut frame = vec![0x8A]; push_masked_len(&mut frame, payload.len());
409 frame.extend_from_slice(&key);
410 apply_mask(&mut payload, &key);
411 frame.extend_from_slice(&payload);
412 stream
413 .write_all(&frame)
414 .map_err(|e| format!("pong: {}", e))?;
415 self.read_message()
416 }
417 bun_uws::ws_codec::Opcode::Pong | bun_uws::ws_codec::Opcode::Continuation => {
418 self.read_message()
419 }
420 }
421 }
422 }
423 }
424
425 fn close(&mut self) -> ::std::result::Result<(), String> {
432 let frame = encode_masked_close_frame();
433 match self {
434 WsConn::Plain(c) => {
435 if c.is_closed() {
436 return Ok(());
437 }
438 c.stream_mut()
439 .write_all(&frame)
440 .map_err(|e| format!("close failed: {}", e))
441 }
442 WsConn::Tls { stream, .. } => {
443 let _ = stream.write_all(&frame);
450 Ok(())
451 }
452 }
453 }
454
455 fn set_nonblocking(&mut self, nonblocking: bool) {
458 match self {
459 WsConn::Plain(c) => {
460 let _ = c.stream_mut().set_nonblocking(nonblocking);
461 }
462 WsConn::Tls { stream, .. } => {
463 let _ = stream.tcp.set_nonblocking(nonblocking);
464 }
465 }
466 }
467}
468
469fn split_authority_and_path(rest: &str, scheme: &str) -> (String, u16, String) {
472 let default_port = if scheme == "wss" { 443 } else { 80 };
473 let (authority, path) = match rest.find('/') {
474 Some(i) => (&rest[..i], rest[i..].to_string()),
475 None => (rest, "/".to_string()),
476 };
477 let (host, port) = match authority.rfind(':') {
478 Some(i) => (
479 authority[..i].to_string(),
480 authority[i + 1..].parse::<u16>().unwrap_or(default_port),
481 ),
482 None => (authority.to_string(), default_port),
483 };
484 (host, port, path)
485}
486
487fn encode_masked_close_frame() -> Vec<u8> {
489 let key = bun_uws::ws_codec::gen_mask_key();
490 let mut frame = vec![0x88];
491 let payload = 1000u16.to_be_bytes();
492 push_masked_len(&mut frame, payload.len());
493 frame.extend_from_slice(&key);
494 let mut masked = payload.to_vec();
495 apply_mask(&mut masked, &key);
496 frame.extend_from_slice(&masked);
497 frame
498}
499
500fn push_masked_len(frame: &mut Vec<u8>, len: usize) {
503 let mask_bit = 0x80u8;
504 if len < 126 {
505 frame.push((len as u8) | mask_bit);
506 } else if len <= u16::MAX as usize {
507 frame.push(126u8 | mask_bit);
508 frame.extend_from_slice(&(len as u16).to_be_bytes());
509 } else {
510 frame.push(127u8 | mask_bit);
511 frame.extend_from_slice(&(len as u64).to_be_bytes());
512 }
513}
514
515static WS_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
519
520struct WsEntry {
526 client: ::std::option::Option<WsConn>,
529 connect_slot: ::std::option::Option<
533 Arc<Mutex<::std::option::Option<::std::result::Result<WsConn, String>>>>,
534 >,
535 close_requested: bool,
538 close_initiated: bool,
542 realm_global: *mut JSObject,
547 js_obj_key: String,
548}
549
550impl WsEntry {
551 fn is_live(&self) -> bool {
552 self.connect_slot.is_some() || self.client.is_some()
553 }
554}
555
556thread_local! {
557 static WS_CONNECTIONS: RefCell<Vec<WsEntry>> = const { RefCell::new(Vec::new()) };
558}
559
560pub fn ws_has_pending() -> bool {
564 WS_CONNECTIONS.with(|c| c.borrow().iter().any(|e| e.is_live()))
565}
566
567pub fn install_websocket_constructor(
568 cx: &mut mozjs::context::JSContext,
569 global: mozjs::rust::Handle<*mut JSObject>,
570) {
571 unsafe {
572 let ws_fun = JS_NewFunction(
573 cx.raw_cx(),
574 Some(websocket_constructor),
575 1,
576 JSFUN_CONSTRUCTOR,
577 c"WebSocket".as_ptr(),
578 );
579 if !ws_fun.is_null() {
580 let ctor_obj = JS_GetFunctionObject(ws_fun);
581 if !ctor_obj.is_null() {
582 let val = mozjs::jsval::ObjectValue(ctor_obj);
583 rooted!(&in(cx) let v = val);
584 JS_DefineProperty(
585 cx.raw_cx(),
586 global.into(),
587 c"WebSocket".as_ptr(),
588 v.handle().into(),
589 (JSPROP_ENUMERATE | JSPROP_PERMANENT) as u32,
590 );
591
592 rooted!(&in(cx) let ctor_root = ctor_obj);
593 for (name, value) in &[
594 ("CONNECTING", 0i32),
595 ("OPEN", 1),
596 ("CLOSING", 2),
597 ("CLOSED", 3),
598 ] {
599 let c_name = ZBox::from_bytes(name.as_bytes());
600 rooted!(&in(cx) let iv = Int32Value(*value));
601 JS_DefineProperty(
602 cx.raw_cx(),
603 ctor_root.handle().into(),
604 c_name.as_ptr(),
605 iv.handle().into(),
606 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
607 );
608 }
609 }
610 }
611 }
612}
613
614unsafe fn ws_trigger_event(
623 cx: *mut JSContext,
624 realm_global: *mut JSObject,
625 ws_obj_key: &str,
626 event_name: &str,
627 data_val: Option<JSVal>,
628) {
629 if realm_global.is_null() {
630 return;
631 }
632 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
633 let cx_ref = &mut wrapped_cx;
634 rooted!(&in(cx_ref) let global_root = realm_global);
635 let mut realm = AutoRealm::new_from_handle(cx_ref, global_root.handle());
636 let cx_ref: &mut mozjs::context::JSContext = &mut realm;
637
638 let has_data = data_val.is_some();
643 let dv_in = data_val.unwrap_or_else(UndefinedValue);
644 rooted!(&in(cx_ref) let data_root = dv_in);
645
646 let ws_obj = match gc_store_get(cx, ws_obj_key) {
647 Some(obj) => obj,
648 None => return,
649 };
650 rooted!(&in(cx_ref) let ws_obj_root = ws_obj);
651 let mut handler_val = UndefinedValue();
652 let c_name = ZBox::from_bytes(event_name.as_bytes());
653 JS_GetProperty(
654 cx,
655 ws_obj_root.handle().into(),
656 c_name.as_ptr(),
657 MutableHandle::<Value> {
658 _phantom_0: ::std::marker::PhantomData,
659 ptr: &mut handler_val,
660 },
661 );
662 if handler_val.is_object() {
663 rooted!(&in(cx_ref) let handler_obj_root = handler_val.to_object());
664 if JS_ObjectIsFunction(handler_obj_root.get()) {
665 rooted!(&in(cx_ref) let handler_jsval = ObjectValue(handler_obj_root.get()));
666
667 rooted!(&in(cx_ref) let event_obj = mozjs_sys::jsapi::JS_NewPlainObject(cx));
668 if !event_obj.get().is_null() {
669 if has_data {
670 JS_DefineProperty(
671 cx,
672 event_obj.handle().into(),
673 c"data".as_ptr(),
674 data_root.handle().into(),
675 JSPROP_ENUMERATE as u32,
676 );
677 }
678 let ev_val = ObjectValue(event_obj.get());
679 let call_args = HandleValueArray {
680 length_: 1,
681 elements_: &ev_val,
682 };
683 let mut rval = UndefinedValue();
684 let ok = JS_CallFunctionValue(
685 cx,
686 ws_obj_root.handle().into(),
687 handler_jsval.handle().into(),
688 &call_args,
689 MutableHandle::<Value> {
690 _phantom_0: ::std::marker::PhantomData,
691 ptr: &mut rval,
692 },
693 );
694 if !ok {
695 let mut exn = UndefinedValue();
703 JS_GetPendingException(
704 cx,
705 MutableHandle::<Value> {
706 _phantom_0: ::std::marker::PhantomData,
707 ptr: &mut exn,
708 },
709 );
710 JS_ClearPendingException(cx);
711 rooted!(&in(cx_ref) let reason_root = exn);
712 if !exn.is_undefined() {
713 crate::uncaught::route_uncaught_exception(cx, exn);
714 }
715 }
716 }
717 }
718 }
719}
720
721unsafe extern "C" fn ws_send(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
722 let args = CallArgs::from_vp(vp, argc);
723 if argc == 0 {
724 JS_ReportErrorUTF8(cx, c"WebSocket.send() requires a message argument".as_ptr());
725 return false;
726 }
727 let msg_val = *args.get(0).ptr;
728
729 let wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
730 rooted!(&in(wrapped_cx) let this_obj = args.thisv().to_object());
731 let mut idx_val = Int32Value(-1);
732 JS_GetProperty(
733 cx,
734 this_obj.handle().into(),
735 c"_wsIdx".as_ptr(),
736 MutableHandle::<Value> {
737 _phantom_0: ::std::marker::PhantomData,
738 ptr: &mut idx_val,
739 },
740 );
741 let idx = idx_val.to_int32() as usize;
742
743 let send_result = WS_CONNECTIONS.with(|c| {
744 let mut conns = c.borrow_mut();
745 match conns.get_mut(idx) {
746 Some(e) if e.client.is_some() => {
749 let s = unsafe_jsstr_to_string(cx, NonNull::new_unchecked(msg_val.to_string()));
750 e.client.as_mut().unwrap().send_text(&s)
751 }
752 Some(e) if e.connect_slot.is_some() => {
753 Err("InvalidStateError: WebSocket is still connecting".to_string())
754 }
755 _ => Err("InvalidStateError: WebSocket is already closed".to_string()),
756 }
757 });
758
759 if let Err(e) = send_result {
760 let msg = format!("WebSocket send failed: {}", e);
761 let c_msg = ZBox::from_bytes(msg.as_bytes());
762 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
763 return false;
764 }
765 args.rval().set(UndefinedValue());
766 true
767}
768
769unsafe extern "C" fn ws_close_fn(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
770 let args = CallArgs::from_vp(vp, _argc);
771 let wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
772 rooted!(&in(wrapped_cx) let this_obj = args.thisv().to_object());
773
774 let mut idx_val = Int32Value(-1);
775 JS_GetProperty(
776 cx,
777 this_obj.handle().into(),
778 c"_wsIdx".as_ptr(),
779 MutableHandle::<Value> {
780 _phantom_0: ::std::marker::PhantomData,
781 ptr: &mut idx_val,
782 },
783 );
784 let idx = idx_val.to_int32() as usize;
785
786 WS_CONNECTIONS.with(|c| {
787 let mut conns = c.borrow_mut();
788 if let Some(e) = conns.get_mut(idx) {
789 if e.client.is_some() && !e.close_initiated {
790 if let Some(client) = &mut e.client {
794 let _ = client.close();
795 }
796 e.close_initiated = true;
797 } else if e.connect_slot.is_some() {
798 e.close_requested = true;
801 }
802 }
803 });
804
805 rooted!(&in(wrapped_cx) let closing_val = Int32Value(2));
806 JS_SetProperty(
807 cx,
808 this_obj.handle().into(),
809 c"readyState".as_ptr(),
810 closing_val.handle().into(),
811 );
812 args.rval().set(UndefinedValue());
813 true
814}
815
816#[allow(unsafe_op_in_unsafe_fn)]
817unsafe extern "C" fn websocket_constructor(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
818 let args = CallArgs::from_vp(vp, argc);
819 if argc == 0 {
820 JS_ReportErrorUTF8(cx, c"WebSocket requires a URL argument".as_ptr());
821 return false;
822 }
823 let url_val = *args.get(0).ptr;
824 if !url_val.is_string() {
825 JS_ReportErrorUTF8(cx, c"WebSocket URL must be a string".as_ptr());
826 return false;
827 }
828 let url = unsafe_jsstr_to_string(cx, NonNull::new_unchecked(url_val.to_string()));
829
830 let wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
831 rooted!(&in(wrapped_cx) let ws_obj = mozjs_sys::jsapi::JS_NewPlainObject(cx));
832 if ws_obj.get().is_null() {
833 args.rval().set(UndefinedValue());
834 return true;
835 }
836
837 {
838 let c_url = ZBox::from_bytes(url.as_bytes());
839 let js_str = JS_NewStringCopyZ(cx, c_url.as_ptr());
840 if !js_str.is_null() {
841 rooted!(&in(wrapped_cx) let v = StringValue(&*js_str));
842 JS_DefineProperty(
843 cx,
844 ws_obj.handle().into(),
845 c"url".as_ptr(),
846 v.handle().into(),
847 JSPROP_ENUMERATE as u32,
848 );
849 }
850 }
851
852 rooted!(&in(wrapped_cx) let state_val = Int32Value(0));
853 JS_DefineProperty(
854 cx,
855 ws_obj.handle().into(),
856 c"readyState".as_ptr(),
857 state_val.handle().into(),
858 JSPROP_ENUMERATE as u32,
859 );
860
861 rooted!(&in(wrapped_cx) let ba_val = Int32Value(0));
862 JS_DefineProperty(
863 cx,
864 ws_obj.handle().into(),
865 c"bufferedAmount".as_ptr(),
866 ba_val.handle().into(),
867 JSPROP_ENUMERATE as u32,
868 );
869
870 for name in &["onopen", "onmessage", "onerror", "onclose"] {
871 let c_name = ZBox::from_bytes(name.as_bytes());
872 rooted!(&in(wrapped_cx) let ud = UndefinedValue());
873 JS_DefineProperty(
874 cx,
875 ws_obj.handle().into(),
876 c_name.as_ptr(),
877 ud.handle().into(),
878 JSPROP_ENUMERATE as u32,
879 );
880 }
881
882 mozjs_sys::jsapi::JS_DefineFunction(
883 cx,
884 ws_obj.handle().into(),
885 c"send".as_ptr(),
886 Some(ws_send),
887 1,
888 JSPROP_ENUMERATE as u32,
889 );
890 mozjs_sys::jsapi::JS_DefineFunction(
891 cx,
892 ws_obj.handle().into(),
893 c"close".as_ptr(),
894 Some(ws_close_fn),
895 0,
896 JSPROP_ENUMERATE as u32,
897 );
898
899 {
902 let (scheme, rest) = if let Some(r) = url.strip_prefix("ws://") {
903 ("ws", r)
904 } else if let Some(r) = url.strip_prefix("wss://") {
905 ("wss", r)
906 } else {
907 ("ws", url.as_str())
908 };
909 let (host, _, _) = split_authority_and_path(rest, scheme);
910 if let ::std::result::Result::Err(e) = crate::permission_bridge::check_net(&host) {
911 let c_msg = ZBox::from_bytes(e.as_bytes());
912 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
913 return false;
914 }
915 }
916
917 let ws_id = WS_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
919 let ws_key = format!("ws_{}", ws_id);
920 gc_store_insert(cx, &ws_key, ws_obj.get());
921
922 let realm_global = CurrentGlobalOrNull(cx);
925 let profile = crate::fetch_api::get_fetch_stealth_profile();
926
927 let slot: Arc<Mutex<::std::option::Option<::std::result::Result<WsConn, String>>>> =
930 Arc::new(Mutex::new(None));
931 let ws_idx = WS_CONNECTIONS.with(|c| {
932 let mut conns = c.borrow_mut();
933 conns.push(WsEntry {
934 client: None,
935 connect_slot: Some(Arc::clone(&slot)),
936 close_requested: false,
937 close_initiated: false,
938 realm_global,
939 js_obj_key: ws_key.clone(),
940 });
941 conns.len() - 1
942 });
943 rooted!(&in(wrapped_cx) let idx_val = Int32Value(ws_idx as i32));
944 JS_DefineProperty(
945 cx,
946 ws_obj.handle().into(),
947 c"_wsIdx".as_ptr(),
948 idx_val.handle().into(),
949 0,
950 );
951
952 let url_owned = url.clone();
958 ::std::thread::spawn(move || {
959 let result = WsConn::connect(&url_owned, &profile);
960 if let Ok(mut guard) = slot.lock() {
961 *guard = Some(result);
962 }
963 });
966
967 args.rval().set(mozjs::jsval::ObjectValue(ws_obj.get()));
971 true
972}
973
974enum PumpAction {
983 ConnectDone(usize, ::std::result::Result<WsConn, String>),
985 TextMessage(usize, String),
987 BinaryMessage(usize, Vec<u8>),
989 Closed(usize, ::std::option::Option<String>),
993}
994
995pub fn ws_pump_all(raw_cx: *mut JSContext) {
998 loop {
999 let action = WS_CONNECTIONS.with(|c| {
1000 let mut conns = c.borrow_mut();
1001 for (idx, e) in conns.iter_mut().enumerate() {
1002 if e.connect_slot.is_some() {
1007 let taken = e
1008 .connect_slot
1009 .as_ref()
1010 .unwrap()
1011 .try_lock()
1012 .ok()
1013 .and_then(|mut guard| guard.take());
1014 if let ::std::option::Option::Some(res) = taken {
1015 e.connect_slot = ::std::option::Option::None;
1016 return ::std::option::Option::Some(PumpAction::ConnectDone(idx, res));
1017 }
1018 continue;
1019 }
1020 let ::std::option::Option::Some(client) = &mut e.client else {
1025 continue;
1026 };
1027 match client.read_message() {
1028 Ok(WsMessage::Text(t)) => {
1029 return ::std::option::Option::Some(PumpAction::TextMessage(idx, t))
1030 }
1031 Ok(WsMessage::Binary(b)) => {
1032 return ::std::option::Option::Some(PumpAction::BinaryMessage(idx, b))
1033 }
1034 Ok(WsMessage::Close) => {
1035 if !e.close_initiated {
1038 let _ = client.close();
1039 }
1040 e.client = ::std::option::Option::None;
1041 return ::std::option::Option::Some(PumpAction::Closed(idx, None));
1042 }
1043 Err(err) if err == "wouldblock" => continue,
1044 Err(err) => {
1045 let _ = client.close();
1047 e.client = ::std::option::Option::None;
1048 return ::std::option::Option::Some(PumpAction::Closed(
1049 idx,
1050 ::std::option::Option::Some(err),
1051 ));
1052 }
1053 }
1054 }
1055 ::std::option::Option::None
1056 });
1057
1058 match action {
1059 ::std::option::Option::Some(PumpAction::ConnectDone(idx, res)) => unsafe {
1060 ws_connect_dispatch(raw_cx, idx, res);
1061 },
1062 ::std::option::Option::Some(PumpAction::TextMessage(idx, text)) => unsafe {
1063 ws_message_dispatch(raw_cx, idx, ::std::option::Option::Some(text), None);
1064 },
1065 ::std::option::Option::Some(PumpAction::BinaryMessage(idx, bytes)) => unsafe {
1066 ws_message_dispatch(raw_cx, idx, None, ::std::option::Option::Some(bytes));
1067 },
1068 ::std::option::Option::Some(PumpAction::Closed(idx, err)) => unsafe {
1069 ws_closed_dispatch(raw_cx, idx, err);
1070 },
1071 ::std::option::Option::None => break,
1072 }
1073 }
1074}
1075
1076struct WsDispatchInfo {
1078 realm_global: *mut JSObject,
1079 js_obj_key: String,
1080 is_open: bool,
1081}
1082
1083fn ws_entry_info(idx: usize) -> ::std::option::Option<WsDispatchInfo> {
1084 WS_CONNECTIONS.with(|c| {
1085 c.borrow().get(idx).map(|e| WsDispatchInfo {
1086 realm_global: e.realm_global,
1087 js_obj_key: e.js_obj_key.clone(),
1088 is_open: e.client.is_some(),
1089 })
1090 })
1091}
1092
1093unsafe fn ws_set_ready_state(
1098 raw_cx: *mut JSContext,
1099 realm_global: *mut JSObject,
1100 ws_key: &str,
1101 state: i32,
1102) {
1103 if realm_global.is_null() {
1104 return;
1105 }
1106 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx));
1109 let cx_ref = &mut wrapped_cx;
1110 rooted!(&in(cx_ref) let global_root = realm_global);
1111 let mut realm = AutoRealm::new_from_handle(cx_ref, global_root.handle());
1112 let cx_ref: &mut mozjs::context::JSContext = &mut realm;
1113 let ws_obj = match gc_store_get(raw_cx, ws_key) {
1114 Some(o) => o,
1115 None => return,
1116 };
1117 rooted!(&in(cx_ref) let ws_obj_root = ws_obj);
1118 rooted!(&in(cx_ref) let state_val = Int32Value(state));
1119 JS_SetProperty(
1120 raw_cx,
1121 ws_obj_root.handle().into(),
1122 c"readyState".as_ptr(),
1123 state_val.handle().into(),
1124 );
1125}
1126
1127unsafe fn ws_connect_dispatch(
1133 raw_cx: *mut JSContext,
1134 idx: usize,
1135 res: ::std::result::Result<WsConn, String>,
1136) {
1137 let info = match ws_entry_info(idx) {
1138 Some(i) => i,
1139 None => return,
1140 };
1141 match res {
1142 Ok(mut client) => {
1143 client.set_nonblocking(true);
1145 enum Landed {
1146 Open,
1147 CloseNow,
1148 }
1149 let landed = WS_CONNECTIONS.with(|c| {
1150 let mut conns = c.borrow_mut();
1151 match conns.get_mut(idx) {
1152 ::std::option::Option::Some(e) if e.close_requested => {
1154 e.close_requested = false;
1155 Landed::CloseNow
1156 }
1157 ::std::option::Option::Some(e) => {
1158 e.client = ::std::option::Option::Some(client);
1159 Landed::Open
1160 }
1161 ::std::option::Option::None => Landed::CloseNow,
1162 }
1163 });
1164 match landed {
1165 Landed::Open => {
1166 ws_set_ready_state(raw_cx, info.realm_global, &info.js_obj_key, 1);
1167 ws_trigger_event(raw_cx, info.realm_global, &info.js_obj_key, "onopen", None);
1168 }
1169 Landed::CloseNow => {
1170 ws_set_ready_state(raw_cx, info.realm_global, &info.js_obj_key, 3);
1173 ws_trigger_event(raw_cx, info.realm_global, &info.js_obj_key, "onclose", None);
1174 gc_store_remove(raw_cx, &info.js_obj_key);
1175 }
1176 }
1177 }
1178 Err(msg) => {
1179 ws_set_ready_state(raw_cx, info.realm_global, &info.js_obj_key, 3);
1181 let data = if !info.realm_global.is_null() {
1184 let mut wrapped_cx =
1185 mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx));
1186 let cx_ref = &mut wrapped_cx;
1187 rooted!(&in(cx_ref) let global_root = info.realm_global);
1188 let mut realm = AutoRealm::new_from_handle(cx_ref, global_root.handle());
1189 let _cx_in_realm: &mut mozjs::context::JSContext = &mut realm;
1190 let c_msg = ZBox::from_bytes(msg.as_bytes());
1191 let js_str = JS_NewStringCopyZ(raw_cx, c_msg.as_ptr());
1192 if !js_str.is_null() {
1193 ::std::option::Option::Some(StringValue(&*js_str))
1194 } else {
1195 ::std::option::Option::None
1196 }
1197 } else {
1198 ::std::option::Option::None
1199 };
1200 ws_trigger_event(raw_cx, info.realm_global, &info.js_obj_key, "onerror", data);
1201 ws_trigger_event(raw_cx, info.realm_global, &info.js_obj_key, "onclose", None);
1202 gc_store_remove(raw_cx, &info.js_obj_key);
1203 }
1204 }
1205}
1206
1207unsafe fn ws_message_dispatch(
1214 raw_cx: *mut JSContext,
1215 idx: usize,
1216 text: ::std::option::Option<String>,
1217 binary: ::std::option::Option<Vec<u8>>,
1218) {
1219 let info = match ws_entry_info(idx) {
1220 Some(i) => i,
1221 None => return,
1222 };
1223 if !info.is_open {
1224 return;
1225 }
1226 if info.realm_global.is_null() {
1227 return;
1228 }
1229 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx));
1233 let cx_ref = &mut wrapped_cx;
1234 rooted!(&in(cx_ref) let global_root = info.realm_global);
1235 let mut realm = AutoRealm::new_from_handle(cx_ref, global_root.handle());
1236 let cx_ref: &mut mozjs::context::JSContext = &mut realm;
1237 let data = if let ::std::option::Option::Some(t) = text {
1238 let c_text = ZBox::from_bytes(t.as_bytes());
1239 let js_str = JS_NewStringCopyZ(raw_cx, c_text.as_ptr());
1240 if js_str.is_null() {
1241 return;
1242 }
1243 StringValue(&*js_str)
1244 } else if let ::std::option::Option::Some(bytes) = binary {
1245 let arr = mozjs_sys::jsapi::JS_NewUint8Array(raw_cx, bytes.len());
1246 if arr.is_null() {
1247 return;
1248 }
1249 rooted!(&in(cx_ref) let arr_root = arr);
1250 if !bytes.is_empty() {
1251 let mut is_shared = false;
1252 let data_ptr = mozjs_sys::jsapi::JS_GetUint8ArrayData(
1255 arr_root.get(),
1256 &mut is_shared,
1257 ::std::ptr::null(),
1258 );
1259 if !data_ptr.is_null() {
1260 ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data_ptr, bytes.len());
1261 }
1262 }
1263 ObjectValue(arr_root.get())
1264 } else {
1265 return;
1266 };
1267 ws_trigger_event(
1268 raw_cx,
1269 info.realm_global,
1270 &info.js_obj_key,
1271 "onmessage",
1272 ::std::option::Option::Some(data),
1273 );
1274}
1275
1276unsafe fn ws_closed_dispatch(
1283 raw_cx: *mut JSContext,
1284 idx: usize,
1285 err: ::std::option::Option<String>,
1286) {
1287 let info = match ws_entry_info(idx) {
1288 Some(i) => i,
1289 None => return,
1290 };
1291 ws_set_ready_state(raw_cx, info.realm_global, &info.js_obj_key, 3);
1292 if let ::std::option::Option::Some(msg) = err {
1293 let data = if !info.realm_global.is_null() {
1296 let mut wrapped_cx =
1297 mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(raw_cx));
1298 let cx_ref = &mut wrapped_cx;
1299 rooted!(&in(cx_ref) let global_root = info.realm_global);
1300 let mut realm = AutoRealm::new_from_handle(cx_ref, global_root.handle());
1301 let _cx_in_realm: &mut mozjs::context::JSContext = &mut realm;
1302 let c_msg = ZBox::from_bytes(msg.as_bytes());
1303 let js_str = JS_NewStringCopyZ(raw_cx, c_msg.as_ptr());
1304 if !js_str.is_null() {
1305 ::std::option::Option::Some(StringValue(&*js_str))
1306 } else {
1307 ::std::option::Option::None
1308 }
1309 } else {
1310 ::std::option::Option::None
1311 };
1312 ws_trigger_event(raw_cx, info.realm_global, &info.js_obj_key, "onerror", data);
1313 }
1314 ws_trigger_event(raw_cx, info.realm_global, &info.js_obj_key, "onclose", None);
1315 gc_store_remove(raw_cx, &info.js_obj_key);
1316}
1317
1318pub fn install_performance(
1321 cx: &mut mozjs::context::JSContext,
1322 global: mozjs::rust::Handle<*mut JSObject>,
1323) {
1324 unsafe {
1325 rooted!(&in(cx) let perf_obj = JS_NewPlainObject(cx));
1326 if perf_obj.get().is_null() {
1327 return;
1328 }
1329 JS_DefineFunction(
1330 cx,
1331 perf_obj.handle(),
1332 c"now".as_ptr(),
1333 Some(performance_now),
1334 0,
1335 JSPROP_ENUMERATE as u32,
1336 );
1337 JS_DefineProperty3(
1338 cx,
1339 global,
1340 c"performance".as_ptr(),
1341 perf_obj.handle(),
1342 JSPROP_ENUMERATE as u32,
1343 );
1344 }
1345}
1346
1347#[allow(unsafe_op_in_unsafe_fn)]
1348unsafe extern "C" fn performance_now(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
1349 let args = CallArgs::from_vp(vp, _argc);
1350 let now = ::std::time::SystemTime::now()
1351 .duration_since(::std::time::UNIX_EPOCH)
1352 .unwrap_or_default();
1353 let ms = now.as_secs_f64() * 1000.0;
1354 args.rval().set(mozjs::jsval::DoubleValue(ms));
1355 true
1356}
1357
1358pub fn install_web_encodings(
1361 cx: &mut mozjs::context::JSContext,
1362 global: mozjs::rust::Handle<*mut JSObject>,
1363) {
1364 unsafe {
1365 let te_fun = JS_NewFunction(
1366 cx.raw_cx(),
1367 Some(text_encoder_constructor),
1368 0,
1369 JSFUN_CONSTRUCTOR,
1370 c"TextEncoder".as_ptr(),
1371 );
1372 if !te_fun.is_null() {
1373 let te_obj = JS_GetFunctionObject(te_fun);
1374 if !te_obj.is_null() {
1375 rooted!(&in(cx) let te_obj_r = te_obj);
1376 rooted!(&in(cx) let proto = JS_NewPlainObject(cx));
1377 if !proto.get().is_null() {
1378 JS_DefineFunction(
1379 cx,
1380 proto.handle(),
1381 c"encode".as_ptr(),
1382 Some(text_encoder_encode),
1383 1,
1384 JSPROP_ENUMERATE as u32,
1385 );
1386 JS_DefineFunction(
1387 cx,
1388 proto.handle(),
1389 c"encodeInto".as_ptr(),
1390 Some(text_encoder_encode_into),
1391 2,
1392 JSPROP_ENUMERATE as u32,
1393 );
1394 JS_DefineProperty3(
1395 cx,
1396 te_obj_r.handle(),
1397 c"prototype".as_ptr(),
1398 proto.handle(),
1399 JSPROP_PERMANENT as u32,
1400 );
1401 }
1402 JS_DefineProperty3(
1403 cx,
1404 global,
1405 c"TextEncoder".as_ptr(),
1406 te_obj_r.handle(),
1407 (JSPROP_ENUMERATE | JSPROP_PERMANENT) as u32,
1408 );
1409 }
1410 }
1411
1412 let td_fun = JS_NewFunction(
1413 cx.raw_cx(),
1414 Some(text_decoder_constructor),
1415 1,
1416 JSFUN_CONSTRUCTOR,
1417 c"TextDecoder".as_ptr(),
1418 );
1419 if !td_fun.is_null() {
1420 let td_obj = JS_GetFunctionObject(td_fun);
1421 if !td_obj.is_null() {
1422 rooted!(&in(cx) let td_obj_r = td_obj);
1423 rooted!(&in(cx) let proto = JS_NewPlainObject(cx));
1424 if !proto.get().is_null() {
1425 JS_DefineFunction(
1426 cx,
1427 proto.handle(),
1428 c"decode".as_ptr(),
1429 Some(text_decoder_decode),
1430 1,
1431 JSPROP_ENUMERATE as u32,
1432 );
1433 JS_DefineProperty3(
1434 cx,
1435 td_obj_r.handle(),
1436 c"prototype".as_ptr(),
1437 proto.handle(),
1438 JSPROP_PERMANENT as u32,
1439 );
1440 }
1441 JS_DefineProperty3(
1442 cx,
1443 global,
1444 c"TextDecoder".as_ptr(),
1445 td_obj_r.handle(),
1446 (JSPROP_ENUMERATE | JSPROP_PERMANENT) as u32,
1447 );
1448 }
1449 }
1450 }
1451}
1452
1453pub fn install_atob_btoa(
1454 cx: &mut mozjs::context::JSContext,
1455 global: mozjs::rust::Handle<*mut JSObject>,
1456) {
1457 unsafe {
1458 JS_DefineFunction(
1459 cx,
1460 global,
1461 c"atob".as_ptr(),
1462 Some(atob_fn),
1463 1,
1464 JSPROP_ENUMERATE as u32,
1465 );
1466 JS_DefineFunction(
1467 cx,
1468 global,
1469 c"btoa".as_ptr(),
1470 Some(btoa_fn),
1471 1,
1472 JSPROP_ENUMERATE as u32,
1473 );
1474 }
1475}
1476
1477#[allow(unsafe_op_in_unsafe_fn)]
1478unsafe extern "C" fn atob_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1479 let args = CallArgs::from_vp(vp, argc);
1480 if argc == 0 || !(*args.get(0).ptr).is_string() {
1481 args.rval().set(UndefinedValue());
1482 return true;
1483 }
1484 let s = unsafe_jsstr_to_string(
1485 cx,
1486 ::std::ptr::NonNull::new_unchecked((*args.get(0).ptr).to_string()),
1487 );
1488 let is_html_space = |c: char| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C');
1492 let cleaned: String = s.chars().filter(|&c| !is_html_space(c)).collect();
1493 let mut input: &str = &cleaned;
1494 if input.len() % 4 == 0 {
1495 if input.ends_with("==") {
1496 input = &input[..input.len() - 2];
1497 } else if input.ends_with('=') {
1498 input = &input[..input.len() - 1];
1499 }
1500 }
1501 if input.len() % 4 == 1 || input.chars().any(|c| !c.is_ascii_alphanumeric() && c != '+' && c != '/')
1502 {
1503 JS_ReportErrorUTF8(
1504 cx,
1505 c"Failed to decode base64: InvalidCharacterError".as_ptr(),
1506 );
1507 return false;
1508 }
1509 match bun_base64::decode_alloc(input.as_bytes()) {
1510 Ok(bytes) => {
1511 let units: Vec<u16> = bytes.iter().map(|&b| b as u16).collect();
1523 let js_str = if units.is_empty() {
1524 JS_NewStringCopyN(cx, c"".as_ptr(), 0)
1525 } else {
1526 JS_NewUCStringCopyN(cx, units.as_ptr(), units.len())
1527 };
1528 if js_str.is_null() {
1529 args.rval().set(UndefinedValue());
1530 } else {
1531 args.rval().set(StringValue(&*js_str));
1532 }
1533 }
1534 Err(_) => {
1535 JS_ReportErrorUTF8(
1536 cx,
1537 c"Failed to decode base64: InvalidCharacterError".as_ptr(),
1538 );
1539 return false;
1540 }
1541 }
1542 true
1543}
1544
1545#[allow(unsafe_op_in_unsafe_fn)]
1546unsafe extern "C" fn btoa_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1547 let args = CallArgs::from_vp(vp, argc);
1548 if argc == 0 || !(*args.get(0).ptr).is_string() {
1549 args.rval().set(UndefinedValue());
1550 return true;
1551 }
1552 let s = unsafe_jsstr_to_string(
1560 cx,
1561 ::std::ptr::NonNull::new_unchecked((*args.get(0).ptr).to_string()),
1562 );
1563 if s.chars().any(|c| c > '\u{FF}') {
1564 JS_ReportErrorUTF8(
1565 cx,
1566 c"Failed to encode base64: InvalidCharacterError".as_ptr(),
1567 );
1568 return false;
1569 }
1570 let octets: Vec<u8> = s.chars().map(|c| c as u8).collect();
1571 let encoded_bytes = bun_base64::encode_alloc(&octets);
1572 let encoded = ::std::str::from_utf8(&encoded_bytes).unwrap_or("");
1573 let c_str = ZBox::from_bytes(encoded.as_bytes());
1574 let js_str = JS_NewStringCopyZ(cx, c_str.as_ptr());
1575 if js_str.is_null() {
1576 args.rval().set(UndefinedValue());
1577 } else {
1578 args.rval().set(StringValue(&*js_str));
1579 }
1580 true
1581}
1582
1583pub fn install_queue_microtask(
1584 cx: &mut mozjs::context::JSContext,
1585 global: mozjs::rust::Handle<*mut JSObject>,
1586) {
1587 unsafe {
1588 JS_DefineFunction(
1589 cx,
1590 global,
1591 c"queueMicrotask".as_ptr(),
1592 Some(queue_microtask_fn),
1593 1,
1594 JSPROP_ENUMERATE as u32,
1595 );
1596 }
1597}
1598
1599#[allow(unsafe_op_in_unsafe_fn)]
1600unsafe extern "C" fn text_encoder_constructor(
1601 cx: *mut JSContext,
1602 argc: u32,
1603 vp: *mut JSVal,
1604) -> bool {
1605 let args = CallArgs::from_vp(vp, argc);
1606 let obj = mozjs_sys::jsapi::JS_NewPlainObject(cx);
1607 if obj.is_null() {
1608 args.rval().set(UndefinedValue());
1609 return true;
1610 }
1611 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
1612 rooted!(&in(wrapped_cx) let obj_r = obj);
1613 let encoding_str = JS_NewStringCopyZ(cx, c"utf-8".as_ptr());
1614 if !encoding_str.is_null() {
1615 let val = StringValue(&*encoding_str);
1616 rooted!(&in(wrapped_cx) let val_root = val);
1617 JS_DefineProperty(
1618 cx,
1619 obj_r.handle().into(),
1620 c"encoding".as_ptr(),
1621 val_root.handle().into(),
1622 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
1623 );
1624 }
1625
1626 JS_DefineFunction(
1627 &mut wrapped_cx,
1628 obj_r.handle(),
1629 c"encode".as_ptr(),
1630 Some(text_encoder_encode),
1631 1,
1632 JSPROP_ENUMERATE as u32,
1633 );
1634 JS_DefineFunction(
1635 &mut wrapped_cx,
1636 obj_r.handle(),
1637 c"encodeInto".as_ptr(),
1638 Some(text_encoder_encode_into),
1639 2,
1640 JSPROP_ENUMERATE as u32,
1641 );
1642
1643 args.rval().set(ObjectValue(obj));
1644 true
1645}
1646
1647#[allow(unsafe_op_in_unsafe_fn)]
1648unsafe extern "C" fn text_encoder_encode(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1649 let args = CallArgs::from_vp(vp, argc);
1650 let input = if argc > 0 {
1651 let v = *args.get(0).ptr;
1652 if v.is_string() {
1653 crate::js_to_rust_string(cx, v)
1654 } else {
1655 String::new()
1656 }
1657 } else {
1658 String::new()
1659 };
1660
1661 let bytes = input.as_bytes();
1662 let wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
1663
1664 let u8_obj = mozjs_sys::jsapi::JS_NewUint8Array(cx, bytes.len());
1669 if u8_obj.is_null() {
1670 args.rval().set(UndefinedValue());
1671 return true;
1672 }
1673 if !bytes.is_empty() {
1674 rooted!(&in(wrapped_cx) let arr = u8_obj);
1675 let mut is_shared = false;
1676 let data_ptr =
1677 mozjs_sys::jsapi::JS_GetUint8ArrayData(arr.get(), &mut is_shared, ::std::ptr::null());
1678 if !data_ptr.is_null() {
1679 ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data_ptr, bytes.len());
1680 }
1681 args.rval().set(ObjectValue(arr.get()));
1682 } else {
1683 args.rval().set(ObjectValue(u8_obj));
1684 }
1685 true
1686}
1687
1688#[allow(unsafe_op_in_unsafe_fn)]
1689unsafe extern "C" fn text_encoder_encode_into(
1690 _cx: *mut JSContext,
1691 _argc: u32,
1692 vp: *mut JSVal,
1693) -> bool {
1694 let args = CallArgs::from_vp(vp, _argc);
1695 args.rval().set(UndefinedValue());
1696 true
1697}
1698
1699#[allow(unsafe_op_in_unsafe_fn)]
1700unsafe extern "C" fn text_decoder_constructor(
1701 cx: *mut JSContext,
1702 argc: u32,
1703 vp: *mut JSVal,
1704) -> bool {
1705 let args = CallArgs::from_vp(vp, argc);
1706 let obj = mozjs_sys::jsapi::JS_NewPlainObject(cx);
1707 if obj.is_null() {
1708 args.rval().set(UndefinedValue());
1709 return true;
1710 }
1711 let encoding = if argc > 0 {
1712 let v = *args.get(0).ptr;
1713 if v.is_string() {
1714 crate::js_to_rust_string(cx, v)
1715 } else {
1716 "utf-8".to_string()
1717 }
1718 } else {
1719 "utf-8".to_string()
1720 };
1721 let encoding_lower = encoding.to_lowercase();
1722 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
1723 rooted!(&in(wrapped_cx) let obj_r = obj);
1724 let encoding_str = JS_NewStringCopyZ(cx, ZBox::from_bytes(encoding_lower.as_bytes()).as_ptr());
1725 if !encoding_str.is_null() {
1726 let val = StringValue(&*encoding_str);
1727 rooted!(&in(wrapped_cx) let val_root = val);
1728 JS_DefineProperty(
1729 cx,
1730 obj_r.handle().into(),
1731 c"encoding".as_ptr(),
1732 val_root.handle().into(),
1733 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
1734 );
1735 }
1736 rooted!(&in(wrapped_cx) let fatal_val = BooleanValue(false));
1737 JS_DefineProperty(
1738 cx,
1739 obj_r.handle().into(),
1740 c"fatal".as_ptr(),
1741 fatal_val.handle().into(),
1742 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
1743 );
1744 rooted!(&in(wrapped_cx) let bom_val = BooleanValue(false));
1745 JS_DefineProperty(
1746 cx,
1747 obj_r.handle().into(),
1748 c"ignoreBOM".as_ptr(),
1749 bom_val.handle().into(),
1750 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
1751 );
1752
1753 JS_DefineFunction(
1754 &mut wrapped_cx,
1755 obj_r.handle(),
1756 c"decode".as_ptr(),
1757 Some(text_decoder_decode),
1758 1,
1759 JSPROP_ENUMERATE as u32,
1760 );
1761
1762 args.rval().set(ObjectValue(obj));
1763 true
1764}
1765
1766#[allow(unsafe_op_in_unsafe_fn)]
1767unsafe extern "C" fn text_decoder_decode(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1768 let args = CallArgs::from_vp(vp, argc);
1769 if argc == 0 {
1770 let empty = JS_NewStringCopyZ(cx, c"".as_ptr());
1771 args.rval().set(if empty.is_null() {
1772 UndefinedValue()
1773 } else {
1774 StringValue(&*empty)
1775 });
1776 return true;
1777 }
1778
1779 let input = *args.get(0).ptr;
1780 if !input.is_object() {
1784 mozjs::error::throw_type_error(
1785 cx,
1786 c"The provided value is not an instance of ArrayBuffer or ArrayBufferView".as_ref(),
1787 );
1788 return false;
1789 }
1790
1791 let bytes: Vec<u8> = {
1798 let wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
1799 rooted!(&in(wrapped_cx) let obj = input.to_object());
1800 let mut len: usize = 0;
1801 let mut is_shared = false;
1802 let mut data: *mut u8 = ::std::ptr::null_mut();
1803 let unwrapped =
1804 mozjs_sys::jsapi::JS_GetObjectAsArrayBufferView(obj.get(), &mut len, &mut is_shared, &mut data);
1805 if !unwrapped.is_null() && !data.is_null() {
1806 ::std::slice::from_raw_parts(data, len).to_vec()
1809 } else if !unwrapped.is_null() {
1810 Vec::new() } else {
1812 let ab_unwrapped =
1813 mozjs_sys::jsapi::JS::GetObjectAsArrayBuffer(obj.get(), &mut len, &mut data);
1814 if !ab_unwrapped.is_null() && !data.is_null() {
1815 ::std::slice::from_raw_parts(data, len).to_vec()
1816 } else if !ab_unwrapped.is_null() {
1817 Vec::new() } else {
1819 mozjs::error::throw_type_error(
1820 cx,
1821 c"The provided value is not an instance of ArrayBuffer or ArrayBufferView"
1822 .as_ref(),
1823 );
1824 return false;
1825 }
1826 }
1827 };
1828
1829 let decoded = String::from_utf8_lossy(&bytes).into_owned();
1832
1833 let utf16: Vec<u16> = decoded.encode_utf16().collect();
1834 let js_str = JS_NewUCStringCopyN(cx, utf16.as_ptr(), utf16.len());
1835 args.rval().set(if js_str.is_null() {
1836 UndefinedValue()
1837 } else {
1838 StringValue(&*js_str)
1839 });
1840 true
1841}
1842
1843#[allow(unsafe_op_in_unsafe_fn)]
1844unsafe extern "C" fn queue_microtask_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1845 let args = CallArgs::from_vp(vp, argc);
1846 if argc == 0 || !(*args.get(0).ptr).is_object() {
1847 return true;
1848 }
1849 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
1850 let cx = &mut wrapped_cx;
1851
1852 rooted!(&in(cx) let callback = (*args.get(0).ptr).to_object());
1853 rooted!(&in(cx) let undef_val = UndefinedValue());
1854 let resolved = CallOriginalPromiseResolve(cx, undef_val.handle());
1855 if resolved.is_null() {
1856 args.rval().set(UndefinedValue());
1857 return true;
1858 }
1859 rooted!(&in(cx) let promise = resolved);
1860 rooted!(&in(cx) let null_reject = ::std::ptr::null_mut::<JSObject>());
1861 CallOriginalPromiseThen(
1862 cx,
1863 promise.handle(),
1864 callback.handle(),
1865 null_reject.handle(),
1866 );
1867 args.rval().set(UndefinedValue());
1868 true
1869}
1870
1871pub fn install_crypto_subtle(cx: &mut mozjs::context::JSContext, global: mozjs::rust::Handle<*mut JSObject>) {
1884 unsafe {
1885 rooted!(&in(cx) let global_root = global.get());
1886 let mut crypto_val = UndefinedValue();
1887 let crypto_found = JS_GetProperty(
1894 cx.raw_cx(),
1895 global_root.handle().into(),
1896 c"crypto".as_ptr(),
1897 MutableHandle::<Value> {
1898 _phantom_0: ::std::marker::PhantomData,
1899 ptr: &mut crypto_val,
1900 },
1901 );
1902 if !crypto_found {
1903 JS_ClearPendingException(cx.raw_cx());
1904 return;
1905 }
1906 if !crypto_val.is_object() {
1907 return;
1908 }
1909 rooted!(&in(cx) let crypto_obj = crypto_val.to_object());
1910 let mut subtle_val = UndefinedValue();
1911 let subtle_found = JS_GetProperty(
1913 cx.raw_cx(),
1914 crypto_obj.handle().into(),
1915 c"subtle".as_ptr(),
1916 MutableHandle::<Value> {
1917 _phantom_0: ::std::marker::PhantomData,
1918 ptr: &mut subtle_val,
1919 },
1920 );
1921 if !subtle_found {
1922 JS_ClearPendingException(cx.raw_cx());
1923 return;
1924 }
1925 if !subtle_val.is_object() {
1926 return;
1927 }
1928 rooted!(&in(cx) let subtle = subtle_val.to_object());
1929 for (name, op, nargs) in [
1930 ("encrypt", subtle_encrypt as unsafe extern "C" fn(*mut JSContext, u32, *mut JSVal) -> bool, 3),
1931 ("decrypt", subtle_decrypt, 3),
1932 ("generateKey", subtle_generate_key, 3),
1933 ("importKey", subtle_import_key, 5),
1934 ("sign", subtle_sign, 3),
1935 ("verify", subtle_verify, 4),
1936 ("digest", subtle_digest, 2),
1941 ] {
1942 JS_DefineFunction(
1943 cx,
1944 subtle.handle(),
1945 ZBox::from_bytes(name.as_bytes()).as_ptr(),
1946 Some(op),
1947 nargs,
1948 JSPROP_ENUMERATE as u32,
1949 );
1950 }
1951 }
1952}
1953
1954fn ct_eq(a: &[u8], b: &[u8]) -> bool {
1958 if a.len() != b.len() {
1959 return false;
1960 }
1961 let mut diff: u8 = 0;
1962 for (x, y) in a.iter().zip(b.iter()) {
1963 diff |= x ^ y;
1964 }
1965 diff == 0
1966}
1967
1968#[allow(unsafe_op_in_unsafe_fn)]
1972unsafe fn subtle_bytes(cx: *mut JSContext, val: JSVal) -> Vec<u8> {
1973 if !val.is_object() {
1974 return Vec::new();
1975 }
1976 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
1977 let cx_ref = &mut wrapped_cx;
1978 rooted!(&in(cx_ref) let obj_root = val.to_object());
1979 let mut length: usize = 0;
1980 let mut is_shared = false;
1981 let mut data_ptr: *mut u8 = ::std::ptr::null_mut();
1982 let u8_unwrapped = mozjs_sys::jsapi::JS_GetObjectAsUint8Array(
1983 obj_root.get(),
1984 &mut length,
1985 &mut is_shared,
1986 &mut data_ptr,
1987 );
1988 if !u8_unwrapped.is_null() && !data_ptr.is_null() && length > 0 {
1989 return ::std::slice::from_raw_parts(data_ptr, length).to_vec();
1990 }
1991 let mut view_length: usize = 0;
1992 let mut view_shared = false;
1993 let mut view_data: *mut u8 = ::std::ptr::null_mut();
1994 let view_unwrapped = mozjs_sys::jsapi::JS_GetObjectAsArrayBufferView(
1995 obj_root.get(),
1996 &mut view_length,
1997 &mut view_shared,
1998 &mut view_data,
1999 );
2000 if !view_unwrapped.is_null() && !view_data.is_null() && view_length > 0 {
2001 return ::std::slice::from_raw_parts(view_data, view_length).to_vec();
2002 }
2003 let ab_len = mozjs_sys::jsapi::JS::GetArrayBufferByteLength(obj_root.get());
2006 if ab_len > 0 {
2007 let mut ab_shared = false;
2008 let ab_data = mozjs_sys::jsapi::JS::GetArrayBufferData(
2009 obj_root.get(),
2010 &mut ab_shared,
2011 ::std::ptr::null(),
2012 );
2013 if !ab_data.is_null() {
2014 return ::std::slice::from_raw_parts(ab_data, ab_len).to_vec();
2015 }
2016 }
2017 Vec::new()
2018}
2019
2020#[allow(unsafe_op_in_unsafe_fn)]
2022unsafe fn subtle_str_prop(cx: *mut JSContext, obj: *mut JSObject, name: &str) -> Option<String> {
2023 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2024 let cx_ref = &mut wrapped_cx;
2025 rooted!(&in(cx_ref) let obj_r = obj);
2026 let c_name = ZBox::from_bytes(name.as_bytes());
2027 let mut v = UndefinedValue();
2028 bao_stealth::engine_props::get_property_clearing(cx, obj_r.handle().into(), c_name.as_cstr(), &mut v);
2032 if v.is_string() {
2033 Some(crate::js_to_rust_string(cx, v))
2034 } else {
2035 None
2036 }
2037}
2038
2039#[allow(unsafe_op_in_unsafe_fn)]
2041unsafe fn subtle_u32_prop(cx: *mut JSContext, obj: *mut JSObject, name: &str) -> Option<u32> {
2042 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2043 let cx_ref = &mut wrapped_cx;
2044 rooted!(&in(cx_ref) let obj_r = obj);
2045 let c_name = ZBox::from_bytes(name.as_bytes());
2046 let mut v = UndefinedValue();
2047 bao_stealth::engine_props::get_property_clearing(cx, obj_r.handle().into(), c_name.as_cstr(), &mut v);
2049 if v.is_int32() && v.to_int32() >= 0 {
2050 Some(v.to_int32() as u32)
2051 } else if v.is_double() && v.to_double() >= 0.0 {
2052 Some(v.to_double() as u32)
2053 } else {
2054 None
2055 }
2056}
2057
2058#[allow(unsafe_op_in_unsafe_fn)]
2060unsafe fn subtle_bytes_prop(cx: *mut JSContext, obj: *mut JSObject, name: &str) -> Option<Vec<u8>> {
2061 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2062 let cx_ref = &mut wrapped_cx;
2063 rooted!(&in(cx_ref) let obj_r = obj);
2064 let c_name = ZBox::from_bytes(name.as_bytes());
2065 let mut v = UndefinedValue();
2066 bao_stealth::engine_props::get_property_clearing(cx, obj_r.handle().into(), c_name.as_cstr(), &mut v);
2068 if v.is_object() {
2069 Some(subtle_bytes(cx, v))
2070 } else {
2071 None
2072 }
2073}
2074
2075#[allow(unsafe_op_in_unsafe_fn)]
2077unsafe fn bytes_to_arraybuffer_val(cx: *mut JSContext, bytes: &[u8]) -> JSVal {
2078 if bytes.is_empty() {
2081 let ab = mozjs_sys::jsapi::JS::NewArrayBuffer(cx, 0);
2082 return if ab.is_null() { UndefinedValue() } else { ObjectValue(ab) };
2083 }
2084 let buf = libc::malloc(bytes.len()) as *mut u8;
2085 if buf.is_null() {
2086 return UndefinedValue();
2087 }
2088 ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, bytes.len());
2089 let ab = mozjs_sys::jsapi::glue::NewArrayBufferWithContents(cx, bytes.len(), buf as *mut core::ffi::c_void);
2090 if ab.is_null() {
2091 libc::free(buf as *mut core::ffi::c_void);
2092 return UndefinedValue();
2093 }
2094 ObjectValue(ab)
2095}
2096
2097#[allow(unsafe_op_in_unsafe_fn)]
2101unsafe fn make_crypto_key(
2102 cx: *mut JSContext,
2103 ktype: &str,
2104 alg_name: &str,
2105 extra_alg: &[(&str, String)],
2106 extractable: bool,
2107 usages: *mut JSObject,
2108 material_slot: &str,
2109 material: &[u8],
2110) -> *mut JSObject {
2111 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2112 let cx_ref = &mut wrapped_cx;
2113 rooted!(&in(cx_ref) let key = JS_NewPlainObject(cx_ref));
2114 if key.get().is_null() {
2115 return ::std::ptr::null_mut();
2116 }
2117 let kh = key.handle().into();
2118
2119 let c_t = ZBox::from_bytes(ktype.as_bytes());
2120 let t_js = JS_NewStringCopyZ(cx, c_t.as_ptr());
2121 if !t_js.is_null() {
2122 rooted!(&in(cx_ref) let tv = StringValue(&*t_js));
2123 JS_DefineProperty(cx, kh, c"type".as_ptr(), tv.handle().into(), (JSPROP_ENUMERATE | JSPROP_READONLY) as u32);
2124 }
2125 rooted!(&in(cx_ref) let ev = BooleanValue(extractable));
2126 JS_DefineProperty(cx, kh, c"extractable".as_ptr(), ev.handle().into(), (JSPROP_ENUMERATE | JSPROP_READONLY) as u32);
2127 rooted!(&in(cx_ref) let uv = ObjectValue(usages));
2128 JS_DefineProperty(cx, kh, c"usages".as_ptr(), uv.handle().into(), (JSPROP_ENUMERATE | JSPROP_READONLY) as u32);
2129
2130 rooted!(&in(cx_ref) let alg_obj = JS_NewPlainObject(cx_ref));
2132 if !alg_obj.get().is_null() {
2133 let ah = alg_obj.handle().into();
2134 let c_n = ZBox::from_bytes(alg_name.as_bytes());
2135 let n_js = JS_NewStringCopyZ(cx, c_n.as_ptr());
2136 if !n_js.is_null() {
2137 rooted!(&in(cx_ref) let nv = StringValue(&*n_js));
2138 JS_DefineProperty(cx, ah, c"name".as_ptr(), nv.handle().into(), (JSPROP_ENUMERATE | JSPROP_READONLY) as u32);
2139 }
2140 for (k, v) in extra_alg {
2141 if *k == "length" {
2142 if let Ok(n) = v.parse::<i32>() {
2144 rooted!(&in(cx_ref) let nv = Int32Value(n));
2145 JS_DefineProperty(
2146 cx,
2147 ah,
2148 ZBox::from_bytes(k.as_bytes()).as_ptr(),
2149 nv.handle().into(),
2150 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
2151 );
2152 }
2153 continue;
2154 }
2155 let c_v = ZBox::from_bytes(v.as_bytes());
2156 let v_js = JS_NewStringCopyZ(cx, c_v.as_ptr());
2157 if !v_js.is_null() {
2158 rooted!(&in(cx_ref) let vv = StringValue(&*v_js));
2159 JS_DefineProperty(
2160 cx,
2161 ah,
2162 ZBox::from_bytes(k.as_bytes()).as_ptr(),
2163 vv.handle().into(),
2164 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
2165 );
2166 }
2167 }
2168 rooted!(&in(cx_ref) let av = ObjectValue(alg_obj.get()));
2169 JS_DefineProperty(cx, kh, c"algorithm".as_ptr(), av.handle().into(), (JSPROP_ENUMERATE | JSPROP_READONLY) as u32);
2170 }
2171
2172 let stash = |slot: &str, bytes: &[u8]| {
2174 let u8v = mozjs_sys::jsapi::JS_NewUint8Array(cx, bytes.len());
2175 if u8v.is_null() {
2176 return;
2177 }
2178 let mut len: usize = 0;
2179 let mut shared = false;
2180 let mut data: *mut u8 = ::std::ptr::null_mut();
2181 let unwrapped = mozjs_sys::jsapi::JS_GetObjectAsUint8Array(u8v, &mut len, &mut shared, &mut data);
2182 if unwrapped.is_null() || data.is_null() || len < bytes.len() {
2183 return;
2184 }
2185 ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len());
2186 rooted!(&in(cx_ref) let mv = ObjectValue(u8v));
2187 JS_DefineProperty(
2188 cx,
2189 kh,
2190 ZBox::from_bytes(slot.as_bytes()).as_ptr(),
2191 mv.handle().into(),
2192 0,
2193 );
2194 };
2195 stash(material_slot, material);
2196 key.get()
2197}
2198
2199#[allow(unsafe_op_in_unsafe_fn)]
2203unsafe fn make_crypto_key_pair(
2204 cx: *mut JSContext,
2205 alg_name: &str,
2206 extra_alg: &[(&str, String)],
2207 extractable: bool,
2208 usages: *mut JSObject,
2209 private_der: &[u8],
2210 public_der: &[u8],
2211) -> *mut JSObject {
2212 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2213 let cx_ref = &mut wrapped_cx;
2214 rooted!(&in(cx_ref) let pair = JS_NewPlainObject(cx_ref));
2215 if pair.get().is_null() {
2216 return ::std::ptr::null_mut();
2217 }
2218 let ph = pair.handle().into();
2219 let private_key = make_crypto_key(cx, "private", alg_name, extra_alg, extractable, usages, "_der", private_der);
2220 let public_key = make_crypto_key(cx, "public", alg_name, extra_alg, extractable, usages, "_der", public_der);
2221 if private_key.is_null() || public_key.is_null() {
2222 return ::std::ptr::null_mut();
2223 }
2224 rooted!(&in(cx_ref) let pv = ObjectValue(private_key));
2225 JS_DefineProperty(cx, ph, c"privateKey".as_ptr(), pv.handle().into(), (JSPROP_ENUMERATE | JSPROP_READONLY) as u32);
2226 rooted!(&in(cx_ref) let uv = ObjectValue(public_key));
2227 JS_DefineProperty(cx, ph, c"publicKey".as_ptr(), uv.handle().into(), (JSPROP_ENUMERATE | JSPROP_READONLY) as u32);
2228 pair.get()
2229}
2230
2231unsafe fn subtle_arg_object(val: JSVal) -> Option<*mut JSObject> {
2235 if val.is_object() {
2236 Some(val.to_object())
2237 } else {
2238 None
2239 }
2240}
2241
2242#[allow(unsafe_op_in_unsafe_fn)]
2244unsafe fn key_material(cx: *mut JSContext, key: *mut JSObject, slot: &str) -> Option<Vec<u8>> {
2245 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2246 let cx_ref = &mut wrapped_cx;
2247 rooted!(&in(cx_ref) let key_r = key);
2248 let mut v = UndefinedValue();
2249 JS_GetProperty(
2250 cx,
2251 key_r.handle().into(),
2252 ZBox::from_bytes(slot.as_bytes()).as_ptr(),
2253 MutableHandle::<Value> {
2254 _phantom_0: ::std::marker::PhantomData,
2255 ptr: &mut v,
2256 },
2257 );
2258 if v.is_object() {
2259 Some(subtle_bytes(cx, v))
2260 } else {
2261 None
2262 }
2263}
2264
2265macro_rules! subtle_promise {
2268 ($cx:expr, $cx_ref:expr, $args:expr) => {{
2269 rooted!(&in($cx_ref) let null_global = ::std::ptr::null_mut::<JSObject>());
2270 let promise = mozjs_sys::jsapi::JS::NewPromiseObject($cx, null_global.handle().into());
2271 if promise.is_null() {
2272 $args.rval().set(UndefinedValue());
2273 ::std::ptr::null_mut::<JSObject>()
2274 } else {
2275 $args.rval().set(ObjectValue(promise));
2276 promise
2277 }
2278 }};
2279}
2280
2281#[allow(unsafe_op_in_unsafe_fn)]
2284unsafe extern "C" fn subtle_digest(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
2285 let args = CallArgs::from_vp(vp, argc);
2286 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2287 let cx_ref = &mut wrapped_cx;
2288 let promise = subtle_promise!(cx, cx_ref, &args);
2289 if promise.is_null() {
2290 return true;
2291 }
2292 rooted!(&in(cx_ref) let promise_root = promise);
2293
2294 let result: ::std::result::Result<Vec<u8>, String> = (|| {
2295 use bun_sha_hmac::sha::hashers;
2296 if argc < 2 {
2297 return Err("digest(algorithm, data) requires 2 arguments".to_string());
2298 }
2299 let alg_val = *args.get(0).ptr;
2300 let name = if alg_val.is_string() {
2301 crate::js_to_rust_string(cx, alg_val).to_uppercase()
2302 } else if alg_val.is_object() {
2303 let obj = alg_val.to_object();
2304 subtle_str_prop(cx, obj, "name").unwrap_or_default().to_uppercase()
2305 } else {
2306 return Err("digest algorithm must be a string or {name}".to_string());
2307 };
2308 let data = subtle_bytes(cx, *args.get(1).ptr);
2309 match name.as_str() {
2310 "SHA-1" | "SHA1" => {
2311 let mut out = [0u8; hashers::SHA1::DIGEST];
2312 hashers::SHA1::hash(&data, &mut out);
2313 Ok(out.to_vec())
2314 }
2315 "SHA-256" | "SHA256" => {
2316 let mut out = [0u8; hashers::SHA256::DIGEST];
2317 hashers::SHA256::hash(&data, &mut out);
2318 Ok(out.to_vec())
2319 }
2320 "SHA-384" | "SHA384" => {
2321 let mut out = [0u8; hashers::SHA384::DIGEST];
2322 hashers::SHA384::hash(&data, &mut out);
2323 Ok(out.to_vec())
2324 }
2325 "SHA-512" | "SHA512" => {
2326 let mut out = [0u8; hashers::SHA512::DIGEST];
2327 hashers::SHA512::hash(&data, &mut out);
2328 Ok(out.to_vec())
2329 }
2330 other => Err(format!("subtle.digest: unsupported algorithm {}", other)),
2331 }
2332 })();
2333
2334 match result {
2335 Ok(bytes) => {
2336 let v = bytes_to_arraybuffer_val(cx, &bytes);
2337 subtle_resolve(cx, promise_root.get(), v);
2338 }
2339 Err(msg) => subtle_reject(cx, promise_root.get(), &format!("subtle.digest: {}", msg)),
2340 }
2341 true
2342}
2343
2344#[allow(unsafe_op_in_unsafe_fn)]
2346unsafe fn subtle_reject(cx: *mut JSContext, promise: *mut JSObject, msg: &str) {
2347 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2348 let cx_ref = &mut wrapped_cx;
2349 rooted!(&in(cx_ref) let promise_root = promise);
2350 let global = CurrentGlobalOrNull(cx);
2351 let err_obj = if !global.is_null() {
2352 rooted!(&in(cx_ref) let global_root = global);
2353 let c_msg = ZBox::from_bytes(msg.as_bytes());
2354 let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
2355 let mut err = UndefinedValue();
2356 if !msg_js.is_null() {
2357 rooted!(&in(cx_ref) let mv = StringValue(&*msg_js));
2358 let elems = [*mv.handle()];
2359 let call_args = HandleValueArray {
2360 length_: 1,
2361 elements_: elems.as_ptr(),
2362 };
2363 let mut type_error_fn = UndefinedValue();
2364 JS_GetProperty(
2365 cx,
2366 global_root.handle().into(),
2367 c"TypeError".as_ptr(),
2368 MutableHandle::<Value> {
2369 _phantom_0: ::std::marker::PhantomData,
2370 ptr: &mut type_error_fn,
2371 },
2372 );
2373 if type_error_fn.is_object() {
2374 rooted!(&in(cx_ref) let fn_val = type_error_fn);
2375 rooted!(&in(cx_ref) let undef_this = ::std::ptr::null_mut::<JSObject>());
2376 if JS_CallFunctionValue(
2377 cx,
2378 undef_this.handle().into(),
2379 fn_val.handle().into(),
2380 &call_args,
2381 MutableHandle::<Value> {
2382 _phantom_0: ::std::marker::PhantomData,
2383 ptr: &mut err,
2384 },
2385 ) && err.is_object() {
2386 err.to_object()
2387 } else {
2388 JS_ClearPendingException(cx);
2389 ::std::ptr::null_mut()
2390 }
2391 } else {
2392 ::std::ptr::null_mut()
2393 }
2394 } else {
2395 ::std::ptr::null_mut()
2396 }
2397 } else {
2398 ::std::ptr::null_mut()
2399 };
2400 if err_obj.is_null() {
2401 rooted!(&in(cx_ref) let obj = JS_NewPlainObject(cx_ref));
2404 if !obj.get().is_null() {
2405 let c_msg = ZBox::from_bytes(msg.as_bytes());
2406 let m_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
2407 if !m_js.is_null() {
2408 rooted!(&in(cx_ref) let mv = StringValue(&*m_js));
2409 JS_DefineProperty(cx, obj.handle().into(), c"message".as_ptr(), mv.handle().into(), JSPROP_ENUMERATE as u32);
2410 }
2411 let c_n = ZBox::from_bytes("TypeError".as_bytes());
2412 let n_js = JS_NewStringCopyZ(cx, c_n.as_ptr());
2413 if !n_js.is_null() {
2414 rooted!(&in(cx_ref) let nv = StringValue(&*n_js));
2415 JS_DefineProperty(cx, obj.handle().into(), c"name".as_ptr(), nv.handle().into(), JSPROP_ENUMERATE as u32);
2416 }
2417 rooted!(&in(cx_ref) let ev = ObjectValue(obj.get()));
2418 mozjs_sys::jsapi::JS::RejectPromise(cx, promise_root.handle().into(), ev.handle().into());
2419 return;
2420 }
2421 return;
2422 }
2423 rooted!(&in(cx_ref) let ev = ObjectValue(err_obj));
2424 mozjs_sys::jsapi::JS::RejectPromise(cx, promise_root.handle().into(), ev.handle().into());
2425}
2426
2427#[allow(unsafe_op_in_unsafe_fn)]
2429unsafe fn subtle_resolve(cx: *mut JSContext, promise: *mut JSObject, val: JSVal) {
2430 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2431 let cx_ref = &mut wrapped_cx;
2432 rooted!(&in(cx_ref) let promise_root = promise);
2433 rooted!(&in(cx_ref) let v = val);
2434 mozjs_sys::jsapi::JS::ResolvePromise(cx, promise_root.handle().into(), v.handle().into());
2435}
2436
2437#[allow(unsafe_op_in_unsafe_fn)]
2439unsafe fn subtle_algo_name(cx: *mut JSContext, val: JSVal) -> ::std::result::Result<(String, *mut JSObject), String> {
2440 if val.is_string() {
2446 let name = crate::js_to_rust_string(cx, val);
2447 if name.is_empty() {
2448 return Err("algorithm name is required".to_string());
2449 }
2450 let mut wrapped_cx =
2451 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
2452 let cx_ref = &mut wrapped_cx;
2453 let obj = JS_NewPlainObject(cx_ref);
2454 if obj.is_null() {
2455 return Err("algorithm object allocation failed".to_string());
2456 }
2457 rooted!(&in(cx_ref) let obj_root = obj);
2458 let js_name = JS_NewStringCopyN(
2459 cx,
2460 name.as_ptr() as *const ::std::os::raw::c_char,
2461 name.len(),
2462 );
2463 if js_name.is_null() {
2464 return Err("algorithm name string allocation failed".to_string());
2465 }
2466 rooted!(&in(cx_ref) let name_val = mozjs::jsval::StringValue(&*js_name));
2467 JS_DefineProperty(
2468 cx,
2469 obj_root.handle().into(),
2470 c"name".as_ptr(),
2471 name_val.handle().into(),
2472 JSPROP_ENUMERATE as u32,
2473 );
2474 return Ok((name, obj_root.get()));
2475 }
2476 if !val.is_object() {
2477 return Err("algorithm identifier must be a string or an object".to_string());
2478 }
2479 let obj = val.to_object();
2480 let name = subtle_str_prop(cx, obj, "name")
2481 .ok_or_else(|| "algorithm.name is required".to_string())?;
2482 Ok((name, obj))
2483}
2484
2485fn aes_cipher_algo(name: &str, key_len: usize) -> ::std::result::Result<bao_crypto::cipher::CipherAlgorithm, String> {
2487 let bits = key_len * 8;
2488 let qualified = match name {
2489 "AES-GCM" => match bits {
2490 128 => "aes-128-gcm",
2491 192 => "aes-192-gcm",
2492 256 => "aes-256-gcm",
2493 _ => return Err(format!("invalid AES-GCM key length: {} bits", bits)),
2494 },
2495 "AES-CBC" => match bits {
2496 128 => "aes-128-cbc",
2497 192 => "aes-192-cbc",
2498 256 => "aes-256-cbc",
2499 _ => return Err(format!("invalid AES-CBC key length: {} bits", bits)),
2500 },
2501 other => return Err(format!("unsupported cipher algorithm: {}", other)),
2502 };
2503 bao_crypto::cipher::parse_algorithm(qualified).map_err(|e| e.to_string())
2504}
2505
2506#[allow(unsafe_op_in_unsafe_fn)]
2509unsafe extern "C" fn subtle_encrypt(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
2510 let args = CallArgs::from_vp(vp, argc);
2511 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2512 let cx_ref = &mut wrapped_cx;
2513 let promise = subtle_promise!(cx, cx_ref, &args);
2514 if promise.is_null() {
2515 return true;
2516 }
2517 rooted!(&in(cx_ref) let promise_root = promise);
2518
2519 let result: ::std::result::Result<Vec<u8>, String> = (|| {
2520 if argc < 3 {
2521 return Err("encrypt(algorithm, key, data) requires 3 arguments".to_string());
2522 }
2523 let (name, alg_obj) = subtle_algo_name(cx, *args.get(0).ptr)?;
2524 let key_obj = subtle_arg_object(*args.get(1).ptr)
2525 .ok_or("encrypt: key must be a CryptoKey".to_string())?;
2526 let data = subtle_bytes(cx, *args.get(2).ptr);
2527 let raw = key_material(cx, key_obj, "_raw")
2528 .ok_or("encrypt: not a symmetric CryptoKey".to_string())?;
2529
2530 match name.as_str() {
2531 "AES-GCM" => {
2532 let iv = subtle_bytes_prop(cx, alg_obj, "iv")
2533 .ok_or("AES-GCM requires an iv".to_string())?;
2534 let aad = subtle_bytes_prop(cx, alg_obj, "additionalData");
2535 if let Some(tl) = subtle_u32_prop(cx, alg_obj, "tagLength") {
2536 if tl != 128 {
2537 return Err(format!("AES-GCM tagLength {} is not supported (128 only)", tl));
2538 }
2539 }
2540 let algo = aes_cipher_algo("AES-GCM", raw.len())?;
2541 let out = bao_crypto::cipher::encrypt(algo, &raw, &iv, aad.as_deref(), &data)
2542 .map_err(|e| e.to_string())?;
2543 let mut combined = out.ciphertext;
2544 combined.extend_from_slice(&out.auth_tag);
2545 Ok(combined)
2546 }
2547 "AES-CBC" => {
2548 let iv = subtle_bytes_prop(cx, alg_obj, "iv")
2549 .ok_or("AES-CBC requires an iv".to_string())?;
2550 let algo = aes_cipher_algo("AES-CBC", raw.len())?;
2551 let mut ctx = bao_crypto::cipher::CipherCtx::new(
2552 algo,
2553 &raw,
2554 &iv,
2555 bao_crypto::cipher::Direction::Encrypt,
2556 )
2557 .map_err(|e| e.to_string())?;
2558 let mut out = ctx.update(&data).map_err(|e| e.to_string())?;
2559 out.extend_from_slice(&ctx.final_ex().map_err(|e| e.to_string())?);
2560 Ok(out)
2561 }
2562 other => Err(format!("subtle.encrypt: unsupported algorithm {}", other)),
2563 }
2564 })();
2565
2566 match result {
2567 Ok(bytes) => {
2568 let v = bytes_to_arraybuffer_val(cx, &bytes);
2569 subtle_resolve(cx, promise_root.get(), v);
2570 }
2571 Err(msg) => subtle_reject(cx, promise_root.get(), &format!("subtle.encrypt: {}", msg)),
2572 }
2573 true
2574}
2575
2576#[allow(unsafe_op_in_unsafe_fn)]
2577unsafe extern "C" fn subtle_decrypt(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
2578 let args = CallArgs::from_vp(vp, argc);
2579 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2580 let cx_ref = &mut wrapped_cx;
2581 let promise = subtle_promise!(cx, cx_ref, &args);
2582 if promise.is_null() {
2583 return true;
2584 }
2585 rooted!(&in(cx_ref) let promise_root = promise);
2586
2587 let result: ::std::result::Result<Vec<u8>, String> = (|| {
2588 if argc < 3 {
2589 return Err("decrypt(algorithm, key, data) requires 3 arguments".to_string());
2590 }
2591 let (name, alg_obj) = subtle_algo_name(cx, *args.get(0).ptr)?;
2592 let key_obj = subtle_arg_object(*args.get(1).ptr)
2593 .ok_or("decrypt: key must be a CryptoKey".to_string())?;
2594 let data = subtle_bytes(cx, *args.get(2).ptr);
2595 let raw = key_material(cx, key_obj, "_raw")
2596 .ok_or("decrypt: not a symmetric CryptoKey".to_string())?;
2597
2598 match name.as_str() {
2599 "AES-GCM" => {
2600 let iv = subtle_bytes_prop(cx, alg_obj, "iv")
2601 .ok_or("AES-GCM requires an iv".to_string())?;
2602 let aad = subtle_bytes_prop(cx, alg_obj, "additionalData");
2603 let tag_len = subtle_u32_prop(cx, alg_obj, "tagLength")
2604 .map_or(16usize, |bits| bits as usize / 8);
2605 if tag_len != 16 {
2606 return Err(format!("AES-GCM tagLength {} bits is not supported (128 only)", tag_len * 8));
2607 }
2608 if data.len() < tag_len {
2609 return Err("AES-GCM ciphertext shorter than the auth tag".to_string());
2610 }
2611 let split = data.len() - tag_len;
2612 let algo = aes_cipher_algo("AES-GCM", raw.len())?;
2613 bao_crypto::cipher::decrypt(
2614 algo,
2615 &raw,
2616 &iv,
2617 aad.as_deref(),
2618 &data[..split],
2619 &data[split..],
2620 )
2621 .map_err(|_| "decryption failed (authentication or parameters)".to_string())
2622 }
2623 "AES-CBC" => {
2624 let iv = subtle_bytes_prop(cx, alg_obj, "iv")
2625 .ok_or("AES-CBC requires an iv".to_string())?;
2626 let algo = aes_cipher_algo("AES-CBC", raw.len())?;
2627 let mut ctx = bao_crypto::cipher::CipherCtx::new(
2628 algo,
2629 &raw,
2630 &iv,
2631 bao_crypto::cipher::Direction::Decrypt,
2632 )
2633 .map_err(|e| e.to_string())?;
2634 let mut out = ctx.update(&data).map_err(|e| e.to_string())?;
2635 out.extend_from_slice(&ctx.final_ex().map_err(|e| e.to_string())?);
2636 Ok(out)
2637 }
2638 other => Err(format!("subtle.decrypt: unsupported algorithm {}", other)),
2639 }
2640 })();
2641
2642 match result {
2643 Ok(bytes) => {
2644 let v = bytes_to_arraybuffer_val(cx, &bytes);
2645 subtle_resolve(cx, promise_root.get(), v);
2646 }
2647 Err(msg) => subtle_reject(cx, promise_root.get(), &format!("subtle.decrypt: {}", msg)),
2648 }
2649 true
2650}
2651
2652#[allow(unsafe_op_in_unsafe_fn)]
2655unsafe extern "C" fn subtle_generate_key(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
2656 let args = CallArgs::from_vp(vp, argc);
2657 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2658 let cx_ref = &mut wrapped_cx;
2659 let promise = subtle_promise!(cx, cx_ref, &args);
2660 if promise.is_null() {
2661 return true;
2662 }
2663 rooted!(&in(cx_ref) let promise_root = promise);
2664
2665 let result: ::std::result::Result<*mut JSObject, String> = (|| {
2666 if argc < 3 {
2667 return Err("generateKey(algorithm, extractable, usages) requires 3 arguments".to_string());
2668 }
2669 let (name, alg_obj) = subtle_algo_name(cx, *args.get(0).ptr)?;
2670 let extractable = (*args.get(1).ptr).to_boolean();
2671 let usages = subtle_arg_object(*args.get(2).ptr)
2672 .ok_or("generateKey: usages must be an array".to_string())?;
2673
2674 match name.as_str() {
2675 "AES-GCM" | "AES-CBC" => {
2676 let bits = subtle_u32_prop(cx, alg_obj, "length")
2677 .ok_or("AES generateKey requires algorithm.length".to_string())?;
2678 if !matches!(bits, 128 | 192 | 256) {
2679 return Err(format!("invalid AES key length {} (128/192/256)", bits));
2680 }
2681 let mut raw = vec![0u8; bits as usize / 8];
2682 bao_crypto::random::rand_bytes(&mut raw).map_err(|e| e.to_string())?;
2683 Ok(make_crypto_key(
2684 cx,
2685 "secret",
2686 &name,
2687 &[("length", bits.to_string())],
2688 extractable,
2689 usages,
2690 "_raw",
2691 &raw,
2692 ))
2693 }
2694 "RSA-RSASSA-PKCS1-v1_5" => {
2695 let bits = subtle_u32_prop(cx, alg_obj, "modulusLength")
2696 .ok_or("RSA generateKey requires modulusLength".to_string())? as usize;
2697 let hash = subtle_str_prop(cx, alg_obj, "hash")
2698 .or_else(|| subtle_str_prop(cx, alg_obj, "hash.name"))
2699 .unwrap_or_else(|| "SHA-256".to_string());
2700 let kp = bao_crypto::keypair::generate_key_pair(&bao_crypto::keypair::KeyPairType::Rsa { bits })
2701 .map_err(|e| e.to_string())?;
2702 Ok(make_crypto_key_pair(
2703 cx,
2704 &name,
2705 &[("hash", hash)],
2706 extractable,
2707 usages,
2708 &kp.private_key_der,
2709 &kp.public_key_der,
2710 ))
2711 }
2712 "ECDSA" => {
2713 let curve = subtle_str_prop(cx, alg_obj, "namedCurve")
2714 .or_else(|| subtle_str_prop(cx, alg_obj, "namedCurve.name"))
2715 .ok_or("ECDSA generateKey requires namedCurve".to_string())?;
2716 let ec_curve = match curve.as_str() {
2717 "P-256" => bao_crypto::keypair::EcCurve::P256,
2718 "P-384" => bao_crypto::keypair::EcCurve::P384,
2719 other => return Err(format!("unsupported ECDSA curve {}", other)),
2720 };
2721 let kp = bao_crypto::keypair::generate_key_pair(&bao_crypto::keypair::KeyPairType::Ec { curve: ec_curve })
2722 .map_err(|e| e.to_string())?;
2723 Ok(make_crypto_key_pair(
2724 cx,
2725 &name,
2726 &[("namedCurve", curve)],
2727 extractable,
2728 usages,
2729 &kp.private_key_der,
2730 &kp.public_key_der,
2731 ))
2732 }
2733 other => Err(format!("subtle.generateKey: unsupported algorithm {}", other)),
2734 }
2735 })();
2736
2737 match result {
2738 Ok(key) if !key.is_null() => {
2739 let v = ObjectValue(key);
2740 subtle_resolve(cx, promise_root.get(), v);
2741 }
2742 Ok(_) => subtle_reject(cx, promise_root.get(), "subtle.generateKey: key construction failed"),
2743 Err(msg) => subtle_reject(cx, promise_root.get(), &format!("subtle.generateKey: {}", msg)),
2744 }
2745 true
2746}
2747
2748#[allow(unsafe_op_in_unsafe_fn)]
2751unsafe extern "C" fn subtle_import_key(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
2752 let args = CallArgs::from_vp(vp, argc);
2753 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2754 let cx_ref = &mut wrapped_cx;
2755 let promise = subtle_promise!(cx, cx_ref, &args);
2756 if promise.is_null() {
2757 return true;
2758 }
2759 rooted!(&in(cx_ref) let promise_root = promise);
2760
2761 let result: ::std::result::Result<*mut JSObject, String> = (|| {
2762 if argc < 5 {
2763 return Err("importKey(format, keyData, algorithm, extractable, usages) requires 5 arguments".to_string());
2764 }
2765 let format = crate::js_to_rust_string(cx, *args.get(0).ptr);
2766 let (name, _alg_obj) = subtle_algo_name(cx, *args.get(2).ptr)?;
2767 let extractable = (*args.get(3).ptr).to_boolean();
2768 let usages = subtle_arg_object(*args.get(4).ptr)
2769 .ok_or("importKey: usages must be an array".to_string())?;
2770
2771 match format.as_str() {
2772 "raw" => {
2773 let raw = subtle_bytes(cx, *args.get(1).ptr);
2774 if raw.is_empty() {
2775 return Err("raw import requires key bytes".to_string());
2776 }
2777 match name.as_str() {
2778 "AES-GCM" | "AES-CBC" => {
2779 if !matches!(raw.len() * 8, 128 | 192 | 256) {
2780 return Err(format!("invalid AES key length {} bits", raw.len() * 8));
2781 }
2782 Ok(make_crypto_key(cx, "secret", &name, &[("length", (raw.len() * 8).to_string())], extractable, usages, "_raw", &raw))
2783 }
2784 "HMAC" => {
2785 let hash = subtle_str_prop(cx, _alg_obj, "hash")
2786 .or_else(|| subtle_str_prop(cx, _alg_obj, "hash.name"))
2787 .ok_or("HMAC import requires algorithm.hash".to_string())?;
2788 Ok(make_crypto_key(cx, "secret", "HMAC", &[("hash", hash)], extractable, usages, "_raw", &raw))
2789 }
2790 other => Err(format!("subtle.importKey raw: unsupported algorithm {}", other)),
2791 }
2792 }
2793 "pkcs8" => {
2794 let der = subtle_bytes(cx, *args.get(1).ptr);
2795 if der.is_empty() {
2796 return Err("pkcs8 import requires DER bytes".to_string());
2797 }
2798 let extras: Vec<(String, String)> = match name.as_str() {
2799 "RSA-RSASSA-PKCS1-v1_5" => {
2800 let hash = subtle_str_prop(cx, _alg_obj, "hash")
2801 .or_else(|| subtle_str_prop(cx, _alg_obj, "hash.name"))
2802 .unwrap_or_else(|| "SHA-256".to_string());
2803 vec![("hash".to_string(), hash)]
2804 }
2805 "ECDSA" => {
2806 let curve = subtle_str_prop(cx, _alg_obj, "namedCurve")
2807 .or_else(|| subtle_str_prop(cx, _alg_obj, "namedCurve.name"))
2808 .unwrap_or_else(|| "P-256".to_string());
2809 vec![("namedCurve".to_string(), curve)]
2810 }
2811 other => return Err(format!("subtle.importKey pkcs8: unsupported algorithm {}", other)),
2812 };
2813 let extras_ref: Vec<(&str, String)> = extras.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
2814 Ok(make_crypto_key(cx, "private", &name, &extras_ref, extractable, usages, "_der", &der))
2815 }
2816 "spki" => {
2817 let der = subtle_bytes(cx, *args.get(1).ptr);
2818 if der.is_empty() {
2819 return Err("spki import requires DER bytes".to_string());
2820 }
2821 let extras: Vec<(String, String)> = match name.as_str() {
2822 "RSA-RSASSA-PKCS1-v1_5" => {
2823 let hash = subtle_str_prop(cx, _alg_obj, "hash")
2824 .or_else(|| subtle_str_prop(cx, _alg_obj, "hash.name"))
2825 .unwrap_or_else(|| "SHA-256".to_string());
2826 vec![("hash".to_string(), hash)]
2827 }
2828 "ECDSA" => {
2829 let curve = subtle_str_prop(cx, _alg_obj, "namedCurve")
2830 .or_else(|| subtle_str_prop(cx, _alg_obj, "namedCurve.name"))
2831 .unwrap_or_else(|| "P-256".to_string());
2832 vec![("namedCurve".to_string(), curve)]
2833 }
2834 other => return Err(format!("subtle.importKey spki: unsupported algorithm {}", other)),
2835 };
2836 let extras_ref: Vec<(&str, String)> = extras.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
2837 Ok(make_crypto_key(cx, "public", &name, &extras_ref, extractable, usages, "_der", &der))
2838 }
2839 "jwk" => {
2840 if !(*args.get(1).ptr).is_object() {
2844 return Err("jwk import requires a JWK object".to_string());
2845 }
2846 let jwk = (*args.get(1).ptr).to_object();
2847 let kty = subtle_str_prop(cx, jwk, "kty").unwrap_or_default();
2848 if kty != "oct" {
2849 return Err(format!("jwk import for kty \"{}\" is not supported (oct only); use pkcs8/spki DER import", kty));
2850 }
2851 let k = subtle_str_prop(cx, jwk, "k").ok_or("oct JWK requires the k field".to_string())?;
2852 let src = k.as_bytes();
2853 let upper = bun_base64::decode_lenient_len(src.len());
2854 let mut out = vec![0u8; upper];
2855 let n = bun_base64::decode_lenient(&mut out, src, true);
2856 out.truncate(n);
2857 if out.is_empty() {
2858 return Err("oct JWK k field decoded to zero bytes".to_string());
2859 }
2860 match name.as_str() {
2861 "AES-GCM" | "AES-CBC" => {
2862 if !matches!(out.len() * 8, 128 | 192 | 256) {
2863 return Err(format!("invalid AES key length {} bits from JWK", out.len() * 8));
2864 }
2865 Ok(make_crypto_key(cx, "secret", &name, &[("length", (out.len() * 8).to_string())], extractable, usages, "_raw", &out))
2866 }
2867 "HMAC" => {
2868 let hash = subtle_str_prop(cx, _alg_obj, "hash")
2869 .or_else(|| subtle_str_prop(cx, _alg_obj, "hash.name"))
2870 .ok_or("HMAC import requires algorithm.hash".to_string())?;
2871 Ok(make_crypto_key(cx, "secret", "HMAC", &[("hash", hash)], extractable, usages, "_raw", &out))
2872 }
2873 other => Err(format!("subtle.importKey jwk: unsupported algorithm {}", other)),
2874 }
2875 }
2876 other => Err(format!("subtle.importKey: unsupported format {}", other)),
2877 }
2878 })();
2879
2880 match result {
2881 Ok(key) if !key.is_null() => {
2882 let v = ObjectValue(key);
2883 subtle_resolve(cx, promise_root.get(), v);
2884 }
2885 Ok(_) => subtle_reject(cx, promise_root.get(), "subtle.importKey: key construction failed"),
2886 Err(msg) => subtle_reject(cx, promise_root.get(), &format!("subtle.importKey: {}", msg)),
2887 }
2888 true
2889}
2890
2891fn subtle_hmac_algorithm(hash: &str) -> ::std::result::Result<bun_sha_hmac::sha::evp::Algorithm, String> {
2894 use bun_sha_hmac::sha::evp::Algorithm;
2895 Ok(match hash.to_uppercase().as_str() {
2896 "SHA-1" => Algorithm::Sha1,
2897 "SHA-224" => Algorithm::Sha224,
2898 "SHA-256" => Algorithm::Sha256,
2899 "SHA-384" => Algorithm::Sha384,
2900 "SHA-512" => Algorithm::Sha512,
2901 other => return Err(format!("unsupported HMAC hash {}", other)),
2902 })
2903}
2904
2905fn subtle_rsa_hash(hash: &str) -> ::std::result::Result<bao_crypto::sign::RsaHash, String> {
2906 Ok(match hash.to_uppercase().as_str() {
2907 "SHA-256" => bao_crypto::sign::RsaHash::Sha256,
2908 "SHA-384" => bao_crypto::sign::RsaHash::Sha384,
2909 "SHA-512" => bao_crypto::sign::RsaHash::Sha512,
2910 other => return Err(format!("unsupported RSA hash {}", other)),
2911 })
2912}
2913
2914#[allow(unsafe_op_in_unsafe_fn)]
2915unsafe extern "C" fn subtle_sign(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
2916 let args = CallArgs::from_vp(vp, argc);
2917 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
2918 let cx_ref = &mut wrapped_cx;
2919 let promise = subtle_promise!(cx, cx_ref, &args);
2920 if promise.is_null() {
2921 return true;
2922 }
2923 rooted!(&in(cx_ref) let promise_root = promise);
2924
2925 let result: ::std::result::Result<Vec<u8>, String> = (|| {
2926 if argc < 3 {
2927 return Err("sign(algorithm, key, data) requires 3 arguments".to_string());
2928 }
2929 let (name, alg_obj) = subtle_algo_name(cx, *args.get(0).ptr)?;
2930 let key_obj = subtle_arg_object(*args.get(1).ptr)
2931 .ok_or("sign: key must be a CryptoKey".to_string())?;
2932 rooted!(&in(cx_ref) let key_obj_root = key_obj);
2936 let data = subtle_bytes(cx, *args.get(2).ptr);
2937
2938 let key_alg = {
2939 let mut v = UndefinedValue();
2940 JS_GetProperty(
2941 cx,
2942 key_obj_root.handle().into(),
2943 c"algorithm".as_ptr(),
2944 MutableHandle::<Value> {
2945 _phantom_0: ::std::marker::PhantomData,
2946 ptr: &mut v,
2947 },
2948 );
2949 if !v.is_object() {
2950 return Err("sign: not a CryptoKey".to_string());
2951 }
2952 v.to_object()
2953 };
2954 let ktype = subtle_str_prop(cx, key_obj, "type").unwrap_or_default();
2955
2956 match name.as_str() {
2957 "HMAC" => {
2958 let raw = key_material(cx, key_obj, "_raw")
2959 .ok_or("sign: not a symmetric CryptoKey".to_string())?;
2960 let hash = subtle_str_prop(cx, alg_obj, "hash")
2965 .or_else(|| subtle_str_prop(cx, key_alg, "hash"))
2966 .unwrap_or_else(|| "SHA-256".to_string());
2967 let algo = subtle_hmac_algorithm(&hash)?;
2968 let mut out = [0u8; bun_sha_hmac::hmac::EVP_MAX_MD_SIZE];
2969 let mac = bun_sha_hmac::hmac::generate(&raw, &data, algo, &mut out)
2970 .ok_or("HMAC computation failed".to_string())?;
2971 Ok(mac.to_vec())
2972 }
2973 "RSA-RSASSA-PKCS1-v1_5" | "RSASSA-PKCS1-v1_5" => {
2974 if ktype != "private" {
2975 return Err("sign: not a private CryptoKey".to_string());
2976 }
2977 let der = key_material(cx, key_obj, "_der")
2978 .ok_or("sign: not a private CryptoKey".to_string())?;
2979 let hash = subtle_str_prop(cx, key_alg, "hash")
2980 .unwrap_or_else(|| "SHA-256".to_string());
2981 let signer = bao_crypto::sign::Signer::from_pkcs8_der(
2982 &bao_crypto::sign::SignAlgorithm::RsaPkcs1v15 { hash: subtle_rsa_hash(&hash)? },
2983 &der,
2984 )
2985 .map_err(|e| e.to_string())?;
2986 signer
2987 .sign(&data, bao_crypto::sign::SignatureFormat::Der)
2988 .map_err(|e| e.to_string())
2989 }
2990 "ECDSA" => {
2991 if ktype != "private" {
2992 return Err("sign: not a private CryptoKey".to_string());
2993 }
2994 let der = key_material(cx, key_obj, "_der")
2995 .ok_or("sign: not a private CryptoKey".to_string())?;
2996 let curve = subtle_str_prop(cx, key_alg, "namedCurve").unwrap_or_else(|| "P-256".to_string());
2997 let algo = match curve.as_str() {
2998 "P-256" => bao_crypto::sign::SignAlgorithm::EcdsaP256,
2999 "P-384" => bao_crypto::sign::SignAlgorithm::EcdsaP384,
3000 other => return Err(format!("unsupported ECDSA curve {}", other)),
3001 };
3002 let signer = bao_crypto::sign::Signer::from_pkcs8_der(&algo, &der)
3003 .map_err(|e| e.to_string())?;
3004 signer
3006 .sign(&data, bao_crypto::sign::SignatureFormat::Raw)
3007 .map_err(|e| e.to_string())
3008 }
3009 other => Err(format!("subtle.sign: unsupported algorithm {}", other)),
3010 }
3011 })();
3012
3013 match result {
3014 Ok(bytes) => {
3015 let v = bytes_to_arraybuffer_val(cx, &bytes);
3016 subtle_resolve(cx, promise_root.get(), v);
3017 }
3018 Err(msg) => subtle_reject(cx, promise_root.get(), &format!("subtle.sign: {}", msg)),
3019 }
3020 true
3021}
3022
3023#[allow(unsafe_op_in_unsafe_fn)]
3024unsafe extern "C" fn subtle_verify(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
3025 let args = CallArgs::from_vp(vp, argc);
3026 let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
3027 let cx_ref = &mut wrapped_cx;
3028 let promise = subtle_promise!(cx, cx_ref, &args);
3029 if promise.is_null() {
3030 return true;
3031 }
3032 rooted!(&in(cx_ref) let promise_root = promise);
3033
3034 let result: ::std::result::Result<bool, String> = (|| {
3035 if argc < 4 {
3036 return Err("verify(algorithm, key, signature, data) requires 4 arguments".to_string());
3037 }
3038 let (name, _alg_obj) = subtle_algo_name(cx, *args.get(0).ptr)?;
3039 let key_obj = subtle_arg_object(*args.get(1).ptr)
3040 .ok_or("verify: key must be a CryptoKey".to_string())?;
3041 rooted!(&in(cx_ref) let key_obj_root = key_obj);
3043 let signature = subtle_bytes(cx, *args.get(2).ptr);
3044 let data = subtle_bytes(cx, *args.get(3).ptr);
3045
3046 let key_alg = {
3047 let mut v = UndefinedValue();
3048 JS_GetProperty(
3049 cx,
3050 key_obj_root.handle().into(),
3051 c"algorithm".as_ptr(),
3052 MutableHandle::<Value> {
3053 _phantom_0: ::std::marker::PhantomData,
3054 ptr: &mut v,
3055 },
3056 );
3057 if !v.is_object() {
3058 return Err("verify: not a CryptoKey".to_string());
3059 }
3060 v.to_object()
3061 };
3062 let ktype = subtle_str_prop(cx, key_obj, "type").unwrap_or_default();
3063
3064 match name.as_str() {
3065 "HMAC" => {
3066 let raw = key_material(cx, key_obj, "_raw")
3067 .ok_or("verify: not a symmetric CryptoKey".to_string())?;
3068 let hash = subtle_str_prop(cx, key_alg, "hash").unwrap_or_else(|| "SHA-256".to_string());
3069 let algo = subtle_hmac_algorithm(&hash)?;
3070 let mut out = [0u8; bun_sha_hmac::hmac::EVP_MAX_MD_SIZE];
3071 let Some(mac) = bun_sha_hmac::hmac::generate(&raw, &data, algo, &mut out) else {
3072 return Err("HMAC computation failed".to_string());
3073 };
3074 Ok(ct_eq(mac, &signature))
3075 }
3076 "RSA-RSASSA-PKCS1-v1_5" | "RSASSA-PKCS1-v1_5" => {
3077 let hash = subtle_str_prop(cx, key_alg, "hash").unwrap_or_else(|| "SHA-256".to_string());
3078 let algo = bao_crypto::sign::SignAlgorithm::RsaPkcs1v15 { hash: subtle_rsa_hash(&hash)? };
3079 let verifier = if ktype == "private" {
3080 let der = key_material(cx, key_obj, "_der")
3081 .ok_or("verify: key carries no DER material".to_string())?;
3082 bao_crypto::verify::Verifier::from_pkcs8_der(&algo, &der)
3083 } else {
3084 let der = key_material(cx, key_obj, "_der")
3085 .ok_or("verify: key carries no DER material".to_string())?;
3086 bao_crypto::verify::Verifier::from_public_der(&algo, &der)
3087 }
3088 .map_err(|e| e.to_string())?;
3089 verifier
3090 .verify(&data, &signature, bao_crypto::sign::SignatureFormat::Der)
3091 .map_err(|e| e.to_string())
3092 }
3093 "ECDSA" => {
3094 let curve = subtle_str_prop(cx, key_alg, "namedCurve").unwrap_or_else(|| "P-256".to_string());
3095 let algo = match curve.as_str() {
3096 "P-256" => bao_crypto::sign::SignAlgorithm::EcdsaP256,
3097 "P-384" => bao_crypto::sign::SignAlgorithm::EcdsaP384,
3098 other => return Err(format!("unsupported ECDSA curve {}", other)),
3099 };
3100 let verifier = if ktype == "private" {
3101 let der = key_material(cx, key_obj, "_der")
3102 .ok_or("verify: key carries no DER material".to_string())?;
3103 bao_crypto::verify::Verifier::from_pkcs8_der(&algo, &der)
3104 } else {
3105 let der = key_material(cx, key_obj, "_der")
3106 .ok_or("verify: key carries no DER material".to_string())?;
3107 bao_crypto::verify::Verifier::from_public_der(&algo, &der)
3108 }
3109 .map_err(|e| e.to_string())?;
3110 verifier
3112 .verify(&data, &signature, bao_crypto::sign::SignatureFormat::Raw)
3113 .map_err(|e| e.to_string())
3114 }
3115 other => Err(format!("subtle.verify: unsupported algorithm {}", other)),
3116 }
3117 })();
3118
3119 match result {
3120 Ok(ok) => {
3121 let v = BooleanValue(ok);
3122 subtle_resolve(cx, promise_root.get(), v);
3123 }
3124 Err(msg) => subtle_reject(cx, promise_root.get(), &format!("subtle.verify: {}", msg)),
3125 }
3126 true
3127}
3128
3129fn localstorage_path() -> ::std::path::PathBuf {
3136 let home = ::std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
3137 let dir = ::std::path::Path::new(&home).join(".bao");
3138 let _ = ::std::fs::create_dir_all(&dir);
3139 dir.join("localstorage.json")
3140}
3141
3142::std::thread_local! {
3143 static LS_STORE: RefCell<Option<::std::collections::BTreeMap<String, String>>> =
3144 const { RefCell::new(None) };
3145}
3146
3147fn ls_with_store<R>(f: impl FnOnce(&mut ::std::collections::BTreeMap<String, String>) -> R) -> R {
3151 LS_STORE.with(|cell| {
3152 let mut guard = cell.borrow_mut();
3153 if guard.is_none() {
3154 let map = ::std::fs::read_to_string(localstorage_path())
3155 .ok()
3156 .and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
3157 .and_then(|v| {
3158 let obj = v.as_object()?;
3159 let mut m = ::std::collections::BTreeMap::new();
3160 for (k, val) in obj {
3161 if let Some(s) = val.as_str() {
3162 m.insert(k.clone(), s.to_string());
3163 }
3164 }
3165 Some(m)
3166 })
3167 .unwrap_or_default();
3168 *guard = Some(map);
3169 }
3170 f(guard.as_mut().unwrap())
3171 })
3172}
3173
3174fn ls_persist() {
3175 LS_STORE.with(|cell| {
3176 if let Some(map) = cell.borrow().as_ref() {
3177 if let Ok(text) = serde_json::to_string_pretty(map) {
3178 let tmp = localstorage_path().with_extension("json.tmp");
3179 if ::std::fs::write(&tmp, text).is_ok() {
3180 let _ = ::std::fs::rename(&tmp, localstorage_path());
3181 }
3182 }
3183 }
3184 });
3185}
3186
3187pub fn install_local_storage(cx: &mut mozjs::context::JSContext, global: mozjs::rust::Handle<*mut JSObject>) {
3190 unsafe {
3191 rooted!(&in(cx) let global_root = global.get());
3192 let mut existing = UndefinedValue();
3193 let found = JS_GetProperty(
3194 cx.raw_cx(),
3195 global_root.handle().into(),
3196 c"localStorage".as_ptr(),
3197 MutableHandle::<Value> {
3198 _phantom_0: ::std::marker::PhantomData,
3199 ptr: &mut existing,
3200 },
3201 );
3202 if !found {
3214 JS_ClearPendingException(cx.raw_cx());
3215 return; }
3217 if existing.is_object() {
3218 return; }
3220 rooted!(&in(cx) let ls = JS_NewPlainObject(cx));
3221 if ls.get().is_null() {
3222 return;
3223 }
3224 let lh = ls.handle();
3225 for (name, op, nargs) in [
3226 ("getItem", ls_get_item as unsafe extern "C" fn(*mut JSContext, u32, *mut JSVal) -> bool, 1),
3227 ("setItem", ls_set_item, 2),
3228 ("removeItem", ls_remove_item, 1),
3229 ("clear", ls_clear, 0),
3230 ("key", ls_key, 1),
3231 ] {
3232 JS_DefineFunction(
3233 cx,
3234 lh,
3235 ZBox::from_bytes(name.as_bytes()).as_ptr(),
3236 Some(op),
3237 nargs,
3238 JSPROP_ENUMERATE as u32,
3239 );
3240 }
3241 JS_DefineProperty1(
3243 cx.raw_cx(),
3244 ls.handle().into(),
3245 c"length".as_ptr(),
3246 Some(ls_length_getter),
3247 None,
3248 (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
3249 );
3250 rooted!(&in(cx) let ls_root = ls.get());
3251 JS_DefineProperty3(
3252 cx,
3253 global,
3254 c"localStorage".as_ptr(),
3255 ls_root.handle(),
3256 (JSPROP_ENUMERATE | JSPROP_PERMANENT) as u32,
3257 );
3258 }
3259}
3260
3261#[allow(unsafe_op_in_unsafe_fn)]
3262unsafe extern "C" fn ls_get_item(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
3263 let args = CallArgs::from_vp(vp, argc);
3264 if argc < 1 || !(*args.get(0).ptr).is_string() {
3265 args.rval().set(UndefinedValue());
3266 return true;
3267 }
3268 let key = crate::js_to_rust_string(cx, *args.get(0).ptr);
3269 ls_with_store(|m| {
3270 let v = m.get(&key).cloned();
3271 match v {
3272 Some(s) => {
3273 let c_s = ZBox::from_bytes(s.as_bytes());
3274 let js = JS_NewStringCopyZ(cx, c_s.as_ptr());
3275 args.rval().set(if js.is_null() {
3276 UndefinedValue()
3277 } else {
3278 StringValue(&*js)
3279 });
3280 }
3281 None => args.rval().set(NullValue()),
3282 }
3283 });
3284 true
3285}
3286
3287#[allow(unsafe_op_in_unsafe_fn)]
3288unsafe extern "C" fn ls_set_item(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
3289 let args = CallArgs::from_vp(vp, argc);
3290 if argc < 2 {
3291 let c_m = ZBox::from_bytes("localStorage.setItem(key, value) requires 2 arguments".as_bytes());
3292 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_m.as_ptr());
3293 return false;
3294 }
3295 let key = crate::js_to_rust_string(cx, *args.get(0).ptr);
3296 let val = crate::js_to_rust_string(cx, *args.get(1).ptr);
3297 ls_with_store(|m| {
3298 m.insert(key, val);
3299 });
3300 ls_persist();
3301 args.rval().set(UndefinedValue());
3302 true
3303}
3304
3305#[allow(unsafe_op_in_unsafe_fn)]
3306unsafe extern "C" fn ls_remove_item(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
3307 let args = CallArgs::from_vp(vp, argc);
3308 if argc < 1 {
3309 args.rval().set(UndefinedValue());
3310 return true;
3311 }
3312 let key = crate::js_to_rust_string(cx, *args.get(0).ptr);
3313 ls_with_store(|m| {
3314 m.remove(&key);
3315 });
3316 ls_persist();
3317 args.rval().set(UndefinedValue());
3318 true
3319}
3320
3321#[allow(unsafe_op_in_unsafe_fn)]
3322unsafe extern "C" fn ls_clear(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
3323 let args = CallArgs::from_vp(vp, _argc);
3324 ls_with_store(|m| {
3325 m.clear();
3326 });
3327 ls_persist();
3328 args.rval().set(UndefinedValue());
3329 true
3330}
3331
3332#[allow(unsafe_op_in_unsafe_fn)]
3333unsafe extern "C" fn ls_key(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
3334 let args = CallArgs::from_vp(vp, argc);
3335 if argc < 1 || !(*args.get(0).ptr).is_int32() {
3336 args.rval().set(NullValue());
3337 return true;
3338 }
3339 let idx = (*args.get(0).ptr).to_int32();
3340 if idx < 0 {
3341 args.rval().set(NullValue());
3342 return true;
3343 }
3344 let val = ls_with_store(|m| {
3345 m.keys().nth(idx as usize).cloned()
3346 });
3347 match val {
3348 Some(k) => {
3349 let c_k = ZBox::from_bytes(k.as_bytes());
3350 let js = JS_NewStringCopyZ(cx, c_k.as_ptr());
3351 args.rval().set(if js.is_null() {
3352 NullValue()
3353 } else {
3354 StringValue(&*js)
3355 });
3356 }
3357 None => args.rval().set(NullValue()),
3358 }
3359 true
3360}
3361
3362#[allow(unsafe_op_in_unsafe_fn)]
3363unsafe extern "C" fn ls_length(_cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
3364 let args = CallArgs::from_vp(vp, _argc);
3365 let n = ls_with_store(|m| m.len());
3366 args.rval().set(Int32Value(n as i32));
3367 true
3368}
3369
3370#[allow(unsafe_op_in_unsafe_fn)]
3371unsafe extern "C" fn ls_length_getter(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
3372 ls_length(cx, _argc, vp)
3373}
3374
3375pub fn install_event_source(cx: &mut mozjs::context::JSContext, global: mozjs::rust::Handle<*mut JSObject>) {
3392 let src = r#"
3393(function() {
3394 var _g = globalThis;
3395
3396 // ── EventSource ──
3397 var CONNECTING = 0, OPEN = 1, CLOSED = 2;
3398
3399 function EventSource(url) {
3400 if (!(this instanceof EventSource)) return new EventSource(url);
3401 this.url = String(url);
3402 this.readyState = CONNECTING;
3403 this._retry = 3000;
3404 this._lastEventId = '';
3405 this._timer = null;
3406 this._listeners = {};
3407 this.onopen = null;
3408 this.onmessage = null;
3409 this.onerror = null;
3410 var self = this;
3411 this._connect();
3412 }
3413 EventSource.CONNECTING = CONNECTING;
3414 EventSource.OPEN = OPEN;
3415 EventSource.CLOSED = CLOSED;
3416
3417 EventSource.prototype._fire = function(type, ev) {
3418 ev = ev || {};
3419 ev.type = type;
3420 var handler = this['on' + type];
3421 if (typeof handler === 'function') handler.call(this, ev);
3422 var list = this._listeners[type];
3423 if (list) {
3424 for (var i = 0; i < list.length; i++) list[i].call(this, ev);
3425 }
3426 };
3427
3428 EventSource.prototype._connect = function() {
3429 var self = this;
3430 if (this.readyState === CLOSED) return;
3431 this.readyState = CONNECTING;
3432 var headers = { 'Accept': 'text/event-stream' };
3433 if (this._lastEventId) headers['Last-Event-ID'] = this._lastEventId;
3434 fetch(this.url, { headers: headers })
3435 .then(function(resp) {
3436 if (!resp.ok) {
3437 self._fail('EventSource: HTTP ' + resp.status + ' for ' + self.url);
3438 return null;
3439 }
3440 // Spec: the response MIME type must be text/event-stream.
3441 var ct = resp.headers && resp.headers.get && resp.headers.get('content-type');
3442 if (ct && String(ct).indexOf('text/event-stream') === -1) {
3443 self._fail('EventSource: response Content-Type "' + ct + '" is not text/event-stream');
3444 return null;
3445 }
3446 self.readyState = OPEN;
3447 self._fire('open');
3448 return resp.text();
3449 })
3450 .then(function(text) {
3451 if (text === null || text === undefined) return;
3452 self._parse(text);
3453 // Server closed the stream — spec: schedule reconnect.
3454 self._scheduleReconnect();
3455 })
3456 .catch(function(err) {
3457 self._fail(err && err.message ? err.message : String(err));
3458 });
3459 };
3460
3461 EventSource.prototype._fail = function(msg) {
3462 if (this.readyState === CLOSED) return;
3463 this._fire('error', { message: msg });
3464 this._scheduleReconnect();
3465 };
3466
3467 EventSource.prototype._scheduleReconnect = function() {
3468 var self = this;
3469 if (this.readyState === CLOSED) return;
3470 this.readyState = CONNECTING;
3471 if (this._timer) clearTimeout(this._timer);
3472 this._timer = setTimeout(function() { self._connect(); }, this._retry);
3473 };
3474
3475 // WHATWG event-stream parsing: lines split on CR/LF/CRLF, fields
3476 // event/data/id/retry, blank line dispatches the accumulated event.
3477 EventSource.prototype._parse = function(text) {
3478 var lines = text.split(/\r\n|\r|\n/);
3479 var dataLines = [], eventName = '', lastId = this._lastEventId;
3480 for (var i = 0; i < lines.length; i++) {
3481 var line = lines[i];
3482 if (line === '') {
3483 if (dataLines.length > 0) {
3484 var ev = { data: dataLines.join('\n'), lastEventId: lastId };
3485 if (eventName !== '' && eventName !== 'message') {
3486 ev.lastEventId = lastId;
3487 this._fire(eventName, ev);
3488 } else {
3489 this._fire('message', ev);
3490 }
3491 }
3492 dataLines = [];
3493 eventName = '';
3494 continue;
3495 }
3496 if (line.charCodeAt(0) === 0x3a /* ':' */) continue; // comment
3497 var colon = line.indexOf(':');
3498 var field, value;
3499 if (colon === -1) { field = line; value = ''; }
3500 else {
3501 field = line.slice(0, colon);
3502 value = line.slice(colon + 1);
3503 if (value.charCodeAt(0) === 0x20 /* ' ' */) value = value.slice(1);
3504 }
3505 if (field === 'event') eventName = value;
3506 else if (field === 'data') dataLines.push(value);
3507 else if (field === 'id') { if (value.indexOf('\0') === -1) lastId = value; }
3508 else if (field === 'retry') {
3509 var n = parseInt(value, 10);
3510 if (!isNaN(n)) this._retry = n;
3511 }
3512 }
3513 this._lastEventId = lastId;
3514 // A trailing non-blank data block without the final blank line is NOT
3515 // dispatched (spec: incomplete event at stream end).
3516 };
3517
3518 EventSource.prototype.close = function() {
3519 if (this._timer) { clearTimeout(this._timer); this._timer = null; }
3520 this.readyState = CLOSED;
3521 };
3522
3523 EventSource.prototype.addEventListener = function(type, cb) {
3524 if (typeof cb !== 'function') return;
3525 if (!this._listeners[type]) this._listeners[type] = [];
3526 this._listeners[type].push(cb);
3527 };
3528 EventSource.prototype.removeEventListener = function(type, cb) {
3529 var list = this._listeners[type];
3530 if (!list) return;
3531 var i = list.indexOf(cb);
3532 if (i !== -1) list.splice(i, 1);
3533 };
3534
3535 _g.EventSource = EventSource;
3536
3537 // ── window/document: CLI leaves them UNDEFINED (team-lead ruling, Node
3538 // parity). An "exists but every property is a throwing placeholder" gate
3539 // is itself the silent-fake shape (feature detection sees a window that
3540 // lies about existing); honest MISSING — typeof window === 'undefined' —
3541 // matches Node and the DOMParser philosophy. Browser pages keep servo's
3542 // real window/document; bare references in CLI throw ReferenceError, which
3543 // IS the explicit signal.
3544})();
3545"#;
3546 unsafe {
3547 let raw = cx.raw_cx();
3548 let mut rval = UndefinedValue();
3549 let opts = mozjs::glue::NewCompileOptions(raw, c"event_source".as_ptr(), 1);
3550 if !opts.is_null() {
3551 let mut src_text = mozjs::rust::transform_str_to_source_text(src);
3552 mozjs_sys::jsapi::JS::Evaluate2(
3553 raw,
3554 opts,
3555 &mut src_text,
3556 MutableHandle::<Value> {
3557 _phantom_0: ::std::marker::PhantomData,
3558 ptr: &mut rval,
3559 },
3560 );
3561 libc::free(opts as *mut _);
3562 }
3563 let _ = global;
3564 }
3565}
3566
3567#[cfg(test)]
3568mod tests {
3569 use super::*;
3570
3571 #[test]
3572 fn parse_ws_url_ws() {
3573 let (host, port, path) = split_authority_and_path("example.com/chat", "ws");
3574 assert_eq!(host, "example.com");
3575 assert_eq!(port, 80);
3576 assert_eq!(path, "/chat");
3577 }
3578
3579 #[test]
3582 fn parse_ws_url_wss_default_port() {
3583 let (host, port, path) = split_authority_and_path("example.com/secure", "wss");
3584 assert_eq!(host, "example.com");
3585 assert_eq!(port, 443);
3586 assert_eq!(path, "/secure");
3587 }
3588
3589 #[test]
3590 fn parse_ws_url_with_port() {
3591 let (host, port, path) = split_authority_and_path("localhost:8080/ws", "ws");
3592 assert_eq!(host, "localhost");
3593 assert_eq!(port, 8080);
3594 assert_eq!(path, "/ws");
3595 }
3596
3597 #[test]
3598 fn parse_ws_url_default_path() {
3599 let (_, _, path) = split_authority_and_path("host/", "ws");
3600 assert_eq!(path, "/");
3601 }
3602
3603 #[test]
3604 fn parse_ws_url_no_path_defaults_to_slash() {
3605 let (_, _, path) = split_authority_and_path("host", "ws");
3606 assert_eq!(path, "/");
3607 }
3608
3609 #[test]
3610 fn parse_ws_url_empty_string() {
3611 let (host, port, path) = split_authority_and_path("", "ws");
3612 assert_eq!(host, "");
3613 assert_eq!(port, 80);
3614 assert_eq!(path, "/");
3615 }
3616
3617 #[test]
3618 fn parse_ws_url_ipv4_with_port() {
3619 let (host, port, path) = split_authority_and_path("127.0.0.1:9222/json", "ws");
3620 assert_eq!(host, "127.0.0.1");
3621 assert_eq!(port, 9222);
3622 assert_eq!(path, "/json");
3623 }
3624
3625 #[test]
3626 fn parse_ws_url_query_string() {
3627 let (host, port, path) = split_authority_and_path("example.com/ws?token=abc", "ws");
3628 assert_eq!(host, "example.com");
3629 assert_eq!(port, 80);
3630 assert!(path.starts_with("/ws"));
3631 }
3632
3633 #[test]
3634 fn parse_ws_url_deep_path() {
3635 let (host, _, path) = split_authority_and_path("host/a/b/c/d", "ws");
3636 assert_eq!(host, "host");
3637 assert_eq!(path, "/a/b/c/d");
3638 }
3639
3640 #[test]
3645 fn push_masked_len_empty() {
3646 let mut frame = Vec::new();
3647 push_masked_len(&mut frame, 0);
3648 assert_eq!(frame.len(), 1);
3649 assert_eq!(frame[0] & 0x7F, 0); }
3651
3652 #[test]
3653 fn push_masked_len_short() {
3654 let mut frame = Vec::new();
3655 push_masked_len(&mut frame, 5);
3656 assert_eq!(frame.len(), 1);
3657 assert_eq!(frame[0] & 0x7F, 5); }
3659
3660 #[test]
3661 fn push_masked_len_medium() {
3662 let mut frame = Vec::new();
3663 let payload = vec![0u8; 200];
3664 push_masked_len(&mut frame, payload.len());
3665 assert_eq!(frame.len(), 3);
3667 assert_eq!(frame[0] & 0x7F, 126); let ext_len = u16::from_be_bytes([frame[1], frame[2]]);
3669 assert_eq!(ext_len, 200);
3670 }
3671
3672 #[test]
3673 fn push_masked_len_large() {
3674 let mut frame = Vec::new();
3675 let payload = vec![0u8; 70000];
3676 push_masked_len(&mut frame, payload.len());
3677 assert_eq!(frame.len(), 9);
3679 assert_eq!(frame[0] & 0x7F, 127); }
3681
3682 #[test]
3683 fn ws_message_debug_variants() {
3684 let text = WsMessage::Text("hello".to_string());
3685 let binary = WsMessage::Binary(vec![1, 2, 3]);
3686 let close = WsMessage::Close;
3687 assert!(format!("{:?}", text).contains("Text"));
3688 assert!(format!("{:?}", binary).contains("Binary"));
3689 assert!(format!("{:?}", close).contains("Close"));
3690 }
3691
3692 #[test]
3695 fn ws_entry_liveness_transitions() {
3696 let connecting = WsEntry {
3697 client: None,
3698 connect_slot: Some(Arc::new(Mutex::new(None))),
3699 close_requested: false,
3700 close_initiated: false,
3701 realm_global: ::std::ptr::null_mut(),
3702 js_obj_key: "ws_test".to_string(),
3703 };
3704 assert!(connecting.is_live(), "connecting entry is live");
3705
3706 let dead = WsEntry {
3707 client: None,
3708 connect_slot: None,
3709 close_requested: false,
3710 close_initiated: false,
3711 realm_global: ::std::ptr::null_mut(),
3712 js_obj_key: "ws_test".to_string(),
3713 };
3714 assert!(
3715 !dead.is_live(),
3716 "dead entry (post-close/failure) is not live"
3717 );
3718 }
3719
3720 #[test]
3724 fn fetch_stealth_profile_getter_roundtrip() {
3725 let saved = crate::fetch_api::get_fetch_stealth_profile();
3726 crate::fetch_api::set_fetch_stealth_profile(Some(
3727 bao_stealth::StealthProfile::firefox_default(),
3728 ));
3729 let got = crate::fetch_api::get_fetch_stealth_profile();
3730 crate::fetch_api::set_fetch_stealth_profile(saved);
3731 let p = got.expect("profile must round-trip");
3732 assert!(
3733 !p.tls.cipher_suites.is_empty(),
3734 "firefox profile must carry cipher suites (wss stealth source)"
3735 );
3736 }
3737}