@mercuryworkshop/proxy-bootstrap 0.0.1
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/dist/.downloads/controller/package/dist/controller.api.js +44 -0
- package/dist/.downloads/controller/package/dist/controller.api.js.map +1 -0
- package/dist/.downloads/controller/package/dist/controller.inject.js +2 -0
- package/dist/.downloads/controller/package/dist/controller.inject.js.map +1 -0
- package/dist/.downloads/controller/package/dist/controller.sw.js +2 -0
- package/dist/.downloads/controller/package/dist/controller.sw.js.map +1 -0
- package/dist/.downloads/controller/package/dist/types/cache.d.ts +39 -0
- package/dist/.downloads/controller/package/dist/types/index.d.ts +74 -0
- package/dist/.downloads/controller/package/dist/types/inject.d.ts +16 -0
- package/dist/.downloads/controller/package/dist/types/sw.d.ts +2 -0
- package/dist/.downloads/controller/package/dist/types/symbols.d.ts +1 -0
- package/dist/.downloads/controller/package/dist/types/typesEntry.d.ts +5 -0
- package/dist/.downloads/controller/package/package.json +16 -0
- package/dist/.downloads/controller/package/src/cache.ts +473 -0
- package/dist/.downloads/controller/package/src/index.ts +809 -0
- package/dist/.downloads/controller/package/src/inject.ts +370 -0
- package/dist/.downloads/controller/package/src/sw.ts +231 -0
- package/dist/.downloads/controller/package/src/symbols.ts +1 -0
- package/dist/.downloads/controller/package/src/types.d.ts +139 -0
- package/dist/.downloads/controller/package/src/typesEntry.ts +6 -0
- package/dist/.downloads/controller/package/tsconfig.json +24 -0
- package/dist/.downloads/controller/package/tsconfig.types.json +16 -0
- package/dist/.downloads/libcurl-transport/package/LICENSE +661 -0
- package/dist/.downloads/libcurl-transport/package/README.md +52 -0
- package/dist/.downloads/libcurl-transport/package/dist/index.d.ts +25 -0
- package/dist/.downloads/libcurl-transport/package/dist/index.js +6500 -0
- package/dist/.downloads/libcurl-transport/package/dist/index.mjs +6481 -0
- package/dist/.downloads/libcurl-transport/package/package.json +37 -0
- package/dist/.downloads/scramjet/package/dist/167400cb144aab22.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/2919e49b986edf8c.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/5aed1d5e48aab205.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/882d77912a3c8e3a.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/ac6aa30297a80464.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/c10a57758af882c8.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/cfd04aaae6955b67.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/d06a90fd413b36cf.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/dda06914899a6c28.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/scramjet.js +34 -0
- package/dist/.downloads/scramjet/package/dist/scramjet.js.map +1 -0
- package/dist/.downloads/scramjet/package/dist/scramjet.mjs +34 -0
- package/dist/.downloads/scramjet/package/dist/scramjet.mjs.map +1 -0
- package/dist/.downloads/scramjet/package/dist/scramjet.wasm +0 -0
- package/dist/.downloads/scramjet/package/dist/scramjet_bundled.js +34 -0
- package/dist/.downloads/scramjet/package/dist/scramjet_bundled.js.map +1 -0
- package/dist/.downloads/scramjet/package/dist/scramjet_bundled.mjs +34 -0
- package/dist/.downloads/scramjet/package/dist/scramjet_bundled.mjs.map +1 -0
- package/dist/.downloads/scramjet/package/dist/types/Tap.d.ts +32 -0
- package/dist/.downloads/scramjet/package/dist/types/client/client.d.ts +115 -0
- package/dist/.downloads/scramjet/package/dist/types/client/entry.d.ts +5 -0
- package/dist/.downloads/scramjet/package/dist/types/client/events.d.ts +10 -0
- package/dist/.downloads/scramjet/package/dist/types/client/global.d.ts +4 -0
- package/dist/.downloads/scramjet/package/dist/types/client/helpers.d.ts +1 -0
- package/dist/.downloads/scramjet/package/dist/types/client/index.d.ts +7 -0
- package/dist/.downloads/scramjet/package/dist/types/client/location.d.ts +2 -0
- package/dist/.downloads/scramjet/package/dist/types/client/shared/eval.d.ts +3 -0
- package/dist/.downloads/scramjet/package/dist/types/client/shared/sourcemaps.d.ts +19 -0
- package/dist/.downloads/scramjet/package/dist/types/client/shared/unproxy.d.ts +19 -0
- package/dist/.downloads/scramjet/package/dist/types/client/shared/wrap.d.ts +4 -0
- package/dist/.downloads/scramjet/package/dist/types/client/singletonbox.d.ts +16 -0
- package/dist/.downloads/scramjet/package/dist/types/client/unproxy.generated.d.ts +50 -0
- package/dist/.downloads/scramjet/package/dist/types/fetch/body.d.ts +3 -0
- package/dist/.downloads/scramjet/package/dist/types/fetch/fetch.d.ts +7 -0
- package/dist/.downloads/scramjet/package/dist/types/fetch/headers.d.ts +19 -0
- package/dist/.downloads/scramjet/package/dist/types/fetch/index.d.ts +128 -0
- package/dist/.downloads/scramjet/package/dist/types/fetch/parse.d.ts +22 -0
- package/dist/.downloads/scramjet/package/dist/types/fetch/util.d.ts +7 -0
- package/dist/.downloads/scramjet/package/dist/types/index.d.ts +11 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/cookie.d.ts +26 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/headers.d.ts +13 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/htmlRules.d.ts +6 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/index.d.ts +51 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/mime.d.ts +39 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/refresh.d.ts +7 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/css.d.ts +4 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/html.d.ts +33 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/index.d.ts +6 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/js.d.ts +11 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/url.d.ts +25 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/wasm.d.ts +7 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/worker.d.ts +3 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/set-cookie-parser.d.ts +20 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/snapshot.d.ts +236 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/sniffEncoding.d.ts +65 -0
- package/dist/.downloads/scramjet/package/dist/types/shared/util.d.ts +2 -0
- package/dist/.downloads/scramjet/package/dist/types/symbols.d.ts +6 -0
- package/dist/.downloads/scramjet/package/dist/types/types.d.ts +68 -0
- package/dist/.downloads/scramjet/package/lib/index.cjs +7 -0
- package/dist/.downloads/scramjet/package/lib/index.d.ts +8 -0
- package/dist/.downloads/scramjet/package/lib/types.d.ts +20 -0
- package/dist/.downloads/scramjet/package/package.json +93 -0
- package/dist/bootstrap-client.js +169 -0
- package/dist/bootstrap-client.js.map +1 -0
- package/dist/bootstrap-server.js +406 -0
- package/dist/bootstrap-server.js.map +1 -0
- package/dist/bootstrap-static.js +476 -0
- package/dist/bootstrap-static.js.map +1 -0
- package/dist/types/client.d.ts +4 -0
- package/dist/types/clientcommon.d.ts +2 -0
- package/dist/types/common.d.ts +30 -0
- package/dist/types/server.d.ts +24 -0
- package/dist/types/static.d.ts +1 -0
- package/package.json +30 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import { type MethodsDefinition, RpcHelper } from "@mercuryworkshop/rpc";
|
|
2
|
+
import {
|
|
3
|
+
BareResponse,
|
|
4
|
+
type ProxyTransport,
|
|
5
|
+
} from "@mercuryworkshop/proxy-transports";
|
|
6
|
+
import type * as ScramjetGlobal from "@mercuryworkshop/scramjet";
|
|
7
|
+
declare const $scramjet: typeof ScramjetGlobal;
|
|
8
|
+
import { deepmerge } from "@fastify/deepmerge";
|
|
9
|
+
import { CONTROLLERFRAME } from "./symbols";
|
|
10
|
+
import type {
|
|
11
|
+
FrameInitHooks,
|
|
12
|
+
SerializedCookieSyncEntry,
|
|
13
|
+
TransportToController,
|
|
14
|
+
Controllerbound,
|
|
15
|
+
ControllerToTransport,
|
|
16
|
+
SWbound,
|
|
17
|
+
WebSocketMessage,
|
|
18
|
+
FrameErrorHooks,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
export { HttpCachePlugin, type HttpCachePluginOptions } from "./cache";
|
|
22
|
+
|
|
23
|
+
export type Config = {
|
|
24
|
+
prefix: string;
|
|
25
|
+
scramjetPath: string;
|
|
26
|
+
injectPath: string;
|
|
27
|
+
wasmPath: string;
|
|
28
|
+
virtualWasmPath: string;
|
|
29
|
+
codec: Record<"encode" | "decode", (input: string) => string>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const config: Config = {
|
|
33
|
+
prefix: "/~/sj/",
|
|
34
|
+
scramjetPath: "/scramjet/scramjet.js",
|
|
35
|
+
injectPath: "/controller/controller.inject.js",
|
|
36
|
+
wasmPath: "/scramjet/scramjet.wasm",
|
|
37
|
+
virtualWasmPath: "scramjet.wasm.js",
|
|
38
|
+
codec: {
|
|
39
|
+
encode: (url: string) => {
|
|
40
|
+
if (!url) return url;
|
|
41
|
+
|
|
42
|
+
return encodeURIComponent(url);
|
|
43
|
+
},
|
|
44
|
+
decode: (url: string) => {
|
|
45
|
+
if (!url) return url;
|
|
46
|
+
|
|
47
|
+
return decodeURIComponent(url);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const scramjetConfig: Partial<ScramjetGlobal.ScramjetConfig> = {
|
|
53
|
+
flags: {
|
|
54
|
+
...$scramjet.defaultConfig.flags,
|
|
55
|
+
allowFailedIntercepts: true,
|
|
56
|
+
},
|
|
57
|
+
maskedfiles: ["inject.js", "scramjet.wasm.js"],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type PersistedCookieState = {
|
|
61
|
+
updatedAt: number;
|
|
62
|
+
cookies: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const COOKIE_DB_NAME = "__scramjet_controller";
|
|
66
|
+
const COOKIE_STORE_NAME = "state";
|
|
67
|
+
const COOKIE_STATE_KEY = "cookies";
|
|
68
|
+
const BROADCASTCHANNEL_NAME = "__scramjet_controller_channel";
|
|
69
|
+
|
|
70
|
+
let cookieDbPromise: Promise<IDBDatabase> | null = null;
|
|
71
|
+
|
|
72
|
+
function parsePersistedCookieState(
|
|
73
|
+
value: unknown
|
|
74
|
+
): PersistedCookieState | null {
|
|
75
|
+
if (
|
|
76
|
+
typeof value !== "object" ||
|
|
77
|
+
value === null ||
|
|
78
|
+
typeof (value as PersistedCookieState).updatedAt !== "number" ||
|
|
79
|
+
!Number.isFinite((value as PersistedCookieState).updatedAt) ||
|
|
80
|
+
typeof (value as PersistedCookieState).cookies !== "string"
|
|
81
|
+
) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return value as PersistedCookieState;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requestToPromise<T>(request: IDBRequest<T>): Promise<T> {
|
|
89
|
+
return new Promise<T>((resolve, reject) => {
|
|
90
|
+
request.onsuccess = () => resolve(request.result);
|
|
91
|
+
request.onerror = () =>
|
|
92
|
+
reject(request.error ?? new Error("IndexedDB request failed"));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function transactionToPromise(transaction: IDBTransaction): Promise<void> {
|
|
97
|
+
return new Promise<void>((resolve, reject) => {
|
|
98
|
+
transaction.oncomplete = () => resolve();
|
|
99
|
+
transaction.onabort = () =>
|
|
100
|
+
reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
101
|
+
transaction.onerror = () =>
|
|
102
|
+
reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function openCookieDatabase(): Promise<IDBDatabase> {
|
|
107
|
+
if (cookieDbPromise) {
|
|
108
|
+
return cookieDbPromise;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
cookieDbPromise = new Promise<IDBDatabase>((resolve, reject) => {
|
|
112
|
+
const request = indexedDB.open(COOKIE_DB_NAME, 1);
|
|
113
|
+
request.onupgradeneeded = () => {
|
|
114
|
+
const db = request.result;
|
|
115
|
+
if (!db.objectStoreNames.contains(COOKIE_STORE_NAME)) {
|
|
116
|
+
db.createObjectStore(COOKIE_STORE_NAME);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
request.onsuccess = () => resolve(request.result);
|
|
120
|
+
request.onerror = () =>
|
|
121
|
+
reject(request.error ?? new Error("Failed to open cookie database"));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return cookieDbPromise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function readCookieState(): Promise<PersistedCookieState | null> {
|
|
128
|
+
try {
|
|
129
|
+
const db = await openCookieDatabase();
|
|
130
|
+
const transaction = db.transaction(COOKIE_STORE_NAME, "readonly");
|
|
131
|
+
const store = transaction.objectStore(COOKIE_STORE_NAME);
|
|
132
|
+
const value = await requestToPromise(store.get(COOKIE_STATE_KEY));
|
|
133
|
+
await transactionToPromise(transaction);
|
|
134
|
+
return parsePersistedCookieState(value);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error("Failed to read persisted controller cookies:", error);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function writeCookieState(
|
|
142
|
+
cookies: string,
|
|
143
|
+
currentUpdatedAt: number
|
|
144
|
+
): Promise<number> {
|
|
145
|
+
try {
|
|
146
|
+
const db = await openCookieDatabase();
|
|
147
|
+
const transaction = db.transaction(COOKIE_STORE_NAME, "readwrite");
|
|
148
|
+
const store = transaction.objectStore(COOKIE_STORE_NAME);
|
|
149
|
+
const existing = parsePersistedCookieState(
|
|
150
|
+
await requestToPromise(store.get(COOKIE_STATE_KEY))
|
|
151
|
+
);
|
|
152
|
+
const updatedAt = Math.max(
|
|
153
|
+
Date.now(),
|
|
154
|
+
currentUpdatedAt + 1,
|
|
155
|
+
(existing?.updatedAt ?? 0) + 1
|
|
156
|
+
);
|
|
157
|
+
const state: PersistedCookieState = {
|
|
158
|
+
updatedAt,
|
|
159
|
+
cookies,
|
|
160
|
+
};
|
|
161
|
+
store.put(state, COOKIE_STATE_KEY);
|
|
162
|
+
await transactionToPromise(transaction);
|
|
163
|
+
return updatedAt;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error("Failed to persist controller cookies:", error);
|
|
166
|
+
return currentUpdatedAt;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function makeId(): string {
|
|
171
|
+
return Math.random().toString(36).substring(2, 10);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const deepMerge = deepmerge();
|
|
175
|
+
|
|
176
|
+
type ControllerInit = {
|
|
177
|
+
serviceworker: ServiceWorker;
|
|
178
|
+
transport: ProxyTransport;
|
|
179
|
+
config?: Partial<Config>;
|
|
180
|
+
scramjetConfig?: Partial<ScramjetGlobal.ScramjetConfig>;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export class Controller {
|
|
184
|
+
id: string;
|
|
185
|
+
config: Config;
|
|
186
|
+
scramjetConfig: ScramjetGlobal.ScramjetConfig;
|
|
187
|
+
prefix: string;
|
|
188
|
+
cookieJar = new $scramjet.CookieJar();
|
|
189
|
+
frames: Frame[] = [];
|
|
190
|
+
serviceWorkerController: ServiceWorker;
|
|
191
|
+
guardServiceWorkerRevive = true;
|
|
192
|
+
|
|
193
|
+
private ready: Promise<void>;
|
|
194
|
+
private readyResolve!: () => void;
|
|
195
|
+
public isReady: boolean = false;
|
|
196
|
+
rpc: RpcHelper<Controllerbound, SWbound>;
|
|
197
|
+
private port: MessagePort | null = null;
|
|
198
|
+
|
|
199
|
+
transport: ProxyTransport;
|
|
200
|
+
private cookieUpdatedAt = 0;
|
|
201
|
+
private cookieSyncPromise: Promise<void> | null = null;
|
|
202
|
+
private cookieSyncDirty = true;
|
|
203
|
+
private cookieSyncChannel = new BroadcastChannel(BROADCASTCHANNEL_NAME);
|
|
204
|
+
|
|
205
|
+
private wasmAlreadyFetched = false;
|
|
206
|
+
private wasmPayload: string | null = null;
|
|
207
|
+
private onTabChannelMessage: (e: MessageEvent) => void = (e) => {
|
|
208
|
+
this.rpc.recieve(e.data);
|
|
209
|
+
};
|
|
210
|
+
private onCookieSyncMessage = (event: MessageEvent) => {
|
|
211
|
+
const updatedAt =
|
|
212
|
+
typeof event.data === "object" && event.data !== null
|
|
213
|
+
? (event.data as { updatedAt?: unknown }).updatedAt
|
|
214
|
+
: undefined;
|
|
215
|
+
if (typeof updatedAt !== "number" || updatedAt <= this.cookieUpdatedAt) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.cookieSyncDirty = true;
|
|
220
|
+
void this.loadSavedCookies();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
private async loadScramjetWasm() {
|
|
224
|
+
if (this.wasmAlreadyFetched) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const resp = await fetch(this.config.wasmPath);
|
|
229
|
+
$scramjet.setWasm(await resp.arrayBuffer());
|
|
230
|
+
this.wasmAlreadyFetched = true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private methods: MethodsDefinition<Controllerbound> = {
|
|
234
|
+
ready: async () => {
|
|
235
|
+
this.readyResolve();
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
this.guardServiceWorkerRevive = false;
|
|
238
|
+
}, 5000);
|
|
239
|
+
},
|
|
240
|
+
request: async (data) => {
|
|
241
|
+
const path = new URL(data.rawUrl).pathname;
|
|
242
|
+
const frame = this.frames.find((f) => path.startsWith(f.prefix));
|
|
243
|
+
if (!frame) throw new Error("No frame found for request");
|
|
244
|
+
try {
|
|
245
|
+
// doesn't actually *load* every request, but hold up requests until the promise finishes
|
|
246
|
+
await this.loadSavedCookies();
|
|
247
|
+
|
|
248
|
+
if (path === frame.prefix + this.config.virtualWasmPath) {
|
|
249
|
+
if (!this.wasmPayload) {
|
|
250
|
+
const resp = await fetch(this.config.wasmPath);
|
|
251
|
+
const buf = await resp.arrayBuffer();
|
|
252
|
+
const b64 = btoa(
|
|
253
|
+
new Uint8Array(buf)
|
|
254
|
+
.reduce(
|
|
255
|
+
(data, byte) => (data.push(String.fromCharCode(byte)), data),
|
|
256
|
+
[] as any
|
|
257
|
+
)
|
|
258
|
+
.join("")
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
this.wasmPayload = `self.WASM = '${b64}';`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
body: this.wasmPayload,
|
|
267
|
+
status: 200,
|
|
268
|
+
statusText: "OK",
|
|
269
|
+
headers: [["Content-Type", "application/javascript"]],
|
|
270
|
+
},
|
|
271
|
+
[],
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const sjheaders = $scramjet.ScramjetHeaders.fromRawHeaders(
|
|
276
|
+
data.initialHeaders
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const fetchresponse = await frame.fetchHandler.handleFetch({
|
|
280
|
+
initialHeaders: sjheaders,
|
|
281
|
+
rawClientUrl: data.rawClientUrl
|
|
282
|
+
? new URL(data.rawClientUrl)
|
|
283
|
+
: undefined,
|
|
284
|
+
rawUrl: new URL(data.rawUrl),
|
|
285
|
+
rawReferrer: data.rawReferrer,
|
|
286
|
+
rawDestination: data.destination,
|
|
287
|
+
method: data.method,
|
|
288
|
+
mode: data.mode,
|
|
289
|
+
referrer: data.referrer,
|
|
290
|
+
body: data.body,
|
|
291
|
+
cache: data.cache,
|
|
292
|
+
clientId: data.clientId,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return [
|
|
296
|
+
{
|
|
297
|
+
body: fetchresponse.body,
|
|
298
|
+
status: fetchresponse.status,
|
|
299
|
+
statusText: fetchresponse.statusText,
|
|
300
|
+
headers: fetchresponse.headers.toRawHeaders(),
|
|
301
|
+
},
|
|
302
|
+
fetchresponse.body instanceof ReadableStream ||
|
|
303
|
+
fetchresponse.body instanceof ArrayBuffer
|
|
304
|
+
? [fetchresponse.body]
|
|
305
|
+
: [],
|
|
306
|
+
];
|
|
307
|
+
} catch (e) {
|
|
308
|
+
const reqcontext: typeof frame.hooks.error.request.context = {
|
|
309
|
+
rawrequest: data,
|
|
310
|
+
};
|
|
311
|
+
const reqprops: typeof frame.hooks.error.request.props = {
|
|
312
|
+
setResponse: undefined,
|
|
313
|
+
suppressError: false,
|
|
314
|
+
};
|
|
315
|
+
await $scramjet.Tap.dispatch(
|
|
316
|
+
frame.hooks.error.request,
|
|
317
|
+
reqcontext,
|
|
318
|
+
reqprops
|
|
319
|
+
);
|
|
320
|
+
if (!reqprops.suppressError) {
|
|
321
|
+
console.error("Error in controller request handler:", e);
|
|
322
|
+
}
|
|
323
|
+
if (reqprops.setResponse) {
|
|
324
|
+
return [reqprops.setResponse, []];
|
|
325
|
+
}
|
|
326
|
+
throw e;
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
initRemoteTransport: async (port) => {
|
|
330
|
+
const rpc = new RpcHelper<TransportToController, ControllerToTransport>(
|
|
331
|
+
{
|
|
332
|
+
request: async ({ remote, method, body, headers }) => {
|
|
333
|
+
const response = await this.transport.request(
|
|
334
|
+
new URL(remote),
|
|
335
|
+
method,
|
|
336
|
+
body,
|
|
337
|
+
headers,
|
|
338
|
+
undefined
|
|
339
|
+
);
|
|
340
|
+
return [response, [response.body]];
|
|
341
|
+
},
|
|
342
|
+
sendSetCookie: async ({ cookies, options }) => {
|
|
343
|
+
await this.loadSavedCookies(true);
|
|
344
|
+
if (options?.clear) {
|
|
345
|
+
this.cookieJar.clear();
|
|
346
|
+
}
|
|
347
|
+
this.applyCookieSyncEntries(cookies);
|
|
348
|
+
await this.persistCookies();
|
|
349
|
+
await this.propagateCookieSync(cookies, options);
|
|
350
|
+
},
|
|
351
|
+
connect: async ({ url, protocols, requestHeaders, port }) => {
|
|
352
|
+
let resolve: (arg: TransportToController["connect"][1]) => void;
|
|
353
|
+
const promise = new Promise<TransportToController["connect"][1]>(
|
|
354
|
+
(res) => (resolve = res)
|
|
355
|
+
);
|
|
356
|
+
const [send, close] = this.transport.connect(
|
|
357
|
+
new URL(url),
|
|
358
|
+
protocols,
|
|
359
|
+
requestHeaders,
|
|
360
|
+
(protocol, extensions) => {
|
|
361
|
+
resolve({
|
|
362
|
+
result: "success",
|
|
363
|
+
protocol: protocol,
|
|
364
|
+
extensions: extensions,
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
(data) => {
|
|
368
|
+
port.postMessage(
|
|
369
|
+
{
|
|
370
|
+
type: "data",
|
|
371
|
+
data: data,
|
|
372
|
+
} as WebSocketMessage,
|
|
373
|
+
data instanceof ArrayBuffer ? [data] : []
|
|
374
|
+
);
|
|
375
|
+
},
|
|
376
|
+
(close, reason) => {
|
|
377
|
+
port.postMessage({
|
|
378
|
+
type: "close",
|
|
379
|
+
code: close,
|
|
380
|
+
reason: reason,
|
|
381
|
+
} as WebSocketMessage);
|
|
382
|
+
},
|
|
383
|
+
(error) => {
|
|
384
|
+
resolve({
|
|
385
|
+
result: "failure",
|
|
386
|
+
error: error,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
port.onmessageerror = (ev) => {
|
|
391
|
+
console.error(
|
|
392
|
+
"Transport port messageerror (this should never happen!)",
|
|
393
|
+
ev
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
port.onmessage = ({ data }: { data: WebSocketMessage }) => {
|
|
397
|
+
if (data.type === "data") {
|
|
398
|
+
send(data.data);
|
|
399
|
+
} else if (data.type === "close") {
|
|
400
|
+
close(data.code, data.reason);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
return [await promise, []];
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
"transport",
|
|
408
|
+
(data, transfer) => port.postMessage(data, transfer)
|
|
409
|
+
);
|
|
410
|
+
port.onmessageerror = (ev) => {
|
|
411
|
+
console.error(
|
|
412
|
+
"Transport port messageerror (this should never happen!)",
|
|
413
|
+
ev
|
|
414
|
+
);
|
|
415
|
+
};
|
|
416
|
+
port.onmessage = (e) => {
|
|
417
|
+
rpc.recieve(e.data);
|
|
418
|
+
};
|
|
419
|
+
rpc.call("ready", undefined, []);
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
constructor(public init: ControllerInit) {
|
|
424
|
+
this.id = makeId();
|
|
425
|
+
this.config = deepMerge(config, init.config || {}) as Config;
|
|
426
|
+
this.scramjetConfig = deepMerge(scramjetConfig, $scramjet.defaultConfig);
|
|
427
|
+
this.scramjetConfig = deepMerge(
|
|
428
|
+
this.scramjetConfig,
|
|
429
|
+
init.scramjetConfig || {}
|
|
430
|
+
) as ScramjetGlobal.ScramjetConfig;
|
|
431
|
+
this.prefix = this.config.prefix + this.id + "/";
|
|
432
|
+
this.serviceWorkerController = init.serviceworker;
|
|
433
|
+
|
|
434
|
+
this.ready = Promise.all([
|
|
435
|
+
new Promise<void>((resolve) => {
|
|
436
|
+
this.readyResolve = resolve;
|
|
437
|
+
}),
|
|
438
|
+
this.loadScramjetWasm(),
|
|
439
|
+
this.loadSavedCookies(true),
|
|
440
|
+
]).then(() => undefined);
|
|
441
|
+
|
|
442
|
+
this.rpc = new RpcHelper<Controllerbound, SWbound>(
|
|
443
|
+
this.methods,
|
|
444
|
+
"tabchannel-" + this.id,
|
|
445
|
+
(data, transfer) => {
|
|
446
|
+
if (!this.port) {
|
|
447
|
+
throw new Error("Port not found");
|
|
448
|
+
}
|
|
449
|
+
this.port.postMessage(data, transfer);
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
this.transport = init.transport;
|
|
453
|
+
|
|
454
|
+
this.cookieSyncChannel.addEventListener(
|
|
455
|
+
"message",
|
|
456
|
+
this.onCookieSyncMessage
|
|
457
|
+
);
|
|
458
|
+
this.setupMessagePort();
|
|
459
|
+
|
|
460
|
+
navigator.serviceWorker.addEventListener("message", (e) => {
|
|
461
|
+
if (
|
|
462
|
+
e.data?.$controller$setCookie &&
|
|
463
|
+
typeof e.data.$controller$setCookie === "object"
|
|
464
|
+
) {
|
|
465
|
+
const payload = e.data.$controller$setCookie as {
|
|
466
|
+
cookies?: SerializedCookieSyncEntry[];
|
|
467
|
+
options?: ScramjetGlobal.CookieSyncOptions;
|
|
468
|
+
id?: string;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
if (payload.options?.clear) {
|
|
472
|
+
this.cookieJar.clear();
|
|
473
|
+
}
|
|
474
|
+
this.applyCookieSyncEntries(payload.cookies);
|
|
475
|
+
|
|
476
|
+
if (typeof payload.id === "string") {
|
|
477
|
+
this.serviceWorkerController.postMessage({
|
|
478
|
+
$sw$setCookieDone: {
|
|
479
|
+
id: payload.id,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (e.data.$controller$swrevive) {
|
|
488
|
+
// if we just spawned the service worker, it will send this even though it's not actually dead
|
|
489
|
+
// TODO: pretty jank, fix at some point
|
|
490
|
+
if (this.guardServiceWorkerRevive) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
this.setupMessagePort();
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private setupMessagePort() {
|
|
499
|
+
if (this.port) {
|
|
500
|
+
this.port.removeEventListener("message", this.onTabChannelMessage);
|
|
501
|
+
try {
|
|
502
|
+
this.port.close();
|
|
503
|
+
} catch {
|
|
504
|
+
// ignore
|
|
505
|
+
}
|
|
506
|
+
this.port = null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const channel = new MessageChannel();
|
|
510
|
+
this.port = channel.port1;
|
|
511
|
+
this.port.addEventListener("message", this.onTabChannelMessage);
|
|
512
|
+
this.port.start();
|
|
513
|
+
|
|
514
|
+
this.serviceWorkerController.postMessage(
|
|
515
|
+
{
|
|
516
|
+
$controller$init: {
|
|
517
|
+
prefix: this.prefix,
|
|
518
|
+
id: this.id,
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
[channel.port2]
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// TODO: should this be a method on the cookie jar?
|
|
526
|
+
private applyCookieSyncEntries(
|
|
527
|
+
cookies: SerializedCookieSyncEntry[] | undefined
|
|
528
|
+
) {
|
|
529
|
+
if (!Array.isArray(cookies)) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for (const entry of cookies) {
|
|
534
|
+
if (typeof entry?.url !== "string" || typeof entry.cookie !== "string") {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
this.cookieJar.setCookies(entry.cookie, new URL(entry.url));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async propagateCookieSync(
|
|
543
|
+
cookies: SerializedCookieSyncEntry[],
|
|
544
|
+
options: ScramjetGlobal.CookieSyncOptions = {}
|
|
545
|
+
): Promise<void> {
|
|
546
|
+
if (!this.port) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
await this.rpc.call("sendSetCookie", {
|
|
551
|
+
cookies,
|
|
552
|
+
options,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private async loadSavedCookies(force = false): Promise<void> {
|
|
557
|
+
if (!force && !this.cookieSyncDirty) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (this.cookieSyncPromise) {
|
|
562
|
+
return this.cookieSyncPromise;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.cookieSyncPromise = (async () => {
|
|
566
|
+
const persisted = await readCookieState();
|
|
567
|
+
if (persisted && persisted.updatedAt > this.cookieUpdatedAt) {
|
|
568
|
+
this.cookieJar.load(persisted.cookies);
|
|
569
|
+
this.cookieUpdatedAt = persisted.updatedAt;
|
|
570
|
+
}
|
|
571
|
+
this.cookieSyncDirty = false;
|
|
572
|
+
})().finally(() => {
|
|
573
|
+
this.cookieSyncPromise = null;
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
return this.cookieSyncPromise;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async persistCookies(): Promise<void> {
|
|
580
|
+
const updatedAt = await writeCookieState(
|
|
581
|
+
this.cookieJar.dump(),
|
|
582
|
+
this.cookieUpdatedAt
|
|
583
|
+
);
|
|
584
|
+
if (updatedAt <= this.cookieUpdatedAt) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this.cookieUpdatedAt = updatedAt;
|
|
589
|
+
this.cookieSyncDirty = false;
|
|
590
|
+
this.cookieSyncChannel.postMessage({
|
|
591
|
+
updatedAt,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
setTransport(transport: ProxyTransport) {
|
|
596
|
+
this.transport = transport;
|
|
597
|
+
for (const frame of this.frames) {
|
|
598
|
+
frame.controller.transport = transport;
|
|
599
|
+
frame.fetchHandler.client.transport = transport;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
createFrame(element?: HTMLIFrameElement): Frame {
|
|
604
|
+
if (!this.ready) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
"Controller is not ready! Try awaiting controller.wait()"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
element ??= document.createElement("iframe");
|
|
610
|
+
const frame = new Frame(this, element);
|
|
611
|
+
this.frames.push(frame);
|
|
612
|
+
return frame;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async wait(): Promise<void> {
|
|
616
|
+
await this.ready;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function base64Encode(text: string) {
|
|
621
|
+
return btoa(
|
|
622
|
+
new TextEncoder()
|
|
623
|
+
.encode(text)
|
|
624
|
+
.reduce(
|
|
625
|
+
(data, byte) => (data.push(String.fromCharCode(byte)), data),
|
|
626
|
+
[] as any
|
|
627
|
+
)
|
|
628
|
+
.join("")
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function yieldGetInjectScripts(
|
|
633
|
+
config: Config,
|
|
634
|
+
sjconfig: ScramjetGlobal.ScramjetConfig,
|
|
635
|
+
prefix: URL,
|
|
636
|
+
cookieJar: ScramjetGlobal.CookieJar,
|
|
637
|
+
codecEncode: (input: string) => string,
|
|
638
|
+
codecDecode: (input: string) => string
|
|
639
|
+
) {
|
|
640
|
+
const getInjectScripts: ScramjetGlobal.ScramjetInterface["getInjectScripts"] =
|
|
641
|
+
(meta, handler, htmlcontext, script) => {
|
|
642
|
+
function base64Encode(text: string) {
|
|
643
|
+
return btoa(
|
|
644
|
+
new TextEncoder()
|
|
645
|
+
.encode(text)
|
|
646
|
+
.reduce(
|
|
647
|
+
(data, byte) => (data.push(String.fromCharCode(byte)), data),
|
|
648
|
+
[] as any
|
|
649
|
+
)
|
|
650
|
+
.join("")
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
return [
|
|
654
|
+
script(config.scramjetPath),
|
|
655
|
+
script(prefix.href + config.virtualWasmPath),
|
|
656
|
+
script(config.injectPath),
|
|
657
|
+
script(
|
|
658
|
+
"data:text/javascript;charset=utf-8;base64," +
|
|
659
|
+
base64Encode(`
|
|
660
|
+
document.querySelectorAll("script[scramjet-injected]").forEach(script => script.remove());
|
|
661
|
+
$scramjetController.load({
|
|
662
|
+
config: ${JSON.stringify(config)},
|
|
663
|
+
sjconfig: ${JSON.stringify(sjconfig)},
|
|
664
|
+
prefix: new URL("${prefix.href}"),
|
|
665
|
+
cookies: ${JSON.stringify(cookieJar.dump())},
|
|
666
|
+
yieldGetInjectScripts: ${yieldGetInjectScripts.toString()},
|
|
667
|
+
codecEncode: ${codecEncode.toString()},
|
|
668
|
+
codecDecode: ${codecDecode.toString()},
|
|
669
|
+
initHeaders: ${JSON.stringify(htmlcontext.headers ?? [])},
|
|
670
|
+
history: ${JSON.stringify(htmlcontext.history ?? [])},
|
|
671
|
+
})
|
|
672
|
+
`)
|
|
673
|
+
),
|
|
674
|
+
];
|
|
675
|
+
};
|
|
676
|
+
return getInjectScripts;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export class Frame {
|
|
680
|
+
id: string;
|
|
681
|
+
prefix: string;
|
|
682
|
+
fetchHandler: ScramjetGlobal.ScramjetFetchHandler;
|
|
683
|
+
hooks: {
|
|
684
|
+
fetch: ScramjetGlobal.FetchHooks;
|
|
685
|
+
init: FrameInitHooks;
|
|
686
|
+
error: FrameErrorHooks;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
get context(): ScramjetGlobal.ScramjetContext {
|
|
690
|
+
return {
|
|
691
|
+
config: this.controller.scramjetConfig,
|
|
692
|
+
prefix: new URL(this.prefix, location.href),
|
|
693
|
+
cookieJar: this.controller.cookieJar,
|
|
694
|
+
interface: {
|
|
695
|
+
getInjectScripts: yieldGetInjectScripts(
|
|
696
|
+
this.controller.config,
|
|
697
|
+
this.controller.scramjetConfig,
|
|
698
|
+
new URL(this.prefix, location.href),
|
|
699
|
+
this.controller.cookieJar,
|
|
700
|
+
this.controller.config.codec.encode,
|
|
701
|
+
this.controller.config.codec.decode
|
|
702
|
+
),
|
|
703
|
+
getWorkerInjectScripts: (meta, type, script) => {
|
|
704
|
+
let str = "";
|
|
705
|
+
|
|
706
|
+
str += script(this.controller.config.scramjetPath);
|
|
707
|
+
str += script(this.prefix + this.controller.config.virtualWasmPath);
|
|
708
|
+
str += script(
|
|
709
|
+
"data:text/javascript;charset=utf-8;base64," +
|
|
710
|
+
base64Encode(`
|
|
711
|
+
(()=>{
|
|
712
|
+
const { ScramjetClient, CookieJar, setWasm } = $scramjet;
|
|
713
|
+
|
|
714
|
+
setWasm(Uint8Array.from(atob(self.WASM), (c) => c.charCodeAt(0)));
|
|
715
|
+
delete self.WASM;
|
|
716
|
+
|
|
717
|
+
const sjconfig = ${JSON.stringify(this.controller.scramjetConfig)};
|
|
718
|
+
const prefix = new URL("${this.prefix}", location.href);
|
|
719
|
+
|
|
720
|
+
const context = {
|
|
721
|
+
config: sjconfig,
|
|
722
|
+
prefix,
|
|
723
|
+
interface: {
|
|
724
|
+
codecEncode: ${this.controller.config.codec.encode.toString()},
|
|
725
|
+
codecDecode: ${this.controller.config.codec.decode.toString()},
|
|
726
|
+
},
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const client = new ScramjetClient(globalThis, {
|
|
730
|
+
context,
|
|
731
|
+
transport: null,
|
|
732
|
+
shouldPassthroughWebsocket: (url) => {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
client.hook();
|
|
738
|
+
})();
|
|
739
|
+
`)
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
return str;
|
|
743
|
+
},
|
|
744
|
+
codecEncode: this.controller.config.codec.encode,
|
|
745
|
+
codecDecode: this.controller.config.codec.decode,
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
constructor(
|
|
751
|
+
public controller: Controller,
|
|
752
|
+
public element: HTMLIFrameElement
|
|
753
|
+
) {
|
|
754
|
+
this.id = makeId();
|
|
755
|
+
this.prefix = this.controller.prefix + this.id + "/";
|
|
756
|
+
|
|
757
|
+
this.fetchHandler = new $scramjet.ScramjetFetchHandler({
|
|
758
|
+
crossOriginIsolated: self.crossOriginIsolated,
|
|
759
|
+
context: this.context,
|
|
760
|
+
transport: controller.transport,
|
|
761
|
+
async sendSetCookie(cookies, options) {
|
|
762
|
+
await controller.persistCookies();
|
|
763
|
+
await controller.propagateCookieSync(
|
|
764
|
+
cookies.map(({ url, cookie }) => ({
|
|
765
|
+
url: url.href,
|
|
766
|
+
cookie,
|
|
767
|
+
})),
|
|
768
|
+
options
|
|
769
|
+
);
|
|
770
|
+
},
|
|
771
|
+
async fetchBlobUrl(url) {
|
|
772
|
+
return BareResponse.fromNativeResponse(await fetch(url));
|
|
773
|
+
},
|
|
774
|
+
async fetchDataUrl(url) {
|
|
775
|
+
return BareResponse.fromNativeResponse(await fetch(url));
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
this.hooks = {
|
|
780
|
+
fetch: this.fetchHandler.hooks.fetch,
|
|
781
|
+
init: $scramjet.Tap.create<FrameInitHooks>(),
|
|
782
|
+
error: $scramjet.Tap.create<FrameErrorHooks>(),
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
element[CONTROLLERFRAME] = this;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
back() {
|
|
789
|
+
this.element.contentWindow?.history.back();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
forward() {
|
|
793
|
+
this.element.contentWindow?.history.forward();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
reload() {
|
|
797
|
+
this.element.contentWindow?.location.reload();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
go(url: string) {
|
|
801
|
+
const encoded = $scramjet.rewriteUrl(url, this.context, {
|
|
802
|
+
//@ts-expect-error
|
|
803
|
+
origin: new URL(location.href),
|
|
804
|
+
//@ts-expect-error
|
|
805
|
+
base: new URL(location.href),
|
|
806
|
+
});
|
|
807
|
+
this.element.src = encoded;
|
|
808
|
+
}
|
|
809
|
+
}
|