diff --git a/src/client/shared/import.ts b/src/client/shared/import.ts index 75c4483..2794d7d 100644 --- a/src/client/shared/import.ts +++ b/src/client/shared/import.ts @@ -1,7 +1,8 @@ import { encodeUrl } from "../shared"; +import { importfn } from "./wrap"; export default function (client, self) { - self.$sImport = function (base) { + self[importfn] = function (base) { return function (url) { const resolved = new URL(url, base).href; diff --git a/src/client/shared/wrap.ts b/src/client/shared/wrap.ts index e6bb63e..533a0fc 100644 --- a/src/client/shared/wrap.ts +++ b/src/client/shared/wrap.ts @@ -14,40 +14,65 @@ export default function (client: ScramjetClient, self: typeof globalThis) { // the main magic of the proxy. all attempts to access any "banned objects" will be redirected here, and instead served a proxy object // this contrasts from how other proxies will leave the root object alone and instead attempt to catch every member access // this presents some issues (see element.ts), but makes us a good bit faster at runtime! + Object.defineProperty(self, wrapfn, { + value: function (identifier: any) { + if (iswindow && identifier instanceof self.Window) { + return client.windowProxy; + } else if (iswindow && identifier instanceof self.parent.self.Window) { + if (ScramjetClient.SCRAMJET in self.parent.self) { + // ... then we're in a subframe, and the parent frame is also in a proxy context, so we should return its proxy + return self.parent.self[ScramjetClient.SCRAMJET].windowProxy; + } else { + // ... then we should pretend we aren't nested and return the current window + return client.windowProxy; + } + } else if (iswindow && identifier instanceof self.top.self.Window) { + // instead of returning top, we need to return the uppermost parent that's inside a scramjet context + let current = self.self; - self[wrapfn] = function (identifier: any) { - if ( - iswindow && - (identifier instanceof self.Window || - identifier instanceof self.top.window.Window || - identifier instanceof self.parent.window.Window) - ) { - // this will break iframe postmessage! - return client.windowProxy; - } else if ( - (iswindow && identifier instanceof Location) || - (isworker && identifier instanceof WorkerLocation) - ) { - return client.locationProxy; - } else if (iswindow && identifier instanceof Document) { - return client.documentProxy; - } else if (isworker && identifier instanceof WorkerGlobalScope) { - return client.windowProxy; - } + for (;;) { + const test = current.parent.self; + if (test === current) break; // there is no parent, actual or emulated. - return identifier; - }; + // ... then `test` represents a window outside of the proxy context, and therefore `current` is the topmost window in the proxy context + if (!(ScramjetClient.SCRAMJET in test)) break; + + // test is also insde a proxy, so we should continue up the chain + current = test; + } + + return current[ScramjetClient.SCRAMJET].windowProxy; + } else if ( + (iswindow && identifier instanceof self.Location) || + (isworker && identifier instanceof self.WorkerLocation) + ) { + return client.locationProxy; + } else if (iswindow && identifier instanceof self.Document) { + return client.documentProxy; + } else if (isworker && identifier instanceof self.WorkerGlobalScope) { + return client.windowProxy; + } + + return identifier; + }, + writable: false, + configurable: false, + }); // location = "..." can't be rewritten as wrapfn(location) = ..., so instead it will actually be rewritten as // ((t)=>$scramjet$tryset(location,"+=",t)||location+=t)(...); // it has to be a discrete function because there's always the possibility that "location" is a local variable // we have to use an IIFE to avoid duplicating side-effects in the getter - self[trysetfn] = function (lhs: any, op: string, rhs: any) { - if (lhs instanceof Location) { - // @ts-ignore - locationProxy.href = rhs; + Object.defineProperty(self, trysetfn, { + value: function (lhs: any, op: string, rhs: any) { + if (lhs instanceof Location) { + // @ts-ignore + locationProxy.href = rhs; - return true; - } - }; + return true; + } + }, + writable: false, + configurable: false, + }); } diff --git a/src/client/window.ts b/src/client/window.ts index 1ada212..ab93f97 100644 --- a/src/client/window.ts +++ b/src/client/window.ts @@ -1,5 +1,6 @@ import { encodeUrl } from "../shared/rewriters/url"; import { ScramjetClient } from "./client"; +import { wrapfn } from "./shared/wrap"; export function createWindowProxy( client: ScramjetClient, @@ -12,11 +13,9 @@ export function createWindowProxy( return client.locationProxy; } else if ( propIsString && - ["window", "top", "self", "globalThis"].includes(prop) + ["window", "top", "self", "globalThis", "parent"].includes(prop) ) { - return client.windowProxy; - } else if (propIsString && prop == "parent") { - return self.parent; + return self[wrapfn](self[prop]); } else if (propIsString && prop === "$scramjet") { return; } diff --git a/src/shared/rewriters/js.ts b/src/shared/rewriters/js.ts index 4247b48..4db866e 100644 --- a/src/shared/rewriters/js.ts +++ b/src/shared/rewriters/js.ts @@ -8,7 +8,6 @@ import { rewrite_js_from_arraybuffer, } from "../../../rewriter/out/rewriter.js"; import "../../../static/wasm.js"; -import { importfn, wrapfn } from "../../client/shared/wrap"; initSync( new WebAssembly.Module( @@ -32,8 +31,8 @@ export function rewriteJs(js: string | ArrayBuffer, origin?: URL) { origin.toString(), self.$scramjet.config.prefix, self.$scramjet.config.codec.encode as any, - wrapfn, - importfn + "$scramjet$wrap", + "$scramjet$import" ) ); } else { @@ -42,8 +41,8 @@ export function rewriteJs(js: string | ArrayBuffer, origin?: URL) { origin.toString(), self.$scramjet.config.prefix, self.$scramjet.config.codec.encode as any, - wrapfn, - importfn + "$scramjet$wrap", + "$scramjet$import" ); } const after = performance.now();