dsh-vpn 0.1.9 → 0.1.11
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/README.md +5 -4
- package/lib/client.js +33 -8
- package/lib/index.js +130 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,12 +38,13 @@ dsh plugin --profile web add dsh-vpn
|
|
|
38
38
|
3. 在「节点二维码」里点「刷新二维码」,用手机或电脑上的客户端扫码导入。
|
|
39
39
|
4. 服务器防火墙的端口由插件自动放行,不用手动操作;安装完成后提示框会列出放行了哪些端口。
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
**密钥登录**:服务器只允许密钥登录时,把「登录方式」选成「密钥」。插件按这个顺序尝试登录,哪把被服务器认可就用哪把:
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
1. 你自己粘贴的私钥(展开「改用我自己已有的私钥」,支持带密码短语的私钥);
|
|
44
|
+
2. 本机已有的钥匙:`~/.ssh/config`(含 `config.d`)里为这台服务器配置的 `IdentityFile`,dsh-vps-manager 的 `~/.ssh/dsh_vps_ed25519`,以及 `~/.ssh/id_ed25519`、`id_ecdsa`、`id_rsa`;
|
|
45
|
+
3. 插件的专用钥匙(纯 JS 生成,不需要本机的 ssh-keygen)。
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
**已经用 dsh-vps-manager 管理的服务器不需要再放任何公钥**:它的钥匙已经在服务器上,直接点「检查已有安装」或「测试连接」即可,页面会列出本机找到的钥匙,测试连接成功后会说明用的是哪一把。本机钥匙都不被认可时,再把页面上显示的专用钥匙公钥放到服务器上(服务商后台的「SSH 密钥」,或在能登录的服务器上执行页面给出的一行命令)。私钥只在本机 dsh 里读取,不会发给页面;带密码短语的本机私钥会跳过。用非 root 用户时,「sudo 密码」一栏填该用户的 sudo 密码;root 或免密 sudo 不用填。
|
|
47
48
|
|
|
48
49
|
**找回已有节点**:重装过 dsh 或 dsh-vpn 后,本地保存的设置会丢失,但 VPS 上的节点还在运行。在 VPN 设置里填好服务器 IP、端口、用户名和密码(或选密钥登录),**不用先保存**,直接点登录方式下面的「检查已有安装」:插件登录服务器读取仍在运行的节点,显示原来的二维码(链接和以前完全一样,已经导入手机或电脑客户端的配置继续可用),按服务器上的实际情况勾选协议和证书类型,并自动保存设置。登录失败时什么都不保存,填的内容也不会被清掉。
|
|
49
50
|
|
package/lib/client.js
CHANGED
|
@@ -51,7 +51,7 @@ var vpnApi = {
|
|
|
51
51
|
getSettings: () => post("/settings/get"),
|
|
52
52
|
saveSettings: (form) => post("/settings/save", form),
|
|
53
53
|
/** Public half of the plugin's dedicated key and the line that installs it. */
|
|
54
|
-
sshKey: () => post("/ssh-key"),
|
|
54
|
+
sshKey: (host = "") => post("/ssh-key", { host }),
|
|
55
55
|
forgetHostKey: () => post("/host-key/forget"),
|
|
56
56
|
test: () => post("/test"),
|
|
57
57
|
status: () => post("/status"),
|
|
@@ -97,6 +97,11 @@ var zh = {
|
|
|
97
97
|
sudoPassword: "sudo \u5BC6\u7801\uFF08\u9009\u586B\uFF09",
|
|
98
98
|
sudoPasswordEmpty: "\u7528 root \u6216\u514D\u5BC6 sudo \u65F6\u4E0D\u7528\u586B",
|
|
99
99
|
keyIntro: "\u7528\u63D2\u4EF6\u7684\u4E13\u7528\u94A5\u5319\u767B\u5F55\u3002\u628A\u4E0B\u9762\u7684\u516C\u94A5\u653E\u5230\u670D\u52A1\u5668\u4E0A\uFF0C\u4EFB\u9009\u4E00\u79CD\u529E\u6CD5\uFF1A",
|
|
100
|
+
keyIntroFallback: "\u5982\u679C\u670D\u52A1\u5668\u4E0D\u8BA4\u4E0A\u9762\u7684\u94A5\u5319\uFF0C\u518D\u7528\u63D2\u4EF6\u7684\u4E13\u7528\u94A5\u5319\uFF1A\u628A\u4E0B\u9762\u7684\u516C\u94A5\u653E\u5230\u670D\u52A1\u5668\u4E0A\uFF0C\u4EFB\u9009\u4E00\u79CD\u529E\u6CD5\uFF1A",
|
|
101
|
+
localKeysFound: "\u672C\u673A\u5DF2\u6709\u7684\u94A5\u5319\uFF08\u4F1A\u5148\u7528\u5B83\u4EEC\u767B\u5F55\uFF09",
|
|
102
|
+
localKeysHint: "\u5305\u62EC ~/.ssh/config \u91CC\u4E3A\u8FD9\u53F0\u670D\u52A1\u5668\u914D\u7F6E\u7684\u94A5\u5319\u3001dsh-vps-manager \u7684 ~/.ssh/dsh_vps_ed25519 \u548C ~/.ssh/id_*\u3002\u670D\u52A1\u5668\u8BA4\u53EF\u5176\u4E2D\u4EFB\u4F55\u4E00\u628A\uFF0C\u5C31\u4E0D\u7528\u518D\u653E\u516C\u94A5\uFF0C\u76F4\u63A5\u70B9\u300C\u68C0\u67E5\u5DF2\u6709\u5B89\u88C5\u300D\u6216\u300C\u6D4B\u8BD5\u8FDE\u63A5\u300D\u3002\u79C1\u94A5\u53EA\u5728\u672C\u673A dsh \u91CC\u8BFB\u53D6\uFF0C\u4E0D\u4F1A\u53D1\u7ED9\u9875\u9762\u3002",
|
|
103
|
+
testOkKey: "\uFF08\u7528 {key} \u767B\u5F55\uFF09",
|
|
104
|
+
dedicatedKey: "\u63D2\u4EF6\u4E13\u7528\u94A5\u5319",
|
|
100
105
|
keyWayConsole: "A. \u670D\u52A1\u5546\u540E\u53F0\u6DFB\u52A0\uFF08\u65B0\u673A\u5668\u63A8\u8350\uFF09",
|
|
101
106
|
keyWayConsoleHint: "\u7C98\u5230\u670D\u52A1\u5546\u7684\u300CSSH \u5BC6\u94A5\u300D\u91CC\uFF0C\u91CD\u88C5\u7CFB\u7EDF\u65F6\u52FE\u9009\u5B83\u3002",
|
|
102
107
|
keyWayCommand: "B. \u5DF2\u7ECF\u80FD\u767B\u5F55\uFF08\u7F51\u9875\u63A7\u5236\u53F0\u6216\u522B\u7684\u7EC8\u7AEF\uFF09",
|
|
@@ -228,6 +233,11 @@ var en = {
|
|
|
228
233
|
sudoPassword: "sudo password (optional)",
|
|
229
234
|
sudoPasswordEmpty: "Not needed for root or passwordless sudo",
|
|
230
235
|
keyIntro: "Log in with the plugin's dedicated key. Put the public key below on the server, either way:",
|
|
236
|
+
keyIntroFallback: "If the server accepts none of the keys above, use the plugin's dedicated key: put the public key below on the server, either way:",
|
|
237
|
+
localKeysFound: "Keys already on this computer (tried first)",
|
|
238
|
+
localKeysHint: "The keys ~/.ssh/config names for this server, dsh-vps-manager's ~/.ssh/dsh_vps_ed25519 and ~/.ssh/id_*. If the server accepts any of them, nothing needs to be added: click Find existing install or Test connection. Private keys are read only by dsh on this computer and never sent to the page.",
|
|
239
|
+
testOkKey: " (logged in with {key})",
|
|
240
|
+
dedicatedKey: "the plugin's dedicated key",
|
|
231
241
|
keyWayConsole: "A. Add it in your provider's console (recommended for new machines)",
|
|
232
242
|
keyWayConsoleHint: `Paste it into your provider's "SSH keys" and select it when reinstalling the OS.`,
|
|
233
243
|
keyWayCommand: "B. If you can already log in (web console or another terminal)",
|
|
@@ -1176,17 +1186,26 @@ function KeyPanel({ t, api, form, saved, set, setForm }) {
|
|
|
1176
1186
|
const [own, setOwn] = (0, import_react.useState)(ownStored);
|
|
1177
1187
|
(0, import_react.useEffect)(() => {
|
|
1178
1188
|
let live = true;
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1189
|
+
const timer = setTimeout(() => {
|
|
1190
|
+
void api.sshKey(form.host).then((r) => {
|
|
1191
|
+
if (live && r.ok) setKey(r);
|
|
1192
|
+
});
|
|
1193
|
+
}, 300);
|
|
1182
1194
|
return () => {
|
|
1183
1195
|
live = false;
|
|
1196
|
+
clearTimeout(timer);
|
|
1184
1197
|
};
|
|
1185
|
-
}, [api]);
|
|
1198
|
+
}, [api, form.host]);
|
|
1199
|
+
const local = key?.localKeys ?? [];
|
|
1186
1200
|
const preparing = t("working");
|
|
1187
1201
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keypanel dshvpn-wide", children: [
|
|
1202
|
+
local.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard dshvpn-success", children: [
|
|
1203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("localKeysFound") }),
|
|
1204
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: local.join("\n") }),
|
|
1205
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("localKeysHint") })
|
|
1206
|
+
] }),
|
|
1188
1207
|
!own && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1189
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: t("keyIntro") }),
|
|
1208
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: local.length > 0 ? t("keyIntroFallback") : t("keyIntro") }),
|
|
1190
1209
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard", children: [
|
|
1191
1210
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("keyWayConsole") }),
|
|
1192
1211
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("keyWayConsoleHint") }),
|
|
@@ -1274,6 +1293,7 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1274
1293
|
const [form, setForm] = (0, import_react.useState)(null);
|
|
1275
1294
|
const [busy, setBusy] = (0, import_react.useState)(null);
|
|
1276
1295
|
const [notice, setNotice] = (0, import_react.useState)(null);
|
|
1296
|
+
const [noticeFrom, setNoticeFrom] = (0, import_react.useState)(null);
|
|
1277
1297
|
const [links, setLinks] = (0, import_react.useState)(null);
|
|
1278
1298
|
const [refreshedAt, setRefreshedAt] = (0, import_react.useState)(null);
|
|
1279
1299
|
const [regenerate, setRegenerate] = (0, import_react.useState)(false);
|
|
@@ -1303,6 +1323,7 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1303
1323
|
async function op(name, call, onOk) {
|
|
1304
1324
|
setBusy(name);
|
|
1305
1325
|
setNotice(null);
|
|
1326
|
+
setNoticeFrom(name);
|
|
1306
1327
|
try {
|
|
1307
1328
|
const r = await call();
|
|
1308
1329
|
setNotice(r.ok ? onOk(r) : { tone: "error", text: errorText(t, r) });
|
|
@@ -1376,6 +1397,7 @@ ${t("cloudHint")}`;
|
|
|
1376
1397
|
return null;
|
|
1377
1398
|
});
|
|
1378
1399
|
const disabled = busy !== null;
|
|
1400
|
+
const noticeBox = (busy !== null || notice) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dshvpn-notice dshvpn-${busy !== null ? "info" : notice?.tone}`, role: "status", children: busy !== null ? busy === "detect" ? t("detecting") : t("working") : notice?.text });
|
|
1379
1401
|
const needsSave = dirty || !saved.configured;
|
|
1380
1402
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn", children: [
|
|
1381
1403
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
|
|
@@ -1432,6 +1454,7 @@ ${t("cloudHint")}`;
|
|
|
1432
1454
|
}, children: busy === "detect" ? t("detecting") : t("detect") }),
|
|
1433
1455
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("detectHint") })
|
|
1434
1456
|
] }),
|
|
1457
|
+
noticeFrom === "detect" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-wide", children: noticeBox }),
|
|
1435
1458
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-wide", children: [
|
|
1436
1459
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("linkAddress") }),
|
|
1437
1460
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: form.linkAddress, placeholder: form.host, onChange: (e) => {
|
|
@@ -1514,7 +1537,9 @@ ${t("cloudHint")}`;
|
|
|
1514
1537
|
onClick: () => {
|
|
1515
1538
|
void op("test", api.test, (r) => {
|
|
1516
1539
|
void refreshSettings();
|
|
1517
|
-
|
|
1540
|
+
const k = r.result?.key;
|
|
1541
|
+
const used = typeof k === "string" ? t("testOkKey", { key: k === "own" ? t("ownKey") : k === "dsh-vpn" ? t("dedicatedKey") : k }) : "";
|
|
1542
|
+
return { tone: "success", text: `${t("testOk", { system: String(r.result?.system ?? "") })}${used}` };
|
|
1518
1543
|
});
|
|
1519
1544
|
},
|
|
1520
1545
|
children: t("test")
|
|
@@ -1599,7 +1624,7 @@ ${fw.backend === "none" ? t("fwNone") : t("fwStatus", { backend: fw.backend, cou
|
|
|
1599
1624
|
}, children: t("uninstall") })
|
|
1600
1625
|
] })
|
|
1601
1626
|
] }),
|
|
1602
|
-
|
|
1627
|
+
noticeFrom !== "detect" && noticeBox,
|
|
1603
1628
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dshvpn-qr-section", children: [
|
|
1604
1629
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-row", children: [
|
|
1605
1630
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("qrSection") }),
|
package/lib/index.js
CHANGED
|
@@ -20199,22 +20199,36 @@ function fingerprint(key) {
|
|
|
20199
20199
|
}
|
|
20200
20200
|
var STDERR_TAIL = 8e3;
|
|
20201
20201
|
var SshSession = class _SshSession {
|
|
20202
|
-
constructor(client, hostKey) {
|
|
20202
|
+
constructor(client, hostKey, keyLabel) {
|
|
20203
20203
|
this.client = client;
|
|
20204
20204
|
this.hostKey = hostKey;
|
|
20205
|
+
this.keyLabel = keyLabel;
|
|
20205
20206
|
}
|
|
20206
20207
|
client;
|
|
20207
20208
|
hostKey;
|
|
20209
|
+
keyLabel;
|
|
20208
20210
|
static async open(target, signal, timeoutMs = 2e4) {
|
|
20209
20211
|
signal.throwIfAborted();
|
|
20210
20212
|
const client = new import_ssh2.Client();
|
|
20211
20213
|
let seen;
|
|
20214
|
+
const keys = target.keys ?? [];
|
|
20215
|
+
let tried = -1;
|
|
20212
20216
|
const config = {
|
|
20213
20217
|
host: target.host,
|
|
20214
20218
|
port: target.port,
|
|
20215
20219
|
username: target.username,
|
|
20216
|
-
//
|
|
20217
|
-
...
|
|
20220
|
+
// Keys in order, or the password: one kind only, never keyboard-interactive or agents.
|
|
20221
|
+
...keys.length > 0 ? {
|
|
20222
|
+
authHandler: (_left, _partial, next) => {
|
|
20223
|
+
tried += 1;
|
|
20224
|
+
const k = keys[tried];
|
|
20225
|
+
if (!k) {
|
|
20226
|
+
next(false);
|
|
20227
|
+
return;
|
|
20228
|
+
}
|
|
20229
|
+
next({ type: "publickey", username: target.username, key: k.key, ...k.passphrase ? { passphrase: k.passphrase } : {} });
|
|
20230
|
+
}
|
|
20231
|
+
} : { password: target.password },
|
|
20218
20232
|
readyTimeout: timeoutMs,
|
|
20219
20233
|
tryKeyboard: false,
|
|
20220
20234
|
hostVerifier: (key) => {
|
|
@@ -20230,7 +20244,7 @@ var SshSession = class _SshSession {
|
|
|
20230
20244
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
20231
20245
|
client.once("ready", () => {
|
|
20232
20246
|
signal.removeEventListener("abort", onAbort);
|
|
20233
|
-
resolve(new _SshSession(client, seen ?? ""));
|
|
20247
|
+
resolve(new _SshSession(client, seen ?? "", keys[tried]?.label));
|
|
20234
20248
|
});
|
|
20235
20249
|
client.once("error", (err) => {
|
|
20236
20250
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -20242,7 +20256,11 @@ var SshSession = class _SshSession {
|
|
|
20242
20256
|
{ expected: target.hostKey, actual: seen }
|
|
20243
20257
|
));
|
|
20244
20258
|
} else if (err.level === "client-authentication") {
|
|
20245
|
-
reject(
|
|
20259
|
+
reject(keys.length > 0 ? new SshError(
|
|
20260
|
+
"key_rejected",
|
|
20261
|
+
`SSH login failed: ${target.username}@${target.host} accepted none of: ${keys.map((k) => k.label).join(", ")}`,
|
|
20262
|
+
{ tried: keys.map((k) => k.label).join("\n") }
|
|
20263
|
+
) : new SshError("auth_failed", "SSH login failed: wrong username or password"));
|
|
20246
20264
|
} else if (err.level === "client-timeout") {
|
|
20247
20265
|
reject(new SshError("timeout", `timed out connecting to ${target.host}:${target.port}`));
|
|
20248
20266
|
} else {
|
|
@@ -20472,9 +20490,10 @@ function apiHandlers(service, uiLang, onUiLang, updater) {
|
|
|
20472
20490
|
return { ok: true, settings: publicView(saved) };
|
|
20473
20491
|
},
|
|
20474
20492
|
// The dedicated key's public half and the line that installs it; never the private key.
|
|
20475
|
-
"/ssh-key": async () => {
|
|
20493
|
+
"/ssh-key": async (body) => {
|
|
20476
20494
|
const { publicKey } = await service.keys.get();
|
|
20477
|
-
|
|
20495
|
+
const host = typeof body.host === "string" ? body.host.trim() : "";
|
|
20496
|
+
return { ok: true, publicKey, command: authorizedKeysCommand(publicKey), localKeys: host ? service.localKeyLabels(host) : [] };
|
|
20478
20497
|
},
|
|
20479
20498
|
"/host-key/forget": async () => {
|
|
20480
20499
|
await service.store.update((cur) => cur && { ...cur, hostKey: void 0 });
|
|
@@ -20588,11 +20607,17 @@ var NotConfiguredError = class extends Error {
|
|
|
20588
20607
|
this.name = "NotConfiguredError";
|
|
20589
20608
|
}
|
|
20590
20609
|
};
|
|
20591
|
-
async function targetOf(s, keys) {
|
|
20610
|
+
async function targetOf(s, keys, local = () => []) {
|
|
20592
20611
|
const base = { host: s.host, port: s.port, username: s.username, password: s.password, hostKey: s.hostKey };
|
|
20593
20612
|
if (s.auth !== "key") return base;
|
|
20594
|
-
|
|
20595
|
-
|
|
20613
|
+
const list = [];
|
|
20614
|
+
if (s.privateKey !== void 0) list.push({ key: s.privateKey, passphrase: s.passphrase, label: "own" });
|
|
20615
|
+
try {
|
|
20616
|
+
list.push(...local(s.host));
|
|
20617
|
+
} catch {
|
|
20618
|
+
}
|
|
20619
|
+
list.push({ key: (await keys.get()).privateKey, label: "dsh-vpn" });
|
|
20620
|
+
return { ...base, keys: list };
|
|
20596
20621
|
}
|
|
20597
20622
|
function resolveProtocols(requested, fallback) {
|
|
20598
20623
|
const list = requested && requested.length > 0 ? [...new Set(requested)] : [...fallback];
|
|
@@ -20616,6 +20641,14 @@ var VpnService = class {
|
|
|
20616
20641
|
config;
|
|
20617
20642
|
keys;
|
|
20618
20643
|
queue = Promise.resolve();
|
|
20644
|
+
/** Labels of this computer's keys that key login would try for `host` (no key material). */
|
|
20645
|
+
localKeyLabels(host) {
|
|
20646
|
+
try {
|
|
20647
|
+
return (this.config.localKeys?.(host) ?? []).map((k) => k.label);
|
|
20648
|
+
} catch {
|
|
20649
|
+
return [];
|
|
20650
|
+
}
|
|
20651
|
+
}
|
|
20619
20652
|
async settings() {
|
|
20620
20653
|
const s = await this.store.get();
|
|
20621
20654
|
if (!s) throw new NotConfiguredError();
|
|
@@ -20645,7 +20678,7 @@ var VpnService = class {
|
|
|
20645
20678
|
}
|
|
20646
20679
|
/** Run the script against `s` (stored or not); throws SSH and config errors. */
|
|
20647
20680
|
async runOn(s, action, args, signal) {
|
|
20648
|
-
const res = await runVpnScript(action, args, await targetOf(s, this.keys), signal, this.config.timeoutMs);
|
|
20681
|
+
const res = await runVpnScript(action, args, await targetOf(s, this.keys, this.config.localKeys), signal, this.config.timeoutMs);
|
|
20649
20682
|
if (!res.result) {
|
|
20650
20683
|
const sudo = /incorrect password|a password is required|not in the sudoers|may not run sudo/i.test(res.log);
|
|
20651
20684
|
return {
|
|
@@ -20745,7 +20778,7 @@ var VpnService = class {
|
|
|
20745
20778
|
let session;
|
|
20746
20779
|
try {
|
|
20747
20780
|
const s = await this.settings();
|
|
20748
|
-
session = await SshSession.open(await targetOf(s, this.keys), signal);
|
|
20781
|
+
session = await SshSession.open(await targetOf(s, this.keys, this.config.localKeys), signal);
|
|
20749
20782
|
const asRoot = s.username === "root";
|
|
20750
20783
|
const cmd = `echo "uid=$(${asRoot ? "" : `sudo -S -p '' `}id -u)"; echo "sys=$(uname -srm)"`;
|
|
20751
20784
|
const res = await session.exec(cmd, asRoot ? "" : `${s.password}
|
|
@@ -20757,7 +20790,7 @@ var VpnService = class {
|
|
|
20757
20790
|
return {
|
|
20758
20791
|
ok: root,
|
|
20759
20792
|
...root ? {} : { error: "sudo_failed", message: `cannot become root as ${s.username}: ${res.stderr.trim()}` },
|
|
20760
|
-
result: { hostKey: session.hostKey, system, root }
|
|
20793
|
+
result: { hostKey: session.hostKey, system, root, ...session.keyLabel ? { key: session.keyLabel } : {} }
|
|
20761
20794
|
};
|
|
20762
20795
|
} catch (err) {
|
|
20763
20796
|
return failure(err);
|
|
@@ -20895,14 +20928,95 @@ function notConfiguredMessage(lang) {
|
|
|
20895
20928
|
return lang === "zh" ? "\u5C1A\u672A\u914D\u7F6E\u670D\u52A1\u5668\u3002\u8BF7\u70B9 dsh \u5DE6\u4E0B\u89D2\u300C\u8BBE\u7F6E\u300D\u2192\u300CVPN \u8BBE\u7F6E\u300D\uFF0C\u586B\u5199\u670D\u52A1\u5668 IP\u3001\u7AEF\u53E3\u3001\u7528\u6237\u540D\u548C\u5BC6\u7801\u3002" : "No server configured. Open dsh Settings (bottom left) \u2192 VPN and fill in the server IP, port, username and password first.";
|
|
20896
20929
|
}
|
|
20897
20930
|
|
|
20931
|
+
// src/local-keys.ts
|
|
20932
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
20933
|
+
import { homedir } from "node:os";
|
|
20934
|
+
import { isAbsolute, join } from "node:path";
|
|
20935
|
+
var DEFAULT_KEYS = ["dsh_vps_ed25519", "id_ed25519", "id_ecdsa", "id_rsa"];
|
|
20936
|
+
function readText(file) {
|
|
20937
|
+
try {
|
|
20938
|
+
return readFileSync(file, "utf8");
|
|
20939
|
+
} catch {
|
|
20940
|
+
return void 0;
|
|
20941
|
+
}
|
|
20942
|
+
}
|
|
20943
|
+
function configTexts(sshDir) {
|
|
20944
|
+
const texts = [];
|
|
20945
|
+
const main = readText(join(sshDir, "config"));
|
|
20946
|
+
if (main !== void 0) texts.push(main);
|
|
20947
|
+
let names = [];
|
|
20948
|
+
try {
|
|
20949
|
+
names = readdirSync(join(sshDir, "config.d")).sort();
|
|
20950
|
+
} catch {
|
|
20951
|
+
}
|
|
20952
|
+
for (const name2 of names) {
|
|
20953
|
+
const t = readText(join(sshDir, "config.d", name2));
|
|
20954
|
+
if (t !== void 0) texts.push(t);
|
|
20955
|
+
}
|
|
20956
|
+
return texts;
|
|
20957
|
+
}
|
|
20958
|
+
function identityFilesFor(host, sshDir, home) {
|
|
20959
|
+
const files = [];
|
|
20960
|
+
for (const text of configTexts(sshDir)) {
|
|
20961
|
+
let patterns = [];
|
|
20962
|
+
let hostName;
|
|
20963
|
+
let identities = [];
|
|
20964
|
+
const flush = () => {
|
|
20965
|
+
const literal = patterns.filter((p) => !/[*?!]/.test(p));
|
|
20966
|
+
if (literal.length > 0 && (hostName === host || literal.includes(host))) {
|
|
20967
|
+
files.push(...identities);
|
|
20968
|
+
}
|
|
20969
|
+
};
|
|
20970
|
+
for (const raw of text.split("\n")) {
|
|
20971
|
+
const line = raw.replace(/\s+#.*$/, "").trim();
|
|
20972
|
+
const m = /^(\w+)\s*=?\s*(.+)$/.exec(line);
|
|
20973
|
+
if (!m || line.startsWith("#")) continue;
|
|
20974
|
+
const key = m[1].toLowerCase();
|
|
20975
|
+
const value = m[2].trim().replace(/^"(.*)"$/, "$1");
|
|
20976
|
+
if (key === "host" || key === "match") {
|
|
20977
|
+
flush();
|
|
20978
|
+
patterns = key === "host" ? value.split(/\s+/) : [];
|
|
20979
|
+
hostName = void 0;
|
|
20980
|
+
identities = [];
|
|
20981
|
+
} else if (key === "hostname") {
|
|
20982
|
+
hostName = value;
|
|
20983
|
+
} else if (key === "identityfile") {
|
|
20984
|
+
const expanded = value.startsWith("~/") ? join(home, value.slice(2)) : value;
|
|
20985
|
+
identities.push(isAbsolute(expanded) ? expanded : join(sshDir, expanded));
|
|
20986
|
+
}
|
|
20987
|
+
}
|
|
20988
|
+
flush();
|
|
20989
|
+
}
|
|
20990
|
+
return files;
|
|
20991
|
+
}
|
|
20992
|
+
function localKeysFor(host, opts = {}) {
|
|
20993
|
+
const home = opts.home ?? homedir();
|
|
20994
|
+
const sshDir = opts.sshDir ?? join(home, ".ssh");
|
|
20995
|
+
const paths = [...identityFilesFor(host, sshDir, home), ...DEFAULT_KEYS.map((n) => join(sshDir, n))];
|
|
20996
|
+
const keys = [];
|
|
20997
|
+
for (const path of [...new Set(paths)]) {
|
|
20998
|
+
const key = readText(path);
|
|
20999
|
+
if (key === void 0 || !key.includes("PRIVATE KEY")) continue;
|
|
21000
|
+
let problem;
|
|
21001
|
+
try {
|
|
21002
|
+
problem = privateKeyProblem(key);
|
|
21003
|
+
} catch {
|
|
21004
|
+
problem = "unreadable";
|
|
21005
|
+
}
|
|
21006
|
+
if (problem) continue;
|
|
21007
|
+
keys.push({ key, label: path.startsWith(home) ? `~${path.slice(home.length)}` : path });
|
|
21008
|
+
}
|
|
21009
|
+
return keys;
|
|
21010
|
+
}
|
|
21011
|
+
|
|
20898
21012
|
// src/update.ts
|
|
20899
|
-
import { readFileSync } from "node:fs";
|
|
21013
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
20900
21014
|
var PACKAGE_NAME = "dsh-vpn";
|
|
20901
21015
|
var MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
20902
21016
|
var NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
20903
21017
|
function currentVersion() {
|
|
20904
21018
|
try {
|
|
20905
|
-
const pkg = JSON.parse(
|
|
21019
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
20906
21020
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
20907
21021
|
} catch {
|
|
20908
21022
|
return "0.0.0";
|
|
@@ -21056,7 +21170,7 @@ function toToolValue(r, uiLang) {
|
|
|
21056
21170
|
function apply(ctx, config) {
|
|
21057
21171
|
const service = new VpnService(
|
|
21058
21172
|
new CredentialSettingsStore(ctx),
|
|
21059
|
-
{ timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider },
|
|
21173
|
+
{ timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider, localKeys: (host) => localKeysFor(host) },
|
|
21060
21174
|
new CredentialKeyStore(ctx)
|
|
21061
21175
|
);
|
|
21062
21176
|
const uiLang = new UiLang();
|
package/package.json
CHANGED