roxxie-proxy-u-prod 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/assets/index-C8me8aPT.js +3669 -0
- package/dist/assets/inject-B58ufVkG.js +43 -0
- package/embed.js +1 -1
- package/examples/single-page.html +2 -2
- package/package.json +1 -1
- package/runtime/index.html +3 -3
- package/scripts/verify.mjs +2 -2
- package/source/packages/chrome/src/Tab/History.test.ts +30 -0
- package/source/packages/chrome/src/Tab/History.ts +3 -1
- package/source/packages/chrome/src/index.ts +5 -0
- package/source/packages/chrome/src/proxy/history-state.test.ts +82 -0
- package/source/packages/chrome/src/proxy/roxxie.ts +9 -0
- package/source/packages/chrome/src/proxy/scramjet.ts +29 -11
- package/source/packages/inject/src/emulators/history.ts +30 -3
- package/source/packages/roxxie-transport/package.json +1 -1
- package/source/packages/roxxie-transport/src/transport.ts +26 -3
- package/source/packages/scramjet/packages/core/dist/scramjet.js +14 -13582
- package/source/packages/scramjet/packages/core/dist/scramjet.js.map +1 -1
- package/source/packages/scramjet/packages/core/dist/scramjet.mjs +14 -13632
- package/source/packages/scramjet/packages/core/dist/scramjet.mjs.map +1 -1
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.js +14 -13584
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.js.map +1 -1
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.mjs +14 -13634
- package/source/packages/scramjet/packages/core/dist/scramjet_bundled.mjs.map +1 -1
- package/source/packages/scramjet/packages/core/dist/types/fetch/user-agent.d.ts +31 -0
- package/source/packages/tracker-protocol/package.json +1 -1
- package/source/packages/tracker-protocol/src/index.ts +6 -1
- package/source/pnpm-lock.yaml +1 -1
- package/standalone.html +2 -2
- package/dist/assets/index-MRhaNu_Q.js +0 -3673
- package/dist/assets/inject-xtH4w5z9.js +0 -43
package/embed.js
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<div id="roxxie-browser"></div>
|
|
19
19
|
<script
|
|
20
20
|
defer
|
|
21
|
-
src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.
|
|
22
|
-
integrity="sha384-
|
|
21
|
+
src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.3.0/embed.js"
|
|
22
|
+
integrity="sha384-njIFCNZAx6YwA02XL4JGMzcsiVqHuTRWutGa/G1NzqEe0t0V+gWPNEe0XCTde9Q3"
|
|
23
23
|
crossorigin="anonymous"
|
|
24
24
|
data-roxxie-target="#roxxie-browser"
|
|
25
25
|
></script>
|
package/package.json
CHANGED
package/runtime/index.html
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/png" sizes="any" href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.
|
|
5
|
+
<link rel="icon" type="image/png" sizes="any" href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.3.0/dist/favicon.png" />
|
|
6
6
|
<meta name="theme-color" content="#222230" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>Roxxie Proxy U</title>
|
|
9
9
|
|
|
10
10
|
<!--ssr-head-->
|
|
11
|
-
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.
|
|
12
|
-
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.
|
|
11
|
+
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.3.0/dist/assets/index-C8me8aPT.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.3.0/dist/assets/index-EeRhLAof.css">
|
|
13
13
|
</head>
|
|
14
14
|
|
|
15
15
|
<body>
|
package/scripts/verify.mjs
CHANGED
|
@@ -118,8 +118,8 @@ if (
|
|
|
118
118
|
throw new Error("Start screen must remain flat and glow-free");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
if (manifest.version !== "0.
|
|
122
|
-
throw new Error("This production release must be version 0.
|
|
121
|
+
if (manifest.version !== "0.3.0") {
|
|
122
|
+
throw new Error("This production release must be version 0.3.0");
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const embedBytes = await readFile(join(packageRoot, "embed.js"));
|
|
@@ -135,3 +135,33 @@ describe("tab history", () => {
|
|
|
135
135
|
expect(restored.canGoForward()).toBe(true);
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
describe("hostile history input", () => {
|
|
140
|
+
it("ignores a non-finite delta instead of stranding the tab", () => {
|
|
141
|
+
const { history } = makeHistory();
|
|
142
|
+
history.push(A);
|
|
143
|
+
history.push(B);
|
|
144
|
+
|
|
145
|
+
// history.go() with no argument used to make index NaN, after which every
|
|
146
|
+
// lookup returned undefined and the tab navigated to /undefined.
|
|
147
|
+
const state = history.go(Number.NaN as number);
|
|
148
|
+
|
|
149
|
+
expect(Number.isFinite(history.index)).toBe(true);
|
|
150
|
+
expect(state).toBeDefined();
|
|
151
|
+
expect(state.url.href).toBe(B.href);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("keeps index finite across repeated bad deltas", () => {
|
|
155
|
+
const { history, tab } = makeHistory();
|
|
156
|
+
history.push(A);
|
|
157
|
+
history.push(B);
|
|
158
|
+
history.push(C);
|
|
159
|
+
|
|
160
|
+
for (const bad of [undefined, null, "x", Number.NaN, Infinity]) {
|
|
161
|
+
history.go(bad as unknown as number);
|
|
162
|
+
expect(Number.isFinite(history.index)).toBe(true);
|
|
163
|
+
expect(history.states[history.index]).toBeDefined();
|
|
164
|
+
}
|
|
165
|
+
expect(tab.navigated.some((href) => href.includes("undefined"))).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -150,7 +150,9 @@ export class History extends StatefulClass {
|
|
|
150
150
|
}
|
|
151
151
|
go(delta: number, navigate: boolean = true): HistoryState {
|
|
152
152
|
const current = this.current();
|
|
153
|
-
|
|
153
|
+
// A non-finite delta would turn index into NaN and strand the tab on an
|
|
154
|
+
// undefined state.
|
|
155
|
+
this.index += Number.isFinite(delta) ? Math.trunc(delta) : 0;
|
|
154
156
|
if (this.index < 0) {
|
|
155
157
|
this.index = 0;
|
|
156
158
|
} else if (this.index >= this.states.length) {
|
|
@@ -8,6 +8,7 @@ import "./style.css";
|
|
|
8
8
|
import.meta.hot?.accept(() => location.reload());
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
applyPreferredNode,
|
|
11
12
|
connectRoxxieTransport,
|
|
12
13
|
installRoxxieTransport,
|
|
13
14
|
subscribeToRoxxieStatus,
|
|
@@ -145,6 +146,10 @@ const prepareBrowser = createRetryableTask(async () => {
|
|
|
145
146
|
await puter.auth.signIn();
|
|
146
147
|
}
|
|
147
148
|
await loadServices();
|
|
149
|
+
// Re-apply a node pinned in Advanced settings. Settings only exist once
|
|
150
|
+
// loadServices has run, and without this the preference is dropped on every
|
|
151
|
+
// reload so node selection silently does nothing.
|
|
152
|
+
applyPreferredNode(settingsService.settings.preferredNodeId);
|
|
148
153
|
// The bundled EasyList/uBlock-compatible blocker must be ready before any
|
|
149
154
|
// restored tab or first navigation is allowed to run.
|
|
150
155
|
await bundledAdBlocker.ready;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors the resolution the injected History emulator performs before sending
|
|
5
|
+
* pushState/replaceState across the RPC boundary. Kept in step with
|
|
6
|
+
* packages/inject/src/emulators/history.ts.
|
|
7
|
+
*/
|
|
8
|
+
function resolveStateUrl(raw: unknown, base: string | URL): string {
|
|
9
|
+
const current = new URL(String(base)).href;
|
|
10
|
+
if (raw === undefined || raw === null) return current;
|
|
11
|
+
try {
|
|
12
|
+
return new URL(String(raw), current).href;
|
|
13
|
+
} catch {
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeDelta(raw: unknown): number {
|
|
19
|
+
const delta = Number(raw);
|
|
20
|
+
return Number.isFinite(delta) ? Math.trunc(delta) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PAGE = "https://example.com/app/page";
|
|
24
|
+
|
|
25
|
+
describe("pushState url resolution", () => {
|
|
26
|
+
it("keeps the current url when the page omits one", () => {
|
|
27
|
+
// This is the /undefined bug: new URL(undefined, base) stringifies to the
|
|
28
|
+
// literal "undefined", so pushState(state, "") navigated to /undefined.
|
|
29
|
+
expect(resolveStateUrl(undefined, PAGE)).toBe(PAGE);
|
|
30
|
+
expect(resolveStateUrl(null, PAGE)).toBe(PAGE);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("never produces a path of /undefined or /null", () => {
|
|
34
|
+
for (const raw of [undefined, null]) {
|
|
35
|
+
const resolved = resolveStateUrl(raw, PAGE);
|
|
36
|
+
expect(resolved).not.toContain("/undefined");
|
|
37
|
+
expect(resolved).not.toContain("/null");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("resolves a relative url against the current page", () => {
|
|
42
|
+
expect(resolveStateUrl("other", PAGE)).toBe(
|
|
43
|
+
"https://example.com/app/other"
|
|
44
|
+
);
|
|
45
|
+
expect(resolveStateUrl("/root", PAGE)).toBe("https://example.com/root");
|
|
46
|
+
expect(resolveStateUrl("?q=1", PAGE)).toBe(
|
|
47
|
+
"https://example.com/app/page?q=1"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("accepts an absolute same-origin url", () => {
|
|
52
|
+
expect(resolveStateUrl("https://example.com/x", PAGE)).toBe(
|
|
53
|
+
"https://example.com/x"
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("keeps the current url when the page supplies an unusable one", () => {
|
|
58
|
+
expect(resolveStateUrl("http://[", PAGE)).toBe(PAGE);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("treats an empty string as the current document, per spec", () => {
|
|
62
|
+
expect(resolveStateUrl("", PAGE)).toBe(PAGE);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("history.go delta", () => {
|
|
67
|
+
it("treats a missing delta as no movement", () => {
|
|
68
|
+
// history.go() with no argument must not make the index NaN.
|
|
69
|
+
expect(normalizeDelta(undefined)).toBe(0);
|
|
70
|
+
expect(normalizeDelta(null)).toBe(0);
|
|
71
|
+
expect(normalizeDelta("nonsense")).toBe(0);
|
|
72
|
+
expect(normalizeDelta(Number.NaN)).toBe(0);
|
|
73
|
+
expect(normalizeDelta(Number.POSITIVE_INFINITY)).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("passes real movement through", () => {
|
|
77
|
+
expect(normalizeDelta(-1)).toBe(-1);
|
|
78
|
+
expect(normalizeDelta(2)).toBe(2);
|
|
79
|
+
expect(normalizeDelta("-3")).toBe(-3);
|
|
80
|
+
expect(normalizeDelta(1.9)).toBe(1);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -35,6 +35,15 @@ export function installRoxxieTransport(): RoxxieProxyTransport {
|
|
|
35
35
|
return installed;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Re-apply a node pin saved in settings. Without this the preference is
|
|
40
|
+
* forgotten on every reload and the browser silently reverts to picking a node
|
|
41
|
+
* automatically.
|
|
42
|
+
*/
|
|
43
|
+
export function applyPreferredNode(nodeId: string | undefined): void {
|
|
44
|
+
roxxieTransport?.setPreferredNode(nodeId || undefined);
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/**
|
|
39
48
|
* Resolves only after tracker discovery, WebRTC, the Adrift handshake, and the
|
|
40
49
|
* node RTT probe have all succeeded. Concurrent Start clicks share one attempt.
|
|
@@ -70,6 +70,21 @@ function findSequence(
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a url supplied by a page, falling back to the tab's current url. A page
|
|
76
|
+
* can legitimately call pushState/replaceState with no url at all, and treating
|
|
77
|
+
* that as a navigation target sent the tab to /undefined.
|
|
78
|
+
*/
|
|
79
|
+
function parsedOr(raw: unknown, fallback: URL): URL | null {
|
|
80
|
+
if (raw === undefined || raw === null || raw === "") return fallback;
|
|
81
|
+
try {
|
|
82
|
+
return new URL(String(raw), fallback);
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
export function reduceSequence(sequence: FrameSequence): Window | null {
|
|
74
89
|
return sequence.reduce<Window | null>((win, idx) => {
|
|
75
90
|
if (!win) return null;
|
|
@@ -135,21 +150,24 @@ class ProxyFrameContext {
|
|
|
135
150
|
);
|
|
136
151
|
},
|
|
137
152
|
history_go: async ({ delta }) => {
|
|
138
|
-
if (tab)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
if (!tab) return;
|
|
154
|
+
const steps = Number(delta);
|
|
155
|
+
if (!Number.isFinite(steps) || steps === 0) return;
|
|
156
|
+
tab.history.go(Math.trunc(steps));
|
|
142
157
|
},
|
|
143
158
|
history_pushState: async ({ url, title, state }) => {
|
|
144
|
-
if (tab)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
if (!tab) return;
|
|
160
|
+
// A page can reach this with no usable url; keeping the current one
|
|
161
|
+
// matches the spec and avoids navigating to a bogus entry.
|
|
162
|
+
const target = parsedOr(url, tab.url);
|
|
163
|
+
if (!target) return;
|
|
164
|
+
tab.history.push(target, title, state, false, true);
|
|
148
165
|
},
|
|
149
166
|
history_replaceState: async ({ url, title, state }) => {
|
|
150
|
-
if (tab)
|
|
151
|
-
|
|
152
|
-
|
|
167
|
+
if (!tab) return;
|
|
168
|
+
const target = parsedOr(url, tab.url);
|
|
169
|
+
if (!target) return;
|
|
170
|
+
tab.history.replace(target, title, state, false);
|
|
153
171
|
},
|
|
154
172
|
newtab: async ({ url }) => {
|
|
155
173
|
const tab = tabsService.newTab(new URL(url));
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { ExecutionContextWrapper } from "../context";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The url argument of pushState/replaceState is optional. When it is omitted
|
|
5
|
+
* or null the spec keeps the document's URL unchanged - but `new URL(undefined,
|
|
6
|
+
* base)` stringifies to the literal "undefined", which navigated the tab to
|
|
7
|
+
* /undefined. Single-page apps call pushState(state, "") constantly, so this
|
|
8
|
+
* fired on ordinary browsing.
|
|
9
|
+
*/
|
|
10
|
+
function resolveStateUrl(raw: unknown, base: string | URL): string {
|
|
11
|
+
const current = new URL(String(base)).href;
|
|
12
|
+
if (raw === undefined || raw === null) return current;
|
|
13
|
+
try {
|
|
14
|
+
return new URL(String(raw), current).href;
|
|
15
|
+
} catch {
|
|
16
|
+
// A url the page cannot navigate to leaves the current one in place,
|
|
17
|
+
// matching what the browser does with an invalid pushState target.
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** history.go(delta) treats a missing or non-finite delta as 0. */
|
|
23
|
+
function normalizeDelta(raw: unknown): number {
|
|
24
|
+
const delta = Number(raw);
|
|
25
|
+
return Number.isFinite(delta) ? Math.trunc(delta) : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
export function setupHistoryEmulation({
|
|
4
29
|
client,
|
|
5
30
|
rpc,
|
|
@@ -12,7 +37,7 @@ export function setupHistoryEmulation({
|
|
|
12
37
|
rpc.call("history_pushState", {
|
|
13
38
|
state: ctx.args[0],
|
|
14
39
|
title: ctx.args[1],
|
|
15
|
-
url:
|
|
40
|
+
url: resolveStateUrl(ctx.args[2], relevantclient.url),
|
|
16
41
|
});
|
|
17
42
|
},
|
|
18
43
|
});
|
|
@@ -23,7 +48,7 @@ export function setupHistoryEmulation({
|
|
|
23
48
|
rpc.call("history_replaceState", {
|
|
24
49
|
state: ctx.args[0],
|
|
25
50
|
title: ctx.args[1],
|
|
26
|
-
url:
|
|
51
|
+
url: resolveStateUrl(ctx.args[2], relevantclient.url),
|
|
27
52
|
});
|
|
28
53
|
},
|
|
29
54
|
});
|
|
@@ -43,7 +68,9 @@ export function setupHistoryEmulation({
|
|
|
43
68
|
});
|
|
44
69
|
client.Proxy("History.prototype.go", {
|
|
45
70
|
apply(ctx) {
|
|
46
|
-
|
|
71
|
+
// history.go() with no argument (or a non-numeric one) means "reload the
|
|
72
|
+
// current entry". Forwarding undefined made the delta arithmetic NaN.
|
|
73
|
+
rpc.call("history_go", { delta: normalizeDelta(ctx.args[0]) });
|
|
47
74
|
|
|
48
75
|
ctx.return(undefined);
|
|
49
76
|
},
|
|
@@ -268,11 +268,34 @@ export class RoxxieProxyTransport implements ProxyTransport {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
/**
|
|
271
|
-
* Pin
|
|
272
|
-
*
|
|
271
|
+
* Pin connections to one node, or pass undefined for automatic selection.
|
|
272
|
+
*
|
|
273
|
+
* If this changes which node should be serving, the current connection is
|
|
274
|
+
* dropped so the choice takes effect immediately - leaving the session on
|
|
275
|
+
* the old node makes the setting look like it does nothing.
|
|
273
276
|
*/
|
|
274
277
|
setPreferredNode(nodeId: string | undefined): void {
|
|
275
|
-
|
|
278
|
+
const next = nodeId || undefined;
|
|
279
|
+
if (next === this.preferredNodeId) return;
|
|
280
|
+
this.preferredNodeId = next;
|
|
281
|
+
const current = this._connectedNode?.node.nodeId;
|
|
282
|
+
if (current !== undefined && next !== undefined && current !== next) {
|
|
283
|
+
this.reconnect();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Drop the current node connection so the next request selects again. Unlike
|
|
289
|
+
* close(), the transport stays usable.
|
|
290
|
+
*/
|
|
291
|
+
reconnect(): void {
|
|
292
|
+
if (this.closed) return;
|
|
293
|
+
const connection = this.connection;
|
|
294
|
+
this.connection = undefined;
|
|
295
|
+
this.connectionPromise = undefined;
|
|
296
|
+
this._connectedNode = undefined;
|
|
297
|
+
connection?.close(new Error("Reselecting Roxxie node"));
|
|
298
|
+
this.publish({ state: "idle" });
|
|
276
299
|
}
|
|
277
300
|
|
|
278
301
|
private async establishConnection(): Promise<RoxxieConnection> {
|