js: serialize FormData bodies to multipart/form-data in fetch
fetch() and XHR did String(body), so passing a FormData as the request body sent the literal "[object Object]" and lost the multipart payload along with its boundary. Servers expecting multipart/form-data replied 400 "Invalid boundary for multipart/form-data", which is what blocked booking.com: the AWS WAF challenge POSTs its proof-of-work solution to /mp_verify as FormData, so the verify always failed and no aws-waf-token was issued. Serialize FormData to a multipart/form-data body with a generated boundary and set the matching Content-Type, the way a real browser does; URLSearchParams bodies get application/x-www-form-urlencoded. XHR.send routes through fetch() so it is covered too. With this, obscura --stealth renders the real booking.com landing page the same as headless Chrome.
This commit is contained in:
1 parent
402de261aa
commit
3eb28da5c6
1 file changed
+48
-2
Vendored
+48
-2
@@ -2906,6 +2906,51 @@ function _installWasmStreamingFallback() {
|
||||
}
|
||||
_installWasmStreamingFallback();
|
||||
|
||||
// Serialize a FormData into a multipart/form-data body the way a browser does
|
||||
// when it is passed as fetch()/XHR body. The previous shim did String(body),
|
||||
// so a FormData became the literal "[object Object]" and the multipart payload
|
||||
// (with its boundary) was lost; servers replied "Invalid boundary for
|
||||
// multipart/form-data" (e.g. the AWS WAF challenge /mp_verify POST).
|
||||
function _formDataToMultipart(fd) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let bnd = '----WebKitFormBoundary';
|
||||
for (let i = 0; i < 16; i++) bnd += chars[Math.floor(Math.random() * chars.length)];
|
||||
let out = '';
|
||||
const entries = fd._d || [];
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const k = entries[i][0], v = entries[i][1];
|
||||
out += '--' + bnd + '\r\n';
|
||||
if (v != null && typeof v === 'object' && v._bytes != null) {
|
||||
out += 'Content-Disposition: form-data; name="' + k + '"; filename="' + (v.name || 'blob') + '"\r\n';
|
||||
out += 'Content-Type: ' + (v.type || 'application/octet-stream') + '\r\n\r\n';
|
||||
try { out += new TextDecoder().decode(v._bytes); } catch (e) {}
|
||||
out += '\r\n';
|
||||
} else {
|
||||
out += 'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + String(v) + '\r\n';
|
||||
}
|
||||
}
|
||||
out += '--' + bnd + '--\r\n';
|
||||
return { boundary: bnd, body: out };
|
||||
}
|
||||
|
||||
// Coerce a fetch()/XHR body into the string op_fetch_url expects, attaching a
|
||||
// Content-Type header for body types that need one (FormData, URLSearchParams).
|
||||
function _serializeBody(initBody, headers) {
|
||||
if (initBody == null || initBody === '') return '';
|
||||
if (initBody instanceof FormData) {
|
||||
const mp = _formDataToMultipart(initBody);
|
||||
headers['Content-Type'] = 'multipart/form-data; boundary=' + mp.boundary;
|
||||
return mp.body;
|
||||
}
|
||||
if (initBody instanceof URLSearchParams) {
|
||||
if (!Object.keys(headers).some(k => k.toLowerCase() === 'content-type')) {
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
||||
}
|
||||
return initBody.toString();
|
||||
}
|
||||
return typeof initBody === 'string' ? initBody : String(initBody);
|
||||
}
|
||||
|
||||
globalThis.fetch = async (input, init = {}) => {
|
||||
let url = typeof input === "string"
|
||||
? input
|
||||
@@ -2919,8 +2964,9 @@ globalThis.fetch = async (input, init = {}) => {
|
||||
} catch(e) { /* keep as-is if URL resolution fails */ }
|
||||
}
|
||||
const method = init.method || (input instanceof Request ? input.method : "GET");
|
||||
const hdrs = JSON.stringify(init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init.headers || {});
|
||||
const body = init.body ? String(init.body) : "";
|
||||
let _h = init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : (init.headers || {});
|
||||
const body = _serializeBody(init.body, _h);
|
||||
const hdrs = JSON.stringify(_h);
|
||||
const fetchMode = init.mode || (input instanceof Request ? input.mode : "cors");
|
||||
const pageOrigin = (function() { try { const u = new URL(_domParse("document_url") || "about:blank"); return u.origin; } catch(e) { return ""; } })();
|
||||
const raw = await Deno.core.ops.op_fetch_url(url, method, hdrs, body, pageOrigin, fetchMode);
|
||||
|
||||
Reference in new issue
Block a user