offsite 0.0.62 → 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.
- package/apps/server/dist/main.mjs +193 -118
- package/apps/server/dist/main.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import zlib, { createGzip } from "node:zlib";
|
|
@@ -31,7 +31,6 @@ import { CookieJar, getPublicSuffix } from "tough-cookie";
|
|
|
31
31
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
32
32
|
import { once } from "node:events";
|
|
33
33
|
import https from "node:https";
|
|
34
|
-
import { buffer } from "node:stream/consumers";
|
|
35
34
|
import { pipeline } from "node:stream/promises";
|
|
36
35
|
import { defaultTreeAdapter, html, parse, serialize } from "parse5";
|
|
37
36
|
import { MagicString } from "magic-string";
|
|
@@ -2777,14 +2776,63 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
|
2777
2776
|
machine: text().notNull(),
|
|
2778
2777
|
server: text().notNull()
|
|
2779
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
|
+
}
|
|
2780
2827
|
// src/services/files.ts
|
|
2781
2828
|
const FILE_LIMIT = 2e4;
|
|
2782
2829
|
const SNIFF_LENGTH = 8e3;
|
|
2783
2830
|
const OUTPUT_LIMIT$1 = 262144;
|
|
2784
2831
|
const READ_LIMIT$1 = 1048576;
|
|
2785
2832
|
const MEDIA_LINK_LIFETIME = 36e5;
|
|
2786
|
-
const
|
|
2833
|
+
const BROWSE_LINK_LIFETIME = 36e5;
|
|
2787
2834
|
const mediaLinks = /* @__PURE__ */ new Map();
|
|
2835
|
+
const browseLinks = /* @__PURE__ */ new Map();
|
|
2788
2836
|
const skipped = /* @__PURE__ */ new Set([
|
|
2789
2837
|
"node_modules",
|
|
2790
2838
|
".git",
|
|
@@ -2878,18 +2926,33 @@ function renameProjectEntry(cwd, file, to) {
|
|
|
2878
2926
|
function deleteProjectEntry(cwd, file) {
|
|
2879
2927
|
rmSync(resolveProjectEntry(cwd, file), { recursive: true });
|
|
2880
2928
|
}
|
|
2881
|
-
function linkProjectMedia(cwd, file) {
|
|
2929
|
+
function linkProjectMedia(cwd, file, device) {
|
|
2882
2930
|
const resolved = path.resolve(realpathSync(cwd), file);
|
|
2883
2931
|
if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
|
|
2884
2932
|
const full = realpathSync(resolved);
|
|
2885
2933
|
const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
|
|
2886
2934
|
if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
|
|
2887
|
-
return addLink(full, mediaType);
|
|
2935
|
+
return addLink(full, mediaType, device);
|
|
2888
2936
|
}
|
|
2889
|
-
function linkProjectDownload(cwd, file) {
|
|
2937
|
+
function linkProjectDownload(cwd, file, device) {
|
|
2890
2938
|
const full = resolveProjectFile(cwd, file);
|
|
2891
2939
|
if (!statSync(full).isFile()) throw new Error(`${file} is not a file`);
|
|
2892
|
-
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);
|
|
2893
2956
|
}
|
|
2894
2957
|
function setBrowseHeaders(response, contentType) {
|
|
2895
2958
|
response.setHeader("content-type", contentType);
|
|
@@ -2903,12 +2966,50 @@ function sniffType(file) {
|
|
|
2903
2966
|
closeSync(handle);
|
|
2904
2967
|
return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
|
|
2905
2968
|
}
|
|
2906
|
-
function
|
|
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) {
|
|
2907
3007
|
for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
|
|
2908
3008
|
const token = randomBytes(32).toString("base64url");
|
|
2909
3009
|
mediaLinks.set(token, {
|
|
2910
3010
|
file,
|
|
2911
3011
|
mediaType,
|
|
3012
|
+
device,
|
|
2912
3013
|
expiresAt: Date.now() + MEDIA_LINK_LIFETIME
|
|
2913
3014
|
});
|
|
2914
3015
|
return token;
|
|
@@ -2917,6 +3018,7 @@ function findMediaLink(token) {
|
|
|
2917
3018
|
if (token === null) return null;
|
|
2918
3019
|
const link = mediaLinks.get(token);
|
|
2919
3020
|
if (link === void 0 || link.expiresAt <= Date.now()) return null;
|
|
3021
|
+
if (link.device !== null && !isPaired(link.device)) return null;
|
|
2920
3022
|
return link;
|
|
2921
3023
|
}
|
|
2922
3024
|
function saveUpload(name, data) {
|
|
@@ -6898,6 +7000,7 @@ const active = /* @__PURE__ */ new Map();
|
|
|
6898
7000
|
const terminals = /* @__PURE__ */ new Map();
|
|
6899
7001
|
const connections = /* @__PURE__ */ new Map();
|
|
6900
7002
|
const paused = /* @__PURE__ */ new Set();
|
|
7003
|
+
const held = /* @__PURE__ */ new Set();
|
|
6901
7004
|
let nextTab = 0;
|
|
6902
7005
|
let draining = null;
|
|
6903
7006
|
async function terminalRequest(client, message) {
|
|
@@ -6957,6 +7060,8 @@ function attach(client) {
|
|
|
6957
7060
|
instance
|
|
6958
7061
|
});
|
|
6959
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();
|
|
6960
7065
|
}
|
|
6961
7066
|
function resize(connection, message) {
|
|
6962
7067
|
const terminal = terminals.get(message.id);
|
|
@@ -7066,9 +7171,14 @@ async function createTab(projectId) {
|
|
|
7066
7171
|
});
|
|
7067
7172
|
active.set(projectId ?? "global", id);
|
|
7068
7173
|
pty.onData((data) => output(id, terminal, Buffer.from(data)));
|
|
7174
|
+
if (connections.size === 0) {
|
|
7175
|
+
pty.pause();
|
|
7176
|
+
held.add(pty);
|
|
7177
|
+
}
|
|
7069
7178
|
pty.onExit(() => {
|
|
7070
7179
|
terminals.delete(id);
|
|
7071
7180
|
paused.delete(pty);
|
|
7181
|
+
held.delete(pty);
|
|
7072
7182
|
if (closeTab(id)) publish();
|
|
7073
7183
|
});
|
|
7074
7184
|
}
|
|
@@ -7424,10 +7534,10 @@ const handlers = {
|
|
|
7424
7534
|
deleteProjectEntry(projectPath(params.projectId), params.path);
|
|
7425
7535
|
return null;
|
|
7426
7536
|
},
|
|
7427
|
-
"files.media": (params) => linkProjectMedia(projectPath(params.projectId), params.path),
|
|
7428
|
-
"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),
|
|
7429
7539
|
"files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
|
|
7430
|
-
"files.browse": () =>
|
|
7540
|
+
"files.browse": (_params, client) => linkBrowse(client.device, client.address),
|
|
7431
7541
|
"browser.open": (params) => openSite(params.url, params.fromPage),
|
|
7432
7542
|
"tasks.output": (params) => taskOutput(params.sessionId, params.path),
|
|
7433
7543
|
"git.status": (params) => gitRepos(projectPath(params.projectId)),
|
|
@@ -7484,9 +7594,11 @@ const handlers = {
|
|
|
7484
7594
|
"terminal.busy": (params) => busyTerminal(params.id),
|
|
7485
7595
|
"terminal.paste": (params) => savePastedImage(params.type, Buffer.from(params.data, "base64"))
|
|
7486
7596
|
};
|
|
7487
|
-
function connectClient(socket) {
|
|
7597
|
+
function connectClient(socket, device, address) {
|
|
7488
7598
|
const client = {
|
|
7489
7599
|
socket,
|
|
7600
|
+
device,
|
|
7601
|
+
address,
|
|
7490
7602
|
heardAt: Date.now(),
|
|
7491
7603
|
opening: null,
|
|
7492
7604
|
sessionId: null,
|
|
@@ -8362,6 +8474,7 @@ const TARGET = /^(https?)\/([^/?#]+)(.*)$/;
|
|
|
8362
8474
|
const SANDBOX = "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-pointer-lock";
|
|
8363
8475
|
const UPSTREAM_TIMEOUT = 6e4;
|
|
8364
8476
|
const MAX_BODY = 8388608;
|
|
8477
|
+
const MAX_PAGE = 33554432;
|
|
8365
8478
|
const MAX_HEADERS = 262144;
|
|
8366
8479
|
const REQUEST_HEADERS_DROPPED = /* @__PURE__ */ new Set([
|
|
8367
8480
|
"accept-encoding",
|
|
@@ -8411,6 +8524,7 @@ const RESPONSE_HEADERS_DROPPED = /* @__PURE__ */ new Set([
|
|
|
8411
8524
|
"service-worker-allowed",
|
|
8412
8525
|
"set-cookie",
|
|
8413
8526
|
"strict-transport-security",
|
|
8527
|
+
"trailer",
|
|
8414
8528
|
"transfer-encoding",
|
|
8415
8529
|
"x-frame-options"
|
|
8416
8530
|
]);
|
|
@@ -8506,9 +8620,11 @@ async function forward(site, target, request, response) {
|
|
|
8506
8620
|
request.pipe(upstream);
|
|
8507
8621
|
const incoming = (await answer)[0];
|
|
8508
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`);
|
|
8509
8625
|
for (const cookie of incoming.headers["set-cookie"] ?? []) await site.jar.setCookie(cookie, target.href, { ignoreError: true });
|
|
8510
8626
|
const kind = rewriteKind(incoming, request);
|
|
8511
|
-
response.statusCode =
|
|
8627
|
+
response.statusCode = status;
|
|
8512
8628
|
copyHeaders(incoming, response, kind !== null);
|
|
8513
8629
|
const location = incoming.headers.location;
|
|
8514
8630
|
if (location !== void 0) {
|
|
@@ -8517,6 +8633,7 @@ async function forward(site, target, request, response) {
|
|
|
8517
8633
|
response.setHeader("location", proxiedPath(site.key, next));
|
|
8518
8634
|
}
|
|
8519
8635
|
if (kind === null) {
|
|
8636
|
+
response.flushHeaders();
|
|
8520
8637
|
await pipeline(incoming, response);
|
|
8521
8638
|
return;
|
|
8522
8639
|
}
|
|
@@ -8616,12 +8733,26 @@ function copyHeaders(incoming, response, rewritten) {
|
|
|
8616
8733
|
}
|
|
8617
8734
|
async function readBody(incoming) {
|
|
8618
8735
|
const encoding = incoming.headers["content-encoding"];
|
|
8619
|
-
if (encoding === void 0 || encoding === "identity") return
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
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();
|
|
8623
8744
|
throw new Error(`the site sent unknown compression: ${encoding}`);
|
|
8624
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
|
+
}
|
|
8625
8756
|
function decodeBody(body, incoming) {
|
|
8626
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";
|
|
8627
8758
|
try {
|
|
@@ -8652,11 +8783,7 @@ async function savePageData(site, kind, request, response) {
|
|
|
8652
8783
|
fail(response, 405, "use POST");
|
|
8653
8784
|
return;
|
|
8654
8785
|
}
|
|
8655
|
-
const body = await
|
|
8656
|
-
if (body.length > MAX_BODY) {
|
|
8657
|
-
fail(response, 413, "too much page data");
|
|
8658
|
-
return;
|
|
8659
|
-
}
|
|
8786
|
+
const body = await readLimited(request, MAX_BODY);
|
|
8660
8787
|
const data = JSON.parse(body.toString());
|
|
8661
8788
|
if (kind === "cookie") {
|
|
8662
8789
|
const write = pageWrite.parse(data);
|
|
@@ -8681,54 +8808,6 @@ function fail(response, status, message) {
|
|
|
8681
8808
|
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
8682
8809
|
response.end(message);
|
|
8683
8810
|
}
|
|
8684
|
-
// src/storage/auth.ts
|
|
8685
|
-
const CODE_LIFETIME = 3e5;
|
|
8686
|
-
const DEVICE_LIFETIME = 2592e6;
|
|
8687
|
-
function addCode(name) {
|
|
8688
|
-
const code = randomBytes(16).toString("base64url");
|
|
8689
|
-
db.delete(pairingCodes).where(lte(pairingCodes.expiresAt, Date.now())).run();
|
|
8690
|
-
db.insert(pairingCodes).values({
|
|
8691
|
-
hash: hash(code),
|
|
8692
|
-
name,
|
|
8693
|
-
expiresAt: Date.now() + CODE_LIFETIME
|
|
8694
|
-
}).run();
|
|
8695
|
-
return code;
|
|
8696
|
-
}
|
|
8697
|
-
function redeemCode(code) {
|
|
8698
|
-
const found = db.delete(pairingCodes).where(eq(pairingCodes.hash, hash(code))).returning().get();
|
|
8699
|
-
if (found === void 0 || found.expiresAt <= Date.now()) return null;
|
|
8700
|
-
const token = randomBytes(32).toString("base64url");
|
|
8701
|
-
const now = Date.now();
|
|
8702
|
-
db.insert(devices).values({
|
|
8703
|
-
hash: hash(token),
|
|
8704
|
-
name: found.name,
|
|
8705
|
-
createdAt: now,
|
|
8706
|
-
expiresAt: now + DEVICE_LIFETIME
|
|
8707
|
-
}).run();
|
|
8708
|
-
return token;
|
|
8709
|
-
}
|
|
8710
|
-
function findDevice(token) {
|
|
8711
|
-
const found = db.select().from(devices).where(and(eq(devices.hash, hash(token)), gt(devices.expiresAt, Date.now()))).get();
|
|
8712
|
-
return found === void 0 ? null : found.hash;
|
|
8713
|
-
}
|
|
8714
|
-
function isPaired(device) {
|
|
8715
|
-
return db.select().from(devices).where(and(eq(devices.hash, device), gt(devices.expiresAt, Date.now()))).get() !== void 0;
|
|
8716
|
-
}
|
|
8717
|
-
function hasDevices() {
|
|
8718
|
-
return db.select().from(devices).where(gt(devices.expiresAt, Date.now())).limit(1).get() !== void 0;
|
|
8719
|
-
}
|
|
8720
|
-
function readDevices() {
|
|
8721
|
-
return db.select().from(devices).orderBy(devices.createdAt).all();
|
|
8722
|
-
}
|
|
8723
|
-
function removeDevices(hashes) {
|
|
8724
|
-
db.delete(devices).where(inArray(devices.hash, hashes)).run();
|
|
8725
|
-
}
|
|
8726
|
-
function recordUse(device) {
|
|
8727
|
-
db.update(devices).set({ lastUsedAt: Date.now() }).where(eq(devices.hash, device)).run();
|
|
8728
|
-
}
|
|
8729
|
-
function hash(secret) {
|
|
8730
|
-
return createHash("sha256").update(secret).digest("hex");
|
|
8731
|
-
}
|
|
8732
8811
|
// src/main.ts
|
|
8733
8812
|
const UNPAIR_CHECK = 5e3;
|
|
8734
8813
|
const TICKET_LIFETIME = 6e4;
|
|
@@ -8794,7 +8873,25 @@ const publicDir = path.join(import.meta.dirname, "..", "..", "client", "dist");
|
|
|
8794
8873
|
const code = args.auth && args.pair && !hasDevices() ? addCode(null) : null;
|
|
8795
8874
|
const tickets = /* @__PURE__ */ new Map();
|
|
8796
8875
|
const server = createServer((request, response) => {
|
|
8797
|
-
|
|
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
|
+
}
|
|
8798
8895
|
if (url.pathname === "/api/media") {
|
|
8799
8896
|
serveMedia(url.searchParams.get("token"), request, response);
|
|
8800
8897
|
return;
|
|
@@ -8821,25 +8918,30 @@ const server = createServer((request, response) => {
|
|
|
8821
8918
|
return;
|
|
8822
8919
|
}
|
|
8823
8920
|
serveFile(url.pathname, request, response);
|
|
8824
|
-
}
|
|
8921
|
+
}
|
|
8825
8922
|
const sockets = new WebSocketServer({
|
|
8826
8923
|
noServer: true,
|
|
8827
8924
|
perMessageDeflate: true
|
|
8828
8925
|
});
|
|
8829
8926
|
const paired = /* @__PURE__ */ new Map();
|
|
8830
8927
|
server.on("upgrade", (request, socket, head) => {
|
|
8831
|
-
const url =
|
|
8928
|
+
const url = URL.parse(request.url ?? "/", "http://localhost");
|
|
8929
|
+
if (url === null) {
|
|
8930
|
+
socket.destroy();
|
|
8931
|
+
return;
|
|
8932
|
+
}
|
|
8832
8933
|
if (url.pathname.startsWith("/api/proxy/")) {
|
|
8833
8934
|
proxySocket(request, socket, head);
|
|
8834
8935
|
return;
|
|
8835
8936
|
}
|
|
8836
8937
|
const device = args.auth ? redeemTicket(url.searchParams.get("ticket")) : null;
|
|
8837
|
-
|
|
8938
|
+
const remoteAddress = request.socket.remoteAddress;
|
|
8939
|
+
if (url.pathname !== "/ws" || remoteAddress === void 0 || args.auth && device === null) {
|
|
8838
8940
|
socket.destroy();
|
|
8839
8941
|
return;
|
|
8840
8942
|
}
|
|
8841
8943
|
sockets.handleUpgrade(request, socket, head, (client) => {
|
|
8842
|
-
connectClient(client);
|
|
8944
|
+
connectClient(client, device, remoteAddress);
|
|
8843
8945
|
if (device === null) return;
|
|
8844
8946
|
paired.set(client, device);
|
|
8845
8947
|
client.on("close", () => paired.delete(client));
|
|
@@ -8948,7 +9050,7 @@ function serveBrowse(pathname, request, response) {
|
|
|
8948
9050
|
}
|
|
8949
9051
|
const rest = pathname.slice(12);
|
|
8950
9052
|
const slash = rest.indexOf("/");
|
|
8951
|
-
if (slash === -1 || rest.slice(0, slash)
|
|
9053
|
+
if (slash === -1 || !canBrowse(rest.slice(0, slash), request.socket.remoteAddress)) {
|
|
8952
9054
|
response.statusCode = 404;
|
|
8953
9055
|
response.end("This browser link expired");
|
|
8954
9056
|
return;
|
|
@@ -8961,13 +9063,23 @@ function serveBrowse(pathname, request, response) {
|
|
|
8961
9063
|
response.end("bad path");
|
|
8962
9064
|
return;
|
|
8963
9065
|
}
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
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}`);
|
|
8968
9080
|
return;
|
|
8969
9081
|
}
|
|
8970
|
-
setBrowseHeaders(response,
|
|
9082
|
+
setBrowseHeaders(response, contentType);
|
|
8971
9083
|
streamFile(file, stats.size, request, response);
|
|
8972
9084
|
}
|
|
8973
9085
|
async function answerOpen(request, response) {
|
|
@@ -8998,43 +9110,6 @@ async function answerOpen(request, response) {
|
|
|
8998
9110
|
response.statusCode = 204;
|
|
8999
9111
|
response.end();
|
|
9000
9112
|
}
|
|
9001
|
-
function streamFile(file, size, request, response) {
|
|
9002
|
-
response.setHeader("accept-ranges", "bytes");
|
|
9003
|
-
if (request.headers.range === void 0) {
|
|
9004
|
-
response.setHeader("content-length", size);
|
|
9005
|
-
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
|
9006
|
-
return;
|
|
9007
|
-
}
|
|
9008
|
-
const range = byteRange(request.headers.range, size);
|
|
9009
|
-
if (range === null) {
|
|
9010
|
-
response.statusCode = 416;
|
|
9011
|
-
response.setHeader("content-range", `bytes */${size}`);
|
|
9012
|
-
response.end();
|
|
9013
|
-
return;
|
|
9014
|
-
}
|
|
9015
|
-
response.statusCode = 206;
|
|
9016
|
-
response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
|
|
9017
|
-
response.setHeader("content-length", range.end - range.start + 1);
|
|
9018
|
-
createReadStream(file, range).on("error", () => response.destroy()).pipe(response);
|
|
9019
|
-
}
|
|
9020
|
-
function byteRange(header, size) {
|
|
9021
|
-
const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);
|
|
9022
|
-
if (match === null) return null;
|
|
9023
|
-
if (match[3] !== void 0) {
|
|
9024
|
-
if (size === 0 || Number(match[3]) === 0) return null;
|
|
9025
|
-
return {
|
|
9026
|
-
start: Math.max(size - Number(match[3]), 0),
|
|
9027
|
-
end: size - 1
|
|
9028
|
-
};
|
|
9029
|
-
}
|
|
9030
|
-
const start = Number(match[1]);
|
|
9031
|
-
const end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1);
|
|
9032
|
-
if (start > end) return null;
|
|
9033
|
-
return {
|
|
9034
|
-
start,
|
|
9035
|
-
end
|
|
9036
|
-
};
|
|
9037
|
-
}
|
|
9038
9113
|
function answerJson(response, body) {
|
|
9039
9114
|
response.setHeader("content-type", "application/json");
|
|
9040
9115
|
response.end(JSON.stringify(body));
|