@maintainer-pro/ai-bridge 0.1.23 → 0.1.25
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/package.json +1 -1
- package/src/cors-cookies.mjs +274 -0
- package/src/daemon.mjs +268 -3
- package/src/share-rewrite.mjs +124 -18
package/package.json
CHANGED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie jar for Bypass CORS. The browser talks to /p/{token}/__cors
|
|
3
|
+
* (admin origin), so it never sends the remote API's cookies. The bridge
|
|
4
|
+
* stores Set-Cookie from those hosts and attaches them on the next hop.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const MAX_COOKIES_PER_SANDBOX = 80;
|
|
11
|
+
const FILE_NAME = "cors-cookies.json";
|
|
12
|
+
|
|
13
|
+
/** @type {Map<string, CookieRecord[]>} */
|
|
14
|
+
const jars = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* name: string,
|
|
19
|
+
* value: string,
|
|
20
|
+
* domain: string,
|
|
21
|
+
* path: string,
|
|
22
|
+
* expires: number | null,
|
|
23
|
+
* secure: boolean,
|
|
24
|
+
* hostOnly: boolean,
|
|
25
|
+
* }} CookieRecord
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
function homeDir() {
|
|
29
|
+
const override = String(process.env.MAINTAINER_PRO_HOME || "").trim();
|
|
30
|
+
return override || path.join(os.homedir(), ".maintainer-pro");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeId(id) {
|
|
34
|
+
return String(id || "")
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/[^\w.-]+/g, "_")
|
|
37
|
+
.slice(0, 80);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function jarPath(sandboxId) {
|
|
41
|
+
const id = sanitizeId(sandboxId);
|
|
42
|
+
if (!id) return "";
|
|
43
|
+
return path.join(homeDir(), "projects", id, FILE_NAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cookieKey(cookie) {
|
|
47
|
+
return `${cookie.name}\0${cookie.domain}\0${cookie.path}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isExpired(cookie, now = Date.now()) {
|
|
51
|
+
return cookie.expires != null && cookie.expires <= now;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hostMatches(hostname, cookie) {
|
|
55
|
+
const host = String(hostname || "").toLowerCase();
|
|
56
|
+
const domain = String(cookie.domain || "").toLowerCase();
|
|
57
|
+
if (!host || !domain) return false;
|
|
58
|
+
if (cookie.hostOnly) return host === domain;
|
|
59
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pathMatches(pathname, cookiePath) {
|
|
63
|
+
const pathName = pathname || "/";
|
|
64
|
+
const prefix = cookiePath || "/";
|
|
65
|
+
if (prefix === "/") return true;
|
|
66
|
+
if (pathName === prefix) return true;
|
|
67
|
+
const dir = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
68
|
+
return pathName.startsWith(dir);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function domainAllowedForHost(host, domain) {
|
|
72
|
+
const h = String(host || "").toLowerCase();
|
|
73
|
+
const d = String(domain || "").replace(/^\./, "").toLowerCase();
|
|
74
|
+
if (!h || !d) return false;
|
|
75
|
+
return h === d || h.endsWith(`.${d}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadJar(sandboxId) {
|
|
79
|
+
const id = sanitizeId(sandboxId);
|
|
80
|
+
if (!id) return [];
|
|
81
|
+
if (jars.has(id)) return jars.get(id) || [];
|
|
82
|
+
let list = [];
|
|
83
|
+
const file = jarPath(id);
|
|
84
|
+
try {
|
|
85
|
+
if (file && fs.existsSync(file)) {
|
|
86
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
87
|
+
if (Array.isArray(raw)) {
|
|
88
|
+
list = raw.filter((row) => row && typeof row.name === "string");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
list = [];
|
|
93
|
+
}
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
list = list.filter((row) => !isExpired(row, now));
|
|
96
|
+
jars.set(id, list);
|
|
97
|
+
return list;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function saveJar(sandboxId, list) {
|
|
101
|
+
const id = sanitizeId(sandboxId);
|
|
102
|
+
if (!id) return;
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const next = list.filter((row) => !isExpired(row, now)).slice(-MAX_COOKIES_PER_SANDBOX);
|
|
105
|
+
jars.set(id, next);
|
|
106
|
+
const file = jarPath(id);
|
|
107
|
+
if (!file) return;
|
|
108
|
+
try {
|
|
109
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
110
|
+
fs.writeFileSync(file, `${JSON.stringify(next)}\n`, "utf8");
|
|
111
|
+
} catch {
|
|
112
|
+
/* ignore */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} raw
|
|
118
|
+
* @param {URL} requestUrl
|
|
119
|
+
* @returns {CookieRecord | null}
|
|
120
|
+
*/
|
|
121
|
+
export function parseSetCookie(raw, requestUrl) {
|
|
122
|
+
const parts = String(raw || "")
|
|
123
|
+
.split(";")
|
|
124
|
+
.map((part) => part.trim())
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
if (!parts.length) return null;
|
|
127
|
+
const nv = parts[0];
|
|
128
|
+
const eq = nv.indexOf("=");
|
|
129
|
+
if (eq <= 0) return null;
|
|
130
|
+
const name = nv.slice(0, eq).trim();
|
|
131
|
+
const value = nv.slice(eq + 1).trim();
|
|
132
|
+
if (!name) return null;
|
|
133
|
+
|
|
134
|
+
const host = String(requestUrl.hostname || "").toLowerCase();
|
|
135
|
+
/** @type {CookieRecord} */
|
|
136
|
+
const cookie = {
|
|
137
|
+
name,
|
|
138
|
+
value,
|
|
139
|
+
domain: host,
|
|
140
|
+
path: "/",
|
|
141
|
+
expires: null,
|
|
142
|
+
secure: false,
|
|
143
|
+
hostOnly: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
for (let i = 1; i < parts.length; i++) {
|
|
147
|
+
const part = parts[i];
|
|
148
|
+
const ieq = part.indexOf("=");
|
|
149
|
+
const key = (ieq >= 0 ? part.slice(0, ieq) : part).trim().toLowerCase();
|
|
150
|
+
const val = ieq >= 0 ? part.slice(ieq + 1).trim() : "";
|
|
151
|
+
if (key === "domain" && val) {
|
|
152
|
+
const domain = val.replace(/^\./, "").toLowerCase();
|
|
153
|
+
if (domainAllowedForHost(host, domain)) {
|
|
154
|
+
cookie.domain = domain;
|
|
155
|
+
cookie.hostOnly = false;
|
|
156
|
+
}
|
|
157
|
+
} else if (key === "path" && val.startsWith("/")) {
|
|
158
|
+
cookie.path = val;
|
|
159
|
+
} else if (key === "max-age") {
|
|
160
|
+
const n = Number(val);
|
|
161
|
+
if (Number.isFinite(n)) cookie.expires = Date.now() + n * 1000;
|
|
162
|
+
} else if (key === "expires") {
|
|
163
|
+
const t = Date.parse(val);
|
|
164
|
+
if (Number.isFinite(t)) cookie.expires = t;
|
|
165
|
+
} else if (key === "secure") {
|
|
166
|
+
cookie.secure = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return cookie;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function headerValues(headers, name) {
|
|
173
|
+
if (!headers || typeof headers !== "object") return [];
|
|
174
|
+
const lower = name.toLowerCase();
|
|
175
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
176
|
+
if (String(key).toLowerCase() !== lower || value == null) continue;
|
|
177
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} sandboxId
|
|
184
|
+
* @param {URL} requestUrl
|
|
185
|
+
* @param {Record<string, unknown>} headers
|
|
186
|
+
*/
|
|
187
|
+
export function storeCorsCookies(sandboxId, requestUrl, headers) {
|
|
188
|
+
if (!sandboxId || !requestUrl) return;
|
|
189
|
+
const lines = headerValues(headers, "set-cookie");
|
|
190
|
+
if (!lines.length) return;
|
|
191
|
+
const list = loadJar(sandboxId);
|
|
192
|
+
const byKey = new Map(list.map((row) => [cookieKey(row), row]));
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const cookie = parseSetCookie(line, requestUrl);
|
|
195
|
+
if (!cookie) continue;
|
|
196
|
+
const key = cookieKey(cookie);
|
|
197
|
+
if (!cookie.value || isExpired(cookie)) {
|
|
198
|
+
byKey.delete(key);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
byKey.set(key, cookie);
|
|
202
|
+
}
|
|
203
|
+
saveJar(sandboxId, [...byKey.values()]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {string} sandboxId
|
|
208
|
+
* @param {URL} requestUrl
|
|
209
|
+
*/
|
|
210
|
+
export function cookieHeaderForUrl(sandboxId, requestUrl) {
|
|
211
|
+
if (!sandboxId || !requestUrl) return "";
|
|
212
|
+
const list = loadJar(sandboxId);
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const https = requestUrl.protocol === "https:";
|
|
215
|
+
const pathName = requestUrl.pathname || "/";
|
|
216
|
+
const matching = [];
|
|
217
|
+
const kept = [];
|
|
218
|
+
for (const cookie of list) {
|
|
219
|
+
if (isExpired(cookie, now)) continue;
|
|
220
|
+
kept.push(cookie);
|
|
221
|
+
if (cookie.secure && !https) continue;
|
|
222
|
+
if (!hostMatches(requestUrl.hostname, cookie)) continue;
|
|
223
|
+
if (!pathMatches(pathName, cookie.path)) continue;
|
|
224
|
+
matching.push(cookie);
|
|
225
|
+
}
|
|
226
|
+
if (kept.length !== list.length) saveJar(sandboxId, kept);
|
|
227
|
+
matching.sort((a, b) => String(b.path).length - String(a.path).length);
|
|
228
|
+
return matching.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Parse a Cookie request header into name → value.
|
|
233
|
+
* @param {string} raw
|
|
234
|
+
*/
|
|
235
|
+
export function parseCookieHeader(raw) {
|
|
236
|
+
/** @type {Map<string, string>} */
|
|
237
|
+
const out = new Map();
|
|
238
|
+
for (const part of String(raw || "").split(";")) {
|
|
239
|
+
const eq = part.indexOf("=");
|
|
240
|
+
if (eq <= 0) continue;
|
|
241
|
+
const name = part.slice(0, eq).trim();
|
|
242
|
+
const value = part.slice(eq + 1).trim();
|
|
243
|
+
if (name) out.set(name, value);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Merge browser cookies (Maintainer Pro share origin, e.g. mp_session)
|
|
250
|
+
* with jar cookies for the remote host. `mp_*` always come from the
|
|
251
|
+
* browser. Other names: jar wins, then any leftover browser cookies.
|
|
252
|
+
* @param {string} incoming
|
|
253
|
+
* @param {string} jar
|
|
254
|
+
*/
|
|
255
|
+
export function mergeCookieHeader(incoming, jar) {
|
|
256
|
+
const browser = parseCookieHeader(incoming);
|
|
257
|
+
const stored = parseCookieHeader(jar);
|
|
258
|
+
/** @type {Map<string, string>} */
|
|
259
|
+
const merged = new Map();
|
|
260
|
+
for (const [name, value] of browser) {
|
|
261
|
+
if (name.toLowerCase().startsWith("mp_")) merged.set(name, value);
|
|
262
|
+
}
|
|
263
|
+
for (const [name, value] of stored) {
|
|
264
|
+
if (name.toLowerCase().startsWith("mp_") && merged.has(name)) continue;
|
|
265
|
+
merged.set(name, value);
|
|
266
|
+
}
|
|
267
|
+
for (const [name, value] of browser) {
|
|
268
|
+
if (merged.has(name)) continue;
|
|
269
|
+
merged.set(name, value);
|
|
270
|
+
}
|
|
271
|
+
return [...merged.entries()]
|
|
272
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
273
|
+
.join("; ");
|
|
274
|
+
}
|
package/src/daemon.mjs
CHANGED
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
leftoverStateKeys,
|
|
39
39
|
LEGACY_TUNNEL_FILE,
|
|
40
40
|
} from "./discarded-tunnels.mjs";
|
|
41
|
+
import {
|
|
42
|
+
cookieHeaderForUrl,
|
|
43
|
+
mergeCookieHeader,
|
|
44
|
+
storeCorsCookies,
|
|
45
|
+
} from "./cors-cookies.mjs";
|
|
41
46
|
|
|
42
47
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
43
48
|
const requireFromHere = createRequire(import.meta.url);
|
|
@@ -2309,6 +2314,7 @@ function shareProxyContext(msg, ws, appId) {
|
|
|
2309
2314
|
acceptEncoding,
|
|
2310
2315
|
port: localPortForProxy(ws, appId),
|
|
2311
2316
|
portUrls: { ...fromWs, ...fromMsg },
|
|
2317
|
+
bypassCors: msg?.bypassCors === true,
|
|
2312
2318
|
};
|
|
2313
2319
|
}
|
|
2314
2320
|
|
|
@@ -2342,9 +2348,13 @@ function replyShareInterceptor(id, ctx, path) {
|
|
|
2342
2348
|
let body = "";
|
|
2343
2349
|
if (pathname === "/__mp/sw.js") {
|
|
2344
2350
|
headers["service-worker-allowed"] = tokenRoot;
|
|
2345
|
-
body = shareServiceWorkerScript(ctx.portUrls, tokenRoot);
|
|
2351
|
+
body = shareServiceWorkerScript(ctx.portUrls, tokenRoot, ctx.bypassCors);
|
|
2346
2352
|
} else {
|
|
2347
|
-
body = shareShimScript(
|
|
2353
|
+
body = shareShimScript(
|
|
2354
|
+
publicPathPrefix(ctx.publicBase),
|
|
2355
|
+
ctx.portUrls,
|
|
2356
|
+
ctx.bypassCors
|
|
2357
|
+
);
|
|
2348
2358
|
}
|
|
2349
2359
|
replyProxyHttp(id, 200, headers, body);
|
|
2350
2360
|
return true;
|
|
@@ -2476,6 +2486,252 @@ async function handleAiProxyHttpFromAdmin(msg, ws) {
|
|
|
2476
2486
|
});
|
|
2477
2487
|
}
|
|
2478
2488
|
|
|
2489
|
+
const EXTERNAL_PROXY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
2490
|
+
const EXTERNAL_PROXY_MAX_REDIRECTS = 5;
|
|
2491
|
+
const EXTERNAL_PROXY_DROP_HEADERS = new Set([
|
|
2492
|
+
"cookie",
|
|
2493
|
+
"set-cookie",
|
|
2494
|
+
"host",
|
|
2495
|
+
"origin",
|
|
2496
|
+
"referer",
|
|
2497
|
+
"referrer",
|
|
2498
|
+
"connection",
|
|
2499
|
+
"keep-alive",
|
|
2500
|
+
"transfer-encoding",
|
|
2501
|
+
"upgrade",
|
|
2502
|
+
"content-length",
|
|
2503
|
+
"te",
|
|
2504
|
+
"trailer",
|
|
2505
|
+
"x-mp-target",
|
|
2506
|
+
"x-forwarded-proto",
|
|
2507
|
+
"x-forwarded-host",
|
|
2508
|
+
"x-forwarded-for",
|
|
2509
|
+
]);
|
|
2510
|
+
|
|
2511
|
+
function parseExternalProxyUrl(value) {
|
|
2512
|
+
let parsed;
|
|
2513
|
+
try {
|
|
2514
|
+
parsed = new URL(String(value || ""));
|
|
2515
|
+
} catch {
|
|
2516
|
+
return null;
|
|
2517
|
+
}
|
|
2518
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
2519
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
2520
|
+
if (parts[0] === "p" && parts[2] === "__cors") return null;
|
|
2521
|
+
return parsed;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
function incomingCookieHeader(incoming) {
|
|
2525
|
+
if (!incoming || typeof incoming !== "object") return "";
|
|
2526
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
2527
|
+
if (String(key).toLowerCase() !== "cookie") continue;
|
|
2528
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2529
|
+
}
|
|
2530
|
+
return "";
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
function externalProxyHeaders(incoming, targetHost) {
|
|
2534
|
+
/** @type {Record<string, string>} */
|
|
2535
|
+
const headers = {};
|
|
2536
|
+
if (incoming && typeof incoming === "object") {
|
|
2537
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
2538
|
+
const lower = String(key).toLowerCase();
|
|
2539
|
+
if (EXTERNAL_PROXY_DROP_HEADERS.has(lower)) continue;
|
|
2540
|
+
if (lower.startsWith("sec-fetch-")) continue;
|
|
2541
|
+
if (typeof value === "string" && value) headers[key] = value;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
headers.host = targetHost;
|
|
2545
|
+
return headers;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
function failExternalProxyHttp(id, error) {
|
|
2549
|
+
proxyHttpReqs.delete(id);
|
|
2550
|
+
bridgeSend({
|
|
2551
|
+
type: "proxy.http.error",
|
|
2552
|
+
id,
|
|
2553
|
+
error: error instanceof Error ? error.message : String(error || "Upstream error"),
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function streamExternalProxyHttp(id, res) {
|
|
2558
|
+
const stream = randomBytes(8).toString("hex");
|
|
2559
|
+
/** @type {Record<string, string>} */
|
|
2560
|
+
const outHeaders = {};
|
|
2561
|
+
for (const [key, value] of Object.entries(res.headers || {})) {
|
|
2562
|
+
if (value == null) continue;
|
|
2563
|
+
const lower = String(key).toLowerCase();
|
|
2564
|
+
if (lower === "set-cookie" || lower === "cookie") continue;
|
|
2565
|
+
outHeaders[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
2566
|
+
}
|
|
2567
|
+
bridgeSend({
|
|
2568
|
+
type: "proxy.http.start",
|
|
2569
|
+
id,
|
|
2570
|
+
stream,
|
|
2571
|
+
status: res.statusCode || 502,
|
|
2572
|
+
headers: outHeaders,
|
|
2573
|
+
});
|
|
2574
|
+
res.on("data", (chunk) => {
|
|
2575
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2576
|
+
for (let offset = 0; offset < buf.length; offset += PROXY_CHUNK_BYTES) {
|
|
2577
|
+
const end = Math.min(offset + PROXY_CHUNK_BYTES, buf.length);
|
|
2578
|
+
bridgeSend({
|
|
2579
|
+
type: "proxy.http.chunk",
|
|
2580
|
+
id,
|
|
2581
|
+
stream,
|
|
2582
|
+
data: Buffer.from(buf.subarray(offset, end)).toString("base64"),
|
|
2583
|
+
eof: false,
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
res.on("end", () => {
|
|
2588
|
+
proxyHttpReqs.delete(id);
|
|
2589
|
+
bridgeSend({
|
|
2590
|
+
type: "proxy.http.chunk",
|
|
2591
|
+
id,
|
|
2592
|
+
stream,
|
|
2593
|
+
data: "",
|
|
2594
|
+
eof: true,
|
|
2595
|
+
});
|
|
2596
|
+
});
|
|
2597
|
+
res.on("error", (err) => {
|
|
2598
|
+
failExternalProxyHttp(id, err);
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
function fetchExternalProxyUrl(entry, url, method, headers, body, redirectsLeft) {
|
|
2603
|
+
const parsed = parseExternalProxyUrl(url);
|
|
2604
|
+
if (!parsed) {
|
|
2605
|
+
failExternalProxyHttp(entry.id, "Invalid external URL");
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const lib = parsed.protocol === "https:" ? https : http;
|
|
2609
|
+
/** @type {Record<string, string>} */
|
|
2610
|
+
const reqHeaders = { ...headers, host: parsed.host };
|
|
2611
|
+
const cookie = mergeCookieHeader(
|
|
2612
|
+
entry.incomingCookie || "",
|
|
2613
|
+
cookieHeaderForUrl(entry.sandboxId, parsed)
|
|
2614
|
+
);
|
|
2615
|
+
if (cookie) reqHeaders.cookie = cookie;
|
|
2616
|
+
else delete reqHeaders.cookie;
|
|
2617
|
+
const sendBody =
|
|
2618
|
+
body?.length && method !== "GET" && method !== "HEAD" ? body : null;
|
|
2619
|
+
if (sendBody) reqHeaders["content-length"] = String(sendBody.length);
|
|
2620
|
+
else delete reqHeaders["content-length"];
|
|
2621
|
+
|
|
2622
|
+
let req;
|
|
2623
|
+
try {
|
|
2624
|
+
req = lib.request(
|
|
2625
|
+
{
|
|
2626
|
+
protocol: parsed.protocol,
|
|
2627
|
+
hostname: parsed.hostname,
|
|
2628
|
+
port: parsed.port || undefined,
|
|
2629
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
2630
|
+
method,
|
|
2631
|
+
headers: reqHeaders,
|
|
2632
|
+
},
|
|
2633
|
+
(res) => {
|
|
2634
|
+
storeCorsCookies(entry.sandboxId, parsed, res.headers);
|
|
2635
|
+
const status = res.statusCode || 502;
|
|
2636
|
+
const loc = res.headers.location;
|
|
2637
|
+
if (
|
|
2638
|
+
loc &&
|
|
2639
|
+
status >= 300 &&
|
|
2640
|
+
status < 400 &&
|
|
2641
|
+
redirectsLeft > 0
|
|
2642
|
+
) {
|
|
2643
|
+
res.resume();
|
|
2644
|
+
let next;
|
|
2645
|
+
try {
|
|
2646
|
+
next = new URL(loc, parsed).href;
|
|
2647
|
+
} catch {
|
|
2648
|
+
failExternalProxyHttp(entry.id, "Invalid redirect");
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
const preserve = status === 307 || status === 308;
|
|
2652
|
+
const nextMethod = preserve
|
|
2653
|
+
? method
|
|
2654
|
+
: method === "HEAD"
|
|
2655
|
+
? "HEAD"
|
|
2656
|
+
: "GET";
|
|
2657
|
+
const nextBody =
|
|
2658
|
+
preserve && nextMethod !== "GET" && nextMethod !== "HEAD"
|
|
2659
|
+
? body
|
|
2660
|
+
: Buffer.alloc(0);
|
|
2661
|
+
fetchExternalProxyUrl(
|
|
2662
|
+
entry,
|
|
2663
|
+
next,
|
|
2664
|
+
nextMethod,
|
|
2665
|
+
headers,
|
|
2666
|
+
nextBody,
|
|
2667
|
+
redirectsLeft - 1
|
|
2668
|
+
);
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
streamExternalProxyHttp(entry.id, res);
|
|
2672
|
+
}
|
|
2673
|
+
);
|
|
2674
|
+
} catch (err) {
|
|
2675
|
+
failExternalProxyHttp(entry.id, err);
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
req.setTimeout(EXTERNAL_PROXY_TIMEOUT_MS, () => {
|
|
2679
|
+
req.destroy(new Error("Upstream timeout"));
|
|
2680
|
+
});
|
|
2681
|
+
req.on("error", (err) => {
|
|
2682
|
+
failExternalProxyHttp(entry.id, err);
|
|
2683
|
+
});
|
|
2684
|
+
entry.req = req;
|
|
2685
|
+
if (sendBody) req.write(sendBody);
|
|
2686
|
+
req.end();
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function startExternalProxyHttp(entry) {
|
|
2690
|
+
const body = Buffer.concat(entry.chunks || []);
|
|
2691
|
+
fetchExternalProxyUrl(
|
|
2692
|
+
entry,
|
|
2693
|
+
entry.url,
|
|
2694
|
+
entry.method,
|
|
2695
|
+
entry.headers,
|
|
2696
|
+
body,
|
|
2697
|
+
EXTERNAL_PROXY_MAX_REDIRECTS
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
function handleExternalProxyHttpFromAdmin(msg) {
|
|
2702
|
+
const id = typeof msg.id === "string" ? msg.id : "";
|
|
2703
|
+
const parsed = parseExternalProxyUrl(msg.externalUrl);
|
|
2704
|
+
if (!id) return;
|
|
2705
|
+
if (!parsed) {
|
|
2706
|
+
failExternalProxyHttp(id, "Invalid external URL");
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
const existing = proxyHttpReqs.get(id);
|
|
2710
|
+
if (existing) {
|
|
2711
|
+
destroyProxyHttpReq(existing);
|
|
2712
|
+
proxyHttpReqs.delete(id);
|
|
2713
|
+
}
|
|
2714
|
+
const method = String(msg.method || "GET").toUpperCase();
|
|
2715
|
+
const initialBody =
|
|
2716
|
+
typeof msg.body === "string" && msg.body
|
|
2717
|
+
? Buffer.from(msg.body, "base64")
|
|
2718
|
+
: null;
|
|
2719
|
+
const entry = {
|
|
2720
|
+
req: null,
|
|
2721
|
+
external: true,
|
|
2722
|
+
id,
|
|
2723
|
+
sandboxId: typeof msg.sandboxId === "string" ? msg.sandboxId : "",
|
|
2724
|
+
incomingCookie: incomingCookieHeader(msg.headers),
|
|
2725
|
+
url: parsed.href,
|
|
2726
|
+
method,
|
|
2727
|
+
headers: externalProxyHeaders(msg.headers, parsed.host),
|
|
2728
|
+
chunks: [],
|
|
2729
|
+
};
|
|
2730
|
+
proxyHttpReqs.set(id, entry);
|
|
2731
|
+
if (initialBody?.length) entry.chunks.push(initialBody);
|
|
2732
|
+
if (msg.bodyEof !== false) startExternalProxyHttp(entry);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2479
2735
|
function handleProxyHttpFromAdmin(msg) {
|
|
2480
2736
|
const id = typeof msg.id === "string" ? msg.id : "";
|
|
2481
2737
|
const sandboxId = typeof msg.sandboxId === "string" ? msg.sandboxId : "";
|
|
@@ -2490,6 +2746,10 @@ function handleProxyHttpFromAdmin(msg) {
|
|
|
2490
2746
|
});
|
|
2491
2747
|
return;
|
|
2492
2748
|
}
|
|
2749
|
+
if (typeof msg.externalUrl === "string" && msg.externalUrl.trim()) {
|
|
2750
|
+
handleExternalProxyHttpFromAdmin(msg);
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2493
2753
|
if (isAiServerAppId(appId, ws)) {
|
|
2494
2754
|
void handleAiProxyHttpFromAdmin(msg, ws);
|
|
2495
2755
|
return;
|
|
@@ -2702,11 +2962,16 @@ function handleProxyHttpBodyFromAdmin(msg) {
|
|
|
2702
2962
|
const id = typeof msg.id === "string" ? msg.id : "";
|
|
2703
2963
|
const entry = proxyHttpReqs.get(id);
|
|
2704
2964
|
if (!entry) return;
|
|
2705
|
-
const req = entry.req || entry;
|
|
2706
2965
|
const chunk =
|
|
2707
2966
|
typeof msg.data === "string" && msg.data
|
|
2708
2967
|
? Buffer.from(msg.data, "base64")
|
|
2709
2968
|
: null;
|
|
2969
|
+
if (entry.external) {
|
|
2970
|
+
if (chunk?.length) entry.chunks.push(chunk);
|
|
2971
|
+
if (msg.eof === true) startExternalProxyHttp(entry);
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
const req = entry.req || entry;
|
|
2710
2975
|
if (entry.rewrite) {
|
|
2711
2976
|
if (chunk?.length) entry.chunks.push(chunk);
|
|
2712
2977
|
if (msg.eof === true) {
|
package/src/share-rewrite.mjs
CHANGED
|
@@ -155,11 +155,33 @@ function isImmutableAssetPath(path) {
|
|
|
155
155
|
return (
|
|
156
156
|
/\/_next\/static\//.test(p) ||
|
|
157
157
|
/\/__nextjs_font\//.test(p) ||
|
|
158
|
-
/\/node_modules\/\.vite\//.test(p) ||
|
|
159
158
|
/\.[0-9a-f]{8,}\.(?:js|css|woff2?)$/i.test(p)
|
|
160
159
|
);
|
|
161
160
|
}
|
|
162
161
|
|
|
162
|
+
function isViteDevSourcePath(path) {
|
|
163
|
+
const raw = String(path || "");
|
|
164
|
+
const p = raw.split("?")[0] || "";
|
|
165
|
+
if (/[?&]t=\d+/.test(raw)) return true;
|
|
166
|
+
if (/\/(?:@vite(?:\/|$)|@react-refresh|@fs\/|@id\/)/.test(p)) return true;
|
|
167
|
+
if (/\/node_modules\/\.vite\//.test(p)) return true;
|
|
168
|
+
if (/\/src\//.test(p)) return true;
|
|
169
|
+
if (
|
|
170
|
+
/\.(?:m?[jt]sx?|vue|svelte|css|scss|sass|less)$/i.test(p) &&
|
|
171
|
+
!/\/node_modules\//.test(p)
|
|
172
|
+
) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isDocumentPath(path) {
|
|
179
|
+
const p = String(path || "").split("?")[0] || "";
|
|
180
|
+
if (!p || p === "/") return true;
|
|
181
|
+
if (/\.html?$/i.test(p)) return true;
|
|
182
|
+
return !/\.[A-Za-z0-9]+$/.test(p);
|
|
183
|
+
}
|
|
184
|
+
|
|
163
185
|
function isNoStorePath(path) {
|
|
164
186
|
const p = String(path || "").split("?")[0] || "";
|
|
165
187
|
return (
|
|
@@ -169,11 +191,9 @@ function isNoStorePath(path) {
|
|
|
169
191
|
/\/_next\/webpack-hmr/i.test(p) ||
|
|
170
192
|
/\/_next\/webpack\/hmr/i.test(p) ||
|
|
171
193
|
/\/__nextjs_original-stack-frames/i.test(p) ||
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
/\/@id\//.test(p) ||
|
|
176
|
-
/\/__mp\//.test(p)
|
|
194
|
+
/\/__mp\//.test(p) ||
|
|
195
|
+
isViteDevSourcePath(path) ||
|
|
196
|
+
isDocumentPath(path)
|
|
177
197
|
);
|
|
178
198
|
}
|
|
179
199
|
|
|
@@ -396,12 +416,12 @@ export function shareTokenRoot(publicBase) {
|
|
|
396
416
|
return prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
397
417
|
}
|
|
398
418
|
|
|
399
|
-
export function shareShimScript(pathPrefix, portUrls) {
|
|
400
|
-
return `(${shareProxyShim.toString()})(${JSON.stringify(pathPrefix || "")},${JSON.stringify(portUrls || {})},${rewriteMappedLocalUrls.toString()});`;
|
|
419
|
+
export function shareShimScript(pathPrefix, portUrls, bypassCors) {
|
|
420
|
+
return `(${shareProxyShim.toString()})(${JSON.stringify(pathPrefix || "")},${JSON.stringify(portUrls || {})},${rewriteMappedLocalUrls.toString()},${JSON.stringify(Boolean(bypassCors))});`;
|
|
401
421
|
}
|
|
402
422
|
|
|
403
|
-
export function shareServiceWorkerScript(portUrls, tokenRoot) {
|
|
404
|
-
return `(${shareServiceWorkerMain.toString()})(${JSON.stringify(portUrls || {})},${JSON.stringify(tokenRoot || "/")},${rewriteMappedLocalUrls.toString()});`;
|
|
423
|
+
export function shareServiceWorkerScript(portUrls, tokenRoot, bypassCors) {
|
|
424
|
+
return `(${shareServiceWorkerMain.toString()})(${JSON.stringify(portUrls || {})},${JSON.stringify(tokenRoot || "/")},${rewriteMappedLocalUrls.toString()},${JSON.stringify(Boolean(bypassCors))});`;
|
|
405
425
|
}
|
|
406
426
|
|
|
407
427
|
function rewriteHeaderUrls(headers, publicBase, port, portUrls) {
|
|
@@ -596,7 +616,7 @@ function gzipIfRequested(payload, headers, acceptEncoding) {
|
|
|
596
616
|
|
|
597
617
|
function rewriteCacheKey(publicBase, path) {
|
|
598
618
|
if (!isImmutableAssetPath(path) && !isLongCacheScriptPath(path)) return null;
|
|
599
|
-
return `${publicBase || ""}\0${String(path || "")
|
|
619
|
+
return `${publicBase || ""}\0${String(path || "")}`;
|
|
600
620
|
}
|
|
601
621
|
|
|
602
622
|
function rememberRewrite(key, payload, status, headers, etag) {
|
|
@@ -1003,10 +1023,11 @@ function injectMaintainerProEmbed(html, aiPublicBase) {
|
|
|
1003
1023
|
/**
|
|
1004
1024
|
* Intercept every browser request and map listed app ports onto share URLs.
|
|
1005
1025
|
*/
|
|
1006
|
-
function shareProxyShim(p, portMap, rewriteMappedLocalUrls) {
|
|
1026
|
+
function shareProxyShim(p, portMap, rewriteMappedLocalUrls, bypassCors) {
|
|
1007
1027
|
if (!p || window.__MP_SHARE_SHIM__) return;
|
|
1008
1028
|
window.__MP_SHARE_SHIM__ = 1;
|
|
1009
1029
|
window.__MP_PORT_MAP__ = portMap || {};
|
|
1030
|
+
window.__MP_BYPASS_CORS__ = Boolean(bypassCors);
|
|
1010
1031
|
function rewriteText(text) {
|
|
1011
1032
|
if (typeof text !== "string" || !text || typeof rewriteMappedLocalUrls !== "function") {
|
|
1012
1033
|
return text;
|
|
@@ -1108,6 +1129,38 @@ function shareProxyShim(p, portMap, rewriteMappedLocalUrls) {
|
|
|
1108
1129
|
} catch (e) {}
|
|
1109
1130
|
return v;
|
|
1110
1131
|
}
|
|
1132
|
+
function isCorsProxyPath(pathname) {
|
|
1133
|
+
var parts = String(pathname || "").split("/").filter(Boolean);
|
|
1134
|
+
return parts[0] === "p" && parts[2] === "__cors";
|
|
1135
|
+
}
|
|
1136
|
+
function shareToken() {
|
|
1137
|
+
try {
|
|
1138
|
+
var parts = location.pathname.split("/").filter(Boolean);
|
|
1139
|
+
if (parts[0] === "p" && parts[1]) return parts[1];
|
|
1140
|
+
} catch (e) {}
|
|
1141
|
+
return "";
|
|
1142
|
+
}
|
|
1143
|
+
function corsProxy(v) {
|
|
1144
|
+
if (!bypassCors || typeof v !== "string" || !v) return v;
|
|
1145
|
+
if (!/^(https?:)\/\//i.test(v)) return v;
|
|
1146
|
+
try {
|
|
1147
|
+
var u = new URL(v);
|
|
1148
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return v;
|
|
1149
|
+
if (loopback(u.hostname)) return v;
|
|
1150
|
+
if (u.origin === location.origin) return v;
|
|
1151
|
+
if (isCorsProxyPath(u.pathname)) return v;
|
|
1152
|
+
var token = shareToken();
|
|
1153
|
+
if (!token) return v;
|
|
1154
|
+
return (
|
|
1155
|
+
location.origin +
|
|
1156
|
+
"/p/" +
|
|
1157
|
+
token +
|
|
1158
|
+
"/__cors?__mp_url=" +
|
|
1159
|
+
encodeURIComponent(u.href)
|
|
1160
|
+
);
|
|
1161
|
+
} catch (e) {}
|
|
1162
|
+
return v;
|
|
1163
|
+
}
|
|
1111
1164
|
function addNet(v) {
|
|
1112
1165
|
if (typeof v !== "string" || !v) return v;
|
|
1113
1166
|
var mapped = mapLocal(v);
|
|
@@ -1115,7 +1168,10 @@ function shareProxyShim(p, portMap, rewriteMappedLocalUrls) {
|
|
|
1115
1168
|
if (/^(https?:|wss?:)\/\//i.test(v)) {
|
|
1116
1169
|
try {
|
|
1117
1170
|
var abs = new URL(v);
|
|
1118
|
-
if (!loopback(abs.hostname))
|
|
1171
|
+
if (!loopback(abs.hostname)) {
|
|
1172
|
+
if (/^wss?:/i.test(abs.protocol)) return v;
|
|
1173
|
+
return corsProxy(v);
|
|
1174
|
+
}
|
|
1119
1175
|
} catch (e) {}
|
|
1120
1176
|
}
|
|
1121
1177
|
if (v.charAt(0) === "/" && v.charAt(1) !== "/") {
|
|
@@ -1299,17 +1355,27 @@ function shareProxyShim(p, portMap, rewriteMappedLocalUrls) {
|
|
|
1299
1355
|
try {
|
|
1300
1356
|
var tokenRoot = p.replace(/\/[^/]+$/, "/");
|
|
1301
1357
|
if (navigator.serviceWorker && tokenRoot.indexOf("/p/") === 0) {
|
|
1302
|
-
navigator.serviceWorker.register(p + "/__mp/sw.js", {
|
|
1358
|
+
navigator.serviceWorker.register(p + "/__mp/sw.js", {
|
|
1359
|
+
scope: tokenRoot,
|
|
1360
|
+
updateViaCache: "none",
|
|
1361
|
+
});
|
|
1303
1362
|
}
|
|
1304
1363
|
} catch (e) {}
|
|
1305
1364
|
}
|
|
1306
1365
|
|
|
1307
|
-
function shareServiceWorkerMain(portMap, tokenRoot, rewriteMappedLocalUrls) {
|
|
1366
|
+
function shareServiceWorkerMain(portMap, tokenRoot, rewriteMappedLocalUrls, bypassCors) {
|
|
1308
1367
|
self.addEventListener("install", function (event) {
|
|
1309
1368
|
event.waitUntil(self.skipWaiting());
|
|
1310
1369
|
});
|
|
1311
1370
|
self.addEventListener("activate", function (event) {
|
|
1312
|
-
event.waitUntil(
|
|
1371
|
+
event.waitUntil(
|
|
1372
|
+
self.clients.claim().then(function () {
|
|
1373
|
+
if (!self.caches) return;
|
|
1374
|
+
return self.caches.keys().then(function (keys) {
|
|
1375
|
+
return Promise.all(keys.map(function (key) { return self.caches.delete(key); }));
|
|
1376
|
+
});
|
|
1377
|
+
})
|
|
1378
|
+
);
|
|
1313
1379
|
});
|
|
1314
1380
|
function rewriteText(text) {
|
|
1315
1381
|
if (typeof text !== "string" || !text) return text;
|
|
@@ -1352,12 +1418,49 @@ function shareServiceWorkerMain(portMap, tokenRoot, rewriteMappedLocalUrls) {
|
|
|
1352
1418
|
}
|
|
1353
1419
|
return "";
|
|
1354
1420
|
}
|
|
1421
|
+
function isCorsProxyPath(pathname) {
|
|
1422
|
+
var parts = String(pathname || "").split("/").filter(Boolean);
|
|
1423
|
+
return parts[0] === "p" && parts[2] === "__cors";
|
|
1424
|
+
}
|
|
1425
|
+
function shareToken() {
|
|
1426
|
+
try {
|
|
1427
|
+
var parts = String(self.registration && self.registration.scope || self.location.pathname)
|
|
1428
|
+
.split("/")
|
|
1429
|
+
.filter(Boolean);
|
|
1430
|
+
if (parts[0] === "p" && parts[1]) return parts[1];
|
|
1431
|
+
} catch (e) {}
|
|
1432
|
+
return "";
|
|
1433
|
+
}
|
|
1434
|
+
function corsProxy(v) {
|
|
1435
|
+
if (!bypassCors || typeof v !== "string" || !v) return v;
|
|
1436
|
+
if (!/^(https?:)\/\//i.test(v)) return v;
|
|
1437
|
+
try {
|
|
1438
|
+
var u = new URL(v, self.location.href);
|
|
1439
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return v;
|
|
1440
|
+
if (loopback(u.hostname)) return v;
|
|
1441
|
+
if (u.origin === self.location.origin) return v;
|
|
1442
|
+
if (isCorsProxyPath(u.pathname)) return v;
|
|
1443
|
+
var token = shareToken();
|
|
1444
|
+
if (!token) return v;
|
|
1445
|
+
return (
|
|
1446
|
+
self.location.origin +
|
|
1447
|
+
"/p/" +
|
|
1448
|
+
token +
|
|
1449
|
+
"/__cors?__mp_url=" +
|
|
1450
|
+
encodeURIComponent(u.href)
|
|
1451
|
+
);
|
|
1452
|
+
} catch (e) {}
|
|
1453
|
+
return v;
|
|
1454
|
+
}
|
|
1355
1455
|
function prefixWith(url, prefix) {
|
|
1356
|
-
if (!prefix) return url;
|
|
1456
|
+
if (!prefix) return corsProxy(url);
|
|
1357
1457
|
try {
|
|
1358
1458
|
if (/^(https?:|wss?:)\/\//i.test(url)) {
|
|
1359
1459
|
var abs = new URL(url);
|
|
1360
|
-
if (!loopback(abs.hostname))
|
|
1460
|
+
if (!loopback(abs.hostname)) {
|
|
1461
|
+
if (/^wss?:/i.test(abs.protocol)) return url;
|
|
1462
|
+
return corsProxy(url);
|
|
1463
|
+
}
|
|
1361
1464
|
}
|
|
1362
1465
|
var u = new URL(url, self.location.href);
|
|
1363
1466
|
var path = u.pathname || "/";
|
|
@@ -1382,11 +1485,13 @@ function shareServiceWorkerMain(portMap, tokenRoot, rewriteMappedLocalUrls) {
|
|
|
1382
1485
|
function mapUrl(v, prefix) {
|
|
1383
1486
|
if (typeof v !== "string" || !v) return v;
|
|
1384
1487
|
if (v.indexOf("/__mp/") !== -1) return v;
|
|
1488
|
+
if (v.indexOf("/__cors") !== -1) return v;
|
|
1385
1489
|
return prefixWith(rewriteText(v), prefix);
|
|
1386
1490
|
}
|
|
1387
1491
|
self.addEventListener("fetch", function (event) {
|
|
1388
1492
|
var req = event.request;
|
|
1389
1493
|
if (req.url.indexOf("/__mp/") !== -1) return;
|
|
1494
|
+
if (req.url.indexOf("/__cors") !== -1) return;
|
|
1390
1495
|
event.respondWith(
|
|
1391
1496
|
(async function () {
|
|
1392
1497
|
var client = null;
|
|
@@ -1417,6 +1522,7 @@ function shareServiceWorkerMain(portMap, tokenRoot, rewriteMappedLocalUrls) {
|
|
|
1417
1522
|
credentials: req.credentials,
|
|
1418
1523
|
redirect: req.redirect,
|
|
1419
1524
|
referrer: req.referrer,
|
|
1525
|
+
cache: "no-store",
|
|
1420
1526
|
};
|
|
1421
1527
|
if (req.mode && req.mode !== "navigate") init.mode = req.mode;
|
|
1422
1528
|
if (body !== undefined) init.body = body;
|