Skip to content

Commit e9690c1

Browse files
committed
formalize behavior of _self and put iframe names in urlmeta
1 parent 59d5e2c commit e9690c1

File tree

8 files changed

+148
-32
lines changed

8 files changed

+148
-32
lines changed

src/client/client.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,93 @@ export class ScramjetClient {
232232

233233
return client.url;
234234
},
235+
get topFrameName() {
236+
if (!iswindow)
237+
throw new Error("topFrameName was called from a worker?");
238+
239+
let currentWin = client.global;
240+
if (currentWin.parent.window == currentWin.window) {
241+
// we're top level & we don't have a frame name
242+
return null;
243+
}
244+
245+
// find the topmost frame that's controlled by scramjet, stopping before the real top frame
246+
while (currentWin.parent.window !== currentWin.window) {
247+
if (!currentWin.parent.window[SCRAMJETCLIENT]) break;
248+
currentWin = currentWin.parent.window;
249+
}
250+
251+
const curclient = currentWin[SCRAMJETCLIENT];
252+
const frame = curclient.descriptors.get(
253+
"window.frameElement",
254+
currentWin
255+
);
256+
if (!frame) {
257+
// we're inside an iframe, but the top frame is scramjet-controlled and top level, so we can't get a top frame name
258+
return null;
259+
}
260+
if (!frame.name) {
261+
// the top frame is scramjet-controlled, but it has no name. this is user error
262+
console.error(
263+
"YOU NEED TO USE `new ScramjetFrame()`! DIRECT IFRAMES WILL NOT WORK"
264+
);
265+
266+
return null;
267+
}
268+
269+
return frame.name;
270+
},
271+
get parentFrameName() {
272+
if (!iswindow)
273+
throw new Error("parentFrameName was called from a worker?");
274+
if (client.global.parent.window == client.global.window) {
275+
// we're top level & we don't have a frame name
276+
return null;
277+
}
278+
279+
let parentWin = client.global.parent.window;
280+
if (parentWin[SCRAMJETCLIENT]) {
281+
// we're inside an iframe, and the parent is scramjet-controlled
282+
const parentClient = parentWin[SCRAMJETCLIENT];
283+
const frame = parentClient.descriptors.get(
284+
"window.frameElement",
285+
parentWin
286+
);
287+
288+
if (!frame) {
289+
// parent is scramjet controlled and top-level. there is no parent frame name
290+
return null;
291+
}
292+
293+
if (!frame.name) {
294+
// the parent frame is scramjet-controlled, but it has no name. this is user error
295+
console.error(
296+
"YOU NEED TO USE `new ScramjetFrame()`! DIRECT IFRAMES WILL NOT WORK"
297+
);
298+
299+
return null;
300+
}
301+
302+
return frame.name;
303+
} else {
304+
// we're inside an iframe, and the parent is not scramjet-controlled
305+
// return our own frame name
306+
const frame = client.descriptors.get(
307+
"window.frameElement",
308+
client.global
309+
);
310+
if (!frame.name) {
311+
// the parent frame is not scramjet-controlled, so we can't get a parent frame name
312+
console.error(
313+
"YOU NEED TO USE `new ScramjetFrame()`! DIRECT IFRAMES WILL NOT WORK"
314+
);
315+
316+
return null;
317+
}
318+
319+
return frame.name;
320+
}
321+
},
235322
};
236323

237324
global[SCRAMJETCLIENT] = this;

src/client/dom/open.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import { rewriteUrl } from "../../shared";
22
import { ScramjetClient } from "../client";
3-
import {
4-
SCRAMJETCLIENT,
5-
SCRAMJETCLIENTNAME,
6-
SCRAMJETFRAMENAME,
7-
} from "../../symbols";
3+
import { SCRAMJETCLIENT } from "../../symbols";
84

95
export default function (client: ScramjetClient) {
106
client.Proxy("window.open", {
117
apply(ctx) {
128
if (ctx.args[0]) ctx.args[0] = rewriteUrl(ctx.args[0], client.meta);
139

14-
if (["_parent", "_top", "_unfencedTop"].includes(ctx.args[1]))
15-
ctx.args[1] = SCRAMJETFRAMENAME; // TODO: this is still technically the wrong behavior
10+
if (ctx.args[1] === "_top" || ctx.args[1] === "_unfencedTop")
11+
ctx.args[1] = client.meta.topFrameName;
12+
if (ctx.args[1] === "_parent") ctx.args[1] = client.meta.parentFrameName;
1613

1714
const realwin = ctx.call();
1815

src/client/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,24 @@ export const isshared = "SharedWorkerGlobalScope" in self;
1414
export const isemulatedsw =
1515
new URL(self.location.href).searchParams.get("dest") === "serviceworker";
1616

17+
function createFrameId() {
18+
return `${Array(8)
19+
.fill(0)
20+
.map(() => Math.floor(Math.random() * 36).toString(36))
21+
.join("")}`;
22+
}
23+
1724
dbg.log("initializing scramjet client");
1825
// if it already exists, that means the handlers have probably already been setup by the parent document
1926
if (!(SCRAMJETCLIENT in <Partial<typeof self>>self)) {
2027
loadCodecs();
2128

2229
const client = new ScramjetClient(self);
30+
const frame: HTMLIFrameElement = self.frameElement as HTMLIFrameElement;
31+
if (frame && !frame.name) {
32+
// all frames need to be named for our logic to work
33+
frame.name = createFrameId();
34+
}
2335

2436
if (self.COOKIE) client.loadcookies(self.COOKIE);
2537

src/controller/frame.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { ScramjetController } from ".";
22
import type { ScramjetClient } from "../client/client";
33
import { ScramjetEvent, ScramjetEvents } from "../client/events";
4-
import { SCRAMJETCLIENT, SCRAMJETFRAME, SCRAMJETFRAMENAME } from "../symbols";
5-
4+
import { SCRAMJETCLIENT, SCRAMJETFRAME } from "../symbols";
5+
function createFrameId() {
6+
return `${Array(8)
7+
.fill(0)
8+
.map(() => Math.floor(Math.random() * 36).toString(36))
9+
.join("")}`;
10+
}
611
export class ScramjetFrame extends EventTarget {
712
constructor(
813
private controller: ScramjetController,
914
public frame: HTMLIFrameElement
1015
) {
1116
super();
12-
frame.name = SCRAMJETFRAMENAME;
17+
frame.name = createFrameId();
1318
frame[SCRAMJETFRAME] = this;
1419
}
1520

src/shared/rewriters/html.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { CookieStore } from "../cookie";
88
import { unrewriteBlob } from "../../shared/rewriters/url";
99
import { $scramjet } from "../../scramjet";
1010
import { getRewriter } from "./wasm";
11-
import { SCRAMJETCLIENTNAME, SCRAMJETFRAMENAME } from "../../symbols";
1211

1312
const encoder = new TextEncoder();
1413
function rewriteHtmlInner(
@@ -231,9 +230,10 @@ export const htmlRules: {
231230
style: "*",
232231
},
233232
{
234-
fn: (value: string) => {
235-
if (["_parent", "_top", "_unfencedTop"].includes(value))
236-
return SCRAMJETFRAMENAME; // TODO: ...
233+
fn: (value: string, meta: URLMeta) => {
234+
if (value === "_top" || value === "_unfencedTop")
235+
return meta.topFrameName;
236+
else if (value === "_parent") return meta.parentFrameName;
237237
else return value;
238238
},
239239
target: ["a", "base"],

src/shared/rewriters/url.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { rewriteJs } from "./js";
44
export type URLMeta = {
55
origin: URL;
66
base: URL;
7+
topFrameName?: string;
8+
parentFrameName?: string;
79
};
810

911
function tryCanParseURL(url: string, origin?: string | URL): URL | null {
@@ -54,13 +56,20 @@ export function rewriteUrl(url: string | URL, meta: URLMeta) {
5456
location.origin +
5557
$scramjet.config.prefix +
5658
$scramjet.codec.encode(realUrl.href) +
59+
(meta.topFrameName
60+
? `?topFrame=${meta.topFrameName}&parentFrame=${meta.parentFrameName}`
61+
: "") +
5762
realHash
5863
);
5964
}
6065
}
6166

6267
export function unrewriteUrl(url: string | URL) {
6368
if (url instanceof URL) url = url.toString();
69+
// remove query string
70+
if (url.includes("?")) {
71+
url = url.split("?")[0];
72+
}
6473

6574
const prefixed = location.origin + $scramjet.config.prefix;
6675

src/symbols.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@
22
export const SCRAMJETCLIENTNAME = "scramjet client global";
33
export const SCRAMJETCLIENT = Symbol.for(SCRAMJETCLIENTNAME);
44
export const SCRAMJETFRAME = Symbol.for("scramjet frame handle");
5-
export const SCRAMJETFRAMENAME = "scramjet_frame";

src/worker/fetch.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,6 @@ import type { URLMeta } from "../shared/rewriters/url";
2626
import { $scramjet, flagEnabled } from "../scramjet";
2727
import { rewriteJsWithMap } from "../shared/rewriters/js";
2828

29-
function newMeta(url: URL): URLMeta {
30-
return {
31-
origin: url,
32-
base: url,
33-
};
34-
}
35-
3629
export async function handleFetch(
3730
this: ScramjetServiceWorker,
3831
request: Request,
@@ -49,6 +42,21 @@ export async function handleFetch(
4942
requestUrl.searchParams.delete("dest");
5043
}
5144

45+
let meta: URLMeta = {};
46+
if (client) {
47+
meta.base = new URL(unrewriteUrl(client.url));
48+
meta.origin = new URL(unrewriteUrl(client.url));
49+
} else {
50+
meta.base = new URL(unrewriteUrl(request.url));
51+
meta.origin = new URL(unrewriteUrl(request.url));
52+
}
53+
if (requestUrl.searchParams.has("topFrame")) {
54+
meta.topFrameName = requestUrl.searchParams.get("topFrame");
55+
}
56+
if (requestUrl.searchParams.has("parentFrame")) {
57+
meta.parentFrameName = requestUrl.searchParams.get("parentFrame");
58+
}
59+
5260
if (requestUrl.pathname === this.config.files.wasm) {
5361
return fetch(this.config.files.wasm).then(async (x) => {
5462
const buf = await x.arrayBuffer();
@@ -89,12 +97,7 @@ export async function handleFetch(
8997
if (response.body) {
9098
body = await rewriteBody(
9199
response as BareResponseFetch,
92-
client
93-
? {
94-
base: new URL(new URL(client.url).origin),
95-
origin: new URL(new URL(client.url).origin),
96-
}
97-
: newMeta(new URL(unrewriteUrl(request.referrer))),
100+
meta,
98101
request.destination,
99102
workerType,
100103
this.cookieStore
@@ -230,7 +233,6 @@ export async function handleFetch(
230233
const unrewrittenReferrer = unrewriteUrl(request.referrer);
231234
if (unrewrittenReferrer) {
232235
const referrerUrl = new URL(unrewrittenReferrer);
233-
const meta = newMeta(url);
234236
siteDirective = await getSiteDirective(
235237
meta,
236238
referrerUrl,
@@ -277,6 +279,7 @@ export async function handleFetch(
277279

278280
return await handleResponse(
279281
url,
282+
meta,
280283
workerType,
281284
request.destination,
282285
request.mode,
@@ -316,6 +319,7 @@ export async function handleFetch(
316319

317320
async function handleResponse(
318321
url: URL,
322+
meta: URLMeta,
319323
workertype: string,
320324
destination: RequestDestination,
321325
mode: RequestMode,
@@ -331,7 +335,7 @@ async function handleResponse(
331335
mode === "navigate" && ["document", "iframe"].includes(destination);
332336
const responseHeaders = await rewriteHeaders(
333337
response.rawHeaders,
334-
newMeta(url),
338+
meta,
335339
bareClient,
336340
{ get: getReferrerPolicy, set: storeReferrerPolicy }
337341
);
@@ -358,7 +362,10 @@ async function handleResponse(
358362
responseHeaders["referrer-policy"]
359363
);
360364

361-
const redirectMeta = newMeta(redirectUrl);
365+
const redirectMeta = {
366+
origin: redirectUrl,
367+
base: redirectUrl,
368+
};
362369
const newSiteDirective = await getSiteDirective(
363370
redirectMeta,
364371
url,
@@ -395,7 +402,7 @@ async function handleResponse(
395402
if (response.body) {
396403
responseBody = await rewriteBody(
397404
response,
398-
newMeta(url),
405+
meta,
399406
destination,
400407
workertype,
401408
cookieStore

0 commit comments

Comments
 (0)