@yezack/dsh-remote-mobile 1.9.2 → 1.10.0
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/CHANGELOG.md +64 -2
- package/README.md +109 -34
- package/README_EN.md +115 -40
- package/lib/auth/crypto.js +3 -1
- package/lib/auth/gate.d.ts +39 -9
- package/lib/auth/gate.js +231 -33
- package/lib/auth/tailscale.d.ts +24 -2
- package/lib/auth/tailscale.js +38 -3
- package/lib/auth/token.d.ts +22 -0
- package/lib/auth/token.js +58 -2
- package/lib/bridge/compat.d.ts +22 -2
- package/lib/bridge/compat.js +579 -336
- package/lib/client.js +67 -14
- package/lib/gateway/detector.d.ts +42 -0
- package/lib/gateway/detector.js +88 -0
- package/lib/gateway/secret.d.ts +23 -0
- package/lib/gateway/secret.js +65 -0
- package/lib/gateway/server.d.ts +108 -0
- package/lib/gateway/server.js +568 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +143 -15
- package/lib/routes/api.js +16 -10
- package/lib/routes/login-page.js +399 -398
- package/lib/styles/style-snippets.js +639 -639
- package/package.json +3 -3
package/lib/client.js
CHANGED
|
@@ -1065,7 +1065,7 @@ var import_jsx_runtime = require("react/jsx-runtime");
|
|
|
1065
1065
|
function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
|
|
1066
1066
|
const tsHost = status.tailscaleIp || "100.x.y.z";
|
|
1067
1067
|
const lanHost = status.lanIp || "127.0.0.1";
|
|
1068
|
-
const port = typeof window !== "undefined" && window.location.port || "3080";
|
|
1068
|
+
const port = status && status.gateway && status.gateway.activePort || typeof window !== "undefined" && window.location.port || "3080";
|
|
1069
1069
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1070
1070
|
"div",
|
|
1071
1071
|
{
|
|
@@ -1099,6 +1099,29 @@ function NetworkCard({ status, toggleTailscale, toggleLan, copyText, lang }) {
|
|
|
1099
1099
|
children: t("netCardDesc", lang)
|
|
1100
1100
|
}
|
|
1101
1101
|
),
|
|
1102
|
+
status?.gateway?.mode === "gateway" && status.gateway.running === false ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1103
|
+
"div",
|
|
1104
|
+
{
|
|
1105
|
+
style: {
|
|
1106
|
+
background: "rgba(239, 68, 68, 0.12)",
|
|
1107
|
+
border: "1px solid rgba(239, 68, 68, 0.35)",
|
|
1108
|
+
borderRadius: "8px",
|
|
1109
|
+
padding: "10px 12px",
|
|
1110
|
+
marginBottom: "16px",
|
|
1111
|
+
fontSize: "12px",
|
|
1112
|
+
color: "var(--dsw-alias-label-error, #ef4444)",
|
|
1113
|
+
lineHeight: "1.5",
|
|
1114
|
+
textAlign: "left"
|
|
1115
|
+
},
|
|
1116
|
+
children: [
|
|
1117
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
1118
|
+
"\u26A0\uFE0F ",
|
|
1119
|
+
lang === "en" ? "Gateway Service Not Ready" : "\u5B89\u5168\u53CD\u5411\u4EE3\u7406\u7F51\u5173\u672A\u5C31\u7EEA"
|
|
1120
|
+
] }),
|
|
1121
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: status.gateway.error ? `${lang === "en" ? "Failure reason: " : "\u542F\u52A8\u5931\u8D25\uFF1A"}${status.gateway.error}` : lang === "en" ? "Gateway is initializing or failed to bind port. External devices cannot connect currently." : "\u7F51\u5173\u6B63\u5728\u542F\u52A8\u6216\u7AEF\u53E3\u7ED1\u5B9A\u5931\u8D25\uFF0C\u5916\u90E8\u624B\u673A\u8BBE\u5907\u5F53\u524D\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5\u3002" })
|
|
1122
|
+
]
|
|
1123
|
+
}
|
|
1124
|
+
) : null,
|
|
1102
1125
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1103
1126
|
"div",
|
|
1104
1127
|
{
|
|
@@ -1568,7 +1591,7 @@ function QrPairingCard({
|
|
|
1568
1591
|
copyDirectLink,
|
|
1569
1592
|
lang
|
|
1570
1593
|
}) {
|
|
1571
|
-
const port = typeof window !== "undefined" && window.location.port || "3080";
|
|
1594
|
+
const port = status && status.gateway && status.gateway.activePort || typeof window !== "undefined" && window.location.port || "3080";
|
|
1572
1595
|
const isExpired = timeLeft <= 0;
|
|
1573
1596
|
const currentHost = selectedTab === "lan" && status.lanIp ? status.lanIp : status.tailscaleIp || status.lanIp || typeof window !== "undefined" && window.location.hostname || "127.0.0.1";
|
|
1574
1597
|
const directLink = `http://${currentHost}:${port}/auth?token=${status.code || ""}`;
|
|
@@ -1606,7 +1629,31 @@ function QrPairingCard({
|
|
|
1606
1629
|
children: t("pairingCardDesc", lang)
|
|
1607
1630
|
}
|
|
1608
1631
|
),
|
|
1609
|
-
|
|
1632
|
+
status?.gateway?.mode === "gateway" && status.gateway.running === false ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1633
|
+
"div",
|
|
1634
|
+
{
|
|
1635
|
+
style: {
|
|
1636
|
+
background: "rgba(239, 68, 68, 0.12)",
|
|
1637
|
+
border: "1px solid rgba(239, 68, 68, 0.35)",
|
|
1638
|
+
borderRadius: "8px",
|
|
1639
|
+
padding: "12px 14px",
|
|
1640
|
+
marginBottom: "14px",
|
|
1641
|
+
fontSize: "13px",
|
|
1642
|
+
color: "var(--dsw-alias-label-error, #ef4444)",
|
|
1643
|
+
lineHeight: "1.6",
|
|
1644
|
+
textAlign: "left"
|
|
1645
|
+
},
|
|
1646
|
+
children: [
|
|
1647
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
|
|
1648
|
+
"\u26A0\uFE0F ",
|
|
1649
|
+
lang === "en" ? "Standalone Gateway Error" : "\u5B89\u5168\u53CD\u5411\u4EE3\u7406\u7F51\u5173\u672A\u5C31\u7EEA"
|
|
1650
|
+
] }),
|
|
1651
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginTop: "4px", fontSize: "12px" }, children: status.gateway.error ? `${lang === "en" ? "Failure reason: " : "\u542F\u52A8\u5931\u8D25\uFF1A"}${status.gateway.error}` : lang === "en" ? "Gateway is initializing or port binding failed. External devices cannot connect currently." : "\u7F51\u5173\u6B63\u5728\u542F\u52A8\u6216\u7AEF\u53E3\u7ED1\u5B9A\u5931\u8D25\uFF0C\u5916\u90E8\u624B\u673A\u8BBE\u5907\u6682\u65F6\u65E0\u6CD5\u626B\u7801\u8FDE\u63A5\u3002" }),
|
|
1652
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginTop: "4px", fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #888)" }, children: lang === "en" ? "Tip: Check if port 3081 is in use or inspect background logs." : "\u6392\u67E5\u5EFA\u8BAE\uFF1A\u8BF7\u68C0\u67E5 3081 \u7AEF\u53E3\u662F\u5426\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528\uFF0C\u6216\u68C0\u67E5\u540E\u53F0\u7EC8\u7AEF\u65E5\u5FD7\u3002" })
|
|
1653
|
+
]
|
|
1654
|
+
}
|
|
1655
|
+
) : null,
|
|
1656
|
+
showQr && !(status?.gateway?.mode === "gateway" && status.gateway.running === false) ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1610
1657
|
"div",
|
|
1611
1658
|
{
|
|
1612
1659
|
style: {
|
|
@@ -1767,28 +1814,30 @@ function QrPairingCard({
|
|
|
1767
1814
|
fontSize: "32px",
|
|
1768
1815
|
fontWeight: "800",
|
|
1769
1816
|
letterSpacing: showCode ? "6px" : "4px",
|
|
1770
|
-
color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-brand-primary, #3b82f6)",
|
|
1817
|
+
color: status?.gateway?.mode === "gateway" && status.gateway.running === false || isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-brand-primary, #3b82f6)",
|
|
1771
1818
|
fontFamily: "monospace",
|
|
1772
1819
|
userSelect: "all"
|
|
1773
1820
|
},
|
|
1774
|
-
children: isExpired ? lang === "en" ? "EXPIRED" : "\u5DF2\u8FC7\u671F" : showCode ? status.code : "\u2022\u2022\u2022\u2022\u2022\u2022"
|
|
1821
|
+
children: status?.gateway?.mode === "gateway" && status.gateway.running === false ? lang === "en" ? "DISABLED" : "\u5DF2\u505C\u7528" : isExpired ? lang === "en" ? "EXPIRED" : "\u5DF2\u8FC7\u671F" : showCode ? status.code : "\u2022\u2022\u2022\u2022\u2022\u2022"
|
|
1775
1822
|
}
|
|
1776
1823
|
),
|
|
1777
1824
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1778
1825
|
"button",
|
|
1779
1826
|
{
|
|
1780
1827
|
type: "button",
|
|
1828
|
+
disabled: status?.gateway?.mode === "gateway" && status.gateway.running === false,
|
|
1781
1829
|
onClick: () => setShowCode(!showCode),
|
|
1782
1830
|
title: showCode ? t("clickToHide", lang) : t("clickToReveal", lang),
|
|
1783
1831
|
style: {
|
|
1784
1832
|
background: "transparent",
|
|
1785
1833
|
border: "none",
|
|
1786
1834
|
color: "var(--dsw-alias-label-secondary, #888)",
|
|
1787
|
-
cursor: "pointer",
|
|
1835
|
+
cursor: status?.gateway?.mode === "gateway" && status.gateway.running === false ? "not-allowed" : "pointer",
|
|
1788
1836
|
display: "flex",
|
|
1789
1837
|
alignItems: "center",
|
|
1790
1838
|
justifyContent: "center",
|
|
1791
|
-
padding: "4px"
|
|
1839
|
+
padding: "4px",
|
|
1840
|
+
opacity: status?.gateway?.mode === "gateway" && status.gateway.running === false ? 0.4 : 1
|
|
1792
1841
|
},
|
|
1793
1842
|
children: showCode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { style: { width: "20px", height: "20px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { style: { width: "20px", height: "20px", fill: "currentColor" }, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" }) })
|
|
1794
1843
|
}
|
|
@@ -1801,7 +1850,7 @@ function QrPairingCard({
|
|
|
1801
1850
|
{
|
|
1802
1851
|
style: {
|
|
1803
1852
|
fontSize: "12px",
|
|
1804
|
-
color: isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-label-secondary, #888)",
|
|
1853
|
+
color: status?.gateway?.mode === "gateway" && status.gateway.running === false || isExpired ? "var(--dsw-alias-label-error, #ef4444)" : "var(--dsw-alias-label-secondary, #888)",
|
|
1805
1854
|
margin: "4px 0 16px 0",
|
|
1806
1855
|
display: "flex",
|
|
1807
1856
|
alignItems: "center",
|
|
@@ -1817,11 +1866,11 @@ function QrPairingCard({
|
|
|
1817
1866
|
width: "8px",
|
|
1818
1867
|
height: "8px",
|
|
1819
1868
|
borderRadius: "50%",
|
|
1820
|
-
background: isExpired ? "#ef4444" : "#10b981"
|
|
1869
|
+
background: status?.gateway?.mode === "gateway" && status.gateway.running === false || isExpired ? "#ef4444" : "#10b981"
|
|
1821
1870
|
}
|
|
1822
1871
|
}
|
|
1823
1872
|
),
|
|
1824
|
-
isExpired ? `\u26A0\uFE0F ${t("codeExpired", lang)}` : `${t("codeValidUntil", lang)}${formatCountdown(timeLeft, lang)}`
|
|
1873
|
+
status?.gateway?.mode === "gateway" && status.gateway.running === false ? `\u26A0\uFE0F ${lang === "en" ? "Gateway Service Error" : "\u7F51\u5173\u670D\u52A1\u5F02\u5E38"}` : isExpired ? `\u26A0\uFE0F ${t("codeExpired", lang)}` : `${t("codeValidUntil", lang)}${formatCountdown(timeLeft, lang)}`
|
|
1825
1874
|
]
|
|
1826
1875
|
}
|
|
1827
1876
|
),
|
|
@@ -1839,6 +1888,7 @@ function QrPairingCard({
|
|
|
1839
1888
|
"button",
|
|
1840
1889
|
{
|
|
1841
1890
|
type: "button",
|
|
1891
|
+
disabled: status?.gateway?.mode === "gateway" && status.gateway.running === false,
|
|
1842
1892
|
onClick: () => setShowQr(!showQr),
|
|
1843
1893
|
style: {
|
|
1844
1894
|
padding: "7px 16px",
|
|
@@ -1848,7 +1898,8 @@ function QrPairingCard({
|
|
|
1848
1898
|
borderRadius: "8px",
|
|
1849
1899
|
fontSize: "13px",
|
|
1850
1900
|
fontWeight: "600",
|
|
1851
|
-
cursor: "pointer",
|
|
1901
|
+
cursor: status?.gateway?.mode === "gateway" && status.gateway.running === false ? "not-allowed" : "pointer",
|
|
1902
|
+
opacity: status?.gateway?.mode === "gateway" && status.gateway.running === false ? 0.4 : 1,
|
|
1852
1903
|
whiteSpace: "nowrap",
|
|
1853
1904
|
flexShrink: 0
|
|
1854
1905
|
},
|
|
@@ -1859,6 +1910,7 @@ function QrPairingCard({
|
|
|
1859
1910
|
"button",
|
|
1860
1911
|
{
|
|
1861
1912
|
type: "button",
|
|
1913
|
+
disabled: status?.gateway?.mode === "gateway" && status.gateway.running === false,
|
|
1862
1914
|
onClick: refreshStatusAndCode,
|
|
1863
1915
|
style: {
|
|
1864
1916
|
padding: "7px 16px",
|
|
@@ -1868,7 +1920,8 @@ function QrPairingCard({
|
|
|
1868
1920
|
borderRadius: "8px",
|
|
1869
1921
|
fontSize: "13px",
|
|
1870
1922
|
fontWeight: "600",
|
|
1871
|
-
cursor: "pointer",
|
|
1923
|
+
cursor: status?.gateway?.mode === "gateway" && status.gateway.running === false ? "not-allowed" : "pointer",
|
|
1924
|
+
opacity: status?.gateway?.mode === "gateway" && status.gateway.running === false ? 0.4 : 1,
|
|
1872
1925
|
whiteSpace: "nowrap",
|
|
1873
1926
|
flexShrink: 0
|
|
1874
1927
|
},
|
|
@@ -3538,8 +3591,8 @@ function StylesCard({ lang, showToast }) {
|
|
|
3538
3591
|
}
|
|
3539
3592
|
|
|
3540
3593
|
// src/client/version.js
|
|
3541
|
-
var PLUGIN_VERSION = "v1.
|
|
3542
|
-
var BUILD_TIME = "2026-10-
|
|
3594
|
+
var PLUGIN_VERSION = "v1.10.0";
|
|
3595
|
+
var BUILD_TIME = "2026-10-22 13:21:34";
|
|
3543
3596
|
|
|
3544
3597
|
// src/client/components/MainSection.jsx
|
|
3545
3598
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 底座运行模式探测器
|
|
3
|
+
*
|
|
4
|
+
* 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
|
|
5
|
+
* 1. 若宿主仅监听本地回环地址 (127.0.0.1 / localhost),则外部网络无法直连,
|
|
6
|
+
* 自适应判定为网关模式 ('gateway'),由插件启动独立安全反代网关 (0.0.0.0:3081);
|
|
7
|
+
* 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
|
|
8
|
+
* 判定为直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
|
|
9
|
+
* 3. 支持用户通过 options.mode ('auto' | 'gateway' | 'direct') 强制覆盖。
|
|
10
|
+
*/
|
|
11
|
+
export type GatewayMode = 'gateway' | 'direct';
|
|
12
|
+
export interface ServerModeDetectionResult {
|
|
13
|
+
/** 最终生效的运行模式 */
|
|
14
|
+
mode: GatewayMode;
|
|
15
|
+
/** 是否为自动探测得出 */
|
|
16
|
+
isAuto: boolean;
|
|
17
|
+
/** 底座核心监听的 host(未配置时默认 127.0.0.1,在 gateway 模式下已安全规范化为回环 target) */
|
|
18
|
+
coreHost: string;
|
|
19
|
+
/** 底座核心监听的端口(默认 3080) */
|
|
20
|
+
corePort: number;
|
|
21
|
+
/** 底座原始配置是否判定为纯回环监听 */
|
|
22
|
+
isLoopback: boolean;
|
|
23
|
+
/** 原始底座 host */
|
|
24
|
+
rawCoreHost: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 判断指定 host 是否属于本地回环地址(支持 localhost, 127.*, ::1, 0:0:0:0:0:0:0:1 等完整形态)
|
|
28
|
+
*/
|
|
29
|
+
export declare function isLoopbackHost(host?: string | null): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* 规范化网关内部转发的目标核心 Host(仅在 gateway 模式下作为 proxy target 使用)
|
|
32
|
+
* 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
|
|
33
|
+
*/
|
|
34
|
+
export declare function normalizeGatewayTargetHost(host: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* 探测底座运行姿态并裁决插件的工作模式
|
|
37
|
+
*
|
|
38
|
+
* @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
|
|
39
|
+
* @param configuredMode 用户显式配置的运行模式 ('auto' | 'gateway' | 'direct')
|
|
40
|
+
* @param defaultCorePort 底座默认核心端口(3080)
|
|
41
|
+
*/
|
|
42
|
+
export declare function detectServerMode(ctx: any, configuredMode?: 'auto' | 'gateway' | 'direct', defaultCorePort?: number): ServerModeDetectionResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 底座运行模式探测器
|
|
3
|
+
*
|
|
4
|
+
* 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
|
|
5
|
+
* 1. 若宿主仅监听本地回环地址 (127.0.0.1 / localhost),则外部网络无法直连,
|
|
6
|
+
* 自适应判定为网关模式 ('gateway'),由插件启动独立安全反代网关 (0.0.0.0:3081);
|
|
7
|
+
* 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
|
|
8
|
+
* 判定为直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
|
|
9
|
+
* 3. 支持用户通过 options.mode ('auto' | 'gateway' | 'direct') 强制覆盖。
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 判断指定 host 是否属于本地回环地址(支持 localhost, 127.*, ::1, 0:0:0:0:0:0:0:1 等完整形态)
|
|
13
|
+
*/
|
|
14
|
+
export function isLoopbackHost(host) {
|
|
15
|
+
if (!host || typeof host !== 'string')
|
|
16
|
+
return true; // 默认回环
|
|
17
|
+
let trimmed = host.trim().toLowerCase();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return true;
|
|
20
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
21
|
+
trimmed = trimmed.slice(1, -1);
|
|
22
|
+
}
|
|
23
|
+
if (trimmed === 'localhost' || trimmed === '::1' || trimmed === '0:0:0:0:0:0:0:1')
|
|
24
|
+
return true;
|
|
25
|
+
if (trimmed === '127.0.0.1' || trimmed.startsWith('127.'))
|
|
26
|
+
return true;
|
|
27
|
+
if (trimmed === '::ffff:127.0.0.1' || trimmed.startsWith('::ffff:127.'))
|
|
28
|
+
return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 规范化网关内部转发的目标核心 Host(仅在 gateway 模式下作为 proxy target 使用)
|
|
33
|
+
* 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeGatewayTargetHost(host) {
|
|
36
|
+
const trimmed = host.trim().toLowerCase();
|
|
37
|
+
if (trimmed === '0.0.0.0' || trimmed === 'localhost')
|
|
38
|
+
return '127.0.0.1';
|
|
39
|
+
if (trimmed === '::' || trimmed === '[::]')
|
|
40
|
+
return '::1';
|
|
41
|
+
// Node http.request 的 hostname 不接受 IPv6 方括号;去掉后由 coreTargetAuthority 重新加回
|
|
42
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
43
|
+
return host.trim().slice(1, -1);
|
|
44
|
+
}
|
|
45
|
+
return host.trim();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 探测底座运行姿态并裁决插件的工作模式
|
|
49
|
+
*
|
|
50
|
+
* @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
|
|
51
|
+
* @param configuredMode 用户显式配置的运行模式 ('auto' | 'gateway' | 'direct')
|
|
52
|
+
* @param defaultCorePort 底座默认核心端口(3080)
|
|
53
|
+
*/
|
|
54
|
+
export function detectServerMode(ctx, configuredMode = 'auto', defaultCorePort = 3080) {
|
|
55
|
+
const webServer = (typeof ctx?.get === 'function' ? ctx.get('webServer') : undefined) || ctx?.webServer;
|
|
56
|
+
const webStartup = (typeof ctx?.get === 'function' ? ctx.get('webStartup') : undefined) || ctx?.webStartup;
|
|
57
|
+
// 提取宿主底座的 host 与 port 配置(保持原始字面量用于模式判定)
|
|
58
|
+
const coreHostRaw = webServer?.config?.host ?? webServer?.options?.host ?? webStartup?.host ?? '127.0.0.1';
|
|
59
|
+
const corePortRaw = webServer?.config?.port ?? webServer?.options?.port ?? webStartup?.port ?? webServer?.port ?? defaultCorePort;
|
|
60
|
+
const rawCoreHost = String(coreHostRaw || '127.0.0.1').trim();
|
|
61
|
+
const corePort = Number(corePortRaw) || defaultCorePort;
|
|
62
|
+
const isLoopback = isLoopbackHost(rawCoreHost);
|
|
63
|
+
let finalMode = 'direct';
|
|
64
|
+
const isAuto = configuredMode === 'auto';
|
|
65
|
+
if (configuredMode === 'gateway') {
|
|
66
|
+
if (!isLoopback && rawCoreHost !== '0.0.0.0' && rawCoreHost !== '::') {
|
|
67
|
+
throw new Error(`[dsh-remote-mobile] 无法在具体外部 IP ("${rawCoreHost}") 上启用网关模式 (mode: 'gateway')。网关模式要求底座监听在本地回环 (127.0.0.1);若需直接对外开放请使用 direct 模式。`);
|
|
68
|
+
}
|
|
69
|
+
finalMode = 'gateway';
|
|
70
|
+
}
|
|
71
|
+
else if (configuredMode === 'direct') {
|
|
72
|
+
finalMode = 'direct';
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// auto 自适应模式:底座为回环则启用网关,底座已对外开放(0.0.0.0 / 具体外部IP)则直连
|
|
76
|
+
finalMode = isLoopback ? 'gateway' : 'direct';
|
|
77
|
+
}
|
|
78
|
+
// 仅对最终生效的 gateway 模式的目标核心地址执行 proxyTarget 规范化
|
|
79
|
+
const coreHost = finalMode === 'gateway' ? normalizeGatewayTargetHost(rawCoreHost) : rawCoreHost;
|
|
80
|
+
return {
|
|
81
|
+
mode: finalMode,
|
|
82
|
+
isAuto,
|
|
83
|
+
coreHost,
|
|
84
|
+
corePort,
|
|
85
|
+
isLoopback,
|
|
86
|
+
rawCoreHost,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成 32 字节高强度密码学随机密钥
|
|
3
|
+
*/
|
|
4
|
+
export declare function generateGatewaySecret(): string;
|
|
5
|
+
/**
|
|
6
|
+
* 设置网关内部共享密钥
|
|
7
|
+
* @param serverOrKey - 关联的 Server 实例(可选)
|
|
8
|
+
* @param secret - 密钥字符串
|
|
9
|
+
*/
|
|
10
|
+
export declare function setGatewaySecret(serverOrKey: any, secret: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* 获取当前有效的网关内部共享密钥
|
|
13
|
+
* @param serverOrKey - 关联的 Server 实例(可选)
|
|
14
|
+
*/
|
|
15
|
+
export declare function getGatewaySecret(serverOrKey?: any): string;
|
|
16
|
+
/**
|
|
17
|
+
* 清除网关内部密钥
|
|
18
|
+
*/
|
|
19
|
+
export declare function clearGatewaySecret(serverOrKey?: any): void;
|
|
20
|
+
/**
|
|
21
|
+
* 恒定时间安全比对两个密钥,防止时序侧信道攻击
|
|
22
|
+
*/
|
|
23
|
+
export declare function safeCompareSecret(candidate: string | string[] | undefined, actual: string): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* 内部网关私密握手密钥管理器
|
|
4
|
+
*
|
|
5
|
+
* 用于网关与底层 DSH HTTP Server 之间的受信通信鉴权:
|
|
6
|
+
* 仅当底层物理连接为回环 Socket (127.0.0.1 / ::1) 且携带匹配的私密握手密钥时,
|
|
7
|
+
* 底层虚拟化补丁才采纳网关上报的真实客户端 IP (x-dsh-real-ip)。
|
|
8
|
+
*
|
|
9
|
+
* 采用全局活跃单例兼 WeakMap 绑定实例机制,完美兼容 Cordis 插件热重载与多实例隔离场景。
|
|
10
|
+
*/
|
|
11
|
+
let globalActiveSecret = '';
|
|
12
|
+
const serverSecretMap = new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* 生成 32 字节高强度密码学随机密钥
|
|
15
|
+
*/
|
|
16
|
+
export function generateGatewaySecret() {
|
|
17
|
+
return randomBytes(32).toString('hex');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 设置网关内部共享密钥
|
|
21
|
+
* @param serverOrKey - 关联的 Server 实例(可选)
|
|
22
|
+
* @param secret - 密钥字符串
|
|
23
|
+
*/
|
|
24
|
+
export function setGatewaySecret(serverOrKey, secret) {
|
|
25
|
+
globalActiveSecret = secret;
|
|
26
|
+
if (serverOrKey && (typeof serverOrKey === 'object' || typeof serverOrKey === 'function')) {
|
|
27
|
+
serverSecretMap.set(serverOrKey, secret);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 获取当前有效的网关内部共享密钥
|
|
32
|
+
* @param serverOrKey - 关联的 Server 实例(可选)
|
|
33
|
+
*/
|
|
34
|
+
export function getGatewaySecret(serverOrKey) {
|
|
35
|
+
if (serverOrKey && (typeof serverOrKey === 'object' || typeof serverOrKey === 'function')) {
|
|
36
|
+
const bound = serverSecretMap.get(serverOrKey);
|
|
37
|
+
if (bound)
|
|
38
|
+
return bound;
|
|
39
|
+
}
|
|
40
|
+
return globalActiveSecret;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 清除网关内部密钥
|
|
44
|
+
*/
|
|
45
|
+
export function clearGatewaySecret(serverOrKey) {
|
|
46
|
+
if (serverOrKey && (typeof serverOrKey === 'object' || typeof serverOrKey === 'function')) {
|
|
47
|
+
serverSecretMap.delete(serverOrKey);
|
|
48
|
+
}
|
|
49
|
+
globalActiveSecret = '';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 恒定时间安全比对两个密钥,防止时序侧信道攻击
|
|
53
|
+
*/
|
|
54
|
+
export function safeCompareSecret(candidate, actual) {
|
|
55
|
+
if (!candidate || !actual)
|
|
56
|
+
return false;
|
|
57
|
+
const candStr = Array.isArray(candidate) ? candidate[0] : candidate;
|
|
58
|
+
if (typeof candStr !== 'string')
|
|
59
|
+
return false;
|
|
60
|
+
const bufCand = Buffer.from(candStr, 'utf8');
|
|
61
|
+
const bufActual = Buffer.from(actual, 'utf8');
|
|
62
|
+
if (bufCand.length !== bufActual.length)
|
|
63
|
+
return false;
|
|
64
|
+
return timingSafeEqual(bufCand, bufActual);
|
|
65
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 移动端远程独立安全网关服务 (GatewayServer)
|
|
3
|
+
*
|
|
4
|
+
* 架构定位:
|
|
5
|
+
* 解决 DSH 0.2.1+ 底层强行禁止 0.0.0.0 监听的问题。
|
|
6
|
+
* DSH 官方 WebServer 继续保持纯粹、安全的 127.0.0.1:3080 本地回环监听(零外部暴露风险);
|
|
7
|
+
* 由本服务在 0.0.0.0:3081 启动专用安全反代网关,接纳局域网与 Tailscale 流量,
|
|
8
|
+
* 执行端到端 RSA 加密、扫码配对、慢哈希密码认证与防暴力破解 IP 熔断,
|
|
9
|
+
* 并将合法的流量全双工代理至底座核心服务,彻底解决换 Wi-Fi 崩溃、无法兼顾多网卡与本机回环失效的痛点。
|
|
10
|
+
*/
|
|
11
|
+
import http, { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
12
|
+
import type { SessionStore } from '../auth/token.js';
|
|
13
|
+
export interface GatewayServerOptions {
|
|
14
|
+
/** 网关监听端口,默认 3081 */
|
|
15
|
+
port?: number;
|
|
16
|
+
/** 网关监听网络地址,默认 0.0.0.0 */
|
|
17
|
+
host?: string;
|
|
18
|
+
/** DSH 宿主核心服务端口,默认 3080 */
|
|
19
|
+
corePort: number;
|
|
20
|
+
/** DSH 宿主核心服务主机,默认 127.0.0.1 */
|
|
21
|
+
coreHost?: string;
|
|
22
|
+
/** 端口冲突时是否允许自动向后递增探测,默认 true */
|
|
23
|
+
autoIncrementPort?: boolean;
|
|
24
|
+
/** WebSocket 升级握手超时(毫秒),默认 10000ms */
|
|
25
|
+
upgradeTimeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export type GatewayServerStatus = 'stopped' | 'starting' | 'running' | 'error';
|
|
28
|
+
export declare class GatewayServer {
|
|
29
|
+
private server;
|
|
30
|
+
private activeSockets;
|
|
31
|
+
private store;
|
|
32
|
+
private gateMiddleware;
|
|
33
|
+
private configuredPort;
|
|
34
|
+
private boundPort;
|
|
35
|
+
private host;
|
|
36
|
+
private corePort;
|
|
37
|
+
private coreHost;
|
|
38
|
+
private coreTargetAuthority;
|
|
39
|
+
private autoIncrementPort;
|
|
40
|
+
private upgradeTimeoutMs;
|
|
41
|
+
private status;
|
|
42
|
+
private lastError;
|
|
43
|
+
private logger?;
|
|
44
|
+
constructor(options: GatewayServerOptions, store: SessionStore, gateMiddleware: (req: IncomingMessage, res: ServerResponse, next?: () => void) => boolean, logger?: any);
|
|
45
|
+
/** 获取网关当前运行状态契约 */
|
|
46
|
+
getStatus(): GatewayServerStatus;
|
|
47
|
+
/** 获取网关最近发生的异常信息 */
|
|
48
|
+
getLastError(): string | null;
|
|
49
|
+
/** 获取网关实际生效绑定的端口 */
|
|
50
|
+
getBoundPort(): number;
|
|
51
|
+
/** 获取网关监听的 host */
|
|
52
|
+
getHost(): string;
|
|
53
|
+
/** 获取内部转发的目标核心端口 */
|
|
54
|
+
getCorePort(): number;
|
|
55
|
+
/** 获取内部底层 Server 实例(供测试或状态检测) */
|
|
56
|
+
getServer(): http.Server | null;
|
|
57
|
+
/**
|
|
58
|
+
* 检验请求的 Host 头是否符合动态白名单(防 DNS-Rebinding 与非法主机注入)
|
|
59
|
+
*/
|
|
60
|
+
isHostAllowed(hostHeader?: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* 获取外部访问协议(兼容 Nginx / Caddy / Tailscale Serve 等反代的 X-Forwarded-Proto)
|
|
63
|
+
*/
|
|
64
|
+
private getExternalProtocol;
|
|
65
|
+
/**
|
|
66
|
+
* 计算转发到底座的真实客户端 IP。
|
|
67
|
+
*
|
|
68
|
+
* 正常 LAN/Tailscale 客户端直接连网关,socket IP 即真实 IP;
|
|
69
|
+
* 同机反向代理从 127.0.0.1 连入时,若门禁已判定为非回环(Host/Forwarded 表明是代理),
|
|
70
|
+
* 则采用 X-Forwarded-For 首个合法 IP,恢复防暴破/审计的真实来源。
|
|
71
|
+
*/
|
|
72
|
+
private getEffectiveClientIp;
|
|
73
|
+
/** 请求是否携带反向代理透传头 */
|
|
74
|
+
private hasForwardedHeaders;
|
|
75
|
+
/**
|
|
76
|
+
* 启动网关服务(带端口冲突自愈探测与状态机落定)
|
|
77
|
+
*/
|
|
78
|
+
start(): Promise<{
|
|
79
|
+
port: number;
|
|
80
|
+
host: string;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* 优雅关闭网关服务,立即释放端口与所有客户端 Socket
|
|
84
|
+
*/
|
|
85
|
+
close(): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* 处理 HTTP 请求:
|
|
88
|
+
* 1. 严格校验 Host 白名单(阻断 DNS-Rebinding)
|
|
89
|
+
* 2. 执行安全门禁 gateMiddleware 与跨站写拦截
|
|
90
|
+
* 3. 构造请求头:抹除客户端伪造 Secret,注入真实 IP 与内部有效握手密钥
|
|
91
|
+
* 4. 实施成对虚拟化决策矩阵
|
|
92
|
+
* 5. 转发到底座并流式透明 Pipe
|
|
93
|
+
*/
|
|
94
|
+
private handleHttpRequest;
|
|
95
|
+
/**
|
|
96
|
+
* 转发已授权的 HTTP 请求至 DSH 本地核心服务
|
|
97
|
+
*/
|
|
98
|
+
private proxyHttpRequest;
|
|
99
|
+
/**
|
|
100
|
+
* 处理 WebSocket Upgrade 协议升级代理(支持超时清除、非 101 状态传递与 CSWSH 防御)
|
|
101
|
+
*/
|
|
102
|
+
private handleUpgrade;
|
|
103
|
+
/**
|
|
104
|
+
* 改写底座 30x 重定向的目标 Location,将回环地址替换为外部真实访问的 Host
|
|
105
|
+
* 兼容 127.0.0.1, localhost, [::1] 以及 127.* 回环变体
|
|
106
|
+
*/
|
|
107
|
+
rewriteRedirectLocation(location: string, originalHost: string, externalProto?: 'http' | 'https'): string;
|
|
108
|
+
}
|