@linxin666/dsh-remote-web-ui 0.3.17 → 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/client.js +249 -90
- package/lib/client.js.map +1 -1
- package/lib/index.js +128 -30
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +313 -117
- package/lib/types/routes.d.ts +23 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +94 -29
- package/lib/types/tunnel.d.ts +8 -0
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +11 -1
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +31 -11
- package/package.json +1 -1
- package/src/client/mobile-adapt.ts +293 -112
- package/src/client/remote.module.css +10 -4
- package/src/routes.ts +92 -25
- package/src/tunnel.ts +12 -1
- package/src/update.ts +30 -11
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
|
-
|
|
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
|
-
})
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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)
|
|
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
|
-
/**
|
|
1057
|
-
|
|
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
|
-
|
|
1119
|
+
const combined = [
|
|
1062
1120
|
...bases,
|
|
1063
1121
|
...publicHost !== void 0 ? [publicHost] : [],
|
|
1064
1122
|
...extraHosts,
|
|
1065
1123
|
...dynamicTrustedHosts
|
|
1066
|
-
]
|
|
1067
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3594
|
+
const text = decodeProcessChunk(chunk);
|
|
3595
|
+
output += text;
|
|
3498
3596
|
if (output.length > OUTPUT_CAP) output = output.slice(output.length - OUTPUT_CAP);
|
|
3499
|
-
currentOutput +=
|
|
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 :
|
|
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+
|
|
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"}
|