use ::std::sync::Arc;
use ::std::sync::atomic::AtomicBool;
use bun_core::ZBox;
use mozjs::conversions::unsafe_jsstr_to_string;
use mozjs::jsapi::*;
use mozjs::jsval::{Int32Value, JSVal, ObjectValue, StringValue, UndefinedValue};
use mozjs::rooted;
use mozjs::rust::wrappers2::JS_DefineFunction;
thread_local! {
static TL_STEALTH_PROFILE: ::std::cell::RefCell<Option<bao_stealth::StealthProfile>> = const { ::std::cell::RefCell::new(None) };
}
pub fn set_fetch_stealth_profile(profile: Option<bao_stealth::StealthProfile>) {
TL_STEALTH_PROFILE.with(|p| *p.borrow_mut() = profile);
}
pub fn is_fetch_stealth_profile_set() -> bool {
TL_STEALTH_PROFILE.with(|p| p.borrow().is_some())
}
pub fn get_fetch_stealth_profile() -> Option<bao_stealth::StealthProfile> {
TL_STEALTH_PROFILE.with(|p| p.borrow().clone())
}
pub fn ensure_default_fetch_stealth_profile() {
if !is_fetch_stealth_profile_set() {
set_fetch_stealth_profile(Some(bao_stealth::StealthProfile::firefox_default()));
}
}
pub fn install_fetch_global(
cx: &mut mozjs::context::JSContext,
global: mozjs::rust::Handle<*mut JSObject>,
) {
unsafe {
JS_DefineFunction(
cx,
global,
c"fetch".as_ptr(),
::std::option::Option::Some(fetch_fn),
1,
JSPROP_ENUMERATE as u32,
);
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn fetch_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let wrapped_cx = mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
if argc == 0 {
JS_ReportErrorUTF8(cx, c"fetch requires a URL or Request argument".as_ptr());
return false;
}
let input_val = *args.get(0).ptr;
let url: String;
let mut method: String = "GET".to_string();
let mut headers: Vec<(String, String)> = Vec::new();
let mut body: Option<Vec<u8>> = None;
let mut signal_val: Option<JSVal> = None;
let mut tls_init: Option<crate::fetch_async::FetchTlsInit> = None;
if input_val.is_string() {
url = crate::js_to_rust_string(cx, input_val);
} else if input_val.is_object() {
rooted!(&in(wrapped_cx) let req_obj = input_val.to_object());
{
let sv = get_val_prop(cx, req_obj.handle(), "_signal");
if sv.is_object() {
signal_val = Some(sv);
}
}
match get_string_prop(cx, req_obj.handle().into(), "url") {
::std::option::Option::Some(u) => url = u,
::std::option::Option::None => {
JS_ReportErrorUTF8(
cx,
c"fetch requires a string URL or a Request object".as_ptr(),
);
return false;
}
}
if let ::std::option::Option::Some(m) =
get_string_prop(cx, req_obj.handle().into(), "method")
{
method = m;
}
let mut h_val = UndefinedValue();
JS_GetProperty(
cx,
req_obj.handle().into(),
c"headers".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut h_val,
},
);
if h_val.is_object() {
headers = parse_headers_init(cx, h_val);
}
if let ::std::option::Option::Some(t) =
get_string_prop(cx, req_obj.handle().into(), "_bodyText")
{
body = Some(t.into_bytes());
} else {
let mut b_val = UndefinedValue();
JS_GetProperty(
cx,
req_obj.handle().into(),
c"_bodyBytes".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut b_val,
},
);
if b_val.is_object() {
body = crate::node_buffer::collect_byte_view(cx, b_val);
} else {
let mut blob_val = UndefinedValue();
JS_GetProperty(
cx,
req_obj.handle().into(),
c"_bodyBlob".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut blob_val,
},
);
if blob_val.is_object() {
match extract_blob_bytes(cx, blob_val) {
Ok(b) => body = b,
Err(msg) => {
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
}
} else {
let mut fd_val = UndefinedValue();
JS_GetProperty(
cx,
req_obj.handle().into(),
c"_bodyFormData".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut fd_val,
},
);
if fd_val.is_object() {
match extract_formdata_multipart(cx, fd_val, &mut headers) {
Ok(b) => body = b,
Err(msg) => {
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
}
}
}
}
}
} else {
JS_ReportErrorUTF8(
cx,
c"fetch requires a string URL or a Request object".as_ptr(),
);
return false;
}
if argc > 1 {
let opts = *args.get(1).ptr;
if opts.is_object() {
rooted!(&in(wrapped_cx) let opts_obj = opts.to_object());
if let ::std::option::Option::Some(m) =
get_string_prop(cx, opts_obj.handle().into(), "method")
{
method = m;
}
let mut h_val = UndefinedValue();
bao_stealth::engine_props::get_property_clearing(
cx,
opts_obj.handle().into(),
c"headers",
&mut h_val,
);
if h_val.is_object() {
headers = parse_headers_init(cx, h_val);
}
let mut has_body = false;
bao_stealth::engine_props::has_property_clearing(
cx,
opts_obj.handle().into(),
c"body",
&mut has_body,
);
if has_body {
let mut b_val = UndefinedValue();
bao_stealth::engine_props::get_property_clearing(
cx,
opts_obj.handle().into(),
c"body",
&mut b_val,
);
match extract_body_bytes(cx, b_val, &mut headers) {
Ok(b) => body = b,
Err(msg) => {
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
}
}
{
let sv = get_val_prop(cx, opts_obj.handle(), "signal");
if sv.is_object() {
signal_val = Some(sv);
}
}
{
let tv = get_val_prop(cx, opts_obj.handle(), "tls");
if !tv.is_undefined() && !tv.is_null() {
match parse_tls_init(cx, tv) {
Ok(t) => tls_init = Some(t),
Err(msg) => {
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
}
}
}
}
}
if url.starts_with("data:") {
unsafe { handle_data_url_fetch(cx, &args, &method, &url) };
return true;
}
if let ::std::option::Option::Some(pos) = url.find("://") {
let host_part = &url[pos + 3..];
let host = host_part
.split('/')
.next()
.unwrap_or(host_part)
.split(':')
.next()
.unwrap_or(host_part);
if let ::std::result::Result::Err(e) = crate::permission_bridge::check_net(host) {
let c_msg = ZBox::from_bytes(e.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
}
let method_upper = method.to_uppercase();
let Some(bun_method) = bun_http::Method::which(method_upper.as_bytes()) else {
let msg = format!(
"fetch: HTTP method \"{}\" is not supported by the Bao HTTP wire layer (supported: IANA method registry tokens such as GET/POST/PROPFIND/REPORT)",
method_upper
);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
};
let mut signal_active: Option<JSVal> = None;
let mut signal_pre_aborted = false;
if let Some(sv) = signal_val {
if sv.is_object() {
rooted!(&in(wrapped_cx) let sig_obj = sv.to_object());
if is_abort_signal_shape(cx, sig_obj.handle()) {
let mut ab_val = UndefinedValue();
JS_GetProperty(
cx,
sig_obj.handle().into(),
c"aborted".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ab_val,
},
);
if ab_val.is_boolean() && ab_val.to_boolean() {
signal_pre_aborted = true;
} else {
signal_active = Some(sv);
}
}
}
}
rooted!(&in(wrapped_cx) let null_global = ::std::ptr::null_mut::<JSObject>());
let promise = mozjs_sys::jsapi::JS::NewPromiseObject(cx, null_global.handle().into());
if promise.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let promise_val = ObjectValue(promise);
if signal_pre_aborted {
rooted!(&in(wrapped_cx) let promise_obj = promise);
unsafe {
crate::fetch_async::reject_promise_with_abort_error(cx, promise_obj.handle().into());
}
args.rval().set(promise_val);
return true;
}
let profile: Option<bao_stealth::StealthProfile> =
TL_STEALTH_PROFILE.with(|p| p.borrow().clone());
if let Some(sv) = signal_active {
let abort_id = crate::fetch_async::new_abort_id();
let flag = Arc::new(AtomicBool::new(false));
unsafe {
crate::fetch_async::start_fetch(
cx,
promise_val,
profile,
bun_method,
url,
headers,
body,
::std::option::Option::Some(crate::fetch_async::AbortRequest {
id: abort_id,
flag: ::std::sync::Arc::clone(&flag),
}),
tls_init,
);
register_abort_listener(cx, sv, abort_id);
}
args.rval().set(promise_val);
return true;
}
unsafe {
crate::fetch_async::start_fetch(
cx,
promise_val,
profile,
bun_method,
url,
headers,
body,
None,
tls_init,
);
}
args.rval().set(promise_val);
true
}
fn parse_data_url(url: &str) -> ::std::result::Result<(String, Vec<u8>), String> {
let rest = &url["data:".len()..];
let Some(comma) = rest.find(',') else {
return Err("fetch data: URL is missing the comma (,) delimiter".to_string());
};
let header = &rest[..comma];
let data = &rest[comma + 1..];
let base64 = header.len() >= 7 && header[..].to_ascii_lowercase().ends_with(";base64");
let mime_raw = if base64 {
&header[..header.len() - ";base64".len()]
} else {
header
};
let mime = if !mime_raw.is_empty() && mime_raw.contains('/') {
mime_raw.to_string()
} else {
"text/plain;charset=US-ASCII".to_string()
};
let decoded = percent_decode(data.as_bytes());
if base64 {
let cleaned: Vec<u8> = decoded
.iter()
.copied()
.filter(|&b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r' | b'\x0c'))
.collect();
let invalid_payload = "fetch data: URL has an invalid base64 payload".to_string();
let data_end = cleaned
.iter()
.position(|&b| b == b'=')
.unwrap_or(cleaned.len());
if cleaned.len() % 4 == 1
|| cleaned.iter().any(|&b| {
!b.is_ascii_alphanumeric() && b != b'+' && b != b'/' && b != b'='
})
|| cleaned[data_end..].iter().any(|&b| b != b'=')
|| cleaned.len() - data_end > 2
{
return Err(invalid_payload);
}
bun_base64::decode_alloc(&cleaned)
.map(|v| (mime, v))
.map_err(|_| invalid_payload)
} else {
Ok((mime, decoded))
}
}
fn percent_decode(input: &[u8]) -> Vec<u8> {
fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
let mut out = Vec::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
if input[i] == b'%' && i + 2 < input.len() {
if let (Some(hi), Some(lo)) = (hex_val(input[i + 1]), hex_val(input[i + 2])) {
out.push(hi << 4 | lo);
i += 3;
continue;
}
}
out.push(input[i]);
i += 1;
}
out
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn reject_promise_type_error(
cx: *mut JSContext,
promise_h: Handle<*mut JSObject>,
msg: &str,
) {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let global = JS::CurrentGlobalOrNull(cx);
if !global.is_null() {
rooted!(&in(cx_ref) let global_root = global);
let mut te_val = UndefinedValue();
JS_GetProperty(
cx,
global_root.handle().into(),
c"TypeError".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut te_val,
},
);
if te_val.is_object() {
rooted!(&in(cx_ref) let te_obj = te_val.to_object());
rooted!(&in(cx_ref) let te_fn = ObjectValue(te_obj.get()));
let c_msg = ZBox::from_bytes(msg.as_bytes());
let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if !msg_js.is_null() {
rooted!(&in(cx_ref) let msg_root = StringValue(&*msg_js));
let elems = [msg_root.get()];
let call_args = HandleValueArray {
length_: 1,
elements_: elems.as_ptr(),
};
rooted!(&in(cx_ref) let undef_this = ::std::ptr::null_mut::<JSObject>());
let mut err_val = UndefinedValue();
let called = JS_CallFunctionValue(
cx,
undef_this.handle().into(),
te_fn.handle().into(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut err_val,
},
);
if called && err_val.is_object() {
rooted!(&in(cx_ref) let err_root = err_val);
JS::RejectPromise(cx, promise_h, err_root.handle().into());
return;
}
}
}
}
rooted!(&in(cx_ref) let err_obj = JS_NewPlainObject(cx));
let c_msg = ZBox::from_bytes(msg.as_bytes());
let msg_js = JS_NewStringCopyZ(cx, c_msg.as_ptr());
if !err_obj.is_null() && !msg_js.is_null() {
rooted!(&in(cx_ref) let msg_root = StringValue(&*msg_js));
JS_DefineProperty(
cx,
err_obj.handle().into(),
c"message".as_ptr(),
msg_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let ev = if err_obj.is_null() {
UndefinedValue()
} else {
ObjectValue(err_obj.get())
});
JS::RejectPromise(cx, promise_h, ev.handle().into());
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn handle_data_url_fetch(cx: *mut JSContext, args: &CallArgs, method: &str, url: &str) {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let null_global = ::std::ptr::null_mut::<JSObject>());
let promise = mozjs_sys::jsapi::JS::NewPromiseObject(cx, null_global.handle().into());
if promise.is_null() {
args.rval().set(UndefinedValue());
return;
}
args.rval().set(ObjectValue(promise));
rooted!(&in(cx_ref) let promise_root = promise);
let promise_h = promise_root.handle().into();
let method_upper = method.to_uppercase();
let outcome: ::std::result::Result<(String, Vec<u8>), String> =
if method_upper != "GET" && method_upper != "HEAD" {
Err(format!(
"fetch data: URL only supports GET/HEAD requests (got {})",
method_upper
))
} else {
parse_data_url(url)
};
let (mime, bytes) = match outcome {
Ok(v) => v,
Err(msg) => {
reject_promise_type_error(cx, promise_h, &msg);
return;
}
};
let global = JS::CurrentGlobalOrNull(cx);
if global.is_null() {
reject_promise_type_error(cx, promise_h, "fetch data: no realm global");
return;
}
rooted!(&in(cx_ref) let global_root = global);
let mut resp_ctor_val = UndefinedValue();
JS_GetProperty(
cx,
global_root.handle().into(),
c"Response".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut resp_ctor_val,
},
);
if !resp_ctor_val.is_object() {
reject_promise_type_error(
cx,
promise_h,
"fetch data: Response class is not available in this realm",
);
return;
}
rooted!(&in(cx_ref) let resp_ctor = resp_ctor_val.to_object());
rooted!(&in(cx_ref) let resp_fn = ObjectValue(resp_ctor.get()));
rooted!(&in(cx_ref) let body_arr = if method_upper == "HEAD" {
::std::ptr::null_mut::<JSObject>()
} else {
mozjs_sys::jsapi::JS_NewUint8Array(cx, bytes.len())
});
if method_upper != "HEAD" && body_arr.is_null() {
reject_promise_type_error(cx, promise_h, "fetch data: body allocation failed");
return;
}
if method_upper != "HEAD" && !bytes.is_empty() {
let mut ta_len: usize = 0;
let mut shared = false;
let mut data: *mut u8 = ::std::ptr::null_mut();
let unwrapped = JS_GetObjectAsUint8Array(body_arr.get(), &mut ta_len, &mut shared, &mut data);
if unwrapped.is_null() || data.is_null() || ta_len < bytes.len() {
reject_promise_type_error(cx, promise_h, "fetch data: body view failed");
return;
}
::std::ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len());
}
rooted!(&in(cx_ref) let init_obj = JS_NewPlainObject(cx));
if init_obj.is_null() {
reject_promise_type_error(cx, promise_h, "fetch data: init allocation failed");
return;
}
rooted!(&in(cx_ref) let status_val = Int32Value(200));
JS_DefineProperty(
cx,
init_obj.handle().into(),
c"status".as_ptr(),
status_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
let st_js = JS_NewStringCopyZ(cx, c"OK".as_ptr());
if !st_js.is_null() {
rooted!(&in(cx_ref) let st_val = StringValue(&*st_js));
JS_DefineProperty(
cx,
init_obj.handle().into(),
c"statusText".as_ptr(),
st_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let headers_obj = JS_NewPlainObject(cx));
if !headers_obj.is_null() {
let c_mime = ZBox::from_bytes(mime.as_bytes());
let mime_js = JS_NewStringCopyZ(cx, c_mime.as_ptr());
if !mime_js.is_null() {
rooted!(&in(cx_ref) let mime_val = StringValue(&*mime_js));
JS_DefineProperty(
cx,
headers_obj.handle().into(),
c"content-type".as_ptr(),
mime_val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
rooted!(&in(cx_ref) let hv = ObjectValue(headers_obj.get()));
JS_DefineProperty(
cx,
init_obj.handle().into(),
c"headers".as_ptr(),
hv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let elems = [
if method_upper == "HEAD" {
UndefinedValue()
} else {
ObjectValue(body_arr.get())
},
ObjectValue(init_obj.get()),
];
let call_args = HandleValueArray {
length_: 2,
elements_: elems.as_ptr(),
};
rooted!(&in(cx_ref) let undef_this = ::std::ptr::null_mut::<JSObject>());
let mut resp_val = UndefinedValue();
let called = JS_CallFunctionValue(
cx,
undef_this.handle().into(),
resp_fn.handle().into(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut resp_val,
},
);
if !called || !resp_val.is_object() {
reject_promise_type_error(
cx,
promise_h,
"fetch data: failed to construct Response",
);
return;
}
rooted!(&in(cx_ref) let resp_root = resp_val);
JS::ResolvePromise(cx, promise_h, resp_root.handle().into());
}
#[allow(non_snake_case)]
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn bao_abort_listener_native(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, argc);
let callee_v = args.calleev();
if callee_v.is_object() {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let callee_obj = callee_v.to_object());
let mut id_val = UndefinedValue();
JS_GetProperty(
cx,
callee_obj.handle().into(),
c"_abortId".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut id_val,
},
);
if id_val.is_int32() {
crate::fetch_async::trigger_abort(id_val.to_int32() as u32);
}
}
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn register_abort_listener(cx: *mut JSContext, signal_val: JSVal, abort_id: u32) {
unsafe {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let signal_obj = signal_val.to_object());
let listener_fn = JS_NewFunction(
cx,
Some(bao_abort_listener_native),
1,
0,
c"__baoFetchAbort".as_ptr(),
);
let listener_fn = JS_NewFunction(
cx,
Some(bao_abort_listener_native),
1,
0,
c"__baoFetchAbort".as_ptr(),
);
if listener_fn.is_null() {
return;
}
let listener_obj = JS_GetFunctionObject(listener_fn);
if listener_obj.is_null() {
return;
}
rooted!(&in(cx_ref) let listener = listener_obj);
rooted!(&in(cx_ref) let id_val = Int32Value(abort_id as i32));
JS_DefineProperty(
cx,
listener.handle().into(),
c"_abortId".as_ptr(),
id_val.handle().into(),
(JSPROP_PERMANENT | JSPROP_READONLY) as u32,
);
let c_type = ZBox::from_bytes(b"abort");
let type_js = JS_NewStringCopyZ(cx, c_type.as_ptr());
if type_js.is_null() {
return;
}
rooted!(&in(cx_ref) let type_val = StringValue(&*type_js));
rooted!(&in(cx_ref) let listener_val = ObjectValue(listener.get()));
let call_args_arr = [type_val.get(), listener_val.get()];
let call_args = HandleValueArray {
length_: call_args_arr.len(),
elements_: call_args_arr.as_ptr(),
};
let mut rval = UndefinedValue();
let added = JS_CallFunctionName(
cx,
signal_obj.handle().into(),
c"addEventListener".as_ptr(),
&call_args,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
);
if !added {
let mut exn = UndefinedValue();
JS_GetPendingException(
cx,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut exn,
},
);
JS_ClearPendingException(cx);
rooted!(&in(cx_ref) let reason_root = exn);
if !exn.is_undefined() {
crate::uncaught::route_uncaught_exception(cx, exn);
}
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn is_abort_signal_shape(
cx: *mut JSContext,
obj: mozjs::rust::Handle<*mut JSObject>,
) -> bool {
unsafe {
let ab_val = get_val_prop(cx, obj, "aborted");
if !ab_val.is_boolean() {
return false;
}
let ael_val = get_val_prop(cx, obj, "addEventListener");
ael_val.is_object() && IsCallable(ael_val.to_object())
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn get_string_prop(
cx: *mut JSContext,
obj: mozjs::rust::Handle<*mut JSObject>,
name: &str,
) -> Option<String> {
unsafe {
let c_name = ZBox::from_bytes(name.as_bytes());
let mut v = UndefinedValue();
bao_stealth::engine_props::get_property_clearing(
cx,
obj.into(),
c_name.as_cstr(),
&mut v,
);
if v.is_string() {
Some(crate::js_to_rust_string(cx, v))
} else {
None
}
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn is_global_ctor(cx: *mut JSContext, constructor_val: JSVal, global_name: &str) -> bool {
unsafe {
if !constructor_val.is_object() {
return false;
}
let global = mozjs_sys::jsapi::JS::CurrentGlobalOrNull(cx);
if global.is_null() {
return false;
}
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let global_rooted = global);
let c_name = ZBox::from_bytes(global_name.as_bytes());
let mut g_val = UndefinedValue();
JS_GetProperty(
cx,
global_rooted.handle().into(),
c_name.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut g_val,
},
);
g_val.is_object() && g_val.to_object() == constructor_val.to_object()
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn is_url_search_params_shape(
cx: *mut JSContext,
obj: mozjs::rust::Handle<*mut JSObject>,
) -> bool {
unsafe {
for name in ["append", "getAll", "entries", "forEach"] {
let c_name = ZBox::from_bytes(name.as_bytes());
let mut v = UndefinedValue();
JS_GetProperty(
cx,
obj.into(),
c_name.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut v,
},
);
if !v.is_object() || !IsCallable(v.to_object()) {
return false;
}
}
true
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn extract_blob_bytes(
cx: *mut JSContext,
blob_val: JSVal,
) -> ::std::result::Result<Option<Vec<u8>>, String> {
unsafe {
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let blob = blob_val.to_object());
let mut chunks_val = UndefinedValue();
JS_GetProperty(
cx,
blob.handle().into(),
c"_chunks".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut chunks_val,
},
);
if !chunks_val.is_object() {
return Err("fetch: Blob bodies without synchronous byte storage are not supported yet (no streaming request-body infrastructure)".to_string());
}
rooted!(&in(wrapped_cx) let chunks = chunks_val.to_object());
let mut is_array = false;
rooted!(&in(wrapped_cx) let arr_probe = chunks_val);
IsArrayObject(cx, arr_probe.handle().into(), &mut is_array);
if !is_array {
return Err("fetch: Blob bodies without synchronous byte storage are not supported yet (no streaming request-body infrastructure)".to_string());
}
let mut len_val = UndefinedValue();
JS_GetProperty(
cx,
chunks.handle().into(),
c"length".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
},
);
let len = if len_val.is_int32() && len_val.to_int32() > 0 {
len_val.to_int32() as usize
} else {
0
};
let mut out: Vec<u8> = Vec::new();
for i in 0..len as u32 {
let mut el = UndefinedValue();
JS_GetElement(
cx,
chunks.handle().into(),
i,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut el,
},
);
match crate::node_buffer::collect_byte_view(cx, el) {
::std::option::Option::Some(bytes) => out.extend_from_slice(&bytes),
::std::option::Option::None => {
return Err("fetch: Blob chunk is not a byte view".to_string());
}
}
}
Ok(Some(out))
}
}
enum MultipartValue {
Text(String),
File {
filename: String,
content_type: String,
bytes: Vec<u8>,
},
}
fn generate_multipart_boundary() -> ::std::result::Result<String, String> {
let mut raw = [0u8; 16];
getrandom::fill(&mut raw)
.map_err(|e| format!("fetch: multipart boundary randomness unavailable: {}", e))?;
let mut s = String::with_capacity("----WebKitFormBoundary".len() + 32);
s.push_str("----WebKitFormBoundary");
for b in raw {
s.push_str(&format!("{:02x}", b));
}
Ok(s)
}
fn encode_multipart(entries: &[(String, MultipartValue)], boundary: &str) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
for (name, value) in entries {
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"\r\n");
out.extend_from_slice(b"Content-Disposition: form-data; name=\"");
out.extend_from_slice(name.as_bytes());
match value {
MultipartValue::Text(text) => {
out.extend_from_slice(b"\"\r\n\r\n");
out.extend_from_slice(text.as_bytes());
}
MultipartValue::File {
filename,
content_type,
bytes,
} => {
out.extend_from_slice(b"\"; filename=\"");
out.extend_from_slice(filename.as_bytes());
out.extend_from_slice(b"\"\r\n");
out.extend_from_slice(b"Content-Type: ");
out.extend_from_slice(content_type.as_bytes());
out.extend_from_slice(b"\r\n\r\n");
out.extend_from_slice(bytes);
}
}
out.extend_from_slice(b"\r\n");
}
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"--\r\n");
out
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn val_to_rust_string(
cx: *mut JSContext,
v: JSVal,
) -> ::std::result::Result<String, String> {
unsafe {
if v.is_string() {
return Ok(crate::js_to_rust_string(cx, v));
}
if v.is_null_or_undefined() {
return Ok(String::new());
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let root = v);
let jsstr = mozjs::rust::ToString(&mut wrapped_cx, root.handle());
if jsstr.is_null() {
return Err(
"fetch: FormData entry name/value could not be converted to string".to_string(),
);
}
let str_val = StringValue(&*jsstr);
Ok(crate::js_to_rust_string(cx, str_val))
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn get_val_prop(
cx: *mut JSContext,
obj: mozjs::rust::Handle<*mut JSObject>,
name: &str,
) -> JSVal {
unsafe {
let c_name = ZBox::from_bytes(name.as_bytes());
let mut v = UndefinedValue();
bao_stealth::engine_props::get_property_clearing(
cx,
obj.into(),
c_name.as_cstr(),
&mut v,
);
v
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn is_formdata_shape(cx: *mut JSContext, obj: mozjs::rust::Handle<*mut JSObject>) -> bool {
unsafe {
let data_val = get_val_prop(cx, obj, "_data");
if !data_val.is_object() {
return false;
}
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let data_probe = data_val);
let mut is_array = false;
IsArrayObject(cx, data_probe.handle().into(), &mut is_array);
if !is_array {
return false;
}
let get_all = get_val_prop(cx, obj, "getAll");
get_all.is_object() && IsCallable(get_all.to_object())
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn extract_formdata_multipart(
cx: *mut JSContext,
formdata_val: JSVal,
headers: &mut Vec<(String, String)>,
) -> ::std::result::Result<Option<Vec<u8>>, String> {
unsafe {
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let form = formdata_val.to_object());
let data_val = get_val_prop(cx, form.handle(), "_data");
if !data_val.is_object() {
return Err("fetch: FormData body has no _data entries array".to_string());
}
rooted!(&in(wrapped_cx) let data = data_val.to_object());
let mut is_array = false;
rooted!(&in(wrapped_cx) let arr_probe = data_val);
IsArrayObject(cx, arr_probe.handle().into(), &mut is_array);
if !is_array {
return Err("fetch: FormData body has no _data entries array".to_string());
}
let len_val = get_val_prop(cx, data.handle(), "length");
let len = if len_val.is_int32() && len_val.to_int32() > 0 {
len_val.to_int32() as usize
} else {
0
};
let boundary = generate_multipart_boundary()?;
let mut entries: Vec<(String, MultipartValue)> = Vec::new();
for i in 0..len as u32 {
let mut el = UndefinedValue();
JS_GetElement(
cx,
data.handle().into(),
i,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut el,
},
);
if !el.is_object() {
return Err("fetch: FormData entry is not an object".to_string());
}
rooted!(&in(wrapped_cx) let entry = el.to_object());
let name_val = get_val_prop(cx, entry.handle(), "name");
let name = val_to_rust_string(cx, name_val)?;
let value_val = get_val_prop(cx, entry.handle(), "value");
if value_val.is_object() {
rooted!(&in(wrapped_cx) let blob = value_val.to_object());
let filename_val = get_val_prop(cx, entry.handle(), "filename");
let mut filename = if filename_val.is_string() {
::std::option::Option::Some(crate::js_to_rust_string(cx, filename_val))
} else {
::std::option::Option::None
};
if filename.as_deref().map_or(true, |f| f.is_empty()) {
let name_prop = get_val_prop(cx, blob.handle(), "name");
filename = if name_prop.is_string() {
::std::option::Option::Some(crate::js_to_rust_string(cx, name_prop))
} else {
::std::option::Option::Some("blob".to_string())
};
}
let type_prop = get_val_prop(cx, blob.handle(), "type");
let content_type = if type_prop.is_string() {
let t = crate::js_to_rust_string(cx, type_prop);
if t.is_empty() {
"application/octet-stream".to_string()
} else {
t
}
} else {
"application/octet-stream".to_string()
};
let bytes = extract_blob_bytes(cx, value_val)?
.ok_or_else(|| {
"fetch: FormData file entry without synchronous byte storage is not supported yet (no streaming request-body infrastructure)".to_string()
})?;
entries.push((
name,
MultipartValue::File {
filename: filename.unwrap_or_else(|| "blob".to_string()),
content_type,
bytes,
},
));
} else {
entries.push((
name,
MultipartValue::Text(val_to_rust_string(cx, value_val)?),
));
}
}
let body = encode_multipart(&entries, &boundary);
let has_ct = headers
.iter()
.any(|(n, _)| n.eq_ignore_ascii_case("content-type"));
if !has_ct {
headers.push((
"Content-Type".to_string(),
format!("multipart/form-data; boundary={}", boundary),
));
}
Ok(Some(body))
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn extract_body_bytes(
cx: *mut JSContext,
body_val: JSVal,
headers: &mut Vec<(String, String)>,
) -> ::std::result::Result<Option<Vec<u8>>, String> {
unsafe {
if body_val.is_null_or_undefined() {
return Ok(None);
}
if body_val.is_string() {
return Ok(Some(crate::js_to_rust_string(cx, body_val).into_bytes()));
}
if !body_val.is_object() {
return Err(format!(
"fetch: unsupported body type (expected string / BufferSource / Blob / URLSearchParams / FormData)"
));
}
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let obj = body_val.to_object());
if let ::std::option::Option::Some(bytes) =
crate::node_buffer::collect_byte_view(cx, body_val)
{
return Ok(Some(bytes));
}
let mut ctor_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"constructor".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ctor_val,
},
);
if is_formdata_shape(cx, obj.handle()) || is_global_ctor(cx, ctor_val, "FormData") {
return extract_formdata_multipart(cx, body_val, headers);
}
if is_url_search_params_shape(cx, obj.handle()) {
let mut s_val = UndefinedValue();
let called = JS_CallFunctionName(
cx,
obj.handle().into(),
c"toString".as_ptr(),
&HandleValueArray::empty(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut s_val,
},
);
if !called || !s_val.is_string() {
return Err("fetch: URLSearchParams body could not be serialized".to_string());
}
let has_ct = headers
.iter()
.any(|(n, _)| n.eq_ignore_ascii_case("content-type"));
if !has_ct {
headers.push((
"Content-Type".to_string(),
"application/x-www-form-urlencoded;charset=UTF-8".to_string(),
));
}
return Ok(Some(crate::js_to_rust_string(cx, s_val).into_bytes()));
}
let mut size_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"size".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut size_val,
},
);
let mut ab_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"arrayBuffer".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut ab_val,
},
);
if size_val.is_number() && ab_val.is_object() && IsCallable(ab_val.to_object()) {
return extract_blob_bytes(cx, body_val);
}
Err("fetch: unsupported body type (expected string / BufferSource / Blob / URLSearchParams / FormData; streams are not supported)".to_string())
}
}
const MAX_HEADER_ENTRIES: usize = 1024;
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn parse_headers_init(cx: *mut JSContext, headers_val: JSVal) -> Vec<(String, String)> {
unsafe {
let mut out: Vec<(String, String)> = Vec::new();
if !headers_val.is_object() {
return out;
}
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let obj = headers_val.to_object());
let mut is_array = false;
rooted!(&in(wrapped_cx) let obj_val = headers_val);
IsArrayObject(cx, obj_val.handle().into(), &mut is_array);
if is_array {
let mut len_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"length".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
},
);
let len = if len_val.is_int32() && len_val.to_int32() > 0 {
(len_val.to_int32() as usize).min(MAX_HEADER_ENTRIES)
} else {
0
};
for i in 0..len as u32 {
let mut el_val = UndefinedValue();
JS_GetElement(
cx,
obj.handle().into(),
i,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut el_val,
},
);
if let Some(pair) = parse_header_entry(cx, el_val) {
out.push(pair);
}
}
return out;
}
let mut entries_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"entries".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut entries_val,
},
);
if entries_val.is_object() && IsCallable(entries_val.to_object()) {
rooted!(&in(wrapped_cx) let _entries_fn = entries_val.to_object());
let mut iter_val = UndefinedValue();
let called = JS_CallFunctionName(
cx,
obj.handle().into(),
c"entries".as_ptr(),
&HandleValueArray::empty(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut iter_val,
},
);
if called && iter_val.is_object() {
rooted!(&in(wrapped_cx) let iter = iter_val.to_object());
loop {
if out.len() >= MAX_HEADER_ENTRIES {
break;
}
let mut next_val = UndefinedValue();
let advanced = JS_CallFunctionName(
cx,
iter.handle().into(),
c"next".as_ptr(),
&HandleValueArray::empty(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut next_val,
},
);
if !advanced || !next_val.is_object() {
break;
}
rooted!(&in(wrapped_cx) let res = next_val.to_object());
let mut done_val = UndefinedValue();
JS_GetProperty(
cx,
res.handle().into(),
c"done".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut done_val,
},
);
if done_val.is_boolean() && done_val.to_boolean() {
break;
}
let mut pair_val = UndefinedValue();
JS_GetProperty(
cx,
res.handle().into(),
c"value".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut pair_val,
},
);
if let Some(pair) = parse_header_entry(cx, pair_val) {
out.push(pair);
}
}
}
return out;
}
let mut ids = mozjs::rust::IdVector::new(cx);
if GetPropertyKeys(cx, obj.handle().into(), JSITER_OWNONLY, ids.handle_mut()) {
for jsid in &*ids {
if !jsid.is_string() {
continue;
}
let key_str_ptr = jsid.to_string();
if key_str_ptr.is_null() {
continue;
}
let key =
unsafe_jsstr_to_string(cx, ::std::ptr::NonNull::new_unchecked(key_str_ptr));
let c_key = ZBox::from_bytes(key.as_bytes());
let mut v_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut v_val,
},
);
if v_val.is_string() {
out.push((key, crate::js_to_rust_string(cx, v_val)));
}
}
}
out
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn parse_header_entry(cx: *mut JSContext, pair_val: JSVal) -> Option<(String, String)> {
unsafe {
if !pair_val.is_object() {
return None;
}
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let pair = pair_val.to_object());
let mut is_array = false;
rooted!(&in(wrapped_cx) let pair_root = pair_val);
IsArrayObject(cx, pair_root.handle().into(), &mut is_array);
if is_array {
let mut n_val = UndefinedValue();
let mut v_val = UndefinedValue();
JS_GetElement(
cx,
pair.handle().into(),
0,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut n_val,
},
);
JS_GetElement(
cx,
pair.handle().into(),
1,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut v_val,
},
);
if n_val.is_string() && v_val.is_string() {
return Some((
crate::js_to_rust_string(cx, n_val),
crate::js_to_rust_string(cx, v_val),
));
}
return None;
}
let mut n_val = UndefinedValue();
let mut v_val = UndefinedValue();
JS_GetProperty(
cx,
pair.handle().into(),
c"name".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut n_val,
},
);
JS_GetProperty(
cx,
pair.handle().into(),
c"value".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut v_val,
},
);
if n_val.is_string() && v_val.is_string() {
return Some((
crate::js_to_rust_string(cx, n_val),
crate::js_to_rust_string(cx, v_val),
));
}
None
}
}
const MAX_CA_ENTRIES: usize = 256;
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn parse_tls_init(
cx: *mut JSContext,
tls_val: JSVal,
) -> ::std::result::Result<crate::fetch_async::FetchTlsInit, String> {
unsafe {
if !tls_val.is_object() {
return Err(
"fetch: init.tls must be an object ({ ca, rejectUnauthorized, servername })"
.to_string(),
);
}
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let obj = tls_val.to_object());
let mut out = crate::fetch_async::FetchTlsInit::default();
let ca_val = get_val_prop(cx, obj.handle(), "ca");
if !ca_val.is_undefined() && !ca_val.is_null() {
let mut ders: Vec<Box<[u8]>> = Vec::new();
collect_ca_ders(cx, ca_val, &mut ders)?;
if ders.is_empty() {
return Err("fetch: init.tls.ca contained no parseable certificate".to_string());
}
out.ca_certs_der = ders.into_boxed_slice();
}
let ra_val = get_val_prop(cx, obj.handle(), "rejectUnauthorized");
if !ra_val.is_undefined() && !ra_val.is_null() {
if !ra_val.is_boolean() {
return Err("fetch: init.tls.rejectUnauthorized must be a boolean".to_string());
}
out.reject_unauthorized = ::std::option::Option::Some(ra_val.to_boolean());
}
let sn_val = get_val_prop(cx, obj.handle(), "servername");
if !sn_val.is_undefined() && !sn_val.is_null() {
if !sn_val.is_string() {
return Err("fetch: init.tls.servername must be a string".to_string());
}
let sn = crate::js_to_rust_string(cx, sn_val);
if sn.is_empty() {
return Err(
"fetch: init.tls.servername must be a non-empty host string".to_string(),
);
}
if sn.as_bytes().contains(&0) {
return Err("fetch: init.tls.servername must not contain NUL".to_string());
}
out.servername = ::std::option::Option::Some(sn);
}
Ok(out)
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn collect_ca_ders(
cx: *mut JSContext,
val: JSVal,
out: &mut Vec<Box<[u8]>>,
) -> ::std::result::Result<(), String> {
unsafe {
if out.len() >= MAX_CA_ENTRIES {
return Err(format!("fetch: init.tls.ca exceeds {} entries", MAX_CA_ENTRIES));
}
if val.is_string() {
let pem = crate::js_to_rust_string(cx, val);
let ders = bao_boringssl_bridge::pem_parse_certs(&pem);
if ders.is_empty() {
return Err(
"fetch: init.tls.ca PEM string contained no parseable certificate".to_string(),
);
}
for der in ders {
if out.len() >= MAX_CA_ENTRIES {
break;
}
out.push(der.into_boxed_slice());
}
return Ok(());
}
if val.is_object() {
let wrapped_cx =
mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let obj = val.to_object());
let mut is_array = false;
rooted!(&in(wrapped_cx) let probe = val);
IsArrayObject(cx, probe.handle().into(), &mut is_array);
if is_array {
let mut len_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"length".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut len_val,
},
);
let len = if len_val.is_int32() && len_val.to_int32() > 0 {
(len_val.to_int32() as usize).min(MAX_CA_ENTRIES)
} else {
0
};
for i in 0..len as u32 {
let mut el = UndefinedValue();
JS_GetElement(
cx,
obj.handle().into(),
i,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut el,
},
);
collect_ca_ders(cx, el, out)?;
}
return Ok(());
}
if let ::std::option::Option::Some(bytes) = crate::node_buffer::collect_byte_view(cx, val)
{
let looks_pem = bytes
.windows(b"-----BEGIN".len())
.any(|w| w == &b"-----BEGIN"[..]);
if looks_pem {
let pem = String::from_utf8_lossy(&bytes).into_owned();
let ders = bao_boringssl_bridge::pem_parse_certs(&pem);
if ders.is_empty() {
return Err(
"fetch: init.tls.ca PEM bytes contained no parseable certificate"
.to_string(),
);
}
for der in ders {
if out.len() >= MAX_CA_ENTRIES {
break;
}
out.push(der.into_boxed_slice());
}
return Ok(());
}
out.push(bytes.into_boxed_slice());
return Ok(());
}
}
Err("fetch: init.tls.ca entries must be PEM strings or DER byte views (Buffer/Uint8Array)".to_string())
}
}
#[cfg(test)]
mod tests {
use super::{MultipartValue, encode_multipart, generate_multipart_boundary};
#[test]
fn cors_bypass_fetch_global_installed_for_page() {
let source = include_str!("fetch_api.rs");
assert!(
source.contains("pub fn install_fetch_global"),
"REQ-SEC-001: install_fetch_global must be pub for page realm installation"
);
}
#[test]
fn cors_bypass_fetch_uses_event_driven_no_cors() {
let source = include_str!("fetch_api.rs");
assert!(
source.contains("crate::fetch_async::start"),
"REQ-SEC-001: fetch must delegate to fetch_async::start"
);
let forbidden_cors = ["cors", "_check"].join("");
assert!(
!source.contains(&forbidden_cors),
"REQ-SEC-001 REGRESSION: fetch must NOT contain cors check"
);
let forbidden_cors_preflight = ["Access-Control", "-Request-Method"].join("");
assert!(
!source.contains(&forbidden_cors_preflight),
"REQ-SEC-001 REGRESSION: fetch must NOT send CORS preflight headers"
);
}
#[test]
fn bce_010_no_spawn_or_drain() {
let source = include_str!("fetch_api.rs");
let forbidden_spawn = ["spawn", "_fetch_worker"].join("");
let forbidden_drain = ["drain", "_pending_fetches"].join("");
let forbidden_blocking = ["do_fetch", "_blocking"].join("");
assert!(
!source.contains(&forbidden_spawn),
"BCE-010 REGRESSION: spawn fetch worker must be removed"
);
assert!(
!source.contains(&forbidden_drain),
"BCE-010 REGRESSION: drain pending fetches must be removed"
);
assert!(
!source.contains(&forbidden_blocking),
"BCE-010 REGRESSION: do fetch blocking must be removed"
);
}
#[test]
fn bce_fetch_h_headers_not_dropped() {
let source = include_str!("fetch_api.rs");
let parse_call = ["parse_", "headers_init"].join("");
assert!(
source.contains(&parse_call),
"BCE-20260814-FETCH-H REGRESSION: fetch_fn must parse init.headers"
);
let dropped_form = ["let headers: Vec<(String, String)> = ", "Vec::new();"].join("");
assert!(
!source.contains(&dropped_form),
"BCE-20260814-FETCH-H REGRESSION: init.headers must not be dropped as an empty Vec"
);
let seq_form = ["[\"name\",\"value\"]"].join("");
assert!(
source.contains(&seq_form),
"BCE-20260814-FETCH-H: sequence pair form must be documented/parseable"
);
}
#[test]
fn init_tls_parsed_and_injected() {
let source = include_str!("fetch_api.rs");
let parse_call = ["parse_", "tls_init"].join("");
assert!(
source.contains(&parse_call),
"TEST-ENG-FETCH-TLS REGRESSION: fetch_fn must parse init.tls"
);
let fail_closed = ["no parseable ", "certificate"].join("");
assert!(
source.contains(&fail_closed),
"TEST-ENG-FETCH-TLS REGRESSION: unparseable init.tls.ca must fail closed"
);
assert!(
source.contains("rejectUnauthorized"),
"TEST-ENG-FETCH-TLS REGRESSION: rejectUnauthorized option missing"
);
assert!(
source.contains("servername"),
"TEST-ENG-FETCH-TLS REGRESSION: servername option missing"
);
}
const TEST_BOUNDARY: &str = "----WebKitFormBoundary0123456789abcdef0123456789abcdef";
#[test]
fn multipart_encode_text_and_file_entries() {
let entries = vec![
(
"field".to_string(),
MultipartValue::Text("hello world".to_string()),
),
(
"upload".to_string(),
MultipartValue::File {
filename: "a.txt".to_string(),
content_type: "text/plain".to_string(),
bytes: b"file-bytes".to_vec(),
},
),
(
"noType".to_string(),
MultipartValue::File {
filename: "blob".to_string(),
content_type: "application/octet-stream".to_string(),
bytes: vec![0u8, 1, 2],
},
),
];
let body = encode_multipart(&entries, TEST_BOUNDARY);
let text = String::from_utf8_lossy(&body).to_string();
let expected = concat!(
"------WebKitFormBoundary0123456789abcdef0123456789abcdef\r\n",
"Content-Disposition: form-data; name=\"field\"\r\n",
"\r\n",
"hello world\r\n",
"------WebKitFormBoundary0123456789abcdef0123456789abcdef\r\n",
"Content-Disposition: form-data; name=\"upload\"; filename=\"a.txt\"\r\n",
"Content-Type: text/plain\r\n",
"\r\n",
"file-bytes\r\n",
"------WebKitFormBoundary0123456789abcdef0123456789abcdef\r\n",
"Content-Disposition: form-data; name=\"noType\"; filename=\"blob\"\r\n",
"Content-Type: application/octet-stream\r\n",
"\r\n",
);
assert!(
text.starts_with(expected),
"multipart per-entry framing mismatch:\n{}",
text
);
assert!(
text.ends_with("------WebKitFormBoundary0123456789abcdef0123456789abcdef--\r\n"),
"multipart terminator missing:\n{}",
text
);
assert!(body.windows(3).any(|w| w == [0u8, 1, 2]));
}
#[test]
fn multipart_encode_empty_formdata() {
let body = encode_multipart(&[], TEST_BOUNDARY);
assert_eq!(
body,
format!("{}--\r\n", format!("--{}", TEST_BOUNDARY)).into_bytes()
);
}
#[test]
fn multipart_boundary_unique_per_generation() {
let a = generate_multipart_boundary().expect("boundary gen");
let b = generate_multipart_boundary().expect("boundary gen");
assert_ne!(a, b, "multipart boundary repeated across generations");
assert!(a.starts_with("----WebKitFormBoundary"));
assert_eq!(a.len(), 22 + 32, "boundary must be prefix + 32 hex chars");
assert!(
a["----WebKitFormBoundary".len()..]
.chars()
.all(|c| c.is_ascii_hexdigit()),
"boundary suffix must be hex"
);
}
}