diff --git a/rspack.config.js b/rspack.config.js index e7cffc9..99996e5 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -18,6 +18,7 @@ export default defineConfig({ client: join(__dirname, "src/client/index.ts"), codecs: join(__dirname, "src/codecs/index.ts"), controller: join(__dirname, "src/controller/index.ts"), + sync: join(__dirname, "src/sync.ts"), }, resolve: { extensions: [".ts", ".js"], diff --git a/src/client/shared/requests/xmlhttprequest.ts b/src/client/shared/requests/xmlhttprequest.ts index 156098c..51de095 100644 --- a/src/client/shared/requests/xmlhttprequest.ts +++ b/src/client/shared/requests/xmlhttprequest.ts @@ -1,10 +1,105 @@ -import { decodeUrl, encodeUrl, rewriteHeaders } from "../../../shared"; +import { config, decodeUrl, encodeUrl, rewriteHeaders } from "../../../shared"; import { ScramjetClient } from "../../client"; export default function (client: ScramjetClient, self: Self) { + const worker = new Worker(config.sync); + const ARGS = Symbol("xhr original args"); + const HEADERS = Symbol("xhr headers"); + client.Proxy("XMLHttpRequest.prototype.open", { apply(ctx) { if (ctx.args[1]) ctx.args[1] = encodeUrl(ctx.args[1], client.meta); + ctx.this[ARGS] = ctx.args; + }, + }); + + client.Proxy("XMLHttpRequest.prototype.setRequestHeader", { + apply(ctx) { + const headers = ctx.this[HEADERS] || (ctx.this[HEADERS] = {}); + headers[ctx.args[0]] = ctx.args[1]; + }, + }); + + client.Proxy("XMLHttpRequest.prototype.send", { + apply(ctx) { + const args = ctx.this[ARGS]; + if (!args || args[2]) return; + + // it's a sync request + // sync xhr to service worker is not supported + // there's a nice way of polyfilling this though, we can spin on an atomic using sharedarraybuffer. this will maintain the sync behavior + + // @ts-expect-error maxbytelength not in types yet i guess + const sab = new SharedArrayBuffer(1024, { maxByteLength: 2147483647 }); + const view = new DataView(sab); + + worker.postMessage({ + sab, + args, + headers: ctx.this[HEADERS], + body: ctx.args[0], + }); + + while (view.getUint8(0) === 0) { + /* spin */ + } + + const status = view.getUint16(1); + const headersLength = view.getUint32(3); + + const headersab = new Uint8Array(headersLength); + headersab.set(new Uint8Array(sab.slice(7, 7 + headersLength))); + const headers = new TextDecoder().decode(headersab); + + const bodyLength = view.getUint32(7 + headersLength); + const bodyab = new Uint8Array(bodyLength); + bodyab.set( + new Uint8Array( + sab.slice(11 + headersLength, 11 + headersLength + bodyLength) + ) + ); + const body = new TextDecoder().decode(bodyab); + + // these should be using proxies to not leak scram strings but who cares + client.RawTrap(ctx.this, "status", { + get() { + return status; + }, + }); + client.RawTrap(ctx.this, "responseText", { + get() { + return body; + }, + }); + client.RawTrap(ctx.this, "response", { + get() { + if (ctx.this.responseType === "arraybuffer") return bodyab.buffer; + return body; + }, + }); + client.RawTrap(ctx.this, "responseXML", { + get() { + const parser = new DOMParser(); + return parser.parseFromString(body, "text/xml"); + }, + }); + client.RawTrap(ctx.this, "getAllResponseHeaders", { + get() { + return () => headers; + }, + }); + client.RawTrap(ctx.this, "getResponseHeader", { + get() { + return (header: string) => { + const re = new RegExp(`^${header}: (.*)$`, "m"); + const match = re.exec(headers); + return match ? match[1] : null; + }; + }, + }); + + // send has no return value right + ctx.return(undefined); }, }); diff --git a/src/controller/index.ts b/src/controller/index.ts index 4203d39..6b6eabc 100644 --- a/src/controller/index.ts +++ b/src/controller/index.ts @@ -26,6 +26,7 @@ export class ScramjetController { thread: "/scramjet.thread.js", client: "/scramjet.client.js", codecs: "/scramjet.codecs.js", + sync: "/scramjet.sync.js", flags: { serviceworkers: false, naiiveRewriter: false, diff --git a/src/sync.ts b/src/sync.ts new file mode 100644 index 0000000..e803def --- /dev/null +++ b/src/sync.ts @@ -0,0 +1,54 @@ +addEventListener( + "message", + ({ + data: { + sab, + args: [method, url, _, username, password], + body, + headers, + }, + }) => { + const view = new DataView(sab); + const u8view = new Uint8Array(sab); + + const xhr = new XMLHttpRequest(); + xhr.responseType = "arraybuffer"; + + // force async since we need it to resolve to the sw + xhr.open(method, url, true, username, password); + + if (headers) + for (const [k, v] of Object.entries(headers)) { + xhr.setRequestHeader(k, v as string); + } + + xhr.send(body); + + xhr.onload = () => { + let cursor = 1; // first byte is the lock + + view.setUint16(cursor, xhr.status); + cursor += 2; + + // next write the header string + const headers = xhr.getAllResponseHeaders(); + view.setUint32(cursor, headers.length); + cursor += 4; + + if (sab.byteLength < cursor + headers.length) + sab.grow(cursor + headers.length); + u8view.set(new TextEncoder().encode(headers), cursor); + cursor += headers.length; + + view.setUint32(cursor, xhr.response.byteLength); + cursor += 4; + + if (sab.byteLength < cursor + xhr.response.byteLength) + sab.grow(cursor + xhr.response.byteLength); + u8view.set(new Uint8Array(xhr.response), cursor); + + // release the lock, main thread will stop spinning now + view.setUint8(0, 1); + }; + } +); diff --git a/src/types.d.ts b/src/types.d.ts index f28c7e7..7532ed6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -42,6 +42,7 @@ interface ScramjetConfig { thread: string; client: string; codecs: string; + sync: string; flags: ScramjetFlags; siteflags?: Record; }