dsh-vpn 0.1.10 → 0.1.12
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 +90 -10
- package/lib/index.js +165 -24
- 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 管理的服务器不需要再放任何公钥**:它的钥匙已经在服务器上。填好 IP,「登录方式」选「密钥」,页面会列出本机找到的钥匙,点这张卡片右上角的「使用」就登录了(不用先保存,登录成功后自动保存)。卡片随后一直显示「已登录:用哪把钥匙登录 用户@服务器」,之后安装、检查和刷新二维码都优先用这把钥匙;改了 IP、端口或用户名,登录状态自动作废。本机钥匙都不被认可时,再把页面上显示的专用钥匙公钥放到服务器上(服务商后台的「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,9 +51,11 @@ 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
|
+
/** Log in with keys (local ones first) using the form as typed; saved once it works. */
|
|
58
|
+
login: (form) => post("/login", form),
|
|
57
59
|
status: () => post("/status"),
|
|
58
60
|
links: () => post("/links"),
|
|
59
61
|
/** Find an earlier installation on the server and adopt its protocols and certificate mode. */
|
|
@@ -97,6 +99,15 @@ var zh = {
|
|
|
97
99
|
sudoPassword: "sudo \u5BC6\u7801\uFF08\u9009\u586B\uFF09",
|
|
98
100
|
sudoPasswordEmpty: "\u7528 root \u6216\u514D\u5BC6 sudo \u65F6\u4E0D\u7528\u586B",
|
|
99
101
|
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",
|
|
102
|
+
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",
|
|
103
|
+
localKeysFound: "\u672C\u673A\u5DF2\u6709\u7684\u94A5\u5319\uFF08\u4F1A\u5148\u7528\u5B83\u4EEC\u767B\u5F55\uFF09",
|
|
104
|
+
useKey: "\u4F7F\u7528",
|
|
105
|
+
relogin: "\u91CD\u65B0\u767B\u5F55",
|
|
106
|
+
loggingIn: "\u767B\u5F55\u4E2D\u2026",
|
|
107
|
+
loggedIn: "\u2713 \u5DF2\u767B\u5F55\uFF1A\u7528 {key} \u767B\u5F55 {user}@{host}\uFF08{time}\uFF09\u3002\u4E4B\u540E\u5B89\u88C5\u3001\u68C0\u67E5\u548C\u5237\u65B0\u4E8C\u7EF4\u7801\u90FD\u4F1A\u5148\u7528\u8FD9\u628A\u94A5\u5319\u3002",
|
|
108
|
+
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\u70B9\u53F3\u4E0A\u89D2\u300C\u4F7F\u7528\u300D\u5373\u53EF\u767B\u5F55\u3002\u79C1\u94A5\u53EA\u5728\u672C\u673A dsh \u91CC\u8BFB\u53D6\uFF0C\u4E0D\u4F1A\u53D1\u7ED9\u9875\u9762\u3002",
|
|
109
|
+
testOkKey: "\uFF08\u7528 {key} \u767B\u5F55\uFF09",
|
|
110
|
+
dedicatedKey: "\u63D2\u4EF6\u4E13\u7528\u94A5\u5319",
|
|
100
111
|
keyWayConsole: "A. \u670D\u52A1\u5546\u540E\u53F0\u6DFB\u52A0\uFF08\u65B0\u673A\u5668\u63A8\u8350\uFF09",
|
|
101
112
|
keyWayConsoleHint: "\u7C98\u5230\u670D\u52A1\u5546\u7684\u300CSSH \u5BC6\u94A5\u300D\u91CC\uFF0C\u91CD\u88C5\u7CFB\u7EDF\u65F6\u52FE\u9009\u5B83\u3002",
|
|
102
113
|
keyWayCommand: "B. \u5DF2\u7ECF\u80FD\u767B\u5F55\uFF08\u7F51\u9875\u63A7\u5236\u53F0\u6216\u522B\u7684\u7EC8\u7AEF\uFF09",
|
|
@@ -228,6 +239,15 @@ var en = {
|
|
|
228
239
|
sudoPassword: "sudo password (optional)",
|
|
229
240
|
sudoPasswordEmpty: "Not needed for root or passwordless sudo",
|
|
230
241
|
keyIntro: "Log in with the plugin's dedicated key. Put the public key below on the server, either way:",
|
|
242
|
+
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:",
|
|
243
|
+
localKeysFound: "Keys already on this computer (tried first)",
|
|
244
|
+
useKey: "Use",
|
|
245
|
+
relogin: "Log in again",
|
|
246
|
+
loggingIn: "Logging in\u2026",
|
|
247
|
+
loggedIn: "\u2713 Logged in: {user}@{host} with {key} ({time}). Install, find and refresh will use this key first.",
|
|
248
|
+
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 Use at the top right to log in. Private keys are read only by dsh on this computer and never sent to the page.",
|
|
249
|
+
testOkKey: " (logged in with {key})",
|
|
250
|
+
dedicatedKey: "the plugin's dedicated key",
|
|
231
251
|
keyWayConsole: "A. Add it in your provider's console (recommended for new machines)",
|
|
232
252
|
keyWayConsoleHint: `Paste it into your provider's "SSH keys" and select it when reinstalling the OS.`,
|
|
233
253
|
keyWayCommand: "B. If you can already log in (web console or another terminal)",
|
|
@@ -385,6 +405,8 @@ var STYLES = `
|
|
|
385
405
|
color: inherit; background: transparent; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
|
|
386
406
|
.dshvpn-detect { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding-top: 4px; }
|
|
387
407
|
.dshvpn-detect small { flex: 1; min-width: 220px; }
|
|
408
|
+
.dshvpn-keycard-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
409
|
+
.dshvpn-loggedin { font-size: 13px; color: var(--dsw-alias-state-success-primary, #2f855a); }
|
|
388
410
|
.dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
|
|
389
411
|
.dshvpn-section-head h2 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
390
412
|
.dshvpn-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
|
|
@@ -1170,23 +1192,64 @@ function Copyable({ text, label, t }) {
|
|
|
1170
1192
|
}, children: copied ? t("copied") : label })
|
|
1171
1193
|
] });
|
|
1172
1194
|
}
|
|
1173
|
-
function KeyPanel({ t, api, form, saved, set, setForm }) {
|
|
1195
|
+
function KeyPanel({ t, api, form, saved, set, setForm, disabled, onLoggedIn }) {
|
|
1196
|
+
const [logging, setLogging] = (0, import_react.useState)(false);
|
|
1197
|
+
const [loginError, setLoginError] = (0, import_react.useState)(null);
|
|
1198
|
+
const loggedIn = saved.loginKey !== null && saved.host === form.host.trim() && saved.port === form.port && saved.username === form.username.trim();
|
|
1199
|
+
const keyName = (label) => label === "own" ? t("ownKey") : label === "dsh-vpn" ? t("dedicatedKey") : label;
|
|
1200
|
+
const login = async () => {
|
|
1201
|
+
setLogging(true);
|
|
1202
|
+
setLoginError(null);
|
|
1203
|
+
const r = await api.login(form);
|
|
1204
|
+
setLogging(false);
|
|
1205
|
+
if (r.ok) onLoggedIn(r.settings);
|
|
1206
|
+
else setLoginError(errorText(t, r));
|
|
1207
|
+
};
|
|
1174
1208
|
const [key, setKey] = (0, import_react.useState)(null);
|
|
1175
1209
|
const ownStored = saved.hasPrivateKey && !form.clearPrivateKey;
|
|
1176
1210
|
const [own, setOwn] = (0, import_react.useState)(ownStored);
|
|
1177
1211
|
(0, import_react.useEffect)(() => {
|
|
1178
1212
|
let live = true;
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1213
|
+
const timer = setTimeout(() => {
|
|
1214
|
+
void api.sshKey(form.host).then((r) => {
|
|
1215
|
+
if (live && r.ok) setKey(r);
|
|
1216
|
+
});
|
|
1217
|
+
}, 300);
|
|
1182
1218
|
return () => {
|
|
1183
1219
|
live = false;
|
|
1220
|
+
clearTimeout(timer);
|
|
1184
1221
|
};
|
|
1185
|
-
}, [api]);
|
|
1222
|
+
}, [api, form.host]);
|
|
1223
|
+
const local = key?.localKeys ?? [];
|
|
1186
1224
|
const preparing = t("working");
|
|
1187
1225
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keypanel dshvpn-wide", children: [
|
|
1188
|
-
|
|
1189
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1226
|
+
(local.length > 0 || loggedIn) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard dshvpn-success", children: [
|
|
1227
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard-head", children: [
|
|
1228
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("localKeysFound") }),
|
|
1229
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1230
|
+
import_dsh_client_ui_primitives.Button,
|
|
1231
|
+
{
|
|
1232
|
+
size: "sm",
|
|
1233
|
+
variant: loggedIn ? "outline" : "primary",
|
|
1234
|
+
disabled: disabled || logging || form.host.trim() === "",
|
|
1235
|
+
onClick: () => {
|
|
1236
|
+
void login();
|
|
1237
|
+
},
|
|
1238
|
+
children: logging ? t("loggingIn") : loggedIn ? t("relogin") : t("useKey")
|
|
1239
|
+
}
|
|
1240
|
+
)
|
|
1241
|
+
] }),
|
|
1242
|
+
loggedIn ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-loggedin", children: t("loggedIn", {
|
|
1243
|
+
key: keyName(saved.loginKey),
|
|
1244
|
+
user: saved.username,
|
|
1245
|
+
host: saved.host,
|
|
1246
|
+
time: saved.loggedInAt ? new Date(saved.loggedInAt).toLocaleString() : ""
|
|
1247
|
+
}) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: local.join("\n") }),
|
|
1248
|
+
loginError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-notice dshvpn-error", children: loginError }),
|
|
1249
|
+
!loggedIn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("localKeysHint") })
|
|
1250
|
+
] }),
|
|
1251
|
+
!own && !loggedIn && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1252
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: local.length > 0 ? t("keyIntroFallback") : t("keyIntro") }),
|
|
1190
1253
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard", children: [
|
|
1191
1254
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("keyWayConsole") }),
|
|
1192
1255
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("keyWayConsoleHint") }),
|
|
@@ -1428,7 +1491,22 @@ ${t("cloudHint")}`;
|
|
|
1428
1491
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: m === "key" ? t("authKey") : t("authPassword") })
|
|
1429
1492
|
] }, m)) })
|
|
1430
1493
|
] }),
|
|
1431
|
-
form.auth === "key" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1494
|
+
form.auth === "key" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1495
|
+
KeyPanel,
|
|
1496
|
+
{
|
|
1497
|
+
t,
|
|
1498
|
+
api,
|
|
1499
|
+
form,
|
|
1500
|
+
saved,
|
|
1501
|
+
set,
|
|
1502
|
+
setForm,
|
|
1503
|
+
disabled,
|
|
1504
|
+
onLoggedIn: (v) => {
|
|
1505
|
+
setSaved(v);
|
|
1506
|
+
setForm(formOf(v));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
),
|
|
1432
1510
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-wide dshvpn-detect", children: [
|
|
1433
1511
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: disabled || form.host.trim() === "", onClick: () => {
|
|
1434
1512
|
void detect();
|
|
@@ -1518,7 +1596,9 @@ ${t("cloudHint")}`;
|
|
|
1518
1596
|
onClick: () => {
|
|
1519
1597
|
void op("test", api.test, (r) => {
|
|
1520
1598
|
void refreshSettings();
|
|
1521
|
-
|
|
1599
|
+
const k = r.result?.key;
|
|
1600
|
+
const used = typeof k === "string" ? t("testOkKey", { key: k === "own" ? t("ownKey") : k === "dsh-vpn" ? t("dedicatedKey") : k }) : "";
|
|
1601
|
+
return { tone: "success", text: `${t("testOk", { system: String(r.result?.system ?? "") })}${used}` };
|
|
1522
1602
|
});
|
|
1523
1603
|
},
|
|
1524
1604
|
children: t("test")
|
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 {
|
|
@@ -20352,7 +20370,9 @@ var EMPTY_VIEW = {
|
|
|
20352
20370
|
protocols: [...DEFAULT_PROTOCOLS],
|
|
20353
20371
|
certMode: "self",
|
|
20354
20372
|
linkAddress: "",
|
|
20355
|
-
hostKey: null
|
|
20373
|
+
hostKey: null,
|
|
20374
|
+
loginKey: null,
|
|
20375
|
+
loggedInAt: null
|
|
20356
20376
|
};
|
|
20357
20377
|
function publicView(s) {
|
|
20358
20378
|
if (!s) return EMPTY_VIEW;
|
|
@@ -20367,7 +20387,9 @@ function publicView(s) {
|
|
|
20367
20387
|
protocols: s.protocols,
|
|
20368
20388
|
certMode: s.certMode,
|
|
20369
20389
|
linkAddress: s.linkAddress ?? "",
|
|
20370
|
-
hostKey: s.hostKey ?? null
|
|
20390
|
+
hostKey: s.hostKey ?? null,
|
|
20391
|
+
loginKey: s.auth === "key" ? s.loginKey ?? null : null,
|
|
20392
|
+
loggedInAt: s.auth === "key" ? s.loggedInAt ?? null : null
|
|
20371
20393
|
};
|
|
20372
20394
|
}
|
|
20373
20395
|
function applyInput(current, input) {
|
|
@@ -20418,7 +20440,9 @@ function applyInput(current, input) {
|
|
|
20418
20440
|
protocols,
|
|
20419
20441
|
certMode,
|
|
20420
20442
|
linkAddress: linkAddress || void 0,
|
|
20421
|
-
hostKey: sameMachine ? current.hostKey : void 0
|
|
20443
|
+
hostKey: sameMachine ? current.hostKey : void 0,
|
|
20444
|
+
// A login is remembered only for the same machine and user.
|
|
20445
|
+
...sameMachine && current.username === username && auth === "key" && current.loginKey ? { loginKey: current.loginKey, loggedInAt: current.loggedInAt } : {}
|
|
20422
20446
|
};
|
|
20423
20447
|
}
|
|
20424
20448
|
var MemoryKeyStore = class {
|
|
@@ -20472,15 +20496,22 @@ function apiHandlers(service, uiLang, onUiLang, updater) {
|
|
|
20472
20496
|
return { ok: true, settings: publicView(saved) };
|
|
20473
20497
|
},
|
|
20474
20498
|
// The dedicated key's public half and the line that installs it; never the private key.
|
|
20475
|
-
"/ssh-key": async () => {
|
|
20499
|
+
"/ssh-key": async (body) => {
|
|
20476
20500
|
const { publicKey } = await service.keys.get();
|
|
20477
|
-
|
|
20501
|
+
const host = typeof body.host === "string" ? body.host.trim() : "";
|
|
20502
|
+
return { ok: true, publicKey, command: authorizedKeysCommand(publicKey), localKeys: host ? service.localKeyLabels(host) : [] };
|
|
20478
20503
|
},
|
|
20479
20504
|
"/host-key/forget": async () => {
|
|
20480
20505
|
await service.store.update((cur) => cur && { ...cur, hostKey: void 0 });
|
|
20481
20506
|
return { ok: true, settings: publicView(await service.store.get()) };
|
|
20482
20507
|
},
|
|
20483
20508
|
"/test": (_body, signal) => service.test(signal),
|
|
20509
|
+
// 「使用」 on the local-keys card: key login with the form as typed; saved once it works.
|
|
20510
|
+
"/login": async (body, signal) => {
|
|
20511
|
+
const candidate = applyInput(await service.store.get(), { ...body, auth: "key" });
|
|
20512
|
+
const r = await service.test(signal, candidate);
|
|
20513
|
+
return r.ok ? { ...r, settings: publicView(await service.store.get()) } : r;
|
|
20514
|
+
},
|
|
20484
20515
|
"/status": (_body, signal) => service.status(signal),
|
|
20485
20516
|
"/links": (_body, signal) => service.links(signal),
|
|
20486
20517
|
// With form fields: try them as they are (no save needed first); they are
|
|
@@ -20588,11 +20619,19 @@ var NotConfiguredError = class extends Error {
|
|
|
20588
20619
|
this.name = "NotConfiguredError";
|
|
20589
20620
|
}
|
|
20590
20621
|
};
|
|
20591
|
-
async function targetOf(s, keys) {
|
|
20622
|
+
async function targetOf(s, keys, local = () => []) {
|
|
20592
20623
|
const base = { host: s.host, port: s.port, username: s.username, password: s.password, hostKey: s.hostKey };
|
|
20593
20624
|
if (s.auth !== "key") return base;
|
|
20594
|
-
|
|
20595
|
-
|
|
20625
|
+
const list = [];
|
|
20626
|
+
if (s.privateKey !== void 0) list.push({ key: s.privateKey, passphrase: s.passphrase, label: "own" });
|
|
20627
|
+
try {
|
|
20628
|
+
list.push(...local(s.host));
|
|
20629
|
+
} catch {
|
|
20630
|
+
}
|
|
20631
|
+
list.push({ key: (await keys.get()).privateKey, label: "dsh-vpn" });
|
|
20632
|
+
const last = list.findIndex((k) => k.label === s.loginKey);
|
|
20633
|
+
if (last > 0) list.unshift(...list.splice(last, 1));
|
|
20634
|
+
return { ...base, keys: list };
|
|
20596
20635
|
}
|
|
20597
20636
|
function resolveProtocols(requested, fallback) {
|
|
20598
20637
|
const list = requested && requested.length > 0 ? [...new Set(requested)] : [...fallback];
|
|
@@ -20616,6 +20655,14 @@ var VpnService = class {
|
|
|
20616
20655
|
config;
|
|
20617
20656
|
keys;
|
|
20618
20657
|
queue = Promise.resolve();
|
|
20658
|
+
/** Labels of this computer's keys that key login would try for `host` (no key material). */
|
|
20659
|
+
localKeyLabels(host) {
|
|
20660
|
+
try {
|
|
20661
|
+
return (this.config.localKeys?.(host) ?? []).map((k) => k.label);
|
|
20662
|
+
} catch {
|
|
20663
|
+
return [];
|
|
20664
|
+
}
|
|
20665
|
+
}
|
|
20619
20666
|
async settings() {
|
|
20620
20667
|
const s = await this.store.get();
|
|
20621
20668
|
if (!s) throw new NotConfiguredError();
|
|
@@ -20645,7 +20692,7 @@ var VpnService = class {
|
|
|
20645
20692
|
}
|
|
20646
20693
|
/** Run the script against `s` (stored or not); throws SSH and config errors. */
|
|
20647
20694
|
async runOn(s, action, args, signal) {
|
|
20648
|
-
const res = await runVpnScript(action, args, await targetOf(s, this.keys), signal, this.config.timeoutMs);
|
|
20695
|
+
const res = await runVpnScript(action, args, await targetOf(s, this.keys, this.config.localKeys), signal, this.config.timeoutMs);
|
|
20649
20696
|
if (!res.result) {
|
|
20650
20697
|
const sudo = /incorrect password|a password is required|not in the sudoers|may not run sudo/i.test(res.log);
|
|
20651
20698
|
return {
|
|
@@ -20738,26 +20785,37 @@ var VpnService = class {
|
|
|
20738
20785
|
}
|
|
20739
20786
|
/**
|
|
20740
20787
|
* Log in, report who we are and whether root is reachable. Pins the host key
|
|
20741
|
-
*
|
|
20788
|
+
* and, for key login, remembers which key worked. With `candidate` (the
|
|
20789
|
+
* settings page's 「使用」 on a local key) the form is tried as it is and
|
|
20790
|
+
* stored only once the login works.
|
|
20742
20791
|
*/
|
|
20743
|
-
test(signal) {
|
|
20792
|
+
test(signal, candidate) {
|
|
20744
20793
|
return this.exclusive(async () => {
|
|
20745
20794
|
let session;
|
|
20746
20795
|
try {
|
|
20747
|
-
const s = await this.settings();
|
|
20748
|
-
session = await SshSession.open(await targetOf(s, this.keys), signal);
|
|
20796
|
+
const s = candidate ?? await this.settings();
|
|
20797
|
+
session = await SshSession.open(await targetOf(s, this.keys, this.config.localKeys), signal);
|
|
20749
20798
|
const asRoot = s.username === "root";
|
|
20750
20799
|
const cmd = `echo "uid=$(${asRoot ? "" : `sudo -S -p '' `}id -u)"; echo "sys=$(uname -srm)"`;
|
|
20751
20800
|
const res = await session.exec(cmd, asRoot ? "" : `${s.password}
|
|
20752
20801
|
`, signal);
|
|
20753
20802
|
const uid = /^uid=(.*)$/m.exec(res.stdout)?.[1];
|
|
20754
20803
|
const system = /^sys=(.*)$/m.exec(res.stdout)?.[1] ?? "";
|
|
20755
|
-
await this.pin(s, session.hostKey);
|
|
20756
20804
|
const root = uid === "0";
|
|
20805
|
+
const hostKey = session.hostKey;
|
|
20806
|
+
const login = s.auth === "key" && session.keyLabel ? { loginKey: session.keyLabel, loggedInAt: (/* @__PURE__ */ new Date()).toISOString() } : {};
|
|
20807
|
+
if (candidate) {
|
|
20808
|
+
if (root) await this.store.update(() => ({ ...candidate, hostKey: candidate.hostKey ?? (hostKey || void 0), ...login }));
|
|
20809
|
+
} else {
|
|
20810
|
+
await this.pin(s, hostKey);
|
|
20811
|
+
if (root && login.loginKey) {
|
|
20812
|
+
await this.store.update((cur) => cur && cur.host === s.host && cur.port === s.port && cur.username === s.username ? { ...cur, ...login } : void 0);
|
|
20813
|
+
}
|
|
20814
|
+
}
|
|
20757
20815
|
return {
|
|
20758
20816
|
ok: root,
|
|
20759
20817
|
...root ? {} : { error: "sudo_failed", message: `cannot become root as ${s.username}: ${res.stderr.trim()}` },
|
|
20760
|
-
result: { hostKey
|
|
20818
|
+
result: { hostKey, system, root, ...session.keyLabel ? { key: session.keyLabel } : {} }
|
|
20761
20819
|
};
|
|
20762
20820
|
} catch (err) {
|
|
20763
20821
|
return failure(err);
|
|
@@ -20789,7 +20847,9 @@ function parse(payload) {
|
|
|
20789
20847
|
protocols: protocols.length > 0 ? protocols : ["reality", "hy2"],
|
|
20790
20848
|
certMode: p.certMode === "ip" ? "ip" : "self",
|
|
20791
20849
|
linkAddress: typeof p.linkAddress === "string" ? p.linkAddress : void 0,
|
|
20792
|
-
hostKey: typeof p.hostKey === "string" ? p.hostKey : void 0
|
|
20850
|
+
hostKey: typeof p.hostKey === "string" ? p.hostKey : void 0,
|
|
20851
|
+
loginKey: typeof p.loginKey === "string" ? p.loginKey : void 0,
|
|
20852
|
+
loggedInAt: typeof p.loggedInAt === "string" ? p.loggedInAt : void 0
|
|
20793
20853
|
};
|
|
20794
20854
|
}
|
|
20795
20855
|
var toPayload = (s) => JSON.parse(JSON.stringify(s));
|
|
@@ -20895,14 +20955,95 @@ function notConfiguredMessage(lang) {
|
|
|
20895
20955
|
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
20956
|
}
|
|
20897
20957
|
|
|
20958
|
+
// src/local-keys.ts
|
|
20959
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
20960
|
+
import { homedir } from "node:os";
|
|
20961
|
+
import { isAbsolute, join } from "node:path";
|
|
20962
|
+
var DEFAULT_KEYS = ["dsh_vps_ed25519", "id_ed25519", "id_ecdsa", "id_rsa"];
|
|
20963
|
+
function readText(file) {
|
|
20964
|
+
try {
|
|
20965
|
+
return readFileSync(file, "utf8");
|
|
20966
|
+
} catch {
|
|
20967
|
+
return void 0;
|
|
20968
|
+
}
|
|
20969
|
+
}
|
|
20970
|
+
function configTexts(sshDir) {
|
|
20971
|
+
const texts = [];
|
|
20972
|
+
const main = readText(join(sshDir, "config"));
|
|
20973
|
+
if (main !== void 0) texts.push(main);
|
|
20974
|
+
let names = [];
|
|
20975
|
+
try {
|
|
20976
|
+
names = readdirSync(join(sshDir, "config.d")).sort();
|
|
20977
|
+
} catch {
|
|
20978
|
+
}
|
|
20979
|
+
for (const name2 of names) {
|
|
20980
|
+
const t = readText(join(sshDir, "config.d", name2));
|
|
20981
|
+
if (t !== void 0) texts.push(t);
|
|
20982
|
+
}
|
|
20983
|
+
return texts;
|
|
20984
|
+
}
|
|
20985
|
+
function identityFilesFor(host, sshDir, home) {
|
|
20986
|
+
const files = [];
|
|
20987
|
+
for (const text of configTexts(sshDir)) {
|
|
20988
|
+
let patterns = [];
|
|
20989
|
+
let hostName;
|
|
20990
|
+
let identities = [];
|
|
20991
|
+
const flush = () => {
|
|
20992
|
+
const literal = patterns.filter((p) => !/[*?!]/.test(p));
|
|
20993
|
+
if (literal.length > 0 && (hostName === host || literal.includes(host))) {
|
|
20994
|
+
files.push(...identities);
|
|
20995
|
+
}
|
|
20996
|
+
};
|
|
20997
|
+
for (const raw of text.split("\n")) {
|
|
20998
|
+
const line = raw.replace(/\s+#.*$/, "").trim();
|
|
20999
|
+
const m = /^(\w+)\s*=?\s*(.+)$/.exec(line);
|
|
21000
|
+
if (!m || line.startsWith("#")) continue;
|
|
21001
|
+
const key = m[1].toLowerCase();
|
|
21002
|
+
const value = m[2].trim().replace(/^"(.*)"$/, "$1");
|
|
21003
|
+
if (key === "host" || key === "match") {
|
|
21004
|
+
flush();
|
|
21005
|
+
patterns = key === "host" ? value.split(/\s+/) : [];
|
|
21006
|
+
hostName = void 0;
|
|
21007
|
+
identities = [];
|
|
21008
|
+
} else if (key === "hostname") {
|
|
21009
|
+
hostName = value;
|
|
21010
|
+
} else if (key === "identityfile") {
|
|
21011
|
+
const expanded = value.startsWith("~/") ? join(home, value.slice(2)) : value;
|
|
21012
|
+
identities.push(isAbsolute(expanded) ? expanded : join(sshDir, expanded));
|
|
21013
|
+
}
|
|
21014
|
+
}
|
|
21015
|
+
flush();
|
|
21016
|
+
}
|
|
21017
|
+
return files;
|
|
21018
|
+
}
|
|
21019
|
+
function localKeysFor(host, opts = {}) {
|
|
21020
|
+
const home = opts.home ?? homedir();
|
|
21021
|
+
const sshDir = opts.sshDir ?? join(home, ".ssh");
|
|
21022
|
+
const paths = [...identityFilesFor(host, sshDir, home), ...DEFAULT_KEYS.map((n) => join(sshDir, n))];
|
|
21023
|
+
const keys = [];
|
|
21024
|
+
for (const path of [...new Set(paths)]) {
|
|
21025
|
+
const key = readText(path);
|
|
21026
|
+
if (key === void 0 || !key.includes("PRIVATE KEY")) continue;
|
|
21027
|
+
let problem;
|
|
21028
|
+
try {
|
|
21029
|
+
problem = privateKeyProblem(key);
|
|
21030
|
+
} catch {
|
|
21031
|
+
problem = "unreadable";
|
|
21032
|
+
}
|
|
21033
|
+
if (problem) continue;
|
|
21034
|
+
keys.push({ key, label: path.startsWith(home) ? `~${path.slice(home.length)}` : path });
|
|
21035
|
+
}
|
|
21036
|
+
return keys;
|
|
21037
|
+
}
|
|
21038
|
+
|
|
20898
21039
|
// src/update.ts
|
|
20899
|
-
import { readFileSync } from "node:fs";
|
|
21040
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
20900
21041
|
var PACKAGE_NAME = "dsh-vpn";
|
|
20901
21042
|
var MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
20902
21043
|
var NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
20903
21044
|
function currentVersion() {
|
|
20904
21045
|
try {
|
|
20905
|
-
const pkg = JSON.parse(
|
|
21046
|
+
const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
20906
21047
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
20907
21048
|
} catch {
|
|
20908
21049
|
return "0.0.0";
|
|
@@ -21056,7 +21197,7 @@ function toToolValue(r, uiLang) {
|
|
|
21056
21197
|
function apply(ctx, config) {
|
|
21057
21198
|
const service = new VpnService(
|
|
21058
21199
|
new CredentialSettingsStore(ctx),
|
|
21059
|
-
{ timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider },
|
|
21200
|
+
{ timeoutMs: config.timeoutMs, acmeProvider: config.acmeProvider, localKeys: (host) => localKeysFor(host) },
|
|
21060
21201
|
new CredentialKeyStore(ctx)
|
|
21061
21202
|
);
|
|
21062
21203
|
const uiLang = new UiLang();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vpn",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "DeepSeek Harness plugin: deploy an encrypted VPN (sing-box) to your own VPS in one click, to protect your connections and privacy on untrusted networks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"anytls",
|