1use ::std::sync::Arc;
20use ::std::sync::atomic::AtomicBool;
21
22use bun_core::ZBox;
23
24use mozjs::conversions::unsafe_jsstr_to_string;
25use mozjs::jsapi::*;
26use mozjs::jsval::{Int32Value, JSVal, ObjectValue, StringValue, UndefinedValue};
27use mozjs::rooted;
28use mozjs::rust::wrappers2::JS_DefineFunction;
29
30thread_local! {
31 static TL_STEALTH_PROFILE: ::std::cell::RefCell<Option<bao_stealth::StealthProfile>> = const { ::std::cell::RefCell::new(None) };
32}
33
34pub fn set_fetch_stealth_profile(profile: Option<bao_stealth::StealthProfile>) {
36 TL_STEALTH_PROFILE.with(|p| *p.borrow_mut() = profile);
37}
38
39pub fn is_fetch_stealth_profile_set() -> bool {
41 TL_STEALTH_PROFILE.with(|p| p.borrow().is_some())
42}
43
44pub fn get_fetch_stealth_profile() -> Option<bao_stealth::StealthProfile> {
49 TL_STEALTH_PROFILE.with(|p| p.borrow().clone())
50}
51
52pub fn ensure_default_fetch_stealth_profile() {
55 if !is_fetch_stealth_profile_set() {
56 set_fetch_stealth_profile(Some(bao_stealth::StealthProfile::firefox_default()));
57 }
58}
59
60pub fn install_fetch_global(
61 cx: &mut mozjs::context::JSContext,
62 global: mozjs::rust::Handle<*mut JSObject>,
63) {
64 unsafe {
65 JS_DefineFunction(
66 cx,
67 global,
68 c"fetch".as_ptr(),
69 ::std::option::Option::Some(fetch_fn),
70 1,
71 JSPROP_ENUMERATE as u32,
72 );
73 }
74}
75
76#[allow(unsafe_op_in_unsafe_fn)]
77unsafe extern "C" fn fetch_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
78 let args = CallArgs::from_vp(vp, argc);
79 let wrapped_cx = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
80 if argc == 0 {
81 JS_ReportErrorUTF8(cx, c"fetch requires a URL or Request argument".as_ptr());
82 return false;
83 }
84
85 let input_val = *args.get(0).ptr;
92 let url: String;
93 let mut method: String = "GET".to_string();
94 let mut headers: Vec<(String, String)> = Vec::new();
95 let mut body: Option<Vec<u8>> = None;
96 let mut signal_val: Option<JSVal> = None;
100 let mut tls_init: Option<crate::fetch_async::FetchTlsInit> = None;
104
105 if input_val.is_string() {
106 url = crate::js_to_rust_string(cx, input_val);
107 } else if input_val.is_object() {
108 rooted!(&in(wrapped_cx) let req_obj = input_val.to_object());
110 {
114 let sv = get_val_prop(cx, req_obj.handle(), "_signal");
115 if sv.is_object() {
116 signal_val = Some(sv);
117 }
118 }
119 match get_string_prop(cx, req_obj.handle().into(), "url") {
120 ::std::option::Option::Some(u) => url = u,
121 ::std::option::Option::None => {
122 JS_ReportErrorUTF8(
123 cx,
124 c"fetch requires a string URL or a Request object".as_ptr(),
125 );
126 return false;
127 }
128 }
129 if let ::std::option::Option::Some(m) =
130 get_string_prop(cx, req_obj.handle().into(), "method")
131 {
132 method = m;
133 }
134 let mut h_val = UndefinedValue();
135 JS_GetProperty(
136 cx,
137 req_obj.handle().into(),
138 c"headers".as_ptr(),
139 MutableHandle::<Value> {
140 _phantom_0: ::std::marker::PhantomData,
141 ptr: &mut h_val,
142 },
143 );
144 if h_val.is_object() {
145 headers = parse_headers_init(cx, h_val);
146 }
147 if let ::std::option::Option::Some(t) =
149 get_string_prop(cx, req_obj.handle().into(), "_bodyText")
150 {
151 body = Some(t.into_bytes());
152 } else {
153 let mut b_val = UndefinedValue();
154 JS_GetProperty(
155 cx,
156 req_obj.handle().into(),
157 c"_bodyBytes".as_ptr(),
158 MutableHandle::<Value> {
159 _phantom_0: ::std::marker::PhantomData,
160 ptr: &mut b_val,
161 },
162 );
163 if b_val.is_object() {
164 body = crate::node_buffer::collect_byte_view(cx, b_val);
165 } else {
166 let mut blob_val = UndefinedValue();
167 JS_GetProperty(
168 cx,
169 req_obj.handle().into(),
170 c"_bodyBlob".as_ptr(),
171 MutableHandle::<Value> {
172 _phantom_0: ::std::marker::PhantomData,
173 ptr: &mut blob_val,
174 },
175 );
176 if blob_val.is_object() {
177 match extract_blob_bytes(cx, blob_val) {
178 Ok(b) => body = b,
179 Err(msg) => {
180 let c_msg = ZBox::from_bytes(msg.as_bytes());
181 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
182 return false;
183 }
184 }
185 } else {
186 let mut fd_val = UndefinedValue();
191 JS_GetProperty(
192 cx,
193 req_obj.handle().into(),
194 c"_bodyFormData".as_ptr(),
195 MutableHandle::<Value> {
196 _phantom_0: ::std::marker::PhantomData,
197 ptr: &mut fd_val,
198 },
199 );
200 if fd_val.is_object() {
201 match extract_formdata_multipart(cx, fd_val, &mut headers) {
202 Ok(b) => body = b,
203 Err(msg) => {
204 let c_msg = ZBox::from_bytes(msg.as_bytes());
205 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
206 return false;
207 }
208 }
209 }
210 }
211 }
212 }
213 } else {
214 JS_ReportErrorUTF8(
215 cx,
216 c"fetch requires a string URL or a Request object".as_ptr(),
217 );
218 return false;
219 }
220
221 if argc > 1 {
223 let opts = *args.get(1).ptr;
224 if opts.is_object() {
225 rooted!(&in(wrapped_cx) let opts_obj = opts.to_object());
227 if let ::std::option::Option::Some(m) =
228 get_string_prop(cx, opts_obj.handle().into(), "method")
229 {
230 method = m;
231 }
232 let mut h_val = UndefinedValue();
233 bao_stealth::engine_props::get_property_clearing(
237 cx,
238 opts_obj.handle().into(),
239 c"headers",
240 &mut h_val,
241 );
242 if h_val.is_object() {
243 headers = parse_headers_init(cx, h_val);
244 }
245 let mut has_body = false;
252 bao_stealth::engine_props::has_property_clearing(
253 cx,
254 opts_obj.handle().into(),
255 c"body",
256 &mut has_body,
257 );
258 if has_body {
259 let mut b_val = UndefinedValue();
260 bao_stealth::engine_props::get_property_clearing(
261 cx,
262 opts_obj.handle().into(),
263 c"body",
264 &mut b_val,
265 );
266 match extract_body_bytes(cx, b_val, &mut headers) {
267 Ok(b) => body = b,
268 Err(msg) => {
269 let c_msg = ZBox::from_bytes(msg.as_bytes());
270 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
271 return false;
272 }
273 }
274 }
275 {
278 let sv = get_val_prop(cx, opts_obj.handle(), "signal");
279 if sv.is_object() {
280 signal_val = Some(sv);
281 }
282 }
283 {
287 let tv = get_val_prop(cx, opts_obj.handle(), "tls");
288 if !tv.is_undefined() && !tv.is_null() {
289 match parse_tls_init(cx, tv) {
290 Ok(t) => tls_init = Some(t),
291 Err(msg) => {
292 let c_msg = ZBox::from_bytes(msg.as_bytes());
293 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
294 return false;
295 }
296 }
297 }
298 }
299 }
300 }
301
302 if url.starts_with("data:") {
310 unsafe { handle_data_url_fetch(cx, &args, &method, &url) };
312 return true;
313 }
314
315 if let ::std::option::Option::Some(pos) = url.find("://") {
316 let host_part = &url[pos + 3..];
317 let host = host_part
318 .split('/')
319 .next()
320 .unwrap_or(host_part)
321 .split(':')
322 .next()
323 .unwrap_or(host_part);
324 if let ::std::result::Result::Err(e) = crate::permission_bridge::check_net(host) {
325 let c_msg = ZBox::from_bytes(e.as_bytes());
326 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
327 return false;
328 }
329 }
330
331 let method_upper = method.to_uppercase();
339 let Some(bun_method) = bun_http::Method::which(method_upper.as_bytes()) else {
340 let msg = format!(
341 "fetch: HTTP method \"{}\" is not supported by the Bao HTTP wire layer (supported: IANA method registry tokens such as GET/POST/PROPFIND/REPORT)",
342 method_upper
343 );
344 let c_msg = ZBox::from_bytes(msg.as_bytes());
345 JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
346 return false;
347 };
348
349 let mut signal_active: Option<JSVal> = None;
355 let mut signal_pre_aborted = false;
356 if let Some(sv) = signal_val {
357 if sv.is_object() {
358 rooted!(&in(wrapped_cx) let sig_obj = sv.to_object());
359 if is_abort_signal_shape(cx, sig_obj.handle()) {
360 let mut ab_val = UndefinedValue();
361 JS_GetProperty(
362 cx,
363 sig_obj.handle().into(),
364 c"aborted".as_ptr(),
365 MutableHandle::<Value> {
366 _phantom_0: ::std::marker::PhantomData,
367 ptr: &mut ab_val,
368 },
369 );
370 if ab_val.is_boolean() && ab_val.to_boolean() {
371 signal_pre_aborted = true;
372 } else {
373 signal_active = Some(sv);
374 }
375 }
376 }
377 }
378
379 rooted!(&in(wrapped_cx) let null_global = ::std::ptr::null_mut::<JSObject>());
382 let promise = mozjs_sys::jsapi::JS::NewPromiseObject(cx, null_global.handle().into());
383 if promise.is_null() {
384 args.rval().set(UndefinedValue());
385 return true;
386 }
387
388 let promise_val = ObjectValue(promise);
389
390 if signal_pre_aborted {
391 rooted!(&in(wrapped_cx) let promise_obj = promise);
394 unsafe {
396 crate::fetch_async::reject_promise_with_abort_error(cx, promise_obj.handle().into());
397 }
398 args.rval().set(promise_val);
399 return true;
400 }
401
402 let profile: Option<bao_stealth::StealthProfile> =
403 TL_STEALTH_PROFILE.with(|p| p.borrow().clone());
404
405 if let Some(sv) = signal_active {
406 let abort_id = crate::fetch_async::new_abort_id();
409 let flag = Arc::new(AtomicBool::new(false));
410 unsafe {
412 crate::fetch_async::start_fetch(
413 cx,
414 promise_val,
415 profile,
416 bun_method,
417 url,
418 headers,
419 body,
420 ::std::option::Option::Some(crate::fetch_async::AbortRequest {
421 id: abort_id,
422 flag: ::std::sync::Arc::clone(&flag),
423 }),
424 tls_init,
425 );
426 register_abort_listener(cx, sv, abort_id);
427 }
428 args.rval().set(promise_val);
429 return true;
430 }
431
432 unsafe {
434 crate::fetch_async::start_fetch(
435 cx,
436 promise_val,
437 profile,
438 bun_method,
439 url,
440 headers,
441 body,
442 None,
443 tls_init,
444 );
445 }
446
447 args.rval().set(promise_val);
448 true
449}
450
451fn parse_data_url(url: &str) -> ::std::result::Result<(String, Vec<u8>), String> {
459 let rest = &url["data:".len()..];
460 let Some(comma) = rest.find(',') else {
461 return Err("fetch data: URL is missing the comma (,) delimiter".to_string());
462 };
463 let header = &rest[..comma];
464 let data = &rest[comma + 1..];
465
466 let base64 = header.len() >= 7 && header[..].to_ascii_lowercase().ends_with(";base64");
468 let mime_raw = if base64 {
469 &header[..header.len() - ";base64".len()]
470 } else {
471 header
472 };
473 let mime = if !mime_raw.is_empty() && mime_raw.contains('/') {
476 mime_raw.to_string()
477 } else {
478 "text/plain;charset=US-ASCII".to_string()
479 };
480
481 let decoded = percent_decode(data.as_bytes());
482 if base64 {
483 let cleaned: Vec<u8> = decoded
490 .iter()
491 .copied()
492 .filter(|&b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r' | b'\x0c'))
493 .collect();
494 let invalid_payload = "fetch data: URL has an invalid base64 payload".to_string();
495 let data_end = cleaned
497 .iter()
498 .position(|&b| b == b'=')
499 .unwrap_or(cleaned.len());
500 if cleaned.len() % 4 == 1
501 || cleaned.iter().any(|&b| {
502 !b.is_ascii_alphanumeric() && b != b'+' && b != b'/' && b != b'='
503 })
504 || cleaned[data_end..].iter().any(|&b| b != b'=')
505 || cleaned.len() - data_end > 2
506 {
507 return Err(invalid_payload);
508 }
509 bun_base64::decode_alloc(&cleaned)
510 .map(|v| (mime, v))
511 .map_err(|_| invalid_payload)
512 } else {
513 Ok((mime, decoded))
514 }
515}
516
517fn percent_decode(input: &[u8]) -> Vec<u8> {
520 fn hex_val(b: u8) -> Option<u8> {
521 match b {
522 b'0'..=b'9' => Some(b - b'0'),
523 b'a'..=b'f' => Some(b - b'a' + 10),
524 b'A'..=b'F' => Some(b - b'A' + 10),
525 _ => None,
526 }
527 }
528 let mut out = Vec::with_capacity(input.len());
529 let mut i = 0;
530 while i < input.len() {
531 if input[i] == b'%' && i + 2 < input.len() {
532 if let (Some(hi), Some(lo)) = (hex_val(input[i + 1]), hex_val(input[i + 2])) {
533 out.push(hi << 4 | lo);
534 i += 3;
535 continue;
536 }
537 }
538 out.push(input[i]);
539 i += 1;
540 }
541 out
542}
543
544#[allow(unsafe_op_in_unsafe_fn)]
553unsafe fn reject_promise_type_error(
554 cx: *mut JSContext,
555 promise_h: Handle<*mut JSObject>,
556 msg: &str,
557) {
558 let mut wrapped_cx =
559 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
560 let cx_ref = &mut wrapped_cx;
561 let global = JS::CurrentGlobalOrNull(cx);
562 if !global.is_null() {
563 rooted!(&in(cx_ref) let global_root = global);
564 let mut te_val = UndefinedValue();
565 JS_GetProperty(
566 cx,
567 global_root.handle().into(),
568 c"TypeError".as_ptr(),
569 MutableHandle::<Value> {
570 _phantom_0: ::std::marker::PhantomData,
571 ptr: &mut te_val,
572 },
573 );
574 if te_val.is_object() {
575 rooted!(&in(cx_ref) let te_obj = te_val.to_object());
576 rooted!(&in(cx_ref) let te_fn = ObjectValue(te_obj.get()));
577 let c_msg = ZBox::from_bytes(msg.as_bytes());
578 let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
579 if !msg_js.is_null() {
580 rooted!(&in(cx_ref) let msg_root = StringValue(&*msg_js));
581 let elems = [msg_root.get()];
582 let call_args = HandleValueArray {
583 length_: 1,
584 elements_: elems.as_ptr(),
585 };
586 rooted!(&in(cx_ref) let undef_this = ::std::ptr::null_mut::<JSObject>());
587 let mut err_val = UndefinedValue();
588 let called = JS_CallFunctionValue(
589 cx,
590 undef_this.handle().into(),
591 te_fn.handle().into(),
592 &call_args,
593 MutableHandle::<Value> {
594 _phantom_0: ::std::marker::PhantomData,
595 ptr: &mut err_val,
596 },
597 );
598 if called && err_val.is_object() {
599 rooted!(&in(cx_ref) let err_root = err_val);
600 JS::RejectPromise(cx, promise_h, err_root.handle().into());
601 return;
602 }
603 }
604 }
605 }
606 rooted!(&in(cx_ref) let err_obj = JS_NewPlainObject(cx));
608 let c_msg = ZBox::from_bytes(msg.as_bytes());
609 let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
610 if !err_obj.is_null() && !msg_js.is_null() {
611 rooted!(&in(cx_ref) let msg_root = StringValue(&*msg_js));
612 JS_DefineProperty(
613 cx,
614 err_obj.handle().into(),
615 c"message".as_ptr(),
616 msg_root.handle().into(),
617 JSPROP_ENUMERATE as u32,
618 );
619 }
620 rooted!(&in(cx_ref) let ev = if err_obj.is_null() {
621 UndefinedValue()
622 } else {
623 ObjectValue(err_obj.get())
624 });
625 JS::RejectPromise(cx, promise_h, ev.handle().into());
626}
627
628#[allow(unsafe_op_in_unsafe_fn)]
638unsafe fn handle_data_url_fetch(cx: *mut JSContext, args: &CallArgs, method: &str, url: &str) {
639 let mut wrapped_cx =
640 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
641 let cx_ref = &mut wrapped_cx;
642
643 rooted!(&in(cx_ref) let null_global = ::std::ptr::null_mut::<JSObject>());
644 let promise = mozjs_sys::jsapi::JS::NewPromiseObject(cx, null_global.handle().into());
645 if promise.is_null() {
646 args.rval().set(UndefinedValue());
647 return;
648 }
649 args.rval().set(ObjectValue(promise));
650 rooted!(&in(cx_ref) let promise_root = promise);
651 let promise_h = promise_root.handle().into();
652
653 let method_upper = method.to_uppercase();
656 let outcome: ::std::result::Result<(String, Vec<u8>), String> =
657 if method_upper != "GET" && method_upper != "HEAD" {
658 Err(format!(
659 "fetch data: URL only supports GET/HEAD requests (got {})",
660 method_upper
661 ))
662 } else {
663 parse_data_url(url)
664 };
665
666 let (mime, bytes) = match outcome {
667 Ok(v) => v,
668 Err(msg) => {
669 reject_promise_type_error(cx, promise_h, &msg);
670 return;
671 }
672 };
673
674 let global = JS::CurrentGlobalOrNull(cx);
678 if global.is_null() {
679 reject_promise_type_error(cx, promise_h, "fetch data: no realm global");
680 return;
681 }
682 rooted!(&in(cx_ref) let global_root = global);
683 let mut resp_ctor_val = UndefinedValue();
684 JS_GetProperty(
685 cx,
686 global_root.handle().into(),
687 c"Response".as_ptr(),
688 MutableHandle::<Value> {
689 _phantom_0: ::std::marker::PhantomData,
690 ptr: &mut resp_ctor_val,
691 },
692 );
693 if !resp_ctor_val.is_object() {
694 reject_promise_type_error(
697 cx,
698 promise_h,
699 "fetch data: Response class is not available in this realm",
700 );
701 return;
702 }
703 rooted!(&in(cx_ref) let resp_ctor = resp_ctor_val.to_object());
704 rooted!(&in(cx_ref) let resp_fn = ObjectValue(resp_ctor.get()));
705
706 rooted!(&in(cx_ref) let body_arr = if method_upper == "HEAD" {
708 ::std::ptr::null_mut::<JSObject>()
709 } else {
710 mozjs_sys::jsapi::JS_NewUint8Array(cx, bytes.len())
711 });
712 if method_upper != "HEAD" && body_arr.is_null() {
713 reject_promise_type_error(cx, promise_h, "fetch data: body allocation failed");
714 return;
715 }
716 if method_upper != "HEAD" && !bytes.is_empty() {
717 let mut ta_len: usize = 0;
718 let mut shared = false;
719 let mut data: *mut u8 = ::std::ptr::null_mut();
720 let unwrapped = JS_GetObjectAsUint8Array(body_arr.get(), &mut ta_len, &mut shared, &mut data);
721 if unwrapped.is_null() || data.is_null() || ta_len < bytes.len() {
722 reject_promise_type_error(cx, promise_h, "fetch data: body view failed");
723 return;
724 }
725 ::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len());
726 }
727
728 rooted!(&in(cx_ref) let init_obj = JS_NewPlainObject(cx));
730 if init_obj.is_null() {
731 reject_promise_type_error(cx, promise_h, "fetch data: init allocation failed");
732 return;
733 }
734 rooted!(&in(cx_ref) let status_val = Int32Value(200));
735 JS_DefineProperty(
736 cx,
737 init_obj.handle().into(),
738 c"status".as_ptr(),
739 status_val.handle().into(),
740 JSPROP_ENUMERATE as u32,
741 );
742 let st_js = JS_NewStringCopyZ(cx, c"OK".as_ptr());
743 if !st_js.is_null() {
744 rooted!(&in(cx_ref) let st_val = StringValue(&*st_js));
745 JS_DefineProperty(
746 cx,
747 init_obj.handle().into(),
748 c"statusText".as_ptr(),
749 st_val.handle().into(),
750 JSPROP_ENUMERATE as u32,
751 );
752 }
753 rooted!(&in(cx_ref) let headers_obj = JS_NewPlainObject(cx));
754 if !headers_obj.is_null() {
755 let c_mime = ZBox::from_bytes(mime.as_bytes());
756 let mime_js = JS_NewStringCopyZ(cx, c_mime.as_ptr());
757 if !mime_js.is_null() {
758 rooted!(&in(cx_ref) let mime_val = StringValue(&*mime_js));
759 JS_DefineProperty(
760 cx,
761 headers_obj.handle().into(),
762 c"content-type".as_ptr(),
763 mime_val.handle().into(),
764 JSPROP_ENUMERATE as u32,
765 );
766 }
767 rooted!(&in(cx_ref) let hv = ObjectValue(headers_obj.get()));
768 JS_DefineProperty(
769 cx,
770 init_obj.handle().into(),
771 c"headers".as_ptr(),
772 hv.handle().into(),
773 JSPROP_ENUMERATE as u32,
774 );
775 }
776
777 let elems = [
778 if method_upper == "HEAD" {
779 UndefinedValue()
780 } else {
781 ObjectValue(body_arr.get())
782 },
783 ObjectValue(init_obj.get()),
784 ];
785 let call_args = HandleValueArray {
786 length_: 2,
787 elements_: elems.as_ptr(),
788 };
789 rooted!(&in(cx_ref) let undef_this = ::std::ptr::null_mut::<JSObject>());
790 let mut resp_val = UndefinedValue();
791 let called = JS_CallFunctionValue(
792 cx,
793 undef_this.handle().into(),
794 resp_fn.handle().into(),
795 &call_args,
796 MutableHandle::<Value> {
797 _phantom_0: ::std::marker::PhantomData,
798 ptr: &mut resp_val,
799 },
800 );
801 if !called || !resp_val.is_object() {
802 reject_promise_type_error(
803 cx,
804 promise_h,
805 "fetch data: failed to construct Response",
806 );
807 return;
808 }
809 rooted!(&in(cx_ref) let resp_root = resp_val);
810 JS::ResolvePromise(cx, promise_h, resp_root.handle().into());
811}
812
813#[allow(non_snake_case)]
820#[allow(unsafe_op_in_unsafe_fn)]
821unsafe extern "C" fn bao_abort_listener_native(
822 cx: *mut JSContext,
823 argc: u32,
824 vp: *mut JSVal,
825) -> bool {
826 let args = CallArgs::from_vp(vp, argc);
827 let callee_v = args.calleev();
828 if callee_v.is_object() {
829 let mut wrapped_cx =
830 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
831 let cx_ref = &mut wrapped_cx;
832 rooted!(&in(cx_ref) let callee_obj = callee_v.to_object());
834 let mut id_val = UndefinedValue();
835 JS_GetProperty(
836 cx,
837 callee_obj.handle().into(),
838 c"_abortId".as_ptr(),
839 MutableHandle::<Value> {
840 _phantom_0: ::std::marker::PhantomData,
841 ptr: &mut id_val,
842 },
843 );
844 if id_val.is_int32() {
845 crate::fetch_async::trigger_abort(id_val.to_int32() as u32);
846 }
847 }
848 args.rval().set(UndefinedValue());
849 true
850}
851
852#[allow(unsafe_op_in_unsafe_fn)]
863unsafe fn register_abort_listener(cx: *mut JSContext, signal_val: JSVal, abort_id: u32) {
864 unsafe {
865 let mut wrapped_cx =
866 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
867 let cx_ref = &mut wrapped_cx;
868 rooted!(&in(cx_ref) let signal_obj = signal_val.to_object());
870
871 let listener_fn = JS_NewFunction(
872 cx,
873 Some(bao_abort_listener_native),
874 1,
875 0,
876 c"__baoFetchAbort".as_ptr(),
877 );
878 let listener_fn = JS_NewFunction(
879 cx,
880 Some(bao_abort_listener_native),
881 1,
882 0,
883 c"__baoFetchAbort".as_ptr(),
884 );
885 if listener_fn.is_null() {
886 return;
887 }
888 let listener_obj = JS_GetFunctionObject(listener_fn);
889 if listener_obj.is_null() {
890 return;
891 }
892 rooted!(&in(cx_ref) let listener = listener_obj);
894
895 rooted!(&in(cx_ref) let id_val = Int32Value(abort_id as i32));
899 JS_DefineProperty(
900 cx,
901 listener.handle().into(),
902 c"_abortId".as_ptr(),
903 id_val.handle().into(),
904 (JSPROP_PERMANENT | JSPROP_READONLY) as u32,
905 );
906
907 let c_type = ZBox::from_bytes(b"abort");
909 let type_js = JS_NewStringCopyZ(cx, c_type.as_ptr());
910 if type_js.is_null() {
911 return;
912 }
913 rooted!(&in(cx_ref) let type_val = StringValue(&*type_js));
914 rooted!(&in(cx_ref) let listener_val = ObjectValue(listener.get()));
915 let call_args_arr = [type_val.get(), listener_val.get()];
916 let call_args = HandleValueArray {
917 length_: call_args_arr.len(),
918 elements_: call_args_arr.as_ptr(),
919 };
920 let mut rval = UndefinedValue();
921 let added = JS_CallFunctionName(
922 cx,
923 signal_obj.handle().into(),
924 c"addEventListener".as_ptr(),
925 &call_args,
926 MutableHandle::<Value> {
927 _phantom_0: ::std::marker::PhantomData,
928 ptr: &mut rval,
929 },
930 );
931 if !added {
932 let mut exn = UndefinedValue();
940 JS_GetPendingException(
941 cx,
942 MutableHandle::<Value> {
943 _phantom_0: ::std::marker::PhantomData,
944 ptr: &mut exn,
945 },
946 );
947 JS_ClearPendingException(cx);
948 rooted!(&in(cx_ref) let reason_root = exn);
949 if !exn.is_undefined() {
950 crate::uncaught::route_uncaught_exception(cx, exn);
951 }
952 }
953 }
954}
955
956#[allow(unsafe_op_in_unsafe_fn)]
965unsafe fn is_abort_signal_shape(
966 cx: *mut JSContext,
967 obj: mozjs::rust::Handle<*mut JSObject>,
968) -> bool {
969 unsafe {
970 let ab_val = get_val_prop(cx, obj, "aborted");
971 if !ab_val.is_boolean() {
972 return false;
973 }
974 let ael_val = get_val_prop(cx, obj, "addEventListener");
975 ael_val.is_object() && IsCallable(ael_val.to_object())
976 }
977}
978
979#[allow(unsafe_op_in_unsafe_fn)]
988unsafe fn get_string_prop(
989 cx: *mut JSContext,
990 obj: mozjs::rust::Handle<*mut JSObject>,
991 name: &str,
992) -> Option<String> {
993 unsafe {
994 let c_name = ZBox::from_bytes(name.as_bytes());
995 let mut v = UndefinedValue();
996 bao_stealth::engine_props::get_property_clearing(
1005 cx,
1006 obj.into(),
1007 c_name.as_cstr(),
1008 &mut v,
1009 );
1010 if v.is_string() {
1011 Some(crate::js_to_rust_string(cx, v))
1012 } else {
1013 None
1014 }
1015 }
1016}
1017
1018#[allow(unsafe_op_in_unsafe_fn)]
1033unsafe fn is_global_ctor(cx: *mut JSContext, constructor_val: JSVal, global_name: &str) -> bool {
1034 unsafe {
1035 if !constructor_val.is_object() {
1036 return false;
1037 }
1038 let global = mozjs_sys::jsapi::JS::CurrentGlobalOrNull(cx);
1039 if global.is_null() {
1040 return false;
1041 }
1042 let wrapped_cx =
1043 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1044 rooted!(&in(wrapped_cx) let global_rooted = global);
1046 let c_name = ZBox::from_bytes(global_name.as_bytes());
1047 let mut g_val = UndefinedValue();
1048 JS_GetProperty(
1049 cx,
1050 global_rooted.handle().into(),
1051 c_name.as_ptr(),
1052 MutableHandle::<Value> {
1053 _phantom_0: ::std::marker::PhantomData,
1054 ptr: &mut g_val,
1055 },
1056 );
1057 g_val.is_object() && g_val.to_object() == constructor_val.to_object()
1058 }
1059}
1060
1061#[allow(unsafe_op_in_unsafe_fn)]
1075unsafe fn is_url_search_params_shape(
1076 cx: *mut JSContext,
1077 obj: mozjs::rust::Handle<*mut JSObject>,
1078) -> bool {
1079 unsafe {
1080 for name in ["append", "getAll", "entries", "forEach"] {
1081 let c_name = ZBox::from_bytes(name.as_bytes());
1082 let mut v = UndefinedValue();
1083 JS_GetProperty(
1084 cx,
1085 obj.into(),
1086 c_name.as_ptr(),
1087 MutableHandle::<Value> {
1088 _phantom_0: ::std::marker::PhantomData,
1089 ptr: &mut v,
1090 },
1091 );
1092 if !v.is_object() || !IsCallable(v.to_object()) {
1093 return false;
1094 }
1095 }
1096 true
1097 }
1098}
1099
1100#[allow(unsafe_op_in_unsafe_fn)]
1107unsafe fn extract_blob_bytes(
1108 cx: *mut JSContext,
1109 blob_val: JSVal,
1110) -> ::std::result::Result<Option<Vec<u8>>, String> {
1111 unsafe {
1112 let wrapped_cx =
1113 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1114 rooted!(&in(wrapped_cx) let blob = blob_val.to_object());
1116 let mut chunks_val = UndefinedValue();
1117 JS_GetProperty(
1118 cx,
1119 blob.handle().into(),
1120 c"_chunks".as_ptr(),
1121 MutableHandle::<Value> {
1122 _phantom_0: ::std::marker::PhantomData,
1123 ptr: &mut chunks_val,
1124 },
1125 );
1126 if !chunks_val.is_object() {
1127 return Err("fetch: Blob bodies without synchronous byte storage are not supported yet (no streaming request-body infrastructure)".to_string());
1131 }
1132 rooted!(&in(wrapped_cx) let chunks = chunks_val.to_object());
1134 let mut is_array = false;
1135 rooted!(&in(wrapped_cx) let arr_probe = chunks_val);
1136 IsArrayObject(cx, arr_probe.handle().into(), &mut is_array);
1137 if !is_array {
1138 return Err("fetch: Blob bodies without synchronous byte storage are not supported yet (no streaming request-body infrastructure)".to_string());
1139 }
1140 let mut len_val = UndefinedValue();
1141 JS_GetProperty(
1142 cx,
1143 chunks.handle().into(),
1144 c"length".as_ptr(),
1145 MutableHandle::<Value> {
1146 _phantom_0: ::std::marker::PhantomData,
1147 ptr: &mut len_val,
1148 },
1149 );
1150 let len = if len_val.is_int32() && len_val.to_int32() > 0 {
1151 len_val.to_int32() as usize
1152 } else {
1153 0
1154 };
1155 let mut out: Vec<u8> = Vec::new();
1156 for i in 0..len as u32 {
1157 let mut el = UndefinedValue();
1158 JS_GetElement(
1159 cx,
1160 chunks.handle().into(),
1161 i,
1162 MutableHandle::<Value> {
1163 _phantom_0: ::std::marker::PhantomData,
1164 ptr: &mut el,
1165 },
1166 );
1167 match crate::node_buffer::collect_byte_view(cx, el) {
1168 ::std::option::Option::Some(bytes) => out.extend_from_slice(&bytes),
1169 ::std::option::Option::None => {
1170 return Err("fetch: Blob chunk is not a byte view".to_string());
1171 }
1172 }
1173 }
1174 Ok(Some(out))
1175 }
1176}
1177
1178enum MultipartValue {
1198 Text(String),
1200 File {
1202 filename: String,
1203 content_type: String,
1204 bytes: Vec<u8>,
1205 },
1206}
1207
1208fn generate_multipart_boundary() -> ::std::result::Result<String, String> {
1210 let mut raw = [0u8; 16];
1211 getrandom::fill(&mut raw)
1212 .map_err(|e| format!("fetch: multipart boundary randomness unavailable: {}", e))?;
1213 let mut s = String::with_capacity("----WebKitFormBoundary".len() + 32);
1214 s.push_str("----WebKitFormBoundary");
1215 for b in raw {
1216 s.push_str(&format!("{:02x}", b));
1217 }
1218 Ok(s)
1219}
1220
1221fn encode_multipart(entries: &[(String, MultipartValue)], boundary: &str) -> Vec<u8> {
1224 let mut out: Vec<u8> = Vec::new();
1225 for (name, value) in entries {
1226 out.extend_from_slice(b"--");
1227 out.extend_from_slice(boundary.as_bytes());
1228 out.extend_from_slice(b"\r\n");
1229 out.extend_from_slice(b"Content-Disposition: form-data; name=\"");
1230 out.extend_from_slice(name.as_bytes());
1231 match value {
1232 MultipartValue::Text(text) => {
1233 out.extend_from_slice(b"\"\r\n\r\n");
1234 out.extend_from_slice(text.as_bytes());
1235 }
1236 MultipartValue::File {
1237 filename,
1238 content_type,
1239 bytes,
1240 } => {
1241 out.extend_from_slice(b"\"; filename=\"");
1242 out.extend_from_slice(filename.as_bytes());
1243 out.extend_from_slice(b"\"\r\n");
1244 out.extend_from_slice(b"Content-Type: ");
1245 out.extend_from_slice(content_type.as_bytes());
1246 out.extend_from_slice(b"\r\n\r\n");
1247 out.extend_from_slice(bytes);
1248 }
1249 }
1250 out.extend_from_slice(b"\r\n");
1251 }
1252 out.extend_from_slice(b"--");
1253 out.extend_from_slice(boundary.as_bytes());
1254 out.extend_from_slice(b"--\r\n");
1255 out
1256}
1257
1258#[allow(unsafe_op_in_unsafe_fn)]
1266unsafe fn val_to_rust_string(
1267 cx: *mut JSContext,
1268 v: JSVal,
1269) -> ::std::result::Result<String, String> {
1270 unsafe {
1271 if v.is_string() {
1272 return Ok(crate::js_to_rust_string(cx, v));
1273 }
1274 if v.is_null_or_undefined() {
1275 return Ok(String::new());
1276 }
1277 let mut wrapped_cx =
1278 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1279 rooted!(&in(wrapped_cx) let root = v);
1280 let jsstr = mozjs::rust::ToString(&mut wrapped_cx, root.handle());
1281 if jsstr.is_null() {
1282 return Err(
1283 "fetch: FormData entry name/value could not be converted to string".to_string(),
1284 );
1285 }
1286 let str_val = StringValue(&*jsstr);
1287 Ok(crate::js_to_rust_string(cx, str_val))
1288 }
1289}
1290
1291#[allow(unsafe_op_in_unsafe_fn)]
1297unsafe fn get_val_prop(
1298 cx: *mut JSContext,
1299 obj: mozjs::rust::Handle<*mut JSObject>,
1300 name: &str,
1301) -> JSVal {
1302 unsafe {
1303 let c_name = ZBox::from_bytes(name.as_bytes());
1304 let mut v = UndefinedValue();
1305 bao_stealth::engine_props::get_property_clearing(
1309 cx,
1310 obj.into(),
1311 c_name.as_cstr(),
1312 &mut v,
1313 );
1314 v
1315 }
1316}
1317
1318#[allow(unsafe_op_in_unsafe_fn)]
1327unsafe fn is_formdata_shape(cx: *mut JSContext, obj: mozjs::rust::Handle<*mut JSObject>) -> bool {
1328 unsafe {
1329 let data_val = get_val_prop(cx, obj, "_data");
1330 if !data_val.is_object() {
1331 return false;
1332 }
1333 let wrapped_cx =
1334 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1335 rooted!(&in(wrapped_cx) let data_probe = data_val);
1336 let mut is_array = false;
1337 IsArrayObject(cx, data_probe.handle().into(), &mut is_array);
1338 if !is_array {
1339 return false;
1340 }
1341 let get_all = get_val_prop(cx, obj, "getAll");
1342 get_all.is_object() && IsCallable(get_all.to_object())
1343 }
1344}
1345
1346#[allow(unsafe_op_in_unsafe_fn)]
1357unsafe fn extract_formdata_multipart(
1358 cx: *mut JSContext,
1359 formdata_val: JSVal,
1360 headers: &mut Vec<(String, String)>,
1361) -> ::std::result::Result<Option<Vec<u8>>, String> {
1362 unsafe {
1363 let wrapped_cx =
1364 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1365 rooted!(&in(wrapped_cx) let form = formdata_val.to_object());
1367
1368 let data_val = get_val_prop(cx, form.handle(), "_data");
1369 if !data_val.is_object() {
1370 return Err("fetch: FormData body has no _data entries array".to_string());
1371 }
1372 rooted!(&in(wrapped_cx) let data = data_val.to_object());
1374 let mut is_array = false;
1375 rooted!(&in(wrapped_cx) let arr_probe = data_val);
1376 IsArrayObject(cx, arr_probe.handle().into(), &mut is_array);
1377 if !is_array {
1378 return Err("fetch: FormData body has no _data entries array".to_string());
1379 }
1380 let len_val = get_val_prop(cx, data.handle(), "length");
1381 let len = if len_val.is_int32() && len_val.to_int32() > 0 {
1382 len_val.to_int32() as usize
1383 } else {
1384 0
1385 };
1386
1387 let boundary = generate_multipart_boundary()?;
1388 let mut entries: Vec<(String, MultipartValue)> = Vec::new();
1389 for i in 0..len as u32 {
1390 let mut el = UndefinedValue();
1391 JS_GetElement(
1392 cx,
1393 data.handle().into(),
1394 i,
1395 MutableHandle::<Value> {
1396 _phantom_0: ::std::marker::PhantomData,
1397 ptr: &mut el,
1398 },
1399 );
1400 if !el.is_object() {
1401 return Err("fetch: FormData entry is not an object".to_string());
1402 }
1403 rooted!(&in(wrapped_cx) let entry = el.to_object());
1405 let name_val = get_val_prop(cx, entry.handle(), "name");
1406 let name = val_to_rust_string(cx, name_val)?;
1407 let value_val = get_val_prop(cx, entry.handle(), "value");
1408
1409 if value_val.is_object() {
1410 rooted!(&in(wrapped_cx) let blob = value_val.to_object());
1413 let filename_val = get_val_prop(cx, entry.handle(), "filename");
1414 let mut filename = if filename_val.is_string() {
1415 ::std::option::Option::Some(crate::js_to_rust_string(cx, filename_val))
1416 } else {
1417 ::std::option::Option::None
1418 };
1419 if filename.as_deref().map_or(true, |f| f.is_empty()) {
1420 let name_prop = get_val_prop(cx, blob.handle(), "name");
1421 filename = if name_prop.is_string() {
1422 ::std::option::Option::Some(crate::js_to_rust_string(cx, name_prop))
1423 } else {
1424 ::std::option::Option::Some("blob".to_string())
1425 };
1426 }
1427 let type_prop = get_val_prop(cx, blob.handle(), "type");
1428 let content_type = if type_prop.is_string() {
1429 let t = crate::js_to_rust_string(cx, type_prop);
1430 if t.is_empty() {
1431 "application/octet-stream".to_string()
1432 } else {
1433 t
1434 }
1435 } else {
1436 "application/octet-stream".to_string()
1437 };
1438 let bytes = extract_blob_bytes(cx, value_val)?
1439 .ok_or_else(|| {
1440 "fetch: FormData file entry without synchronous byte storage is not supported yet (no streaming request-body infrastructure)".to_string()
1441 })?;
1442 entries.push((
1443 name,
1444 MultipartValue::File {
1445 filename: filename.unwrap_or_else(|| "blob".to_string()),
1446 content_type,
1447 bytes,
1448 },
1449 ));
1450 } else {
1451 entries.push((
1452 name,
1453 MultipartValue::Text(val_to_rust_string(cx, value_val)?),
1454 ));
1455 }
1456 }
1457
1458 let body = encode_multipart(&entries, &boundary);
1459 let has_ct = headers
1460 .iter()
1461 .any(|(n, _)| n.eq_ignore_ascii_case("content-type"));
1462 if !has_ct {
1463 headers.push((
1464 "Content-Type".to_string(),
1465 format!("multipart/form-data; boundary={}", boundary),
1466 ));
1467 }
1468 Ok(Some(body))
1469 }
1470}
1471
1472#[allow(unsafe_op_in_unsafe_fn)]
1485unsafe fn extract_body_bytes(
1486 cx: *mut JSContext,
1487 body_val: JSVal,
1488 headers: &mut Vec<(String, String)>,
1489) -> ::std::result::Result<Option<Vec<u8>>, String> {
1490 unsafe {
1491 if body_val.is_null_or_undefined() {
1492 return Ok(None);
1493 }
1494 if body_val.is_string() {
1495 return Ok(Some(crate::js_to_rust_string(cx, body_val).into_bytes()));
1496 }
1497 if !body_val.is_object() {
1498 return Err(format!(
1499 "fetch: unsupported body type (expected string / BufferSource / Blob / URLSearchParams / FormData)"
1500 ));
1501 }
1502 let wrapped_cx =
1503 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1504 rooted!(&in(wrapped_cx) let obj = body_val.to_object());
1506
1507 if let ::std::option::Option::Some(bytes) =
1509 crate::node_buffer::collect_byte_view(cx, body_val)
1510 {
1511 return Ok(Some(bytes));
1512 }
1513
1514 let mut ctor_val = UndefinedValue();
1516 JS_GetProperty(
1517 cx,
1518 obj.handle().into(),
1519 c"constructor".as_ptr(),
1520 MutableHandle::<Value> {
1521 _phantom_0: ::std::marker::PhantomData,
1522 ptr: &mut ctor_val,
1523 },
1524 );
1525
1526 if is_formdata_shape(cx, obj.handle()) || is_global_ctor(cx, ctor_val, "FormData") {
1530 return extract_formdata_multipart(cx, body_val, headers);
1531 }
1532
1533 if is_url_search_params_shape(cx, obj.handle()) {
1535 let mut s_val = UndefinedValue();
1537 let called = JS_CallFunctionName(
1538 cx,
1539 obj.handle().into(),
1540 c"toString".as_ptr(),
1541 &HandleValueArray::empty(),
1542 MutableHandle::<Value> {
1543 _phantom_0: ::std::marker::PhantomData,
1544 ptr: &mut s_val,
1545 },
1546 );
1547 if !called || !s_val.is_string() {
1548 return Err("fetch: URLSearchParams body could not be serialized".to_string());
1549 }
1550 let has_ct = headers
1551 .iter()
1552 .any(|(n, _)| n.eq_ignore_ascii_case("content-type"));
1553 if !has_ct {
1554 headers.push((
1555 "Content-Type".to_string(),
1556 "application/x-www-form-urlencoded;charset=UTF-8".to_string(),
1557 ));
1558 }
1559 return Ok(Some(crate::js_to_rust_string(cx, s_val).into_bytes()));
1560 }
1561
1562 let mut size_val = UndefinedValue();
1565 JS_GetProperty(
1566 cx,
1567 obj.handle().into(),
1568 c"size".as_ptr(),
1569 MutableHandle::<Value> {
1570 _phantom_0: ::std::marker::PhantomData,
1571 ptr: &mut size_val,
1572 },
1573 );
1574 let mut ab_val = UndefinedValue();
1575 JS_GetProperty(
1576 cx,
1577 obj.handle().into(),
1578 c"arrayBuffer".as_ptr(),
1579 MutableHandle::<Value> {
1580 _phantom_0: ::std::marker::PhantomData,
1581 ptr: &mut ab_val,
1582 },
1583 );
1584 if size_val.is_number() && ab_val.is_object() && IsCallable(ab_val.to_object()) {
1585 return extract_blob_bytes(cx, body_val);
1586 }
1587
1588 Err("fetch: unsupported body type (expected string / BufferSource / Blob / URLSearchParams / FormData; streams are not supported)".to_string())
1589 }
1590}
1591
1592const MAX_HEADER_ENTRIES: usize = 1024;
1598
1599#[allow(unsafe_op_in_unsafe_fn)]
1616unsafe fn parse_headers_init(cx: *mut JSContext, headers_val: JSVal) -> Vec<(String, String)> {
1617 unsafe {
1618 let mut out: Vec<(String, String)> = Vec::new();
1619 if !headers_val.is_object() {
1620 return out;
1621 }
1622 let wrapped_cx =
1623 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1624 rooted!(&in(wrapped_cx) let obj = headers_val.to_object());
1626
1627 let mut is_array = false;
1629 rooted!(&in(wrapped_cx) let obj_val = headers_val);
1630 IsArrayObject(cx, obj_val.handle().into(), &mut is_array);
1631 if is_array {
1632 let mut len_val = UndefinedValue();
1633 JS_GetProperty(
1634 cx,
1635 obj.handle().into(),
1636 c"length".as_ptr(),
1637 MutableHandle::<Value> {
1638 _phantom_0: ::std::marker::PhantomData,
1639 ptr: &mut len_val,
1640 },
1641 );
1642 let len = if len_val.is_int32() && len_val.to_int32() > 0 {
1643 (len_val.to_int32() as usize).min(MAX_HEADER_ENTRIES)
1644 } else {
1645 0
1646 };
1647 for i in 0..len as u32 {
1648 let mut el_val = UndefinedValue();
1649 JS_GetElement(
1650 cx,
1651 obj.handle().into(),
1652 i,
1653 MutableHandle::<Value> {
1654 _phantom_0: ::std::marker::PhantomData,
1655 ptr: &mut el_val,
1656 },
1657 );
1658 if let Some(pair) = parse_header_entry(cx, el_val) {
1659 out.push(pair);
1660 }
1661 }
1662 return out;
1663 }
1664
1665 let mut entries_val = UndefinedValue();
1669 JS_GetProperty(
1670 cx,
1671 obj.handle().into(),
1672 c"entries".as_ptr(),
1673 MutableHandle::<Value> {
1674 _phantom_0: ::std::marker::PhantomData,
1675 ptr: &mut entries_val,
1676 },
1677 );
1678 if entries_val.is_object() && IsCallable(entries_val.to_object()) {
1679 rooted!(&in(wrapped_cx) let _entries_fn = entries_val.to_object());
1681 let mut iter_val = UndefinedValue();
1682 let called = JS_CallFunctionName(
1683 cx,
1684 obj.handle().into(),
1685 c"entries".as_ptr(),
1686 &HandleValueArray::empty(),
1687 MutableHandle::<Value> {
1688 _phantom_0: ::std::marker::PhantomData,
1689 ptr: &mut iter_val,
1690 },
1691 );
1692 if called && iter_val.is_object() {
1693 rooted!(&in(wrapped_cx) let iter = iter_val.to_object());
1695 loop {
1696 if out.len() >= MAX_HEADER_ENTRIES {
1697 break;
1698 }
1699 let mut next_val = UndefinedValue();
1700 let advanced = JS_CallFunctionName(
1701 cx,
1702 iter.handle().into(),
1703 c"next".as_ptr(),
1704 &HandleValueArray::empty(),
1705 MutableHandle::<Value> {
1706 _phantom_0: ::std::marker::PhantomData,
1707 ptr: &mut next_val,
1708 },
1709 );
1710 if !advanced || !next_val.is_object() {
1711 break;
1712 }
1713 rooted!(&in(wrapped_cx) let res = next_val.to_object());
1715 let mut done_val = UndefinedValue();
1716 JS_GetProperty(
1717 cx,
1718 res.handle().into(),
1719 c"done".as_ptr(),
1720 MutableHandle::<Value> {
1721 _phantom_0: ::std::marker::PhantomData,
1722 ptr: &mut done_val,
1723 },
1724 );
1725 if done_val.is_boolean() && done_val.to_boolean() {
1726 break;
1727 }
1728 let mut pair_val = UndefinedValue();
1729 JS_GetProperty(
1730 cx,
1731 res.handle().into(),
1732 c"value".as_ptr(),
1733 MutableHandle::<Value> {
1734 _phantom_0: ::std::marker::PhantomData,
1735 ptr: &mut pair_val,
1736 },
1737 );
1738 if let Some(pair) = parse_header_entry(cx, pair_val) {
1739 out.push(pair);
1740 }
1741 }
1742 }
1743 return out;
1744 }
1745
1746 let mut ids = mozjs::rust::IdVector::new(cx);
1750 if GetPropertyKeys(cx, obj.handle().into(), JSITER_OWNONLY, ids.handle_mut()) {
1751 for jsid in &*ids {
1752 if !jsid.is_string() {
1753 continue;
1754 }
1755 let key_str_ptr = jsid.to_string();
1756 if key_str_ptr.is_null() {
1757 continue;
1758 }
1759 let key =
1760 unsafe_jsstr_to_string(cx, ::std::ptr::NonNull::new_unchecked(key_str_ptr));
1761 let c_key = ZBox::from_bytes(key.as_bytes());
1762 let mut v_val = UndefinedValue();
1763 JS_GetProperty(
1764 cx,
1765 obj.handle().into(),
1766 c_key.as_ptr(),
1767 MutableHandle::<Value> {
1768 _phantom_0: ::std::marker::PhantomData,
1769 ptr: &mut v_val,
1770 },
1771 );
1772 if v_val.is_string() {
1773 out.push((key, crate::js_to_rust_string(cx, v_val)));
1774 }
1775 }
1776 }
1777 out
1778 }
1779}
1780
1781#[allow(unsafe_op_in_unsafe_fn)]
1789unsafe fn parse_header_entry(cx: *mut JSContext, pair_val: JSVal) -> Option<(String, String)> {
1790 unsafe {
1791 if !pair_val.is_object() {
1792 return None;
1793 }
1794 let wrapped_cx =
1795 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1796 rooted!(&in(wrapped_cx) let pair = pair_val.to_object());
1798
1799 let mut is_array = false;
1800 rooted!(&in(wrapped_cx) let pair_root = pair_val);
1801 IsArrayObject(cx, pair_root.handle().into(), &mut is_array);
1802 if is_array {
1803 let mut n_val = UndefinedValue();
1805 let mut v_val = UndefinedValue();
1806 JS_GetElement(
1807 cx,
1808 pair.handle().into(),
1809 0,
1810 MutableHandle::<Value> {
1811 _phantom_0: ::std::marker::PhantomData,
1812 ptr: &mut n_val,
1813 },
1814 );
1815 JS_GetElement(
1816 cx,
1817 pair.handle().into(),
1818 1,
1819 MutableHandle::<Value> {
1820 _phantom_0: ::std::marker::PhantomData,
1821 ptr: &mut v_val,
1822 },
1823 );
1824 if n_val.is_string() && v_val.is_string() {
1825 return Some((
1826 crate::js_to_rust_string(cx, n_val),
1827 crate::js_to_rust_string(cx, v_val),
1828 ));
1829 }
1830 return None;
1831 }
1832
1833 let mut n_val = UndefinedValue();
1835 let mut v_val = UndefinedValue();
1836 JS_GetProperty(
1837 cx,
1838 pair.handle().into(),
1839 c"name".as_ptr(),
1840 MutableHandle::<Value> {
1841 _phantom_0: ::std::marker::PhantomData,
1842 ptr: &mut n_val,
1843 },
1844 );
1845 JS_GetProperty(
1846 cx,
1847 pair.handle().into(),
1848 c"value".as_ptr(),
1849 MutableHandle::<Value> {
1850 _phantom_0: ::std::marker::PhantomData,
1851 ptr: &mut v_val,
1852 },
1853 );
1854 if n_val.is_string() && v_val.is_string() {
1855 return Some((
1856 crate::js_to_rust_string(cx, n_val),
1857 crate::js_to_rust_string(cx, v_val),
1858 ));
1859 }
1860 None
1861 }
1862}
1863
1864const MAX_CA_ENTRIES: usize = 256;
1871
1872#[allow(unsafe_op_in_unsafe_fn)]
1894unsafe fn parse_tls_init(
1895 cx: *mut JSContext,
1896 tls_val: JSVal,
1897) -> ::std::result::Result<crate::fetch_async::FetchTlsInit, String> {
1898 unsafe {
1899 if !tls_val.is_object() {
1900 return Err(
1901 "fetch: init.tls must be an object ({ ca, rejectUnauthorized, servername })"
1902 .to_string(),
1903 );
1904 }
1905 let wrapped_cx =
1906 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1907 rooted!(&in(wrapped_cx) let obj = tls_val.to_object());
1909
1910 let mut out = crate::fetch_async::FetchTlsInit::default();
1911
1912 let ca_val = get_val_prop(cx, obj.handle(), "ca");
1914 if !ca_val.is_undefined() && !ca_val.is_null() {
1915 let mut ders: Vec<Box<[u8]>> = Vec::new();
1916 collect_ca_ders(cx, ca_val, &mut ders)?;
1917 if ders.is_empty() {
1918 return Err("fetch: init.tls.ca contained no parseable certificate".to_string());
1919 }
1920 out.ca_certs_der = ders.into_boxed_slice();
1921 }
1922
1923 let ra_val = get_val_prop(cx, obj.handle(), "rejectUnauthorized");
1925 if !ra_val.is_undefined() && !ra_val.is_null() {
1926 if !ra_val.is_boolean() {
1927 return Err("fetch: init.tls.rejectUnauthorized must be a boolean".to_string());
1928 }
1929 out.reject_unauthorized = ::std::option::Option::Some(ra_val.to_boolean());
1930 }
1931
1932 let sn_val = get_val_prop(cx, obj.handle(), "servername");
1934 if !sn_val.is_undefined() && !sn_val.is_null() {
1935 if !sn_val.is_string() {
1936 return Err("fetch: init.tls.servername must be a string".to_string());
1937 }
1938 let sn = crate::js_to_rust_string(cx, sn_val);
1939 if sn.is_empty() {
1940 return Err(
1941 "fetch: init.tls.servername must be a non-empty host string".to_string(),
1942 );
1943 }
1944 if sn.as_bytes().contains(&0) {
1945 return Err("fetch: init.tls.servername must not contain NUL".to_string());
1946 }
1947 out.servername = ::std::option::Option::Some(sn);
1948 }
1949
1950 Ok(out)
1951 }
1952}
1953
1954#[allow(unsafe_op_in_unsafe_fn)]
1964unsafe fn collect_ca_ders(
1965 cx: *mut JSContext,
1966 val: JSVal,
1967 out: &mut Vec<Box<[u8]>>,
1968) -> ::std::result::Result<(), String> {
1969 unsafe {
1970 if out.len() >= MAX_CA_ENTRIES {
1971 return Err(format!("fetch: init.tls.ca exceeds {} entries", MAX_CA_ENTRIES));
1972 }
1973 if val.is_string() {
1976 let pem = crate::js_to_rust_string(cx, val);
1977 let ders = bao_boringssl_bridge::pem_parse_certs(&pem);
1978 if ders.is_empty() {
1979 return Err(
1980 "fetch: init.tls.ca PEM string contained no parseable certificate".to_string(),
1981 );
1982 }
1983 for der in ders {
1984 if out.len() >= MAX_CA_ENTRIES {
1985 break;
1986 }
1987 out.push(der.into_boxed_slice());
1988 }
1989 return Ok(());
1990 }
1991 if val.is_object() {
1992 let wrapped_cx =
1993 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1994 rooted!(&in(wrapped_cx) let obj = val.to_object());
1996
1997 let mut is_array = false;
1999 rooted!(&in(wrapped_cx) let probe = val);
2000 IsArrayObject(cx, probe.handle().into(), &mut is_array);
2001 if is_array {
2002 let mut len_val = UndefinedValue();
2003 JS_GetProperty(
2004 cx,
2005 obj.handle().into(),
2006 c"length".as_ptr(),
2007 MutableHandle::<Value> {
2008 _phantom_0: ::std::marker::PhantomData,
2009 ptr: &mut len_val,
2010 },
2011 );
2012 let len = if len_val.is_int32() && len_val.to_int32() > 0 {
2013 (len_val.to_int32() as usize).min(MAX_CA_ENTRIES)
2014 } else {
2015 0
2016 };
2017 for i in 0..len as u32 {
2018 let mut el = UndefinedValue();
2019 JS_GetElement(
2020 cx,
2021 obj.handle().into(),
2022 i,
2023 MutableHandle::<Value> {
2024 _phantom_0: ::std::marker::PhantomData,
2025 ptr: &mut el,
2026 },
2027 );
2028 collect_ca_ders(cx, el, out)?;
2029 }
2030 return Ok(());
2031 }
2032
2033 if let ::std::option::Option::Some(bytes) = crate::node_buffer::collect_byte_view(cx, val)
2038 {
2039 let looks_pem = bytes
2040 .windows(b"-----BEGIN".len())
2041 .any(|w| w == &b"-----BEGIN"[..]);
2042 if looks_pem {
2043 let pem = String::from_utf8_lossy(&bytes).into_owned();
2044 let ders = bao_boringssl_bridge::pem_parse_certs(&pem);
2045 if ders.is_empty() {
2046 return Err(
2047 "fetch: init.tls.ca PEM bytes contained no parseable certificate"
2048 .to_string(),
2049 );
2050 }
2051 for der in ders {
2052 if out.len() >= MAX_CA_ENTRIES {
2053 break;
2054 }
2055 out.push(der.into_boxed_slice());
2056 }
2057 return Ok(());
2058 }
2059 out.push(bytes.into_boxed_slice());
2060 return Ok(());
2061 }
2062 }
2063 Err("fetch: init.tls.ca entries must be PEM strings or DER byte views (Buffer/Uint8Array)".to_string())
2064 }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069 use super::{MultipartValue, encode_multipart, generate_multipart_boundary};
2070
2071 #[test]
2076 fn cors_bypass_fetch_global_installed_for_page() {
2077 let source = include_str!("fetch_api.rs");
2078 assert!(
2079 source.contains("pub fn install_fetch_global"),
2080 "REQ-SEC-001: install_fetch_global must be pub for page realm installation"
2081 );
2082 }
2083
2084 #[test]
2086 fn cors_bypass_fetch_uses_event_driven_no_cors() {
2087 let source = include_str!("fetch_api.rs");
2088 assert!(
2089 source.contains("crate::fetch_async::start"),
2090 "REQ-SEC-001: fetch must delegate to fetch_async::start"
2091 );
2092 let forbidden_cors = ["cors", "_check"].join("");
2094 assert!(
2095 !source.contains(&forbidden_cors),
2096 "REQ-SEC-001 REGRESSION: fetch must NOT contain cors check"
2097 );
2098 let forbidden_cors_preflight = ["Access-Control", "-Request-Method"].join("");
2100 assert!(
2101 !source.contains(&forbidden_cors_preflight),
2102 "REQ-SEC-001 REGRESSION: fetch must NOT send CORS preflight headers"
2103 );
2104 }
2105
2106 #[test]
2108 fn bce_010_no_spawn_or_drain() {
2109 let source = include_str!("fetch_api.rs");
2110 let forbidden_spawn = ["spawn", "_fetch_worker"].join("");
2112 let forbidden_drain = ["drain", "_pending_fetches"].join("");
2113 let forbidden_blocking = ["do_fetch", "_blocking"].join("");
2114 assert!(
2115 !source.contains(&forbidden_spawn),
2116 "BCE-010 REGRESSION: spawn fetch worker must be removed"
2117 );
2118 assert!(
2119 !source.contains(&forbidden_drain),
2120 "BCE-010 REGRESSION: drain pending fetches must be removed"
2121 );
2122 assert!(
2123 !source.contains(&forbidden_blocking),
2124 "BCE-010 REGRESSION: do fetch blocking must be removed"
2125 );
2126 }
2127
2128 #[test]
2132 fn bce_fetch_h_headers_not_dropped() {
2133 let source = include_str!("fetch_api.rs");
2134 let parse_call = ["parse_", "headers_init"].join("");
2135 assert!(
2136 source.contains(&parse_call),
2137 "BCE-20260814-FETCH-H REGRESSION: fetch_fn must parse init.headers"
2138 );
2139 let dropped_form = ["let headers: Vec<(String, String)> = ", "Vec::new();"].join("");
2140 assert!(
2141 !source.contains(&dropped_form),
2142 "BCE-20260814-FETCH-H REGRESSION: init.headers must not be dropped as an empty Vec"
2143 );
2144 let seq_form = ["[\"name\",\"value\"]"].join("");
2146 assert!(
2147 source.contains(&seq_form),
2148 "BCE-20260814-FETCH-H: sequence pair form must be documented/parseable"
2149 );
2150 }
2151
2152 #[test]
2157 fn init_tls_parsed_and_injected() {
2158 let source = include_str!("fetch_api.rs");
2159 let parse_call = ["parse_", "tls_init"].join("");
2160 assert!(
2161 source.contains(&parse_call),
2162 "TEST-ENG-FETCH-TLS REGRESSION: fetch_fn must parse init.tls"
2163 );
2164 let fail_closed = ["no parseable ", "certificate"].join("");
2167 assert!(
2168 source.contains(&fail_closed),
2169 "TEST-ENG-FETCH-TLS REGRESSION: unparseable init.tls.ca must fail closed"
2170 );
2171 assert!(
2173 source.contains("rejectUnauthorized"),
2174 "TEST-ENG-FETCH-TLS REGRESSION: rejectUnauthorized option missing"
2175 );
2176 assert!(
2178 source.contains("servername"),
2179 "TEST-ENG-FETCH-TLS REGRESSION: servername option missing"
2180 );
2181 }
2182
2183 const TEST_BOUNDARY: &str = "----WebKitFormBoundary0123456789abcdef0123456789abcdef";
2187
2188 #[test]
2191 fn multipart_encode_text_and_file_entries() {
2192 let entries = vec![
2193 (
2194 "field".to_string(),
2195 MultipartValue::Text("hello world".to_string()),
2196 ),
2197 (
2198 "upload".to_string(),
2199 MultipartValue::File {
2200 filename: "a.txt".to_string(),
2201 content_type: "text/plain".to_string(),
2202 bytes: b"file-bytes".to_vec(),
2203 },
2204 ),
2205 (
2206 "noType".to_string(),
2207 MultipartValue::File {
2208 filename: "blob".to_string(),
2209 content_type: "application/octet-stream".to_string(),
2210 bytes: vec![0u8, 1, 2],
2211 },
2212 ),
2213 ];
2214 let body = encode_multipart(&entries, TEST_BOUNDARY);
2215 let text = String::from_utf8_lossy(&body).to_string();
2216 let expected = concat!(
2217 "------WebKitFormBoundary0123456789abcdef0123456789abcdef\r\n",
2218 "Content-Disposition: form-data; name=\"field\"\r\n",
2219 "\r\n",
2220 "hello world\r\n",
2221 "------WebKitFormBoundary0123456789abcdef0123456789abcdef\r\n",
2222 "Content-Disposition: form-data; name=\"upload\"; filename=\"a.txt\"\r\n",
2223 "Content-Type: text/plain\r\n",
2224 "\r\n",
2225 "file-bytes\r\n",
2226 "------WebKitFormBoundary0123456789abcdef0123456789abcdef\r\n",
2227 "Content-Disposition: form-data; name=\"noType\"; filename=\"blob\"\r\n",
2228 "Content-Type: application/octet-stream\r\n",
2229 "\r\n",
2230 );
2231 assert!(
2232 text.starts_with(expected),
2233 "multipart per-entry framing mismatch:\n{}",
2234 text
2235 );
2236 assert!(
2237 text.ends_with("------WebKitFormBoundary0123456789abcdef0123456789abcdef--\r\n"),
2238 "multipart terminator missing:\n{}",
2239 text
2240 );
2241 assert!(body.windows(3).any(|w| w == [0u8, 1, 2]));
2243 }
2244
2245 #[test]
2248 fn multipart_encode_empty_formdata() {
2249 let body = encode_multipart(&[], TEST_BOUNDARY);
2250 assert_eq!(
2251 body,
2252 format!("{}--\r\n", format!("--{}", TEST_BOUNDARY)).into_bytes()
2253 );
2254 }
2255
2256 #[test]
2258 fn multipart_boundary_unique_per_generation() {
2259 let a = generate_multipart_boundary().expect("boundary gen");
2260 let b = generate_multipart_boundary().expect("boundary gen");
2261 assert_ne!(a, b, "multipart boundary repeated across generations");
2262 assert!(a.starts_with("----WebKitFormBoundary"));
2263 assert_eq!(a.len(), 22 + 32, "boundary must be prefix + 32 hex chars");
2264 assert!(
2265 a["----WebKitFormBoundary".len()..]
2266 .chars()
2267 .all(|c| c.is_ascii_hexdigit()),
2268 "boundary suffix must be hex"
2269 );
2270 }
2271}