@linxin666/dsh-remote-web-ui 0.3.18 → 0.3.19

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/lib/index.js CHANGED
@@ -702,6 +702,27 @@ function writeJson(res, status, body, headers = {}) {
702
702
  * browser markers are same-origin.
703
703
  */
704
704
  function isTrustedApiRequest(request, trustedHosts) {
705
+ if (!isTrustedHost(request, trustedHosts)) return false;
706
+ if (request.headers["sec-fetch-site"] === "cross-site") return false;
707
+ const origin = request.headers.origin;
708
+ if (origin === void 0) return true;
709
+ try {
710
+ return new URL(origin).host === new URL(`http://${request.headers.host ?? ""}`).host;
711
+ } catch {
712
+ return false;
713
+ }
714
+ }
715
+ /**
716
+ * Whether the request's Host is an authority this surface serves: loopback, or
717
+ * a trusted entry (exact `host:port`, or port-less `host` matching any port).
718
+ * The browser-marker checks stay in {@link isTrustedApiRequest}: a pairing
719
+ * entry page is a navigation, and its authority is the token or device
720
+ * credential it carries, not who linked to it.
721
+ * @param request - the node HTTP request.
722
+ * @param trustedHosts - non-loopback authorities this surface serves.
723
+ * @returns true when the Host is ours.
724
+ */
725
+ function isTrustedHost(request, trustedHosts) {
705
726
  const host = request.headers.host;
706
727
  if (typeof host !== "string") return false;
707
728
  let hostUrl;
@@ -711,18 +732,24 @@ function isTrustedApiRequest(request, trustedHosts) {
711
732
  return false;
712
733
  }
713
734
  const hostname = hostUrl.hostname;
714
- if (!(isLoopbackClient(request) || trustedHosts.some((entry) => {
735
+ return isLoopbackClient(request) || trustedHosts.some((entry) => {
715
736
  const entryUrl = new URL(`http://${entry}`);
716
737
  return entryUrl.port === "" ? entryUrl.hostname === hostname : entryUrl.host === hostUrl.host;
717
- }))) return false;
718
- if (request.headers["sec-fetch-site"] === "cross-site") return false;
719
- const origin = request.headers.origin;
720
- if (origin === void 0) return true;
721
- try {
722
- return new URL(origin).host === hostUrl.host;
723
- } catch {
724
- return false;
725
- }
738
+ });
739
+ }
740
+ /**
741
+ * Whether the request is a top-level document navigation — the shape every
742
+ * phone uses to open a pairing link. Browsers label it `Sec-Fetch-Mode:
743
+ * navigate` with `Sec-Fetch-Dest: document`; in-app browsers (WeChat, and
744
+ * anything else wrapping a WKWebView) additionally attach `Sec-Fetch-Site:
745
+ * cross-site` or an opaque `Origin: null`, which the API fence must keep
746
+ * refusing but which says nothing about a document navigation: the pairing
747
+ * token or the device id in the URL is what authorizes it.
748
+ * @param request - the node HTTP request.
749
+ * @returns true for a top-level document navigation.
750
+ */
751
+ function isTopLevelDocumentNavigation(request) {
752
+ return request.headers["sec-fetch-mode"] === "navigate" && request.headers["sec-fetch-dest"] === "document";
726
753
  }
727
754
  /**
728
755
  * Test whether a hostname represents a private local-area network (RFC 1918 / ULA / mDNS / loopback).
@@ -772,7 +799,7 @@ function isNonCrossSite(request) {
772
799
  * the lanFence fallback use this to serve container-bridged and reverse-proxy
773
800
  * topologies whose Host is not among the advertised or configured authorities.
774
801
  */
775
- function privateLanHostOf(request) {
802
+ function privateLanHostOf(request, navigation = false) {
776
803
  const host = request.headers.host;
777
804
  if (typeof host !== "string") return void 0;
778
805
  let hostName = "";
@@ -781,7 +808,8 @@ function privateLanHostOf(request) {
781
808
  } catch {
782
809
  return;
783
810
  }
784
- if (hostName === "" || !isPrivateOrLocalHostname(hostName) || !isNonCrossSite(request)) return void 0;
811
+ if (hostName === "" || !isPrivateOrLocalHostname(hostName)) return void 0;
812
+ if (!(navigation && isTopLevelDocumentNavigation(request)) && !isNonCrossSite(request)) return void 0;
785
813
  return host;
786
814
  }
787
815
  /** Cap on pairing request bodies (tokens and workspace ids are tiny). */
@@ -800,6 +828,31 @@ function acceptLimitKey(socketIp, forwarded, bucket) {
800
828
  return `${bucket}|${socketIp}`;
801
829
  }
802
830
  /**
831
+ * Reasons a phone-facing entry page refused a request, logged once per shape
832
+ * so a real device failure is diagnosable from the host console without a
833
+ * debug build. Keyed by `reason|host|path`; bounded by construction (a handful
834
+ * of reasons, and only the first occurrence of each is printed).
835
+ */
836
+ const loggedEntryRefusals = /* @__PURE__ */ new Set();
837
+ /**
838
+ * Log one entry-page refusal (first occurrence per shape) and cap the set.
839
+ * @param request - the refused request.
840
+ * @param path - the entry path that refused it.
841
+ * @param reason - why the fence refused.
842
+ */
843
+ function logEntryRefusal(request, path, reason) {
844
+ const key = `${reason}|${request.headers.host ?? ""}|${path}`;
845
+ if (loggedEntryRefusals.has(key) || loggedEntryRefusals.size >= 64) return;
846
+ loggedEntryRefusals.add(key);
847
+ const markers = [
848
+ `sec-fetch-site=${request.headers["sec-fetch-site"] ?? "-"}`,
849
+ `sec-fetch-mode=${request.headers["sec-fetch-mode"] ?? "-"}`,
850
+ `sec-fetch-dest=${request.headers["sec-fetch-dest"] ?? "-"}`,
851
+ `origin=${request.headers.origin ?? "-"}`
852
+ ].join(" ");
853
+ console.log(`remote-web-ui: refused ${path} from host ${request.headers.host ?? "-"} (${reason}; ${markers})`);
854
+ }
855
+ /**
803
856
  * FIFO-bounded Set insert: past `max` entries the oldest one is evicted
804
857
  * (a Set iterates in insertion order). The dynamic trusted-host table is fed
805
858
  * by caller-controlled Host headers, so it must stay bounded; an evicted
@@ -1053,18 +1106,24 @@ function makeRoutes(deps) {
1053
1106
  const dynamicTrustedHosts = /* @__PURE__ */ new Set();
1054
1107
  /** Loopback-only fence: the desktop panel's control endpoints. */
1055
1108
  const loopbackFence = (req) => isTrustedApiRequest(req, []);
1056
- /** Phone-facing fence: loopback, the service's live LAN literals, configured public host, extra trusted hosts, or dynamically paired hosts. */
1057
- const lanFence = (req) => {
1109
+ /**
1110
+ * Phone-facing fence: loopback, the service's live LAN literals, configured
1111
+ * public host, extra trusted hosts, or dynamically paired hosts. `navigation`
1112
+ * relaxes the browser-marker checks for a top-level document navigation (the
1113
+ * pairing entry pages), never for an API or subresource request.
1114
+ */
1115
+ const lanFence = (req, navigation = false) => {
1058
1116
  const publicHost = publicHostOf(service.publicBaseUrl);
1059
1117
  const bases = service.lanAddresses;
1060
1118
  const extraHosts = typeof deps.trustedHosts === "function" ? deps.trustedHosts() : deps.trustedHosts ?? [];
1061
- if (isTrustedApiRequest(req, [
1119
+ const combined = [
1062
1120
  ...bases,
1063
1121
  ...publicHost !== void 0 ? [publicHost] : [],
1064
1122
  ...extraHosts,
1065
1123
  ...dynamicTrustedHosts
1066
- ])) return true;
1067
- const privateLanHost = privateLanHostOf(req);
1124
+ ];
1125
+ if (navigation && isTopLevelDocumentNavigation(req) ? isTrustedHost(req, combined) : isTrustedApiRequest(req, combined)) return true;
1126
+ const privateLanHost = privateLanHostOf(req, navigation);
1068
1127
  if (privateLanHost !== void 0) {
1069
1128
  const cookieDeviceId = readCookie(req.headers.cookie, service.config.cookieName);
1070
1129
  let queryDeviceId = null;
@@ -1349,8 +1408,9 @@ function makeRoutes(deps) {
1349
1408
  */
1350
1409
  const handleAcceptPage = async (req, res) => {
1351
1410
  if (!requireMethod(req, res, "GET")) return;
1352
- const privateLanHost = privateLanHostOf(req);
1353
- if (!lanFence(req) && privateLanHost === void 0) {
1411
+ const privateLanHost = privateLanHostOf(req, true);
1412
+ if (!lanFence(req, true) && privateLanHost === void 0) {
1413
+ logEntryRefusal(req, PAIR_PATHS.acceptPage, "untrusted-host");
1354
1414
  res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
1355
1415
  res.end("forbidden");
1356
1416
  return;
@@ -1367,7 +1427,8 @@ function makeRoutes(deps) {
1367
1427
  code: "invalid"
1368
1428
  } : service.accept(token, typeof ua === "string" ? ua : void 0);
1369
1429
  if (!result.ok) {
1370
- if (!lanFence(req)) {
1430
+ if (!lanFence(req, true)) {
1431
+ logEntryRefusal(req, PAIR_PATHS.acceptPage, "untrusted-host-after-invalid-token");
1371
1432
  res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
1372
1433
  res.end("forbidden");
1373
1434
  return;
@@ -1407,7 +1468,8 @@ function makeRoutes(deps) {
1407
1468
  */
1408
1469
  const handleAppPage = async (req, res) => {
1409
1470
  if (!requireMethod(req, res, "GET")) return;
1410
- if (!lanFence(req)) {
1471
+ if (!lanFence(req, true)) {
1472
+ logEntryRefusal(req, PAIR_PATHS.appPage, "untrusted-host");
1411
1473
  res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
1412
1474
  res.end("forbidden");
1413
1475
  return;
@@ -1455,7 +1517,7 @@ function makeRoutes(deps) {
1455
1517
  */
1456
1518
  const handleAppServiceWorker = (req, res) => {
1457
1519
  if (!requireMethod(req, res, "GET")) return;
1458
- if (!lanFence(req)) {
1520
+ if (!lanFence(req, true)) {
1459
1521
  res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
1460
1522
  res.end("forbidden");
1461
1523
  return;
@@ -2885,10 +2947,28 @@ function quickTunnelFlags(originHostHeader) {
2885
2947
  "--http-host-header": originHostHeader
2886
2948
  };
2887
2949
  }
2950
+ /**
2951
+ * Command-line arguments for a named tunnel with a persistent Cloudflare token.
2952
+ * Note: `--no-autoupdate` and `--protocol` are flags for the `tunnel` command
2953
+ * and must appear BEFORE the `run` subcommand. Putting them after `run` causes
2954
+ * cloudflared to exit immediately with "flag provided but not defined: -no-autoupdate"
2955
+ * (issues #1432, #1433).
2956
+ */
2957
+ function namedTunnelArgs(token) {
2958
+ return [
2959
+ "tunnel",
2960
+ "--no-autoupdate",
2961
+ "--protocol",
2962
+ "http2",
2963
+ "run",
2964
+ "--token",
2965
+ token
2966
+ ];
2967
+ }
2888
2968
  /** Default factory: the cloudflared package's quick and named tunnels. */
2889
2969
  function defaultFactory(target) {
2890
2970
  if (target.kind === "quick") return Tunnel.quick(target.targetUrl, quickTunnelFlags(target.originHostHeader));
2891
- return namedTunnelHandle(Tunnel.withToken(target.token, { ...SHARED_TUNNEL_FLAGS }), target.publicUrl);
2971
+ return namedTunnelHandle(new Tunnel(namedTunnelArgs(target.token)), target.publicUrl);
2892
2972
  }
2893
2973
  /** Node timers. */
2894
2974
  const nodeTimer = {
@@ -3424,10 +3504,27 @@ async function checkUpdates(deps) {
3424
3504
  const OUTPUT_CAP = 16 * 1024;
3425
3505
  /**
3426
3506
  * Windows cmd command-not-found stderr. With shell:true a missing shim
3427
- * exits with code 1 (cmd cannot report ENOENT), so the fallback chain
3428
- * detects this message instead of the spawn error event.
3507
+ * exits with code 1 or 9009 (cmd cannot report ENOENT), so the fallback chain
3508
+ * detects this message instead of the spawn error event. Supports both English
3509
+ * and localized (e.g. Chinese) cmd error messages.
3429
3510
  */
3430
- const WIN_CMD_MISSING_RE = /not recognized as an internal or external command/i;
3511
+ const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|不是内部或外部命令/i;
3512
+ /**
3513
+ * Decode process output with tolerant decoding:
3514
+ * Defaults to UTF-8; if invalid byte sequences are encountered (common on
3515
+ * Windows consoles using code page 936/GBK), falls back to GBK via TextDecoder.
3516
+ */
3517
+ function decodeProcessChunk(chunk) {
3518
+ try {
3519
+ return new TextDecoder("utf-8", { fatal: true }).decode(chunk);
3520
+ } catch {
3521
+ try {
3522
+ return new TextDecoder("gbk").decode(chunk);
3523
+ } catch {
3524
+ return chunk.toString("utf8");
3525
+ }
3526
+ }
3527
+ }
3431
3528
  /**
3432
3529
  * Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
3433
3530
  * silently skips same-day releases — `pnpm update --latest` then exits 0
@@ -3494,9 +3591,10 @@ function runUpdate(deps) {
3494
3591
  let output = "";
3495
3592
  let currentOutput = "";
3496
3593
  const append = (chunk) => {
3497
- output += chunk.toString("utf8");
3594
+ const text = decodeProcessChunk(chunk);
3595
+ output += text;
3498
3596
  if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP);
3499
- currentOutput += chunk.toString("utf8");
3597
+ currentOutput += text;
3500
3598
  if (currentOutput.length > OUTPUT_CAP) currentOutput = currentOutput.slice(currentOutput.length - OUTPUT_CAP);
3501
3599
  };
3502
3600
  let currentChild;
@@ -3566,7 +3664,7 @@ function runUpdate(deps) {
3566
3664
  });
3567
3665
  child.on("close", (code) => {
3568
3666
  if (settled || finished) return;
3569
- if (platform === "win32" && code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput)) {
3667
+ if (platform === "win32" && (code === 9009 || code !== 0 && WIN_CMD_MISSING_RE.test(currentOutput))) {
3570
3668
  runCandidate(index + 1);
3571
3669
  return;
3572
3670
  }
@@ -3575,7 +3673,7 @@ function runUpdate(deps) {
3575
3673
  ok: code === 0,
3576
3674
  exitCode: code,
3577
3675
  output,
3578
- error: code === 0 ? void 0 : "pnpm exited with code " + String(code),
3676
+ error: code === 0 ? void 0 : `${candidate.command} exited with code ` + String(code),
3579
3677
  ...code === 0 ? {} : { errorCode: "pnpm-failed" }
3580
3678
  });
3581
3679
  });
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AA+ND;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAqqBvC"}
1
+ {"version":3,"file":"mobile-adapt.d.ts","sourceRoot":"","sources":["../../../src/client/mobile-adapt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,yDAAyD;IACzD,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IAClC,yDAAyD;IACzD,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAA;IACjC;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,KAAK,MAAM,CAAC,GAAG,IAAI,CAAA;IAC9C,wEAAwE;IACxE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,4GAA4G;IAC5G,iBAAiB,IAAI,IAAI,CAAA;CAC1B;AA+PD;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CA0zBvC"}