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.
@@ -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 browseToken = randomBytes(32).toString("base64url");
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 addLink(file, mediaType) {
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": () => browseToken,
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 = incoming.statusCode ?? 502;
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 buffer(incoming);
8620
- if (encoding === "gzip") return buffer(incoming.pipe(zlib.createGunzip()));
8621
- if (encoding === "deflate") return buffer(incoming.pipe(zlib.createInflate()));
8622
- if (encoding === "br") return buffer(incoming.pipe(zlib.createBrotliDecompress()));
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 buffer(request);
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
- 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
+ }
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 = 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
+ }
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
- 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) {
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) !== browseToken) {
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
- const stats = statSync(file, { throwIfNoEntry: false });
8965
- if (stats === void 0 || !stats.isFile()) {
8966
- response.statusCode = 404;
8967
- 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}`);
8968
9080
  return;
8969
9081
  }
8970
- setBrowseHeaders(response, browseTypes.get(path.extname(file).toLowerCase()) ?? sniffType(file));
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));