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 { createProxyServer } from "http-proxy-3";
29
- import { OUTGOING_PASSES } from "http-proxy-3/dist/lib/http-proxy/passes/web-outgoing.js";
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 browseToken = randomBytes(32).toString("base64url");
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 addLink(file, mediaType) {
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": () => browseToken,
7436
- "browser.proxy": (params) => proxyPort(params.origin),
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/storage/auth.ts
7541
- const CODE_LIFETIME = 3e5;
7542
- const DEVICE_LIFETIME = 2592e6;
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
- function redeemCode(code) {
7554
- const found = db.delete(pairingCodes).where(eq(pairingCodes.hash, hash(code))).returning().get();
7555
- if (found === void 0 || found.expiresAt <= Date.now()) return null;
7556
- const token = randomBytes(32).toString("base64url");
7557
- const now = Date.now();
7558
- db.insert(devices).values({
7559
- hash: hash(token),
7560
- name: found.name,
7561
- createdAt: now,
7562
- expiresAt: now + DEVICE_LIFETIME
7563
- }).run();
7564
- return token;
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 findDevice(token) {
7567
- const found = db.select().from(devices).where(and(eq(devices.hash, hash(token)), gt(devices.expiresAt, Date.now()))).get();
7568
- return found === void 0 ? null : found.hash;
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 isPaired(device) {
7571
- return db.select().from(devices).where(and(eq(devices.hash, device), gt(devices.expiresAt, Date.now()))).get() !== void 0;
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 hasDevices() {
7574
- return db.select().from(devices).where(gt(devices.expiresAt, Date.now())).limit(1).get() !== void 0;
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 readDevices() {
7577
- return db.select().from(devices).orderBy(devices.createdAt).all();
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 removeDevices(hashes) {
7580
- db.delete(devices).where(inArray(devices.hash, hashes)).run();
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 recordUse(device) {
7583
- db.update(devices).set({ lastUsedAt: Date.now() }).where(eq(devices.hash, device)).run();
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 hash(secret) {
7586
- return createHash("sha256").update(secret).digest("hex");
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
- const url = new URL(request.url ?? "/", "http://localhost");
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 = new URL(request.url ?? "/", "http://localhost");
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
- if (url.pathname !== "/ws" || args.auth && device === null) {
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) !== browseToken) {
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
- const stats = statSync(file, { throwIfNoEntry: false });
7814
- if (stats === void 0 || !stats.isFile()) {
7815
- response.statusCode = 404;
7816
- response.end(`${file} is not a file`);
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.setHeader("content-type", browseType(file));
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));