offsite 0.0.61 → 0.0.63
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.
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { appendFileSync, chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
2
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
3
3
|
import http, { createServer } from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { createGzip } from "node:zlib";
|
|
5
|
+
import zlib, { createGzip } from "node:zlib";
|
|
6
6
|
import { consola } from "consola";
|
|
7
7
|
import { WebSocketServer } from "ws";
|
|
8
8
|
import { z } from "zod";
|
|
@@ -25,9 +25,16 @@ import { integer, real, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
|
25
25
|
import sharp from "sharp";
|
|
26
26
|
import { promisify } from "node:util";
|
|
27
27
|
import webPush from "web-push";
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
28
|
+
import dns from "node:dns";
|
|
29
|
+
import { BlockList, isIP } from "node:net";
|
|
30
|
+
import { CookieJar, getPublicSuffix } from "tough-cookie";
|
|
30
31
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
32
|
+
import { once } from "node:events";
|
|
33
|
+
import https from "node:https";
|
|
34
|
+
import { pipeline } from "node:stream/promises";
|
|
35
|
+
import { defaultTreeAdapter, html, parse, serialize } from "parse5";
|
|
36
|
+
import { MagicString } from "magic-string";
|
|
37
|
+
import { parseSync } from "oxc-parser";
|
|
31
38
|
// ../core/src/media.ts
|
|
32
39
|
const mediaTypes = /* @__PURE__ */ new Map([
|
|
33
40
|
[".png", "image/png"],
|
|
@@ -2769,13 +2776,63 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
|
2769
2776
|
machine: text().notNull(),
|
|
2770
2777
|
server: text().notNull()
|
|
2771
2778
|
});
|
|
2779
|
+
// src/storage/auth.ts
|
|
2780
|
+
const CODE_LIFETIME = 3e5;
|
|
2781
|
+
const DEVICE_LIFETIME = 2592e6;
|
|
2782
|
+
function addCode(name) {
|
|
2783
|
+
const code = randomBytes(16).toString("base64url");
|
|
2784
|
+
db.delete(pairingCodes).where(lte(pairingCodes.expiresAt, Date.now())).run();
|
|
2785
|
+
db.insert(pairingCodes).values({
|
|
2786
|
+
hash: hash(code),
|
|
2787
|
+
name,
|
|
2788
|
+
expiresAt: Date.now() + CODE_LIFETIME
|
|
2789
|
+
}).run();
|
|
2790
|
+
return code;
|
|
2791
|
+
}
|
|
2792
|
+
function redeemCode(code) {
|
|
2793
|
+
const found = db.delete(pairingCodes).where(eq(pairingCodes.hash, hash(code))).returning().get();
|
|
2794
|
+
if (found === void 0 || found.expiresAt <= Date.now()) return null;
|
|
2795
|
+
const token = randomBytes(32).toString("base64url");
|
|
2796
|
+
const now = Date.now();
|
|
2797
|
+
db.insert(devices).values({
|
|
2798
|
+
hash: hash(token),
|
|
2799
|
+
name: found.name,
|
|
2800
|
+
createdAt: now,
|
|
2801
|
+
expiresAt: now + DEVICE_LIFETIME
|
|
2802
|
+
}).run();
|
|
2803
|
+
return token;
|
|
2804
|
+
}
|
|
2805
|
+
function findDevice(token) {
|
|
2806
|
+
const found = db.select().from(devices).where(and(eq(devices.hash, hash(token)), gt(devices.expiresAt, Date.now()))).get();
|
|
2807
|
+
return found === void 0 ? null : found.hash;
|
|
2808
|
+
}
|
|
2809
|
+
function isPaired(device) {
|
|
2810
|
+
return db.select().from(devices).where(and(eq(devices.hash, device), gt(devices.expiresAt, Date.now()))).get() !== void 0;
|
|
2811
|
+
}
|
|
2812
|
+
function hasDevices() {
|
|
2813
|
+
return db.select().from(devices).where(gt(devices.expiresAt, Date.now())).limit(1).get() !== void 0;
|
|
2814
|
+
}
|
|
2815
|
+
function readDevices() {
|
|
2816
|
+
return db.select().from(devices).orderBy(devices.createdAt).all();
|
|
2817
|
+
}
|
|
2818
|
+
function removeDevices(hashes) {
|
|
2819
|
+
db.delete(devices).where(inArray(devices.hash, hashes)).run();
|
|
2820
|
+
}
|
|
2821
|
+
function recordUse(device) {
|
|
2822
|
+
db.update(devices).set({ lastUsedAt: Date.now() }).where(eq(devices.hash, device)).run();
|
|
2823
|
+
}
|
|
2824
|
+
function hash(secret) {
|
|
2825
|
+
return createHash("sha256").update(secret).digest("hex");
|
|
2826
|
+
}
|
|
2772
2827
|
// src/services/files.ts
|
|
2773
2828
|
const FILE_LIMIT = 2e4;
|
|
2829
|
+
const SNIFF_LENGTH = 8e3;
|
|
2774
2830
|
const OUTPUT_LIMIT$1 = 262144;
|
|
2775
2831
|
const READ_LIMIT$1 = 1048576;
|
|
2776
2832
|
const MEDIA_LINK_LIFETIME = 36e5;
|
|
2777
|
-
const
|
|
2833
|
+
const BROWSE_LINK_LIFETIME = 36e5;
|
|
2778
2834
|
const mediaLinks = /* @__PURE__ */ new Map();
|
|
2835
|
+
const browseLinks = /* @__PURE__ */ new Map();
|
|
2779
2836
|
const skipped = /* @__PURE__ */ new Set([
|
|
2780
2837
|
"node_modules",
|
|
2781
2838
|
".git",
|
|
@@ -2869,25 +2926,90 @@ function renameProjectEntry(cwd, file, to) {
|
|
|
2869
2926
|
function deleteProjectEntry(cwd, file) {
|
|
2870
2927
|
rmSync(resolveProjectEntry(cwd, file), { recursive: true });
|
|
2871
2928
|
}
|
|
2872
|
-
function linkProjectMedia(cwd, file) {
|
|
2929
|
+
function linkProjectMedia(cwd, file, device) {
|
|
2873
2930
|
const resolved = path.resolve(realpathSync(cwd), file);
|
|
2874
2931
|
if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
|
|
2875
2932
|
const full = realpathSync(resolved);
|
|
2876
2933
|
const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
|
|
2877
2934
|
if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
|
|
2878
|
-
return addLink(full, mediaType);
|
|
2935
|
+
return addLink(full, mediaType, device);
|
|
2879
2936
|
}
|
|
2880
|
-
function linkProjectDownload(cwd, file) {
|
|
2937
|
+
function linkProjectDownload(cwd, file, device) {
|
|
2881
2938
|
const full = resolveProjectFile(cwd, file);
|
|
2882
2939
|
if (!statSync(full).isFile()) throw new Error(`${file} is not a file`);
|
|
2883
|
-
return addLink(full, "application/octet-stream");
|
|
2940
|
+
return addLink(full, "application/octet-stream", device);
|
|
2941
|
+
}
|
|
2942
|
+
function linkBrowse(device, address) {
|
|
2943
|
+
for (const [token, link] of browseLinks) if (link.expiresAt <= Date.now()) browseLinks.delete(token);
|
|
2944
|
+
const token = randomBytes(32).toString("base64url");
|
|
2945
|
+
browseLinks.set(token, {
|
|
2946
|
+
device,
|
|
2947
|
+
address,
|
|
2948
|
+
expiresAt: Date.now() + BROWSE_LINK_LIFETIME
|
|
2949
|
+
});
|
|
2950
|
+
return token;
|
|
2951
|
+
}
|
|
2952
|
+
function canBrowse(token, address) {
|
|
2953
|
+
const link = browseLinks.get(token);
|
|
2954
|
+
if (link === void 0 || link.expiresAt <= Date.now() || link.address !== address) return false;
|
|
2955
|
+
return link.device === null || isPaired(link.device);
|
|
2956
|
+
}
|
|
2957
|
+
function setBrowseHeaders(response, contentType) {
|
|
2958
|
+
response.setHeader("content-type", contentType);
|
|
2959
|
+
response.setHeader("cache-control", "no-cache");
|
|
2960
|
+
if (/^(text\/html|image\/svg|application\/xml)/.test(contentType)) response.setHeader("content-security-policy", "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads");
|
|
2884
2961
|
}
|
|
2885
|
-
function
|
|
2962
|
+
function sniffType(file) {
|
|
2963
|
+
const head = Buffer.alloc(SNIFF_LENGTH);
|
|
2964
|
+
const handle = openSync(file, "r");
|
|
2965
|
+
const read = readSync(handle, head, 0, SNIFF_LENGTH, 0);
|
|
2966
|
+
closeSync(handle);
|
|
2967
|
+
return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
|
|
2968
|
+
}
|
|
2969
|
+
function streamFile(file, size, request, response) {
|
|
2970
|
+
response.setHeader("accept-ranges", "bytes");
|
|
2971
|
+
if (request.headers.range === void 0) {
|
|
2972
|
+
response.setHeader("content-length", size);
|
|
2973
|
+
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
const range = byteRange(request.headers.range, size);
|
|
2977
|
+
if (range === null) {
|
|
2978
|
+
response.statusCode = 416;
|
|
2979
|
+
response.setHeader("content-range", `bytes */${size}`);
|
|
2980
|
+
response.end();
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
response.statusCode = 206;
|
|
2984
|
+
response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
|
|
2985
|
+
response.setHeader("content-length", range.end - range.start + 1);
|
|
2986
|
+
createReadStream(file, range).on("error", () => response.destroy()).pipe(response);
|
|
2987
|
+
}
|
|
2988
|
+
function byteRange(header, size) {
|
|
2989
|
+
const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);
|
|
2990
|
+
if (match === null) return null;
|
|
2991
|
+
if (match[3] !== void 0) {
|
|
2992
|
+
if (size === 0 || Number(match[3]) === 0) return null;
|
|
2993
|
+
return {
|
|
2994
|
+
start: Math.max(size - Number(match[3]), 0),
|
|
2995
|
+
end: size - 1
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
const start = Number(match[1]);
|
|
2999
|
+
const end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1);
|
|
3000
|
+
if (start > end) return null;
|
|
3001
|
+
return {
|
|
3002
|
+
start,
|
|
3003
|
+
end
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
function addLink(file, mediaType, device) {
|
|
2886
3007
|
for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
|
|
2887
3008
|
const token = randomBytes(32).toString("base64url");
|
|
2888
3009
|
mediaLinks.set(token, {
|
|
2889
3010
|
file,
|
|
2890
3011
|
mediaType,
|
|
3012
|
+
device,
|
|
2891
3013
|
expiresAt: Date.now() + MEDIA_LINK_LIFETIME
|
|
2892
3014
|
});
|
|
2893
3015
|
return token;
|
|
@@ -2896,6 +3018,7 @@ function findMediaLink(token) {
|
|
|
2896
3018
|
if (token === null) return null;
|
|
2897
3019
|
const link = mediaLinks.get(token);
|
|
2898
3020
|
if (link === void 0 || link.expiresAt <= Date.now()) return null;
|
|
3021
|
+
if (link.device !== null && !isPaired(link.device)) return null;
|
|
2899
3022
|
return link;
|
|
2900
3023
|
}
|
|
2901
3024
|
function saveUpload(name, data) {
|
|
@@ -5553,6 +5676,114 @@ var Run = class {
|
|
|
5553
5676
|
});
|
|
5554
5677
|
}
|
|
5555
5678
|
};
|
|
5679
|
+
// src/lib/proxy/sites.ts
|
|
5680
|
+
const sites = /* @__PURE__ */ new Map();
|
|
5681
|
+
const siteNames = /* @__PURE__ */ new Map();
|
|
5682
|
+
const privateAddresses = new BlockList();
|
|
5683
|
+
for (const range of [
|
|
5684
|
+
["0.0.0.0", 8],
|
|
5685
|
+
["10.0.0.0", 8],
|
|
5686
|
+
["100.64.0.0", 10],
|
|
5687
|
+
["127.0.0.0", 8],
|
|
5688
|
+
["169.254.0.0", 16],
|
|
5689
|
+
["172.16.0.0", 12],
|
|
5690
|
+
["192.0.0.0", 24],
|
|
5691
|
+
["192.168.0.0", 16],
|
|
5692
|
+
["198.18.0.0", 15],
|
|
5693
|
+
["224.0.0.0", 4],
|
|
5694
|
+
["240.0.0.0", 4]
|
|
5695
|
+
]) privateAddresses.addSubnet(range[0], range[1], "ipv4");
|
|
5696
|
+
for (const range of [
|
|
5697
|
+
["::", 128],
|
|
5698
|
+
["::1", 128],
|
|
5699
|
+
["fc00::", 7],
|
|
5700
|
+
["fe80::", 10],
|
|
5701
|
+
["ff00::", 8]
|
|
5702
|
+
]) privateAddresses.addSubnet(range[0], range[1], "ipv6");
|
|
5703
|
+
function proxiedPath(key, target) {
|
|
5704
|
+
return `/api/proxy/${key}/${target.protocol === "https:" ? "https" : "http"}/${target.host}${target.pathname}${target.search}${target.hash}`;
|
|
5705
|
+
}
|
|
5706
|
+
function linkRewriter(key, base) {
|
|
5707
|
+
return (value) => {
|
|
5708
|
+
const text = value.trim();
|
|
5709
|
+
if (text === "" || text.startsWith("#")) return value;
|
|
5710
|
+
if (text.startsWith(`/api/proxy/${key}/`)) return text;
|
|
5711
|
+
const target = URL.parse(text, base);
|
|
5712
|
+
if (target === null || target.protocol !== "http:" && target.protocol !== "https:") return value;
|
|
5713
|
+
return proxiedPath(key, target);
|
|
5714
|
+
};
|
|
5715
|
+
}
|
|
5716
|
+
function findSite(key) {
|
|
5717
|
+
return sites.get(key) ?? null;
|
|
5718
|
+
}
|
|
5719
|
+
async function openSite(address, fromPage) {
|
|
5720
|
+
const target = new URL(address);
|
|
5721
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error(`${address} is not an http or https address`);
|
|
5722
|
+
const local = await isLocalHost(hostOf(target));
|
|
5723
|
+
if (local && fromPage) throw new Error("A website cannot open an address on this machine");
|
|
5724
|
+
const name = local ? `local ${target.hostname}` : `public ${getPublicSuffix(target.hostname) ?? target.hostname}`;
|
|
5725
|
+
const existing = siteNames.get(name);
|
|
5726
|
+
if (existing !== void 0) return proxiedPath(existing.key, target);
|
|
5727
|
+
const site = {
|
|
5728
|
+
key: randomBytes(16).toString("hex"),
|
|
5729
|
+
local,
|
|
5730
|
+
jar: new CookieJar(void 0, {
|
|
5731
|
+
allowSpecialUseDomain: true,
|
|
5732
|
+
rejectPublicSuffixes: !local
|
|
5733
|
+
}),
|
|
5734
|
+
storage: /* @__PURE__ */ new Map()
|
|
5735
|
+
};
|
|
5736
|
+
sites.set(site.key, site);
|
|
5737
|
+
siteNames.set(name, site);
|
|
5738
|
+
return proxiedPath(site.key, target);
|
|
5739
|
+
}
|
|
5740
|
+
function hostOf(target) {
|
|
5741
|
+
return target.hostname.replace(/^\[|\]$/g, "");
|
|
5742
|
+
}
|
|
5743
|
+
function checkAddress(site, address, port, ownPort) {
|
|
5744
|
+
if (!isPrivate(address)) return;
|
|
5745
|
+
if (!site.local) throw new Error("Websites cannot reach addresses on this machine");
|
|
5746
|
+
if (port === ownPort) throw new Error("Pages cannot reach Offsite itself");
|
|
5747
|
+
}
|
|
5748
|
+
function guardedLookup(site, port, ownPort) {
|
|
5749
|
+
return (hostname, options, callback) => {
|
|
5750
|
+
dns.lookup(hostname, {
|
|
5751
|
+
...options,
|
|
5752
|
+
all: true
|
|
5753
|
+
}, (error, addresses) => {
|
|
5754
|
+
if (error !== null) {
|
|
5755
|
+
callback(error, "", 0);
|
|
5756
|
+
return;
|
|
5757
|
+
}
|
|
5758
|
+
try {
|
|
5759
|
+
for (const item of addresses) checkAddress(site, item.address, port, ownPort);
|
|
5760
|
+
} catch (caught) {
|
|
5761
|
+
callback(caught, "", 0);
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5764
|
+
if (options.all === true) {
|
|
5765
|
+
callback(null, addresses);
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
const first = addresses[0];
|
|
5769
|
+
if (first === void 0) {
|
|
5770
|
+
callback(/* @__PURE__ */ new Error(`${hostname} has no address`), "", 0);
|
|
5771
|
+
return;
|
|
5772
|
+
}
|
|
5773
|
+
callback(null, first.address, first.family);
|
|
5774
|
+
});
|
|
5775
|
+
};
|
|
5776
|
+
}
|
|
5777
|
+
function isPrivate(address) {
|
|
5778
|
+
const family = isIP(address);
|
|
5779
|
+
if (family === 4) return privateAddresses.check(address, "ipv4");
|
|
5780
|
+
if (family === 6) return privateAddresses.check(address, "ipv6");
|
|
5781
|
+
throw new Error(`${address} is not an IP address`);
|
|
5782
|
+
}
|
|
5783
|
+
async function isLocalHost(hostname) {
|
|
5784
|
+
if (isIP(hostname) !== 0) return isPrivate(hostname);
|
|
5785
|
+
return (await dns.promises.lookup(hostname, { all: true })).some((item) => isPrivate(item.address));
|
|
5786
|
+
}
|
|
5556
5787
|
// src/services/message-search.ts
|
|
5557
5788
|
const READ_LIMIT = 30;
|
|
5558
5789
|
const CODEX_READ_LIMIT = 10;
|
|
@@ -5687,140 +5918,6 @@ function bestMatch(query, sessionId, messages) {
|
|
|
5687
5918
|
}
|
|
5688
5919
|
return best;
|
|
5689
5920
|
}
|
|
5690
|
-
// src/services/proxy.ts
|
|
5691
|
-
const proxies = /* @__PURE__ */ new Map();
|
|
5692
|
-
const pageScriptPath = "/__offsite/browser.js";
|
|
5693
|
-
function pageScript(origin) {
|
|
5694
|
-
return `(() => {
|
|
5695
|
-
if (window.parent === window || window.navigation === undefined) {
|
|
5696
|
-
return
|
|
5697
|
-
}
|
|
5698
|
-
const post = (message) =>
|
|
5699
|
-
window.parent.postMessage({ offsite: 'browser', ...message }, '*')
|
|
5700
|
-
const report = () =>
|
|
5701
|
-
post({
|
|
5702
|
-
event: 'location',
|
|
5703
|
-
url: ${JSON.stringify(origin)} + location.pathname + location.search + location.hash,
|
|
5704
|
-
proxied: location.href,
|
|
5705
|
-
back: navigation.canGoBack,
|
|
5706
|
-
forward: navigation.canGoForward,
|
|
5707
|
-
})
|
|
5708
|
-
addEventListener('pageshow', report)
|
|
5709
|
-
navigation.addEventListener('currententrychange', report)
|
|
5710
|
-
navigation.addEventListener('navigate', (event) => {
|
|
5711
|
-
if (!event.destination.sameDocument && event.downloadRequest === null) {
|
|
5712
|
-
post({ event: 'navigate' })
|
|
5713
|
-
}
|
|
5714
|
-
})
|
|
5715
|
-
addEventListener('message', (event) => {
|
|
5716
|
-
if (event.source !== window.parent || event.data?.offsite !== 'browser') {
|
|
5717
|
-
return
|
|
5718
|
-
}
|
|
5719
|
-
if (event.data.command === 'back') {
|
|
5720
|
-
navigation.back()
|
|
5721
|
-
}
|
|
5722
|
-
if (event.data.command === 'forward') {
|
|
5723
|
-
navigation.forward()
|
|
5724
|
-
}
|
|
5725
|
-
if (event.data.command === 'reload') {
|
|
5726
|
-
location.reload()
|
|
5727
|
-
}
|
|
5728
|
-
})
|
|
5729
|
-
})()
|
|
5730
|
-
`;
|
|
5731
|
-
}
|
|
5732
|
-
function proxyPort(origin) {
|
|
5733
|
-
const target = new URL(origin);
|
|
5734
|
-
if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error(`${origin} is not an http or https address`);
|
|
5735
|
-
const existing = proxies.get(target.origin);
|
|
5736
|
-
if (existing !== void 0) return existing;
|
|
5737
|
-
const started = startProxy(target);
|
|
5738
|
-
proxies.set(target.origin, started);
|
|
5739
|
-
started.catch(() => proxies.delete(target.origin));
|
|
5740
|
-
return started;
|
|
5741
|
-
}
|
|
5742
|
-
function startProxy(target) {
|
|
5743
|
-
const options = {
|
|
5744
|
-
target,
|
|
5745
|
-
changeOrigin: true,
|
|
5746
|
-
ws: true,
|
|
5747
|
-
secure: false,
|
|
5748
|
-
autoRewrite: true,
|
|
5749
|
-
protocolRewrite: "http",
|
|
5750
|
-
cookieDomainRewrite: "",
|
|
5751
|
-
selfHandleResponse: true
|
|
5752
|
-
};
|
|
5753
|
-
const proxy = createProxyServer(options);
|
|
5754
|
-
proxy.on("proxyReq", (outgoing, request) => {
|
|
5755
|
-
rewriteOrigin(target, outgoing, request);
|
|
5756
|
-
if (request.headers.accept?.startsWith("text/html") === true) outgoing.removeHeader("accept-encoding");
|
|
5757
|
-
});
|
|
5758
|
-
proxy.on("proxyReqWs", (outgoing, request) => rewriteOrigin(target, outgoing, request));
|
|
5759
|
-
proxy.on("proxyRes", async (incoming, request, response) => {
|
|
5760
|
-
try {
|
|
5761
|
-
await rewriteRedirect(target, incoming, request);
|
|
5762
|
-
} catch (error) {
|
|
5763
|
-
response.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
|
5764
|
-
response.end(`Could not follow the redirect from ${target.origin}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5765
|
-
return;
|
|
5766
|
-
}
|
|
5767
|
-
delete incoming.headers["x-frame-options"];
|
|
5768
|
-
delete incoming.headers["content-security-policy"];
|
|
5769
|
-
if (incoming.headers["set-cookie"] !== void 0) incoming.headers["set-cookie"] = incoming.headers["set-cookie"].map((cookie) => cookie.replace(/;\s*secure/i, "").replace(/;\s*samesite=none/i, ""));
|
|
5770
|
-
const page = request.method === "GET" && request.headers.accept?.startsWith("text/html") === true && incoming.statusCode !== 204 && incoming.statusCode !== 304 && /^text\/html/i.test(incoming.headers["content-type"] ?? "") && incoming.headers["content-encoding"] === void 0;
|
|
5771
|
-
if (page) delete incoming.headers["content-length"];
|
|
5772
|
-
for (const pass of Object.values(OUTGOING_PASSES)) pass(request, response, incoming, options);
|
|
5773
|
-
if (!page) {
|
|
5774
|
-
incoming.pipe(response);
|
|
5775
|
-
return;
|
|
5776
|
-
}
|
|
5777
|
-
const chunks = [];
|
|
5778
|
-
incoming.on("data", (chunk) => chunks.push(chunk));
|
|
5779
|
-
incoming.on("end", () => response.end(Buffer.from(Buffer.concat(chunks).toString("latin1").replace(/^(\xEF\xBB\xBF)?\s*(<!doctype[^>]*>)?/i, `$&<script src="${pageScriptPath}"><\/script>`), "latin1")));
|
|
5780
|
-
});
|
|
5781
|
-
proxy.on("error", (error, _request, response) => {
|
|
5782
|
-
if (!(response instanceof http.ServerResponse)) {
|
|
5783
|
-
response.destroy();
|
|
5784
|
-
return;
|
|
5785
|
-
}
|
|
5786
|
-
if (!response.headersSent) response.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
|
5787
|
-
response.end(`Could not reach ${target.origin}: ${error.message}`);
|
|
5788
|
-
});
|
|
5789
|
-
const server = http.createServer((request, response) => {
|
|
5790
|
-
if (request.url === pageScriptPath) {
|
|
5791
|
-
response.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
|
|
5792
|
-
response.end(pageScript(target.origin));
|
|
5793
|
-
return;
|
|
5794
|
-
}
|
|
5795
|
-
proxy.web(request, response);
|
|
5796
|
-
});
|
|
5797
|
-
server.on("upgrade", (request, socket, head) => proxy.ws(request, socket, head));
|
|
5798
|
-
return new Promise((resolve, reject) => {
|
|
5799
|
-
server.once("error", reject);
|
|
5800
|
-
server.listen(0, args.host, () => {
|
|
5801
|
-
const address = server.address();
|
|
5802
|
-
if (address === null || typeof address === "string") {
|
|
5803
|
-
reject(/* @__PURE__ */ new Error(`the proxy for ${target.origin} has no port`));
|
|
5804
|
-
return;
|
|
5805
|
-
}
|
|
5806
|
-
resolve(address.port);
|
|
5807
|
-
});
|
|
5808
|
-
});
|
|
5809
|
-
}
|
|
5810
|
-
async function rewriteRedirect(target, incoming, request) {
|
|
5811
|
-
const location = incoming.headers.location;
|
|
5812
|
-
if (location === void 0 || !/^30[1278]$/.test(String(incoming.statusCode))) return;
|
|
5813
|
-
const next = new URL(location, target);
|
|
5814
|
-
if (next.origin === target.origin || next.protocol !== "http:" && next.protocol !== "https:") return;
|
|
5815
|
-
if (request.headers.host === void 0) throw new Error("the redirected request has no host");
|
|
5816
|
-
const proxied = new URL(`http://${request.headers.host}`);
|
|
5817
|
-
proxied.port = String(await proxyPort(next.origin));
|
|
5818
|
-
incoming.headers.location = `${proxied.origin}${next.pathname}${next.search}${next.hash}`;
|
|
5819
|
-
}
|
|
5820
|
-
function rewriteOrigin(target, outgoing, request) {
|
|
5821
|
-
if (request.headers.origin !== void 0) outgoing.setHeader("origin", target.origin);
|
|
5822
|
-
if (request.headers.referer !== void 0) outgoing.setHeader("referer", request.headers.referer.replace(`http://${request.headers.host}`, target.origin));
|
|
5823
|
-
}
|
|
5824
5921
|
// src/services/runs.ts
|
|
5825
5922
|
const locks = /* @__PURE__ */ new Map();
|
|
5826
5923
|
function stopAll() {
|
|
@@ -6903,6 +7000,7 @@ const active = /* @__PURE__ */ new Map();
|
|
|
6903
7000
|
const terminals = /* @__PURE__ */ new Map();
|
|
6904
7001
|
const connections = /* @__PURE__ */ new Map();
|
|
6905
7002
|
const paused = /* @__PURE__ */ new Set();
|
|
7003
|
+
const held = /* @__PURE__ */ new Set();
|
|
6906
7004
|
let nextTab = 0;
|
|
6907
7005
|
let draining = null;
|
|
6908
7006
|
async function terminalRequest(client, message) {
|
|
@@ -6961,7 +7059,9 @@ function attach(client) {
|
|
|
6961
7059
|
layout: layout(),
|
|
6962
7060
|
instance
|
|
6963
7061
|
});
|
|
6964
|
-
for (const tab of tabs) forward(connection, tab.id, true, Buffer.concat(terminals.get(tab.id)?.history ?? []));
|
|
7062
|
+
for (const tab of tabs) forward$1(connection, tab.id, true, Buffer.concat(terminals.get(tab.id)?.history ?? []));
|
|
7063
|
+
for (const pty of held) pty.resume();
|
|
7064
|
+
held.clear();
|
|
6965
7065
|
}
|
|
6966
7066
|
function resize(connection, message) {
|
|
6967
7067
|
const terminal = terminals.get(message.id);
|
|
@@ -7071,9 +7171,14 @@ async function createTab(projectId) {
|
|
|
7071
7171
|
});
|
|
7072
7172
|
active.set(projectId ?? "global", id);
|
|
7073
7173
|
pty.onData((data) => output(id, terminal, Buffer.from(data)));
|
|
7174
|
+
if (connections.size === 0) {
|
|
7175
|
+
pty.pause();
|
|
7176
|
+
held.add(pty);
|
|
7177
|
+
}
|
|
7074
7178
|
pty.onExit(() => {
|
|
7075
7179
|
terminals.delete(id);
|
|
7076
7180
|
paused.delete(pty);
|
|
7181
|
+
held.delete(pty);
|
|
7077
7182
|
if (closeTab(id)) publish();
|
|
7078
7183
|
});
|
|
7079
7184
|
}
|
|
@@ -7097,7 +7202,7 @@ function output(id, terminal, data) {
|
|
|
7097
7202
|
terminal.historySize -= terminal.history[0].length;
|
|
7098
7203
|
terminal.history.shift();
|
|
7099
7204
|
}
|
|
7100
|
-
for (const connection of connections.values()) forward(connection, id, false, data);
|
|
7205
|
+
for (const connection of connections.values()) forward$1(connection, id, false, data);
|
|
7101
7206
|
scan(id, terminal, data);
|
|
7102
7207
|
if ([...connections.values()].some((connection) => connection.client.socket.bufferedAmount > OUTPUT_LIMIT)) {
|
|
7103
7208
|
terminal.pty.pause();
|
|
@@ -7202,7 +7307,7 @@ function publish() {
|
|
|
7202
7307
|
tabs
|
|
7203
7308
|
});
|
|
7204
7309
|
}
|
|
7205
|
-
function forward(connection, id, replay, data) {
|
|
7310
|
+
function forward$1(connection, id, replay, data) {
|
|
7206
7311
|
connection.client.socket.send(encodeFrame(replay ? 3 : 2, id, data));
|
|
7207
7312
|
}
|
|
7208
7313
|
function send(connection, event) {
|
|
@@ -7429,11 +7534,11 @@ const handlers = {
|
|
|
7429
7534
|
deleteProjectEntry(projectPath(params.projectId), params.path);
|
|
7430
7535
|
return null;
|
|
7431
7536
|
},
|
|
7432
|
-
"files.media": (params) => linkProjectMedia(projectPath(params.projectId), params.path),
|
|
7433
|
-
"files.download": (params) => linkProjectDownload(projectPath(params.projectId), params.path),
|
|
7537
|
+
"files.media": (params, client) => linkProjectMedia(projectPath(params.projectId), params.path, client.device),
|
|
7538
|
+
"files.download": (params, client) => linkProjectDownload(projectPath(params.projectId), params.path, client.device),
|
|
7434
7539
|
"files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
|
|
7435
|
-
"files.browse": () =>
|
|
7436
|
-
"browser.
|
|
7540
|
+
"files.browse": (_params, client) => linkBrowse(client.device, client.address),
|
|
7541
|
+
"browser.open": (params) => openSite(params.url, params.fromPage),
|
|
7437
7542
|
"tasks.output": (params) => taskOutput(params.sessionId, params.path),
|
|
7438
7543
|
"git.status": (params) => gitRepos(projectPath(params.projectId)),
|
|
7439
7544
|
"git.diff": (params) => projectDiff(projectPath(params.projectId)),
|
|
@@ -7489,9 +7594,11 @@ const handlers = {
|
|
|
7489
7594
|
"terminal.busy": (params) => busyTerminal(params.id),
|
|
7490
7595
|
"terminal.paste": (params) => savePastedImage(params.type, Buffer.from(params.data, "base64"))
|
|
7491
7596
|
};
|
|
7492
|
-
function connectClient(socket) {
|
|
7597
|
+
function connectClient(socket, device, address) {
|
|
7493
7598
|
const client = {
|
|
7494
7599
|
socket,
|
|
7600
|
+
device,
|
|
7601
|
+
address,
|
|
7495
7602
|
heardAt: Date.now(),
|
|
7496
7603
|
opening: null,
|
|
7497
7604
|
sessionId: null,
|
|
@@ -7537,58 +7644,1173 @@ async function answer(client, text) {
|
|
|
7537
7644
|
});
|
|
7538
7645
|
}
|
|
7539
7646
|
}
|
|
7540
|
-
// src/
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
function addCode(name) {
|
|
7544
|
-
const code = randomBytes(16).toString("base64url");
|
|
7545
|
-
db.delete(pairingCodes).where(lte(pairingCodes.expiresAt, Date.now())).run();
|
|
7546
|
-
db.insert(pairingCodes).values({
|
|
7547
|
-
hash: hash(code),
|
|
7548
|
-
name,
|
|
7549
|
-
expiresAt: Date.now() + CODE_LIFETIME
|
|
7550
|
-
}).run();
|
|
7551
|
-
return code;
|
|
7647
|
+
// src/lib/proxy/css.ts
|
|
7648
|
+
function rewriteCss(source, rewrite) {
|
|
7649
|
+
return source.replace(/url\(\s*(['"]?)(.*?)\1\s*\)/gi, (_match, quote, value) => `url(${quote}${rewrite(value)}${quote})`).replace(/@import\s+(['"])(.*?)\1/gi, (_match, quote, value) => `@import ${quote}${rewrite(value)}${quote}`);
|
|
7552
7650
|
}
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7651
|
+
// src/lib/proxy/script.ts
|
|
7652
|
+
const CACHE_LIMIT = 500;
|
|
7653
|
+
const MARKERS = /\b(?:location|top|parent|import)\b/;
|
|
7654
|
+
const WINDOWS = /* @__PURE__ */ new Set([
|
|
7655
|
+
"window",
|
|
7656
|
+
"self",
|
|
7657
|
+
"globalThis",
|
|
7658
|
+
"frames"
|
|
7659
|
+
]);
|
|
7660
|
+
const FUNCTIONS = /* @__PURE__ */ new Set([
|
|
7661
|
+
"FunctionDeclaration",
|
|
7662
|
+
"FunctionExpression",
|
|
7663
|
+
"ArrowFunctionExpression"
|
|
7664
|
+
]);
|
|
7665
|
+
const PATTERNS = /* @__PURE__ */ new Set([
|
|
7666
|
+
"ObjectPattern",
|
|
7667
|
+
"ArrayPattern",
|
|
7668
|
+
"RestElement",
|
|
7669
|
+
"AssignmentPattern",
|
|
7670
|
+
"Property"
|
|
7671
|
+
]);
|
|
7672
|
+
const NAMES = /* @__PURE__ */ new Set([
|
|
7673
|
+
"ImportSpecifier",
|
|
7674
|
+
"ImportDefaultSpecifier",
|
|
7675
|
+
"ImportNamespaceSpecifier",
|
|
7676
|
+
"ExportSpecifier",
|
|
7677
|
+
"LabeledStatement",
|
|
7678
|
+
"BreakStatement",
|
|
7679
|
+
"ContinueStatement"
|
|
7680
|
+
]);
|
|
7681
|
+
const KEYS = /* @__PURE__ */ new Set([
|
|
7682
|
+
"Property",
|
|
7683
|
+
"MethodDefinition",
|
|
7684
|
+
"PropertyDefinition"
|
|
7685
|
+
]);
|
|
7686
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7687
|
+
function rewriteScript(source, rewrite, scope) {
|
|
7688
|
+
if (!MARKERS.test(source)) return source;
|
|
7689
|
+
const id = createHash("sha1").update(scope).update(source).digest("base64");
|
|
7690
|
+
const known = cache.get(id);
|
|
7691
|
+
if (known !== void 0) return known;
|
|
7692
|
+
const rewritten = rewriteSource(source, rewrite);
|
|
7693
|
+
if (cache.size >= CACHE_LIMIT) cache.clear();
|
|
7694
|
+
cache.set(id, rewritten);
|
|
7695
|
+
return rewritten;
|
|
7696
|
+
}
|
|
7697
|
+
function rewriteHandler(source, rewrite) {
|
|
7698
|
+
const rewritten = rewriteSource(`function handler(event){${source}
|
|
7699
|
+
}`, rewrite);
|
|
7700
|
+
return rewritten.slice(24, rewritten.length - 2);
|
|
7701
|
+
}
|
|
7702
|
+
function rewriteSource(source, rewrite) {
|
|
7703
|
+
const parsed = parseSync("page.js", source, {
|
|
7704
|
+
lang: "js",
|
|
7705
|
+
sourceType: "unambiguous"
|
|
7706
|
+
});
|
|
7707
|
+
if (parsed.errors.some((error) => error.severity === "Error")) return source;
|
|
7708
|
+
const output = new MagicString(source);
|
|
7709
|
+
walk(parsed.program, null, false, {
|
|
7710
|
+
source,
|
|
7711
|
+
output,
|
|
7712
|
+
rewrite
|
|
7713
|
+
});
|
|
7714
|
+
return output.toString();
|
|
7715
|
+
}
|
|
7716
|
+
function walk(node, parent, binding, context) {
|
|
7717
|
+
visit(node, parent, binding, context);
|
|
7718
|
+
for (const field of Object.keys(node)) {
|
|
7719
|
+
const value = node[field];
|
|
7720
|
+
const position = {
|
|
7721
|
+
node,
|
|
7722
|
+
field,
|
|
7723
|
+
parent
|
|
7724
|
+
};
|
|
7725
|
+
if (Array.isArray(value)) {
|
|
7726
|
+
for (const item of value) if (isNode(item)) walk(item, position, inBinding(node, field, binding), context);
|
|
7727
|
+
} else if (isNode(value)) walk(value, position, inBinding(node, field, binding), context);
|
|
7728
|
+
}
|
|
7565
7729
|
}
|
|
7566
|
-
function
|
|
7567
|
-
const
|
|
7568
|
-
|
|
7730
|
+
function visit(node, parent, binding, context) {
|
|
7731
|
+
const output = context.output;
|
|
7732
|
+
if ((node.type === "ImportDeclaration" || node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") && isNode(node.source)) {
|
|
7733
|
+
const specifier = String(node.source.value);
|
|
7734
|
+
if (specifier.startsWith("/") || /^[a-z][a-z\d+.-]*:/i.test(specifier)) output.overwrite(node.source.start, node.source.end, JSON.stringify(context.rewrite(specifier)));
|
|
7735
|
+
return;
|
|
7736
|
+
}
|
|
7737
|
+
if (node.type === "ImportExpression" && isNode(node.source)) {
|
|
7738
|
+
wrap(output, node.source, "__oi");
|
|
7739
|
+
return;
|
|
7740
|
+
}
|
|
7741
|
+
if (node.type === "AssignmentExpression") {
|
|
7742
|
+
if (node.operator === "=" && isNode(node.left) && isNode(node.right) && isLocationTarget(node.left)) {
|
|
7743
|
+
const current = context.source.slice(node.left.start, node.left.end);
|
|
7744
|
+
output.appendRight(node.right.start, `__ou(${current},`);
|
|
7745
|
+
output.appendLeft(node.right.end, ")");
|
|
7746
|
+
}
|
|
7747
|
+
return;
|
|
7748
|
+
}
|
|
7749
|
+
if (node.type === "Identifier") {
|
|
7750
|
+
visitIdentifier(node, parent, binding, output);
|
|
7751
|
+
return;
|
|
7752
|
+
}
|
|
7753
|
+
if (node.type === "MemberExpression" && !binding) visitMember(node, parent, output);
|
|
7569
7754
|
}
|
|
7570
|
-
function
|
|
7571
|
-
|
|
7755
|
+
function visitIdentifier(node, parent, binding, output) {
|
|
7756
|
+
const name = String(node.name);
|
|
7757
|
+
if (name !== "location" && name !== "top" && name !== "parent") return;
|
|
7758
|
+
const wrapper = name === "location" ? "__ol" : "__ow";
|
|
7759
|
+
if (parent?.node.type === "Property" && parent.field === "value" && parent.node.shorthand === true && !binding) {
|
|
7760
|
+
output.appendRight(node.start, `${name}:${wrapper}(`);
|
|
7761
|
+
output.appendLeft(node.end, ")");
|
|
7762
|
+
return;
|
|
7763
|
+
}
|
|
7764
|
+
if (binding || !isRead(node, parent)) return;
|
|
7765
|
+
wrap(output, node, wrapper);
|
|
7572
7766
|
}
|
|
7573
|
-
function
|
|
7574
|
-
|
|
7767
|
+
function visitMember(node, parent, output) {
|
|
7768
|
+
if (!isNode(node.property) || !isNode(node.object)) return;
|
|
7769
|
+
if (node.computed === true) return;
|
|
7770
|
+
const name = String(node.property.name);
|
|
7771
|
+
if (name === "url" && node.object.type === "MetaProperty" && isRead(node, parent)) {
|
|
7772
|
+
wrap(output, node, "__om");
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
7775
|
+
if (name === "location" && isRead(node, parent) && !insideChain(parent)) {
|
|
7776
|
+
wrap(output, node, "__ol");
|
|
7777
|
+
return;
|
|
7778
|
+
}
|
|
7779
|
+
if ((name === "top" || name === "parent") && node.object.type === "Identifier" && WINDOWS.has(String(node.object.name)) && isRead(node, parent) && !insideChain(parent)) wrap(output, node, "__ow");
|
|
7780
|
+
}
|
|
7781
|
+
function isRead(node, parent) {
|
|
7782
|
+
if (parent === null) return true;
|
|
7783
|
+
const owner = parent.node;
|
|
7784
|
+
const field = parent.field;
|
|
7785
|
+
if (owner.type === "MemberExpression" && field === "property") return owner.computed === true;
|
|
7786
|
+
if (KEYS.has(owner.type) && field === "key") return owner.computed === true;
|
|
7787
|
+
if (NAMES.has(owner.type)) return false;
|
|
7788
|
+
if (owner.type === "CallExpression" && field === "callee") return false;
|
|
7789
|
+
if (owner.type === "UpdateExpression") return false;
|
|
7790
|
+
if (owner.type === "AssignmentExpression" && field === "left") return false;
|
|
7791
|
+
if (owner.type === "UnaryExpression") return owner.operator !== "delete" && owner.operator !== "typeof";
|
|
7792
|
+
return true;
|
|
7575
7793
|
}
|
|
7576
|
-
function
|
|
7577
|
-
|
|
7794
|
+
function inBinding(node, field, binding) {
|
|
7795
|
+
if (node.type === "VariableDeclarator") return field === "id";
|
|
7796
|
+
if (FUNCTIONS.has(node.type)) return field === "params" || field === "id";
|
|
7797
|
+
if (node.type === "ClassExpression" || node.type === "ClassDeclaration") return field === "id";
|
|
7798
|
+
if (node.type === "CatchClause") return field === "param";
|
|
7799
|
+
if (node.type === "ForInStatement" || node.type === "ForOfStatement") return field === "left";
|
|
7800
|
+
if (node.type === "AssignmentExpression") return field === "left";
|
|
7801
|
+
if (node.type === "AssignmentPattern" && field === "right") return false;
|
|
7802
|
+
if (node.type === "Property" && field === "key") return false;
|
|
7803
|
+
return binding && PATTERNS.has(node.type);
|
|
7804
|
+
}
|
|
7805
|
+
function isLocationTarget(node) {
|
|
7806
|
+
if (node.type === "Identifier") return node.name === "location";
|
|
7807
|
+
return node.type === "MemberExpression" && node.computed !== true && isNode(node.property) && node.property.name === "location";
|
|
7808
|
+
}
|
|
7809
|
+
function insideChain(parent) {
|
|
7810
|
+
return parent !== null && parent.node.type === "MemberExpression" && parent.field === "object" && hasChain(parent);
|
|
7811
|
+
}
|
|
7812
|
+
function hasChain(parent) {
|
|
7813
|
+
if (parent === null) return false;
|
|
7814
|
+
if (parent.node.type === "ChainExpression") return true;
|
|
7815
|
+
return hasChain(parent.parent);
|
|
7816
|
+
}
|
|
7817
|
+
function wrap(output, node, name) {
|
|
7818
|
+
output.appendRight(node.start, `${name}(`);
|
|
7819
|
+
output.appendLeft(node.end, ")");
|
|
7820
|
+
}
|
|
7821
|
+
function isNode(value) {
|
|
7822
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
7823
|
+
}
|
|
7824
|
+
// src/lib/proxy/html.ts
|
|
7825
|
+
const LINKS = /* @__PURE__ */ new Set([
|
|
7826
|
+
"href",
|
|
7827
|
+
"src",
|
|
7828
|
+
"action",
|
|
7829
|
+
"formaction",
|
|
7830
|
+
"poster",
|
|
7831
|
+
"data",
|
|
7832
|
+
"background"
|
|
7833
|
+
]);
|
|
7834
|
+
const LINK_TAGS = /* @__PURE__ */ new Set([
|
|
7835
|
+
"a",
|
|
7836
|
+
"area",
|
|
7837
|
+
"audio",
|
|
7838
|
+
"base",
|
|
7839
|
+
"body",
|
|
7840
|
+
"button",
|
|
7841
|
+
"embed",
|
|
7842
|
+
"form",
|
|
7843
|
+
"iframe",
|
|
7844
|
+
"img",
|
|
7845
|
+
"input",
|
|
7846
|
+
"link",
|
|
7847
|
+
"object",
|
|
7848
|
+
"script",
|
|
7849
|
+
"source",
|
|
7850
|
+
"td",
|
|
7851
|
+
"track",
|
|
7852
|
+
"video"
|
|
7853
|
+
]);
|
|
7854
|
+
const REMOVED = /* @__PURE__ */ new Set([
|
|
7855
|
+
"integrity",
|
|
7856
|
+
"crossorigin",
|
|
7857
|
+
"nonce",
|
|
7858
|
+
"ping"
|
|
7859
|
+
]);
|
|
7860
|
+
const NAVIGATING = /* @__PURE__ */ new Set([
|
|
7861
|
+
"a",
|
|
7862
|
+
"area",
|
|
7863
|
+
"base",
|
|
7864
|
+
"form"
|
|
7865
|
+
]);
|
|
7866
|
+
const SCRIPT_TYPES = /* @__PURE__ */ new Set([
|
|
7867
|
+
"",
|
|
7868
|
+
"module",
|
|
7869
|
+
"text/javascript",
|
|
7870
|
+
"application/javascript",
|
|
7871
|
+
"text/ecmascript",
|
|
7872
|
+
"application/ecmascript"
|
|
7873
|
+
]);
|
|
7874
|
+
const META_REMOVED = /* @__PURE__ */ new Set([
|
|
7875
|
+
"content-security-policy",
|
|
7876
|
+
"content-security-policy-report-only",
|
|
7877
|
+
"x-frame-options"
|
|
7878
|
+
]);
|
|
7879
|
+
function rewriteHtml(source, key, base, script) {
|
|
7880
|
+
const document = parse(source);
|
|
7881
|
+
rewriteChildren(document, {
|
|
7882
|
+
key,
|
|
7883
|
+
base,
|
|
7884
|
+
rewrite: linkRewriter(key, base)
|
|
7885
|
+
});
|
|
7886
|
+
const head = findHead(document);
|
|
7887
|
+
const runtime = defaultTreeAdapter.createElement("script", html.NS.HTML, []);
|
|
7888
|
+
defaultTreeAdapter.insertText(runtime, script);
|
|
7889
|
+
const first = head.childNodes[0];
|
|
7890
|
+
if (first === void 0) defaultTreeAdapter.appendChild(head, runtime);
|
|
7891
|
+
else defaultTreeAdapter.insertBefore(head, runtime, first);
|
|
7892
|
+
return serialize(document);
|
|
7893
|
+
}
|
|
7894
|
+
function findHead(node) {
|
|
7895
|
+
for (const child of node.childNodes) if ("tagName" in child) {
|
|
7896
|
+
if (child.tagName === "head") return child;
|
|
7897
|
+
if (child.tagName === "html") return findHead(child);
|
|
7898
|
+
}
|
|
7899
|
+
throw new Error("the page has no head");
|
|
7900
|
+
}
|
|
7901
|
+
function rewriteChildren(node, page) {
|
|
7902
|
+
const remove = [];
|
|
7903
|
+
for (const child of node.childNodes) {
|
|
7904
|
+
if (!("tagName" in child)) continue;
|
|
7905
|
+
if (rewriteElement(child, page)) {
|
|
7906
|
+
remove.push(child);
|
|
7907
|
+
continue;
|
|
7908
|
+
}
|
|
7909
|
+
rewriteChildren(child, page);
|
|
7910
|
+
if ("content" in child) rewriteChildren(child.content, page);
|
|
7911
|
+
}
|
|
7912
|
+
for (const child of remove) defaultTreeAdapter.detachNode(child);
|
|
7913
|
+
}
|
|
7914
|
+
function rewriteElement(element, page) {
|
|
7915
|
+
const tag = element.tagName;
|
|
7916
|
+
if (tag === "meta") {
|
|
7917
|
+
const equiv = attribute(element, "http-equiv")?.value.toLowerCase();
|
|
7918
|
+
if (equiv !== void 0 && META_REMOVED.has(equiv)) return true;
|
|
7919
|
+
if (attribute(element, "name")?.value.toLowerCase() === "referrer") return true;
|
|
7920
|
+
if (equiv === "refresh") rewriteRefresh(element, page);
|
|
7921
|
+
}
|
|
7922
|
+
if (tag === "base") {
|
|
7923
|
+
const href = attribute(element, "href")?.value;
|
|
7924
|
+
const next = href === void 0 ? null : URL.parse(href, page.base);
|
|
7925
|
+
if (next !== null) {
|
|
7926
|
+
page.base = next;
|
|
7927
|
+
page.rewrite = linkRewriter(page.key, next);
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
element.attrs = element.attrs.filter((item) => !REMOVED.has(item.name));
|
|
7931
|
+
if (NAVIGATING.has(tag)) element.attrs = element.attrs.filter((item) => item.name !== "target" || !/^_(top|parent|blank)$/i.test(item.value.trim()));
|
|
7932
|
+
for (const item of element.attrs) if (LINKS.has(item.name) && LINK_TAGS.has(tag)) item.value = page.rewrite(item.value);
|
|
7933
|
+
else if (item.name === "srcset" || item.name === "imagesrcset") item.value = rewriteSrcset(item.value, page.rewrite);
|
|
7934
|
+
else if (item.name === "style") item.value = rewriteCss(item.value, page.rewrite);
|
|
7935
|
+
else if (item.name.startsWith("on")) item.value = rewriteHandler(item.value, page.rewrite);
|
|
7936
|
+
if (tag === "style") rewriteText(element, (text) => rewriteCss(text, page.rewrite));
|
|
7937
|
+
if (tag === "script") rewriteInlineScript(element, page);
|
|
7938
|
+
return false;
|
|
7939
|
+
}
|
|
7940
|
+
function rewriteInlineScript(element, page) {
|
|
7941
|
+
const type = (attribute(element, "type")?.value ?? "").trim().toLowerCase();
|
|
7942
|
+
if (type === "importmap") {
|
|
7943
|
+
rewriteText(element, (text) => JSON.stringify(rewriteMapLinks(JSON.parse(text), page)));
|
|
7944
|
+
return;
|
|
7945
|
+
}
|
|
7946
|
+
if (SCRIPT_TYPES.has(type)) rewriteText(element, (text) => rewriteScript(text, page.rewrite, `${page.key} ${page.base.origin}`));
|
|
7947
|
+
}
|
|
7948
|
+
function rewriteMapLinks(value, page) {
|
|
7949
|
+
if (typeof value === "string") return page.rewrite(value);
|
|
7950
|
+
if (typeof value !== "object" || value === null) return value;
|
|
7951
|
+
return Object.fromEntries(Object.entries(value).map((entry) => [entry[0], rewriteMapLinks(entry[1], page)]));
|
|
7952
|
+
}
|
|
7953
|
+
function rewriteText(element, rewrite) {
|
|
7954
|
+
for (const child of element.childNodes) if ("value" in child) child.value = rewrite(child.value);
|
|
7955
|
+
}
|
|
7956
|
+
function rewriteRefresh(element, page) {
|
|
7957
|
+
const content = attribute(element, "content");
|
|
7958
|
+
if (content === void 0) return;
|
|
7959
|
+
content.value = content.value.replace(/(url\s*=\s*['"]?)([^'"]+)/i, (_match, label, url) => `${label}${page.rewrite(url)}`);
|
|
7960
|
+
}
|
|
7961
|
+
function rewriteSrcset(value, rewrite) {
|
|
7962
|
+
return value.split(/,\s+/).map((candidate) => {
|
|
7963
|
+
const parts = candidate.trim().split(/\s+/);
|
|
7964
|
+
const url = parts[0];
|
|
7965
|
+
if (url === void 0 || url === "") return candidate;
|
|
7966
|
+
return [rewrite(url), ...parts.slice(1)].join(" ");
|
|
7967
|
+
}).join(", ");
|
|
7968
|
+
}
|
|
7969
|
+
function attribute(element, name) {
|
|
7970
|
+
return element.attrs.find((item) => item.name === name);
|
|
7971
|
+
}
|
|
7972
|
+
// src/lib/proxy/page-script.ts
|
|
7973
|
+
function pageScript(config) {
|
|
7974
|
+
const json = JSON.stringify(config).replaceAll("<", "\\u003c");
|
|
7975
|
+
return `(${PAGE_SCRIPT})(${json})`;
|
|
7976
|
+
}
|
|
7977
|
+
const PAGE_SCRIPT = String.raw`function (config) {
|
|
7978
|
+
if (self.__ol) return
|
|
7979
|
+
const inWindow = typeof document !== 'undefined'
|
|
7980
|
+
const realLocation = self.location
|
|
7981
|
+
const origin = config.origin ?? realLocation.origin
|
|
7982
|
+
const prefix = '/api/proxy/' + config.key + '/'
|
|
7983
|
+
const realFetch = self.fetch.bind(self)
|
|
7984
|
+
const native = { get: inWindow && Element.prototype.getAttribute, set: inWindow && Element.prototype.setAttribute }
|
|
7985
|
+
const realPost = inWindow && window.postMessage.bind(window)
|
|
7986
|
+
const panel = inWindow && window.parent !== window && window.parent === window.top ? window.parent : null
|
|
7987
|
+
const skipped = /^\s*(data|blob|javascript|about|mailto|tel|sms|file):/i
|
|
7988
|
+
const schemes = { 'http:': 'http', 'https:': 'https', 'ws:': 'http', 'wss:': 'https' }
|
|
7989
|
+
|
|
7990
|
+
function hide(name, value) {
|
|
7991
|
+
Object.defineProperty(self, name, { value, enumerable: false, configurable: true, writable: true })
|
|
7992
|
+
}
|
|
7993
|
+
function decode(value) {
|
|
7994
|
+
const text = String(value)
|
|
7995
|
+
const index = text.indexOf(prefix)
|
|
7996
|
+
if (index === -1 || (index !== 0 && !text.startsWith(origin))) return null
|
|
7997
|
+
const rest = text.slice(index + prefix.length)
|
|
7998
|
+
const slash = rest.indexOf('/')
|
|
7999
|
+
const scheme = rest.slice(0, slash)
|
|
8000
|
+
if (scheme !== 'http' && scheme !== 'https') return null
|
|
8001
|
+
return scheme + '://' + rest.slice(slash + 1)
|
|
8002
|
+
}
|
|
8003
|
+
function original(value) {
|
|
8004
|
+
return decode(value) ?? value
|
|
8005
|
+
}
|
|
8006
|
+
function pageUrl() {
|
|
8007
|
+
return decode(realLocation.href) ?? config.url
|
|
8008
|
+
}
|
|
8009
|
+
function baseUrl() {
|
|
8010
|
+
const base = inWindow && document.querySelector('base[href]')
|
|
8011
|
+
return base ? (decode(native.get.call(base, 'href')) ?? pageUrl()) : pageUrl()
|
|
8012
|
+
}
|
|
8013
|
+
function encode(value, base) {
|
|
8014
|
+
const text = String(value)
|
|
8015
|
+
if (text === '' || text.startsWith('#') || skipped.test(text) || decode(text) !== null) return text
|
|
8016
|
+
const url = URL.parse(text, base ?? baseUrl())
|
|
8017
|
+
if (url === null || schemes[url.protocol] === undefined) return text
|
|
8018
|
+
return prefix + schemes[url.protocol] + '/' + url.host + url.pathname + url.search + url.hash
|
|
8019
|
+
}
|
|
8020
|
+
function absolute(value) {
|
|
8021
|
+
const encoded = encode(value)
|
|
8022
|
+
return encoded.startsWith('/') ? origin + encoded : encoded
|
|
8023
|
+
}
|
|
8024
|
+
function encodeSet(value) {
|
|
8025
|
+
return String(value).split(/,\s+/).map(function (item) {
|
|
8026
|
+
const parts = item.trim().split(/\s+/)
|
|
8027
|
+
parts[0] = encode(parts[0])
|
|
8028
|
+
return parts.join(' ')
|
|
8029
|
+
}).join(', ')
|
|
8030
|
+
}
|
|
8031
|
+
function socketUrl(value) {
|
|
8032
|
+
return (origin.startsWith('https') ? 'wss://' : 'ws://') + origin.replace(/^https?:\/\//, '') + encode(value)
|
|
8033
|
+
}
|
|
8034
|
+
function postPanel(message) {
|
|
8035
|
+
if (panel) panel.postMessage(Object.assign({ offsite: 'browser' }, message), '*')
|
|
8036
|
+
}
|
|
8037
|
+
function navigate(value, replace) {
|
|
8038
|
+
const next = URL.parse(String(value), baseUrl())
|
|
8039
|
+
if (next === null) return
|
|
8040
|
+
const web = next.protocol === 'http:' || next.protocol === 'https:'
|
|
8041
|
+
if (panel && web && next.host !== new URL(pageUrl()).host) {
|
|
8042
|
+
postPanel({ event: 'open', url: next.href })
|
|
8043
|
+
return
|
|
8044
|
+
}
|
|
8045
|
+
if (replace) realLocation.replace(encode(next.href))
|
|
8046
|
+
else realLocation.assign(encode(next.href))
|
|
8047
|
+
}
|
|
8048
|
+
|
|
8049
|
+
const fakeLocation = {}
|
|
8050
|
+
for (const part of ['href', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin']) {
|
|
8051
|
+
Object.defineProperty(fakeLocation, part, {
|
|
8052
|
+
enumerable: true,
|
|
8053
|
+
get() {
|
|
8054
|
+
return new URL(pageUrl())[part]
|
|
8055
|
+
},
|
|
8056
|
+
set(value) {
|
|
8057
|
+
if (part === 'origin') return
|
|
8058
|
+
if (part === 'hash') {
|
|
8059
|
+
realLocation.hash = value
|
|
8060
|
+
return
|
|
8061
|
+
}
|
|
8062
|
+
if (part === 'href') {
|
|
8063
|
+
navigate(value)
|
|
8064
|
+
return
|
|
8065
|
+
}
|
|
8066
|
+
const next = new URL(pageUrl())
|
|
8067
|
+
next[part] = value
|
|
8068
|
+
navigate(next.href)
|
|
8069
|
+
},
|
|
8070
|
+
})
|
|
8071
|
+
}
|
|
8072
|
+
fakeLocation.assign = function (value) { navigate(value) }
|
|
8073
|
+
fakeLocation.replace = function (value) { navigate(value, true) }
|
|
8074
|
+
fakeLocation.reload = function () { realLocation.reload() }
|
|
8075
|
+
fakeLocation.toString = function () { return pageUrl() }
|
|
8076
|
+
fakeLocation.valueOf = function () { return fakeLocation }
|
|
8077
|
+
fakeLocation[Symbol.toPrimitive] = function () { return pageUrl() }
|
|
8078
|
+
fakeLocation.ancestorOrigins = []
|
|
8079
|
+
|
|
8080
|
+
function pageTop() {
|
|
8081
|
+
if (!inWindow || window.parent === window) return self
|
|
8082
|
+
let current = window
|
|
8083
|
+
while (current.parent !== window.top) current = current.parent
|
|
8084
|
+
return current
|
|
8085
|
+
}
|
|
8086
|
+
hide('__ol', function (value) { return value === realLocation ? fakeLocation : value })
|
|
8087
|
+
hide('__ow', function (value) {
|
|
8088
|
+
if (!inWindow) return value
|
|
8089
|
+
if (value === window.top) return pageTop()
|
|
8090
|
+
return value
|
|
8091
|
+
})
|
|
8092
|
+
hide('__ou', function (current, value) {
|
|
8093
|
+
if (current === realLocation || (inWindow && current === window.top.location)) return encode(value)
|
|
8094
|
+
return value
|
|
8095
|
+
})
|
|
8096
|
+
hide('__oi', function (value) {
|
|
8097
|
+
const text = String(value)
|
|
8098
|
+
return /^(\/|https?:)/.test(text) ? absolute(text) : text
|
|
8099
|
+
})
|
|
8100
|
+
hide('__om', original)
|
|
8101
|
+
|
|
8102
|
+
function patch(owner, name, wrap) {
|
|
8103
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, name)
|
|
8104
|
+
if (descriptor === undefined) return
|
|
8105
|
+
Object.defineProperty(owner, name, Object.assign({}, descriptor, wrap(descriptor)))
|
|
8106
|
+
}
|
|
8107
|
+
function getter(owner, name, read) {
|
|
8108
|
+
patch(owner, name, function (descriptor) {
|
|
8109
|
+
return { get() { return read(descriptor.get.call(this), this) } }
|
|
8110
|
+
})
|
|
8111
|
+
}
|
|
8112
|
+
|
|
8113
|
+
self.fetch = function (input, init) {
|
|
8114
|
+
const options = Object.assign({}, init, { credentials: 'omit' })
|
|
8115
|
+
if (options.mode === 'same-origin') options.mode = 'cors'
|
|
8116
|
+
if (input instanceof RealRequest) return realFetch(input, options)
|
|
8117
|
+
return realFetch(absolute(input), options)
|
|
8118
|
+
}
|
|
8119
|
+
const RealRequest = self.Request
|
|
8120
|
+
self.Request = class Request extends RealRequest {
|
|
8121
|
+
constructor(input, init) {
|
|
8122
|
+
super(input instanceof RealRequest ? input : absolute(input), init)
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
getter(RealRequest.prototype, 'url', original)
|
|
8126
|
+
getter(Response.prototype, 'url', original)
|
|
8127
|
+
const realOpen = XMLHttpRequest.prototype.open
|
|
8128
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
8129
|
+
const rest = Array.prototype.slice.call(arguments, 2)
|
|
8130
|
+
return realOpen.call(this, method, encode(url), ...rest)
|
|
8131
|
+
}
|
|
8132
|
+
patch(XMLHttpRequest.prototype, 'withCredentials', function () {
|
|
8133
|
+
return { get() { return false }, set() {} }
|
|
8134
|
+
})
|
|
8135
|
+
getter(XMLHttpRequest.prototype, 'responseURL', original)
|
|
8136
|
+
self.WebSocket = new Proxy(self.WebSocket, {
|
|
8137
|
+
construct(target, args) {
|
|
8138
|
+
args[0] = socketUrl(args[0])
|
|
8139
|
+
return Reflect.construct(target, args)
|
|
8140
|
+
},
|
|
8141
|
+
})
|
|
8142
|
+
if (self.EventSource) {
|
|
8143
|
+
self.EventSource = new Proxy(self.EventSource, {
|
|
8144
|
+
construct(target, args) {
|
|
8145
|
+
return new target(encode(args[0]))
|
|
8146
|
+
},
|
|
8147
|
+
})
|
|
8148
|
+
}
|
|
8149
|
+
function workerUrl(value, options) {
|
|
8150
|
+
const text = String(value)
|
|
8151
|
+
if (skipped.test(text)) return text
|
|
8152
|
+
const target = URL.parse(text, baseUrl())
|
|
8153
|
+
if (target === null) return text
|
|
8154
|
+
const runtime = origin + prefix + 'runtime.js?url=' + encodeURIComponent(target.href) + '&origin=' + encodeURIComponent(origin)
|
|
8155
|
+
const script = absolute(target.href)
|
|
8156
|
+
const code = options && options.type === 'module'
|
|
8157
|
+
? 'import ' + JSON.stringify(runtime) + ';import ' + JSON.stringify(script) + ';'
|
|
8158
|
+
: 'importScripts(' + JSON.stringify(runtime) + ');importScripts(' + JSON.stringify(script) + ');'
|
|
8159
|
+
return URL.createObjectURL(new Blob([code], { type: 'text/javascript' }))
|
|
8160
|
+
}
|
|
8161
|
+
for (const name of ['Worker', 'SharedWorker']) {
|
|
8162
|
+
if (self[name]) {
|
|
8163
|
+
self[name] = new Proxy(self[name], {
|
|
8164
|
+
construct(target, args) {
|
|
8165
|
+
args[0] = workerUrl(args[0], args[1])
|
|
8166
|
+
return Reflect.construct(target, args)
|
|
8167
|
+
},
|
|
8168
|
+
})
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
if (!inWindow) {
|
|
8172
|
+
if (self.importScripts) {
|
|
8173
|
+
const realImport = self.importScripts.bind(self)
|
|
8174
|
+
self.importScripts = function () {
|
|
8175
|
+
realImport(...Array.from(arguments, function (item) { return absolute(item) }))
|
|
8176
|
+
}
|
|
8177
|
+
}
|
|
8178
|
+
return
|
|
8179
|
+
}
|
|
8180
|
+
|
|
8181
|
+
delete Navigator.prototype.serviceWorker
|
|
8182
|
+
Object.defineProperty(window, 'indexedDB', { value: undefined, configurable: true })
|
|
8183
|
+
Object.defineProperty(window, 'caches', {
|
|
8184
|
+
configurable: true,
|
|
8185
|
+
value: {
|
|
8186
|
+
keys() { return Promise.resolve([]) },
|
|
8187
|
+
has() { return Promise.resolve(false) },
|
|
8188
|
+
delete() { return Promise.resolve(false) },
|
|
8189
|
+
match() { return Promise.resolve(undefined) },
|
|
8190
|
+
open() { return Promise.reject(new DOMException('Cache storage is off', 'SecurityError')) },
|
|
8191
|
+
},
|
|
8192
|
+
})
|
|
8193
|
+
Object.defineProperty(Navigator.prototype, 'locks', {
|
|
8194
|
+
configurable: true,
|
|
8195
|
+
value: {
|
|
8196
|
+
request(name, options, callback) {
|
|
8197
|
+
const run = callback ?? options
|
|
8198
|
+
return Promise.resolve().then(function () { return run({ name, mode: 'exclusive' }) })
|
|
8199
|
+
},
|
|
8200
|
+
query() { return Promise.resolve({ held: [], pending: [] }) },
|
|
8201
|
+
},
|
|
8202
|
+
})
|
|
8203
|
+
const blankFrames = new Proxy(window, {
|
|
8204
|
+
get(target, name) {
|
|
8205
|
+
if (name === 'document') throw new DOMException('Helper frames are blocked', 'SecurityError')
|
|
8206
|
+
const value = Reflect.get(target, name)
|
|
8207
|
+
return typeof value === 'function' && value.prototype === undefined ? value.bind(target) : value
|
|
8208
|
+
},
|
|
8209
|
+
})
|
|
8210
|
+
getter(HTMLIFrameElement.prototype, 'contentWindow', function (value, frame) {
|
|
8211
|
+
const blank = native.get.call(frame, 'src') === null && native.get.call(frame, 'srcdoc') === null
|
|
8212
|
+
return blank && value !== null ? blankFrames : value
|
|
8213
|
+
})
|
|
8214
|
+
const realBeacon = navigator.sendBeacon.bind(navigator)
|
|
8215
|
+
navigator.sendBeacon = function (url, data) { return realBeacon(encode(url), data) }
|
|
8216
|
+
|
|
8217
|
+
const properties = [
|
|
8218
|
+
[HTMLAnchorElement, 'href'], [HTMLAreaElement, 'href'], [HTMLLinkElement, 'href'], [HTMLBaseElement, 'href'],
|
|
8219
|
+
[HTMLImageElement, 'src'], [HTMLScriptElement, 'src'], [HTMLIFrameElement, 'src'], [HTMLMediaElement, 'src'],
|
|
8220
|
+
[HTMLSourceElement, 'src'], [HTMLEmbedElement, 'src'], [HTMLInputElement, 'src'], [HTMLTrackElement, 'src'],
|
|
8221
|
+
[HTMLFormElement, 'action'], [HTMLButtonElement, 'formAction'], [HTMLInputElement, 'formAction'],
|
|
8222
|
+
[HTMLObjectElement, 'data'], [HTMLVideoElement, 'poster'],
|
|
8223
|
+
]
|
|
8224
|
+
for (const item of properties) {
|
|
8225
|
+
patch(item[0].prototype, item[1], function (descriptor) {
|
|
8226
|
+
return {
|
|
8227
|
+
get() { return original(descriptor.get.call(this)) },
|
|
8228
|
+
set(value) { descriptor.set.call(this, encode(value)) },
|
|
8229
|
+
}
|
|
8230
|
+
})
|
|
8231
|
+
}
|
|
8232
|
+
const RealAudio = window.Audio
|
|
8233
|
+
window.Audio = function Audio(source) {
|
|
8234
|
+
return source === undefined ? new RealAudio() : new RealAudio(encode(source))
|
|
8235
|
+
}
|
|
8236
|
+
window.Audio.prototype = RealAudio.prototype
|
|
8237
|
+
for (const owner of [HTMLImageElement, HTMLSourceElement]) {
|
|
8238
|
+
patch(owner.prototype, 'srcset', function (descriptor) {
|
|
8239
|
+
return {
|
|
8240
|
+
get() { return descriptor.get.call(this) },
|
|
8241
|
+
set(value) { descriptor.set.call(this, encodeSet(value)) },
|
|
8242
|
+
}
|
|
8243
|
+
})
|
|
8244
|
+
}
|
|
8245
|
+
for (const owner of [HTMLScriptElement, HTMLLinkElement]) {
|
|
8246
|
+
patch(owner.prototype, 'integrity', function () {
|
|
8247
|
+
return { get() { return '' }, set() {} }
|
|
8248
|
+
})
|
|
8249
|
+
}
|
|
8250
|
+
for (const part of ['protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin']) {
|
|
8251
|
+
for (const owner of [HTMLAnchorElement, HTMLAreaElement]) {
|
|
8252
|
+
getter(owner.prototype, part, function (value, element) {
|
|
8253
|
+
const url = URL.parse(element.href)
|
|
8254
|
+
return url === null ? value : url[part]
|
|
8255
|
+
})
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
|
|
8259
|
+
const linkTags = new Set(['A', 'AREA', 'AUDIO', 'BASE', 'BUTTON', 'EMBED', 'FORM', 'IFRAME', 'IMG', 'INPUT', 'LINK', 'OBJECT', 'SCRIPT', 'SOURCE', 'TRACK', 'VIDEO'])
|
|
8260
|
+
const linkNames = new Set(['href', 'src', 'action', 'formaction', 'poster', 'data'])
|
|
8261
|
+
Element.prototype.setAttribute = function (name, value) {
|
|
8262
|
+
const lower = String(name).toLowerCase()
|
|
8263
|
+
if (lower === 'integrity' || lower === 'crossorigin') return
|
|
8264
|
+
if (linkNames.has(lower) && linkTags.has(this.tagName)) {
|
|
8265
|
+
native.set.call(this, name, encode(value))
|
|
8266
|
+
return
|
|
8267
|
+
}
|
|
8268
|
+
if (lower === 'srcset') {
|
|
8269
|
+
native.set.call(this, name, encodeSet(value))
|
|
8270
|
+
return
|
|
8271
|
+
}
|
|
8272
|
+
native.set.call(this, name, value)
|
|
8273
|
+
}
|
|
8274
|
+
Element.prototype.getAttribute = function (name) {
|
|
8275
|
+
const value = native.get.call(this, name)
|
|
8276
|
+
if (value === null) return null
|
|
8277
|
+
const url = decode(value)
|
|
8278
|
+
if (url === null) return value
|
|
8279
|
+
const parsed = URL.parse(url)
|
|
8280
|
+
if (parsed === null || parsed.origin !== new URL(pageUrl()).origin) return url
|
|
8281
|
+
return parsed.pathname + parsed.search + parsed.hash
|
|
8282
|
+
}
|
|
8283
|
+
function encodeSelector(selector) {
|
|
8284
|
+
return String(selector).replace(/\[(src|href)(\^?=)(["']?)([^"'\]]+)\3\]/g, function (match, name, operator, quote, value) {
|
|
8285
|
+
if (!/^(\/|https?:\/\/)/.test(value)) return match
|
|
8286
|
+
return '[' + name + operator + '"' + encode(value) + '"]'
|
|
8287
|
+
})
|
|
8288
|
+
}
|
|
8289
|
+
for (const owner of [Document, DocumentFragment, Element]) {
|
|
8290
|
+
for (const name of ['querySelector', 'querySelectorAll']) {
|
|
8291
|
+
const realQuery = owner.prototype[name]
|
|
8292
|
+
owner.prototype[name] = function (selector) {
|
|
8293
|
+
return realQuery.call(this, encodeSelector(selector))
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
for (const name of ['matches', 'closest']) {
|
|
8298
|
+
const realMatch = Element.prototype[name]
|
|
8299
|
+
Element.prototype[name] = function (selector) {
|
|
8300
|
+
return realMatch.call(this, encodeSelector(selector))
|
|
8301
|
+
}
|
|
8302
|
+
}
|
|
8303
|
+
function encodeCss(text) {
|
|
8304
|
+
return String(text).replace(/url\(\s*(['"]?)(.*?)\1\s*\)/g, function (match, quote, url) {
|
|
8305
|
+
return 'url(' + quote + encode(url) + quote + ')'
|
|
8306
|
+
})
|
|
8307
|
+
}
|
|
8308
|
+
for (const name of ['insertRule', 'replace', 'replaceSync']) {
|
|
8309
|
+
const realRule = CSSStyleSheet.prototype[name]
|
|
8310
|
+
CSSStyleSheet.prototype[name] = function (text, index) {
|
|
8311
|
+
return realRule.call(this, encodeCss(text), index)
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
function fixElement(element) {
|
|
8315
|
+
const style = native.get.call(element, 'style')
|
|
8316
|
+
if (style !== null) {
|
|
8317
|
+
const next = encodeCss(style)
|
|
8318
|
+
if (next !== style) native.set.call(element, 'style', next)
|
|
8319
|
+
}
|
|
8320
|
+
if (element.tagName === 'STYLE') {
|
|
8321
|
+
const text = element.textContent
|
|
8322
|
+
const next = encodeCss(text)
|
|
8323
|
+
if (next !== text) element.textContent = next
|
|
8324
|
+
return
|
|
8325
|
+
}
|
|
8326
|
+
if (!linkTags.has(element.tagName)) return
|
|
8327
|
+
for (const name of linkNames) {
|
|
8328
|
+
const value = native.get.call(element, name)
|
|
8329
|
+
if (value !== null) {
|
|
8330
|
+
const next = encode(value)
|
|
8331
|
+
if (next !== value) native.set.call(element, name, next)
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
const set = native.get.call(element, 'srcset')
|
|
8335
|
+
if (set !== null && !set.includes(prefix)) native.set.call(element, 'srcset', encodeSet(set))
|
|
8336
|
+
}
|
|
8337
|
+
new MutationObserver(function (records) {
|
|
8338
|
+
for (const record of records) {
|
|
8339
|
+
if (record.type === 'attributes' || record.target.nodeName === 'STYLE') fixElement(record.target)
|
|
8340
|
+
for (const node of record.addedNodes) {
|
|
8341
|
+
if (node.nodeType !== 1) continue
|
|
8342
|
+
fixElement(node)
|
|
8343
|
+
for (const child of node.querySelectorAll('[src],[href],[action],[srcset],[poster],[data],[style],style')) fixElement(child)
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
}).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] })
|
|
8347
|
+
|
|
8348
|
+
getter(Document.prototype, 'URL', pageUrl)
|
|
8349
|
+
getter(Document.prototype, 'documentURI', pageUrl)
|
|
8350
|
+
getter(Node.prototype, 'baseURI', baseUrl)
|
|
8351
|
+
getter(Document.prototype, 'referrer', original)
|
|
8352
|
+
patch(Document.prototype, 'domain', function () {
|
|
8353
|
+
return { get() { return new URL(pageUrl()).hostname }, set() {} }
|
|
8354
|
+
})
|
|
8355
|
+
Object.defineProperty(window, 'origin', { get() { return new URL(pageUrl()).origin }, configurable: true })
|
|
8356
|
+
getter(MessageEvent.prototype, 'origin', function (value) {
|
|
8357
|
+
return value === 'null' ? new URL(pageUrl()).origin : value
|
|
8358
|
+
})
|
|
8359
|
+
window.postMessage = function (message, target, transfer) {
|
|
8360
|
+
if (typeof target === 'object' && target !== null) return realPost(message, Object.assign({}, target, { targetOrigin: '*' }))
|
|
8361
|
+
return realPost(message, '*', transfer)
|
|
8362
|
+
}
|
|
8363
|
+
|
|
8364
|
+
const cookies = new Map()
|
|
8365
|
+
for (const item of config.cookies.split('; ')) {
|
|
8366
|
+
const index = item.indexOf('=')
|
|
8367
|
+
if (index > 0) cookies.set(item.slice(0, index), item.slice(index + 1))
|
|
8368
|
+
}
|
|
8369
|
+
patch(Document.prototype, 'cookie', function () {
|
|
8370
|
+
return {
|
|
8371
|
+
get() { return Array.from(cookies, function (item) { return item[0] + '=' + item[1] }).join('; ') },
|
|
8372
|
+
set(value) {
|
|
8373
|
+
const text = String(value)
|
|
8374
|
+
const pair = text.split(';')[0]
|
|
8375
|
+
const index = pair.indexOf('=')
|
|
8376
|
+
const name = pair.slice(0, index).trim()
|
|
8377
|
+
const age = /;\s*max-age=(-?\d+)/i.exec(text)
|
|
8378
|
+
const expires = /;\s*expires=([^;]+)/i.exec(text)
|
|
8379
|
+
const gone = (age && Number(age[1]) <= 0) || (expires && Date.parse(expires[1]) <= Date.now())
|
|
8380
|
+
if (gone) cookies.delete(name)
|
|
8381
|
+
else cookies.set(name, pair.slice(index + 1).trim())
|
|
8382
|
+
realFetch(prefix + 'cookie', { method: 'POST', body: JSON.stringify({ url: pageUrl(), cookie: text }) })
|
|
8383
|
+
},
|
|
8384
|
+
}
|
|
8385
|
+
})
|
|
8386
|
+
|
|
8387
|
+
function storageArea(initial, save) {
|
|
8388
|
+
const data = new Map(Object.entries(initial))
|
|
8389
|
+
const area = {
|
|
8390
|
+
getItem(name) { return data.has(String(name)) ? data.get(String(name)) : null },
|
|
8391
|
+
setItem(name, value) { data.set(String(name), String(value)); save(data) },
|
|
8392
|
+
removeItem(name) { data.delete(String(name)); save(data) },
|
|
8393
|
+
clear() { data.clear(); save(data) },
|
|
8394
|
+
key(index) { return Array.from(data.keys())[index] ?? null },
|
|
8395
|
+
get length() { return data.size },
|
|
8396
|
+
}
|
|
8397
|
+
return new Proxy(area, {
|
|
8398
|
+
get(target, name) {
|
|
8399
|
+
if (name in target) return Reflect.get(target, name)
|
|
8400
|
+
return typeof name === 'string' ? data.get(name) : undefined
|
|
8401
|
+
},
|
|
8402
|
+
set(target, name, value) { target.setItem(name, value); return true },
|
|
8403
|
+
deleteProperty(target, name) { target.removeItem(name); return true },
|
|
8404
|
+
has(target, name) { return name in target || data.has(name) },
|
|
8405
|
+
ownKeys() { return Array.from(data.keys()) },
|
|
8406
|
+
getOwnPropertyDescriptor(target, name) {
|
|
8407
|
+
return data.has(name) ? { value: data.get(name), enumerable: true, configurable: true, writable: true } : undefined
|
|
8408
|
+
},
|
|
8409
|
+
})
|
|
8410
|
+
}
|
|
8411
|
+
let saveTimer = null
|
|
8412
|
+
let pending = null
|
|
8413
|
+
function sendStorage(keepalive) {
|
|
8414
|
+
if (pending === null) return
|
|
8415
|
+
const body = JSON.stringify({ origin: new URL(pageUrl()).origin, data: pending })
|
|
8416
|
+
pending = null
|
|
8417
|
+
realFetch(prefix + 'storage', { method: 'POST', body, keepalive: keepalive && body.length < 60000 })
|
|
8418
|
+
}
|
|
8419
|
+
const local = storageArea(config.storage, function (data) {
|
|
8420
|
+
pending = Object.fromEntries(data)
|
|
8421
|
+
clearTimeout(saveTimer)
|
|
8422
|
+
saveTimer = setTimeout(function () { sendStorage(false) }, 50)
|
|
8423
|
+
})
|
|
8424
|
+
const session = storageArea({}, function () {})
|
|
8425
|
+
Object.defineProperty(window, 'localStorage', { get() { return local }, configurable: true })
|
|
8426
|
+
Object.defineProperty(window, 'sessionStorage', { get() { return session }, configurable: true })
|
|
8427
|
+
addEventListener('pagehide', function () { sendStorage(true) })
|
|
8428
|
+
|
|
8429
|
+
function report(replaced) {
|
|
8430
|
+
postPanel({ event: 'location', url: pageUrl(), replaced: replaced === true })
|
|
8431
|
+
}
|
|
8432
|
+
for (const name of ['pushState', 'replaceState']) {
|
|
8433
|
+
const realHistory = History.prototype[name]
|
|
8434
|
+
History.prototype[name] = function (state, title, url) {
|
|
8435
|
+
const result = realHistory.call(this, state, title, url === undefined || url === null ? url : encode(url))
|
|
8436
|
+
report(name === 'replaceState')
|
|
8437
|
+
return result
|
|
8438
|
+
}
|
|
8439
|
+
}
|
|
8440
|
+
const realWindowOpen = window.open
|
|
8441
|
+
window.open = function (url, target, features) {
|
|
8442
|
+
if (url === undefined || url === null || url === '') return realWindowOpen.call(window, url, target, features)
|
|
8443
|
+
if (features) return realWindowOpen.call(window, encode(url), target, features)
|
|
8444
|
+
navigate(url)
|
|
8445
|
+
return window
|
|
8446
|
+
}
|
|
8447
|
+
if (panel === null) return
|
|
8448
|
+
report()
|
|
8449
|
+
addEventListener('popstate', report)
|
|
8450
|
+
addEventListener('hashchange', report)
|
|
8451
|
+
addEventListener('pageshow', report)
|
|
8452
|
+
addEventListener('pagehide', function () { postPanel({ event: 'loading' }) })
|
|
8453
|
+
addEventListener('click', function (event) {
|
|
8454
|
+
if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey) return
|
|
8455
|
+
const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null
|
|
8456
|
+
if (anchor === null || anchor.hasAttribute('download')) return
|
|
8457
|
+
const next = URL.parse(anchor.href)
|
|
8458
|
+
if (next === null || !/^https?:$/.test(next.protocol)) return
|
|
8459
|
+
if (next.host === new URL(pageUrl()).host) return
|
|
8460
|
+
event.preventDefault()
|
|
8461
|
+
navigate(next.href)
|
|
8462
|
+
})
|
|
8463
|
+
addEventListener('message', function (event) {
|
|
8464
|
+
if (event.source !== panel || !event.data || event.data.offsite !== 'browser') return
|
|
8465
|
+
event.stopImmediatePropagation()
|
|
8466
|
+
if (event.data.command === 'back') history.back()
|
|
8467
|
+
if (event.data.command === 'forward') history.forward()
|
|
8468
|
+
if (event.data.command === 'reload') realLocation.reload()
|
|
8469
|
+
})
|
|
8470
|
+
}`;
|
|
8471
|
+
// src/lib/proxy/proxy.ts
|
|
8472
|
+
const ROUTE = /^\/api\/proxy\/([^/]+)\/(.*)$/;
|
|
8473
|
+
const TARGET = /^(https?)\/([^/?#]+)(.*)$/;
|
|
8474
|
+
const SANDBOX = "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-pointer-lock";
|
|
8475
|
+
const UPSTREAM_TIMEOUT = 6e4;
|
|
8476
|
+
const MAX_BODY = 8388608;
|
|
8477
|
+
const MAX_PAGE = 33554432;
|
|
8478
|
+
const MAX_HEADERS = 262144;
|
|
8479
|
+
const REQUEST_HEADERS_DROPPED = /* @__PURE__ */ new Set([
|
|
8480
|
+
"accept-encoding",
|
|
8481
|
+
"cdn-loop",
|
|
8482
|
+
"connection",
|
|
8483
|
+
"cookie",
|
|
8484
|
+
"forwarded",
|
|
8485
|
+
"host",
|
|
8486
|
+
"keep-alive",
|
|
8487
|
+
"origin",
|
|
8488
|
+
"proxy-authorization",
|
|
8489
|
+
"proxy-connection",
|
|
8490
|
+
"referer",
|
|
8491
|
+
"sec-fetch-site",
|
|
8492
|
+
"sec-fetch-user",
|
|
8493
|
+
"te",
|
|
8494
|
+
"trailer",
|
|
8495
|
+
"transfer-encoding",
|
|
8496
|
+
"true-client-ip",
|
|
8497
|
+
"upgrade",
|
|
8498
|
+
"via",
|
|
8499
|
+
"x-forwarded-for",
|
|
8500
|
+
"x-forwarded-host",
|
|
8501
|
+
"x-forwarded-port",
|
|
8502
|
+
"x-forwarded-proto",
|
|
8503
|
+
"x-real-ip"
|
|
8504
|
+
]);
|
|
8505
|
+
const RESPONSE_HEADERS_DROPPED = /* @__PURE__ */ new Set([
|
|
8506
|
+
"alt-svc",
|
|
8507
|
+
"clear-site-data",
|
|
8508
|
+
"connection",
|
|
8509
|
+
"content-security-policy",
|
|
8510
|
+
"content-security-policy-report-only",
|
|
8511
|
+
"expect-ct",
|
|
8512
|
+
"feature-policy",
|
|
8513
|
+
"keep-alive",
|
|
8514
|
+
"link",
|
|
8515
|
+
"location",
|
|
8516
|
+
"nel",
|
|
8517
|
+
"origin-agent-cluster",
|
|
8518
|
+
"permissions-policy",
|
|
8519
|
+
"public-key-pins",
|
|
8520
|
+
"referrer-policy",
|
|
8521
|
+
"refresh",
|
|
8522
|
+
"report-to",
|
|
8523
|
+
"reporting-endpoints",
|
|
8524
|
+
"service-worker-allowed",
|
|
8525
|
+
"set-cookie",
|
|
8526
|
+
"strict-transport-security",
|
|
8527
|
+
"trailer",
|
|
8528
|
+
"transfer-encoding",
|
|
8529
|
+
"x-frame-options"
|
|
8530
|
+
]);
|
|
8531
|
+
const pageWrite = z.object({
|
|
8532
|
+
url: z.string(),
|
|
8533
|
+
cookie: z.string()
|
|
8534
|
+
});
|
|
8535
|
+
const storageWrite = z.object({
|
|
8536
|
+
origin: z.string(),
|
|
8537
|
+
data: z.record(z.string(), z.string())
|
|
8538
|
+
});
|
|
8539
|
+
function serveProxy(request, response) {
|
|
8540
|
+
answerProxy(request, response).catch((error) => {
|
|
8541
|
+
fail(response, 502, error instanceof Error ? error.message : String(error));
|
|
8542
|
+
});
|
|
7578
8543
|
}
|
|
7579
|
-
function
|
|
7580
|
-
|
|
8544
|
+
async function answerProxy(request, response) {
|
|
8545
|
+
const route = ROUTE.exec(request.url ?? "");
|
|
8546
|
+
const site = route === null ? null : findSite(route[1] ?? "");
|
|
8547
|
+
response.setHeader("access-control-allow-origin", request.headers.origin ?? "*");
|
|
8548
|
+
response.setHeader("access-control-allow-credentials", "true");
|
|
8549
|
+
response.setHeader("access-control-expose-headers", "*");
|
|
8550
|
+
if (route === null || site === null) {
|
|
8551
|
+
fail(response, 404, "This page expired. Press reload to open it again.");
|
|
8552
|
+
return;
|
|
8553
|
+
}
|
|
8554
|
+
response.setHeader("content-security-policy", pagePolicy(site, request));
|
|
8555
|
+
response.setHeader("referrer-policy", "unsafe-url");
|
|
8556
|
+
if (request.method === "OPTIONS") {
|
|
8557
|
+
response.setHeader("access-control-allow-methods", request.headers["access-control-request-method"] ?? "*");
|
|
8558
|
+
response.setHeader("access-control-allow-headers", request.headers["access-control-request-headers"] ?? "*");
|
|
8559
|
+
response.statusCode = 204;
|
|
8560
|
+
response.end();
|
|
8561
|
+
return;
|
|
8562
|
+
}
|
|
8563
|
+
const rest = route[2] ?? "";
|
|
8564
|
+
if (rest.startsWith("runtime.js")) {
|
|
8565
|
+
serveRuntime(site, rest, response);
|
|
8566
|
+
return;
|
|
8567
|
+
}
|
|
8568
|
+
if (rest === "cookie" || rest === "storage") {
|
|
8569
|
+
await savePageData(site, rest, request, response);
|
|
8570
|
+
return;
|
|
8571
|
+
}
|
|
8572
|
+
const target = readTarget(rest);
|
|
8573
|
+
if (target === null) {
|
|
8574
|
+
fail(response, 404, "not found");
|
|
8575
|
+
return;
|
|
8576
|
+
}
|
|
8577
|
+
await forward(site, target, request, response);
|
|
7581
8578
|
}
|
|
7582
|
-
function
|
|
7583
|
-
|
|
8579
|
+
function proxySocket(request, socket, head) {
|
|
8580
|
+
socket.on("error", () => socket.destroy());
|
|
8581
|
+
try {
|
|
8582
|
+
connectSocket(request, socket, head);
|
|
8583
|
+
} catch {
|
|
8584
|
+
socket.destroy();
|
|
8585
|
+
}
|
|
7584
8586
|
}
|
|
7585
|
-
function
|
|
7586
|
-
|
|
8587
|
+
function connectSocket(request, socket, head) {
|
|
8588
|
+
const route = ROUTE.exec(request.url ?? "");
|
|
8589
|
+
const site = route === null ? null : findSite(route[1] ?? "");
|
|
8590
|
+
const target = route === null ? null : readTarget(route[2] ?? "");
|
|
8591
|
+
if (site === null || target === null) {
|
|
8592
|
+
socket.destroy();
|
|
8593
|
+
return;
|
|
8594
|
+
}
|
|
8595
|
+
const headers = upstreamHeaders(site, target, request);
|
|
8596
|
+
headers.connection = "Upgrade";
|
|
8597
|
+
headers.upgrade = request.headers.upgrade;
|
|
8598
|
+
const upstream = openUpstream(site, target, request, headers);
|
|
8599
|
+
upstream.on("upgrade", (answer, remote, remoteHead) => {
|
|
8600
|
+
const lines = [`HTTP/1.1 101 ${answer.statusMessage}`];
|
|
8601
|
+
for (let index = 0; index < answer.rawHeaders.length; index += 2) lines.push(`${answer.rawHeaders[index]}: ${answer.rawHeaders[index + 1]}`);
|
|
8602
|
+
socket.write(`${lines.join("\r\n")}\r\n\r\n`);
|
|
8603
|
+
if (remoteHead.length > 0) socket.write(remoteHead);
|
|
8604
|
+
if (head.length > 0) remote.write(head);
|
|
8605
|
+
remote.on("error", () => socket.destroy());
|
|
8606
|
+
socket.on("close", () => remote.destroy());
|
|
8607
|
+
remote.pipe(socket).pipe(remote);
|
|
8608
|
+
});
|
|
8609
|
+
upstream.on("response", () => socket.destroy());
|
|
8610
|
+
upstream.on("error", () => socket.destroy());
|
|
8611
|
+
upstream.end();
|
|
8612
|
+
}
|
|
8613
|
+
async function forward(site, target, request, response) {
|
|
8614
|
+
const headers = upstreamHeaders(site, target, request);
|
|
8615
|
+
headers["accept-encoding"] = "gzip, deflate, br";
|
|
8616
|
+
const upstream = openUpstream(site, target, request, headers);
|
|
8617
|
+
response.on("close", () => upstream.destroy());
|
|
8618
|
+
const answer = once(upstream, "response");
|
|
8619
|
+
upstream.on("error", () => void 0);
|
|
8620
|
+
request.pipe(upstream);
|
|
8621
|
+
const incoming = (await answer)[0];
|
|
8622
|
+
upstream.setTimeout(0);
|
|
8623
|
+
const status = incoming.statusCode;
|
|
8624
|
+
if (status === void 0 || status < 200 || status > 999) throw new Error(`${target.origin} sent an invalid status`);
|
|
8625
|
+
for (const cookie of incoming.headers["set-cookie"] ?? []) await site.jar.setCookie(cookie, target.href, { ignoreError: true });
|
|
8626
|
+
const kind = rewriteKind(incoming, request);
|
|
8627
|
+
response.statusCode = status;
|
|
8628
|
+
copyHeaders(incoming, response, kind !== null);
|
|
8629
|
+
const location = incoming.headers.location;
|
|
8630
|
+
if (location !== void 0) {
|
|
8631
|
+
const next = URL.parse(location, target);
|
|
8632
|
+
if (next === null) throw new Error(`${target.origin} sent a bad redirect`);
|
|
8633
|
+
response.setHeader("location", proxiedPath(site.key, next));
|
|
8634
|
+
}
|
|
8635
|
+
if (kind === null) {
|
|
8636
|
+
response.flushHeaders();
|
|
8637
|
+
await pipeline(incoming, response);
|
|
8638
|
+
return;
|
|
8639
|
+
}
|
|
8640
|
+
const text = decodeBody(await readBody(incoming), incoming);
|
|
8641
|
+
response.setHeader("content-type", contentType(kind));
|
|
8642
|
+
response.end(await rewriteBody(site, target, kind, text));
|
|
8643
|
+
}
|
|
8644
|
+
function openUpstream(site, target, request, headers) {
|
|
8645
|
+
const secure = target.protocol === "https:";
|
|
8646
|
+
const port = Number(target.port) || (secure ? 443 : 80);
|
|
8647
|
+
const ownPort = request.socket.localPort ?? 0;
|
|
8648
|
+
const hostname = hostOf(target);
|
|
8649
|
+
if (target.host === request.headers.host) throw new Error("Pages cannot reach Offsite itself");
|
|
8650
|
+
if (isIP(hostname) !== 0) checkAddress(site, hostname, port, ownPort);
|
|
8651
|
+
const upstream = (secure ? https : http).request(target, {
|
|
8652
|
+
method: request.method,
|
|
8653
|
+
headers,
|
|
8654
|
+
lookup: guardedLookup(site, port, ownPort),
|
|
8655
|
+
maxHeaderSize: MAX_HEADERS,
|
|
8656
|
+
timeout: UPSTREAM_TIMEOUT,
|
|
8657
|
+
rejectUnauthorized: !site.local
|
|
8658
|
+
});
|
|
8659
|
+
upstream.on("timeout", () => upstream.destroy(/* @__PURE__ */ new Error(`${target.host} took too long to answer`)));
|
|
8660
|
+
return upstream;
|
|
8661
|
+
}
|
|
8662
|
+
function upstreamHeaders(site, target, request) {
|
|
8663
|
+
const headers = {};
|
|
8664
|
+
for (const name of Object.keys(request.headers)) if (!REQUEST_HEADERS_DROPPED.has(name) && !name.startsWith("cf-")) headers[name] = request.headers[name];
|
|
8665
|
+
headers.host = target.host;
|
|
8666
|
+
const cookie = site.jar.getCookieStringSync(target.href);
|
|
8667
|
+
if (cookie !== "") headers.cookie = cookie;
|
|
8668
|
+
const referer = refererOf(site, request);
|
|
8669
|
+
if (referer !== null) headers.referer = referer;
|
|
8670
|
+
else if (request.method !== "GET" && request.method !== "HEAD") headers.referer = `${target.origin}/`;
|
|
8671
|
+
if (request.headers.origin !== void 0) headers.origin = referer === null ? target.origin : new URL(referer).origin;
|
|
8672
|
+
return headers;
|
|
8673
|
+
}
|
|
8674
|
+
function refererOf(site, request) {
|
|
8675
|
+
const referer = request.headers.referer;
|
|
8676
|
+
const prefix = `/api/proxy/${site.key}/`;
|
|
8677
|
+
if (referer === void 0 || !referer.includes(prefix)) return null;
|
|
8678
|
+
return readTarget(referer.slice(referer.indexOf(prefix) + prefix.length))?.href ?? null;
|
|
8679
|
+
}
|
|
8680
|
+
function readTarget(rest) {
|
|
8681
|
+
const match = TARGET.exec(rest);
|
|
8682
|
+
if (match === null) return null;
|
|
8683
|
+
return URL.parse(`${match[1]}://${match[2]}${match[3]}`);
|
|
8684
|
+
}
|
|
8685
|
+
function pagePolicy(site, request) {
|
|
8686
|
+
const host = request.headers.host;
|
|
8687
|
+
if (host === void 0) throw new Error("the request has no host");
|
|
8688
|
+
const sources = [
|
|
8689
|
+
"http",
|
|
8690
|
+
"https",
|
|
8691
|
+
"ws",
|
|
8692
|
+
"wss"
|
|
8693
|
+
].map((scheme) => `${scheme}://${host}/api/proxy/${site.key}/`).join(" ");
|
|
8694
|
+
return `${SANDBOX}; default-src ${sources} 'unsafe-inline' 'unsafe-eval' data: blob:; form-action ${sources}; base-uri ${sources}`;
|
|
8695
|
+
}
|
|
8696
|
+
function rewriteKind(incoming, request) {
|
|
8697
|
+
if (request.headers["sec-fetch-dest"] === "empty") return null;
|
|
8698
|
+
const type = incoming.headers["content-type"]?.split(";")[0]?.trim() ?? "";
|
|
8699
|
+
if (type === "text/html" || type === "application/xhtml+xml") return "html";
|
|
8700
|
+
if (type === "text/css") return "css";
|
|
8701
|
+
if (/\b(java|ecma)script\b/.test(type)) return "script";
|
|
8702
|
+
return null;
|
|
8703
|
+
}
|
|
8704
|
+
function contentType(kind) {
|
|
8705
|
+
if (kind === "html") return "text/html; charset=utf-8";
|
|
8706
|
+
if (kind === "css") return "text/css; charset=utf-8";
|
|
8707
|
+
return "text/javascript; charset=utf-8";
|
|
8708
|
+
}
|
|
8709
|
+
async function rewriteBody(site, target, kind, text) {
|
|
8710
|
+
const rewrite = linkRewriter(site.key, target);
|
|
8711
|
+
if (kind === "css") return rewriteCss(text, rewrite);
|
|
8712
|
+
if (kind === "script") return rewriteScript(text, rewrite, `${site.key} ${target.origin}`);
|
|
8713
|
+
const script = pageScript({
|
|
8714
|
+
key: site.key,
|
|
8715
|
+
url: target.href,
|
|
8716
|
+
cookies: site.jar.getCookieStringSync(target.href, { http: false }),
|
|
8717
|
+
storage: site.storage.get(target.origin) ?? {},
|
|
8718
|
+
origin: null
|
|
8719
|
+
});
|
|
8720
|
+
return rewriteHtml(text, site.key, target, script);
|
|
8721
|
+
}
|
|
8722
|
+
function copyHeaders(incoming, response, rewritten) {
|
|
8723
|
+
for (const name of Object.keys(incoming.headers)) {
|
|
8724
|
+
const value = incoming.headers[name];
|
|
8725
|
+
if (value === void 0 || RESPONSE_HEADERS_DROPPED.has(name) || name.startsWith("access-control-") || name.startsWith("cross-origin-")) continue;
|
|
8726
|
+
if (rewritten && (name === "content-length" || name === "content-encoding" || name === "content-md5" || name === "etag")) continue;
|
|
8727
|
+
response.setHeader(name, value);
|
|
8728
|
+
}
|
|
8729
|
+
if (rewritten) {
|
|
8730
|
+
const cache = incoming.headers["cache-control"];
|
|
8731
|
+
response.setHeader("cache-control", cache === void 0 ? "no-transform" : `${cache}, no-transform`);
|
|
8732
|
+
}
|
|
8733
|
+
}
|
|
8734
|
+
async function readBody(incoming) {
|
|
8735
|
+
const encoding = incoming.headers["content-encoding"];
|
|
8736
|
+
if (encoding === void 0 || encoding === "identity") return readLimited(incoming, MAX_PAGE);
|
|
8737
|
+
const decoder = createDecoder(encoding);
|
|
8738
|
+
return (await Promise.all([readLimited(decoder, MAX_PAGE), pipeline(incoming, decoder)]))[0];
|
|
8739
|
+
}
|
|
8740
|
+
function createDecoder(encoding) {
|
|
8741
|
+
if (encoding === "gzip") return zlib.createGunzip();
|
|
8742
|
+
if (encoding === "deflate") return zlib.createInflate();
|
|
8743
|
+
if (encoding === "br") return zlib.createBrotliDecompress();
|
|
8744
|
+
throw new Error(`the site sent unknown compression: ${encoding}`);
|
|
8745
|
+
}
|
|
8746
|
+
async function readLimited(source, limit) {
|
|
8747
|
+
const chunks = [];
|
|
8748
|
+
let size = 0;
|
|
8749
|
+
for await (const chunk of source) {
|
|
8750
|
+
size += chunk.length;
|
|
8751
|
+
if (size > limit) throw new Error("This page is too large");
|
|
8752
|
+
chunks.push(chunk);
|
|
8753
|
+
}
|
|
8754
|
+
return Buffer.concat(chunks);
|
|
8755
|
+
}
|
|
8756
|
+
function decodeBody(body, incoming) {
|
|
8757
|
+
const declared = /charset=["']?([\w-]+)/i.exec(incoming.headers["content-type"] ?? "")?.[1] ?? /<meta[^>]+charset=["']?([\w-]+)/i.exec(body.subarray(0, 1024).toString("latin1"))?.[1] ?? "utf-8";
|
|
8758
|
+
try {
|
|
8759
|
+
return new TextDecoder(declared).decode(body);
|
|
8760
|
+
} catch {
|
|
8761
|
+
return new TextDecoder("utf-8").decode(body);
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
function serveRuntime(site, rest, response) {
|
|
8765
|
+
const query = new URL(rest, "http://localhost").searchParams;
|
|
8766
|
+
const url = query.get("url");
|
|
8767
|
+
const origin = query.get("origin");
|
|
8768
|
+
if (url === null || origin === null) {
|
|
8769
|
+
fail(response, 400, "the runtime needs a url and an origin");
|
|
8770
|
+
return;
|
|
8771
|
+
}
|
|
8772
|
+
response.setHeader("content-type", "text/javascript; charset=utf-8");
|
|
8773
|
+
response.end(pageScript({
|
|
8774
|
+
key: site.key,
|
|
8775
|
+
url,
|
|
8776
|
+
cookies: "",
|
|
8777
|
+
storage: {},
|
|
8778
|
+
origin
|
|
8779
|
+
}));
|
|
8780
|
+
}
|
|
8781
|
+
async function savePageData(site, kind, request, response) {
|
|
8782
|
+
if (request.method !== "POST") {
|
|
8783
|
+
fail(response, 405, "use POST");
|
|
8784
|
+
return;
|
|
8785
|
+
}
|
|
8786
|
+
const body = await readLimited(request, MAX_BODY);
|
|
8787
|
+
const data = JSON.parse(body.toString());
|
|
8788
|
+
if (kind === "cookie") {
|
|
8789
|
+
const write = pageWrite.parse(data);
|
|
8790
|
+
await site.jar.setCookie(write.cookie, write.url, {
|
|
8791
|
+
ignoreError: true,
|
|
8792
|
+
http: false
|
|
8793
|
+
});
|
|
8794
|
+
}
|
|
8795
|
+
if (kind === "storage") {
|
|
8796
|
+
const write = storageWrite.parse(data);
|
|
8797
|
+
site.storage.set(write.origin, write.data);
|
|
8798
|
+
}
|
|
8799
|
+
response.statusCode = 204;
|
|
8800
|
+
response.end();
|
|
8801
|
+
}
|
|
8802
|
+
function fail(response, status, message) {
|
|
8803
|
+
if (response.headersSent) {
|
|
8804
|
+
response.destroy();
|
|
8805
|
+
return;
|
|
8806
|
+
}
|
|
8807
|
+
response.statusCode = status;
|
|
8808
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
8809
|
+
response.end(message);
|
|
7587
8810
|
}
|
|
7588
8811
|
// src/main.ts
|
|
7589
8812
|
const UNPAIR_CHECK = 5e3;
|
|
7590
8813
|
const TICKET_LIFETIME = 6e4;
|
|
7591
|
-
const SNIFF_LENGTH = 8e3;
|
|
7592
8814
|
const openBody = z.object({ path: z.string() });
|
|
7593
8815
|
const contentTypes = {
|
|
7594
8816
|
".html": "text/html; charset=utf-8",
|
|
@@ -7651,7 +8873,25 @@ const publicDir = path.join(import.meta.dirname, "..", "..", "client", "dist");
|
|
|
7651
8873
|
const code = args.auth && args.pair && !hasDevices() ? addCode(null) : null;
|
|
7652
8874
|
const tickets = /* @__PURE__ */ new Map();
|
|
7653
8875
|
const server = createServer((request, response) => {
|
|
7654
|
-
|
|
8876
|
+
try {
|
|
8877
|
+
answerRequest(request, response);
|
|
8878
|
+
} catch (error) {
|
|
8879
|
+
console.warn(`a request failed: ${error}`);
|
|
8880
|
+
if (response.headersSent) {
|
|
8881
|
+
response.destroy();
|
|
8882
|
+
return;
|
|
8883
|
+
}
|
|
8884
|
+
response.statusCode = 500;
|
|
8885
|
+
response.end("request failed");
|
|
8886
|
+
}
|
|
8887
|
+
});
|
|
8888
|
+
function answerRequest(request, response) {
|
|
8889
|
+
const url = URL.parse(request.url ?? "/", "http://localhost");
|
|
8890
|
+
if (url === null) {
|
|
8891
|
+
response.statusCode = 400;
|
|
8892
|
+
response.end("bad url");
|
|
8893
|
+
return;
|
|
8894
|
+
}
|
|
7655
8895
|
if (url.pathname === "/api/media") {
|
|
7656
8896
|
serveMedia(url.searchParams.get("token"), request, response);
|
|
7657
8897
|
return;
|
|
@@ -7660,6 +8900,10 @@ const server = createServer((request, response) => {
|
|
|
7660
8900
|
serveBrowse(url.pathname, request, response);
|
|
7661
8901
|
return;
|
|
7662
8902
|
}
|
|
8903
|
+
if (url.pathname.startsWith("/api/proxy/")) {
|
|
8904
|
+
serveProxy(request, response);
|
|
8905
|
+
return;
|
|
8906
|
+
}
|
|
7663
8907
|
if (url.pathname === `/api/open/${openSecret}`) {
|
|
7664
8908
|
answerOpen(request, response);
|
|
7665
8909
|
return;
|
|
@@ -7674,21 +8918,30 @@ const server = createServer((request, response) => {
|
|
|
7674
8918
|
return;
|
|
7675
8919
|
}
|
|
7676
8920
|
serveFile(url.pathname, request, response);
|
|
7677
|
-
}
|
|
8921
|
+
}
|
|
7678
8922
|
const sockets = new WebSocketServer({
|
|
7679
8923
|
noServer: true,
|
|
7680
8924
|
perMessageDeflate: true
|
|
7681
8925
|
});
|
|
7682
8926
|
const paired = /* @__PURE__ */ new Map();
|
|
7683
8927
|
server.on("upgrade", (request, socket, head) => {
|
|
7684
|
-
const url =
|
|
8928
|
+
const url = URL.parse(request.url ?? "/", "http://localhost");
|
|
8929
|
+
if (url === null) {
|
|
8930
|
+
socket.destroy();
|
|
8931
|
+
return;
|
|
8932
|
+
}
|
|
8933
|
+
if (url.pathname.startsWith("/api/proxy/")) {
|
|
8934
|
+
proxySocket(request, socket, head);
|
|
8935
|
+
return;
|
|
8936
|
+
}
|
|
7685
8937
|
const device = args.auth ? redeemTicket(url.searchParams.get("ticket")) : null;
|
|
7686
|
-
|
|
8938
|
+
const remoteAddress = request.socket.remoteAddress;
|
|
8939
|
+
if (url.pathname !== "/ws" || remoteAddress === void 0 || args.auth && device === null) {
|
|
7687
8940
|
socket.destroy();
|
|
7688
8941
|
return;
|
|
7689
8942
|
}
|
|
7690
8943
|
sockets.handleUpgrade(request, socket, head, (client) => {
|
|
7691
|
-
connectClient(client);
|
|
8944
|
+
connectClient(client, device, remoteAddress);
|
|
7692
8945
|
if (device === null) return;
|
|
7693
8946
|
paired.set(client, device);
|
|
7694
8947
|
client.on("close", () => paired.delete(client));
|
|
@@ -7797,7 +9050,7 @@ function serveBrowse(pathname, request, response) {
|
|
|
7797
9050
|
}
|
|
7798
9051
|
const rest = pathname.slice(12);
|
|
7799
9052
|
const slash = rest.indexOf("/");
|
|
7800
|
-
if (slash === -1 || rest.slice(0, slash)
|
|
9053
|
+
if (slash === -1 || !canBrowse(rest.slice(0, slash), request.socket.remoteAddress)) {
|
|
7801
9054
|
response.statusCode = 404;
|
|
7802
9055
|
response.end("This browser link expired");
|
|
7803
9056
|
return;
|
|
@@ -7810,25 +9063,25 @@ function serveBrowse(pathname, request, response) {
|
|
|
7810
9063
|
response.end("bad path");
|
|
7811
9064
|
return;
|
|
7812
9065
|
}
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
9066
|
+
let stats;
|
|
9067
|
+
let contentType;
|
|
9068
|
+
try {
|
|
9069
|
+
stats = statSync(file, { throwIfNoEntry: false });
|
|
9070
|
+
if (stats === void 0 || !stats.isFile()) {
|
|
9071
|
+
response.statusCode = 404;
|
|
9072
|
+
response.end(`${file} is not a file`);
|
|
9073
|
+
return;
|
|
9074
|
+
}
|
|
9075
|
+
accessSync(file, constants.R_OK);
|
|
9076
|
+
contentType = browseTypes.get(path.extname(file).toLowerCase()) ?? sniffType(file);
|
|
9077
|
+
} catch (error) {
|
|
9078
|
+
response.statusCode = 500;
|
|
9079
|
+
response.end(`Could not read ${file}: ${error}`);
|
|
7817
9080
|
return;
|
|
7818
9081
|
}
|
|
7819
|
-
response
|
|
7820
|
-
response.setHeader("cache-control", "no-cache");
|
|
9082
|
+
setBrowseHeaders(response, contentType);
|
|
7821
9083
|
streamFile(file, stats.size, request, response);
|
|
7822
9084
|
}
|
|
7823
|
-
function browseType(file) {
|
|
7824
|
-
const known = browseTypes.get(path.extname(file).toLowerCase());
|
|
7825
|
-
if (known !== void 0) return known;
|
|
7826
|
-
const head = Buffer.alloc(SNIFF_LENGTH);
|
|
7827
|
-
const handle = openSync(file, "r");
|
|
7828
|
-
const read = readSync(handle, head, 0, SNIFF_LENGTH, 0);
|
|
7829
|
-
closeSync(handle);
|
|
7830
|
-
return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
|
|
7831
|
-
}
|
|
7832
9085
|
async function answerOpen(request, response) {
|
|
7833
9086
|
if (request.method !== "POST") {
|
|
7834
9087
|
response.statusCode = 405;
|
|
@@ -7857,43 +9110,6 @@ async function answerOpen(request, response) {
|
|
|
7857
9110
|
response.statusCode = 204;
|
|
7858
9111
|
response.end();
|
|
7859
9112
|
}
|
|
7860
|
-
function streamFile(file, size, request, response) {
|
|
7861
|
-
response.setHeader("accept-ranges", "bytes");
|
|
7862
|
-
if (request.headers.range === void 0) {
|
|
7863
|
-
response.setHeader("content-length", size);
|
|
7864
|
-
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
|
7865
|
-
return;
|
|
7866
|
-
}
|
|
7867
|
-
const range = byteRange(request.headers.range, size);
|
|
7868
|
-
if (range === null) {
|
|
7869
|
-
response.statusCode = 416;
|
|
7870
|
-
response.setHeader("content-range", `bytes */${size}`);
|
|
7871
|
-
response.end();
|
|
7872
|
-
return;
|
|
7873
|
-
}
|
|
7874
|
-
response.statusCode = 206;
|
|
7875
|
-
response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
|
|
7876
|
-
response.setHeader("content-length", range.end - range.start + 1);
|
|
7877
|
-
createReadStream(file, range).on("error", () => response.destroy()).pipe(response);
|
|
7878
|
-
}
|
|
7879
|
-
function byteRange(header, size) {
|
|
7880
|
-
const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);
|
|
7881
|
-
if (match === null) return null;
|
|
7882
|
-
if (match[3] !== void 0) {
|
|
7883
|
-
if (size === 0 || Number(match[3]) === 0) return null;
|
|
7884
|
-
return {
|
|
7885
|
-
start: Math.max(size - Number(match[3]), 0),
|
|
7886
|
-
end: size - 1
|
|
7887
|
-
};
|
|
7888
|
-
}
|
|
7889
|
-
const start = Number(match[1]);
|
|
7890
|
-
const end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1);
|
|
7891
|
-
if (start > end) return null;
|
|
7892
|
-
return {
|
|
7893
|
-
start,
|
|
7894
|
-
end
|
|
7895
|
-
};
|
|
7896
|
-
}
|
|
7897
9113
|
function answerJson(response, body) {
|
|
7898
9114
|
response.setHeader("content-type", "application/json");
|
|
7899
9115
|
response.end(JSON.stringify(body));
|