dsh-vpn 0.1.11 → 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 +1 -1
- package/lib/client.js +68 -9
- package/lib/index.js +36 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ dsh plugin --profile web add dsh-vpn
|
|
|
44
44
|
2. 本机已有的钥匙:`~/.ssh/config`(含 `config.d`)里为这台服务器配置的 `IdentityFile`,dsh-vps-manager 的 `~/.ssh/dsh_vps_ed25519`,以及 `~/.ssh/id_ed25519`、`id_ecdsa`、`id_rsa`;
|
|
45
45
|
3. 插件的专用钥匙(纯 JS 生成,不需要本机的 ssh-keygen)。
|
|
46
46
|
|
|
47
|
-
**已经用 dsh-vps-manager
|
|
47
|
+
**已经用 dsh-vps-manager 管理的服务器不需要再放任何公钥**:它的钥匙已经在服务器上。填好 IP,「登录方式」选「密钥」,页面会列出本机找到的钥匙,点这张卡片右上角的「使用」就登录了(不用先保存,登录成功后自动保存)。卡片随后一直显示「已登录:用哪把钥匙登录 用户@服务器」,之后安装、检查和刷新二维码都优先用这把钥匙;改了 IP、端口或用户名,登录状态自动作废。本机钥匙都不被认可时,再把页面上显示的专用钥匙公钥放到服务器上(服务商后台的「SSH 密钥」,或在能登录的服务器上执行页面给出的一行命令)。私钥只在本机 dsh 里读取,不会发给页面;带密码短语的本机私钥会跳过。用非 root 用户时,「sudo 密码」一栏填该用户的 sudo 密码;root 或免密 sudo 不用填。
|
|
48
48
|
|
|
49
49
|
**找回已有节点**:重装过 dsh 或 dsh-vpn 后,本地保存的设置会丢失,但 VPS 上的节点还在运行。在 VPN 设置里填好服务器 IP、端口、用户名和密码(或选密钥登录),**不用先保存**,直接点登录方式下面的「检查已有安装」:插件登录服务器读取仍在运行的节点,显示原来的二维码(链接和以前完全一样,已经导入手机或电脑客户端的配置继续可用),按服务器上的实际情况勾选协议和证书类型,并自动保存设置。登录失败时什么都不保存,填的内容也不会被清掉。
|
|
50
50
|
|
package/lib/client.js
CHANGED
|
@@ -54,6 +54,8 @@ var vpnApi = {
|
|
|
54
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. */
|
|
@@ -99,7 +101,11 @@ var zh = {
|
|
|
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",
|
|
100
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",
|
|
101
103
|
localKeysFound: "\u672C\u673A\u5DF2\u6709\u7684\u94A5\u5319\uFF08\u4F1A\u5148\u7528\u5B83\u4EEC\u767B\u5F55\uFF09",
|
|
102
|
-
|
|
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",
|
|
103
109
|
testOkKey: "\uFF08\u7528 {key} \u767B\u5F55\uFF09",
|
|
104
110
|
dedicatedKey: "\u63D2\u4EF6\u4E13\u7528\u94A5\u5319",
|
|
105
111
|
keyWayConsole: "A. \u670D\u52A1\u5546\u540E\u53F0\u6DFB\u52A0\uFF08\u65B0\u673A\u5668\u63A8\u8350\uFF09",
|
|
@@ -235,7 +241,11 @@ var en = {
|
|
|
235
241
|
keyIntro: "Log in with the plugin's dedicated key. Put the public key below on the server, either way:",
|
|
236
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:",
|
|
237
243
|
localKeysFound: "Keys already on this computer (tried first)",
|
|
238
|
-
|
|
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.",
|
|
239
249
|
testOkKey: " (logged in with {key})",
|
|
240
250
|
dedicatedKey: "the plugin's dedicated key",
|
|
241
251
|
keyWayConsole: "A. Add it in your provider's console (recommended for new machines)",
|
|
@@ -395,6 +405,8 @@ var STYLES = `
|
|
|
395
405
|
color: inherit; background: transparent; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
|
|
396
406
|
.dshvpn-detect { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding-top: 4px; }
|
|
397
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); }
|
|
398
410
|
.dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
|
|
399
411
|
.dshvpn-section-head h2 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
400
412
|
.dshvpn-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
|
|
@@ -1180,7 +1192,19 @@ function Copyable({ text, label, t }) {
|
|
|
1180
1192
|
}, children: copied ? t("copied") : label })
|
|
1181
1193
|
] });
|
|
1182
1194
|
}
|
|
1183
|
-
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
|
+
};
|
|
1184
1208
|
const [key, setKey] = (0, import_react.useState)(null);
|
|
1185
1209
|
const ownStored = saved.hasPrivateKey && !form.clearPrivateKey;
|
|
1186
1210
|
const [own, setOwn] = (0, import_react.useState)(ownStored);
|
|
@@ -1199,12 +1223,32 @@ function KeyPanel({ t, api, form, saved, set, setForm }) {
|
|
|
1199
1223
|
const local = key?.localKeys ?? [];
|
|
1200
1224
|
const preparing = t("working");
|
|
1201
1225
|
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.
|
|
1204
|
-
|
|
1205
|
-
|
|
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") })
|
|
1206
1250
|
] }),
|
|
1207
|
-
!own && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1251
|
+
!own && !loggedIn && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1208
1252
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshvpn-muted", children: local.length > 0 ? t("keyIntroFallback") : t("keyIntro") }),
|
|
1209
1253
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-keycard", children: [
|
|
1210
1254
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("keyWayConsole") }),
|
|
@@ -1447,7 +1491,22 @@ ${t("cloudHint")}`;
|
|
|
1447
1491
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: m === "key" ? t("authKey") : t("authPassword") })
|
|
1448
1492
|
] }, m)) })
|
|
1449
1493
|
] }),
|
|
1450
|
-
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
|
+
),
|
|
1451
1510
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-wide dshvpn-detect", children: [
|
|
1452
1511
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: disabled || form.host.trim() === "", onClick: () => {
|
|
1453
1512
|
void detect();
|
package/lib/index.js
CHANGED
|
@@ -20370,7 +20370,9 @@ var EMPTY_VIEW = {
|
|
|
20370
20370
|
protocols: [...DEFAULT_PROTOCOLS],
|
|
20371
20371
|
certMode: "self",
|
|
20372
20372
|
linkAddress: "",
|
|
20373
|
-
hostKey: null
|
|
20373
|
+
hostKey: null,
|
|
20374
|
+
loginKey: null,
|
|
20375
|
+
loggedInAt: null
|
|
20374
20376
|
};
|
|
20375
20377
|
function publicView(s) {
|
|
20376
20378
|
if (!s) return EMPTY_VIEW;
|
|
@@ -20385,7 +20387,9 @@ function publicView(s) {
|
|
|
20385
20387
|
protocols: s.protocols,
|
|
20386
20388
|
certMode: s.certMode,
|
|
20387
20389
|
linkAddress: s.linkAddress ?? "",
|
|
20388
|
-
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
|
|
20389
20393
|
};
|
|
20390
20394
|
}
|
|
20391
20395
|
function applyInput(current, input) {
|
|
@@ -20436,7 +20440,9 @@ function applyInput(current, input) {
|
|
|
20436
20440
|
protocols,
|
|
20437
20441
|
certMode,
|
|
20438
20442
|
linkAddress: linkAddress || void 0,
|
|
20439
|
-
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 } : {}
|
|
20440
20446
|
};
|
|
20441
20447
|
}
|
|
20442
20448
|
var MemoryKeyStore = class {
|
|
@@ -20500,6 +20506,12 @@ function apiHandlers(service, uiLang, onUiLang, updater) {
|
|
|
20500
20506
|
return { ok: true, settings: publicView(await service.store.get()) };
|
|
20501
20507
|
},
|
|
20502
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
|
+
},
|
|
20503
20515
|
"/status": (_body, signal) => service.status(signal),
|
|
20504
20516
|
"/links": (_body, signal) => service.links(signal),
|
|
20505
20517
|
// With form fields: try them as they are (no save needed first); they are
|
|
@@ -20617,6 +20629,8 @@ async function targetOf(s, keys, local = () => []) {
|
|
|
20617
20629
|
} catch {
|
|
20618
20630
|
}
|
|
20619
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));
|
|
20620
20634
|
return { ...base, keys: list };
|
|
20621
20635
|
}
|
|
20622
20636
|
function resolveProtocols(requested, fallback) {
|
|
@@ -20771,13 +20785,15 @@ var VpnService = class {
|
|
|
20771
20785
|
}
|
|
20772
20786
|
/**
|
|
20773
20787
|
* Log in, report who we are and whether root is reachable. Pins the host key
|
|
20774
|
-
*
|
|
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.
|
|
20775
20791
|
*/
|
|
20776
|
-
test(signal) {
|
|
20792
|
+
test(signal, candidate) {
|
|
20777
20793
|
return this.exclusive(async () => {
|
|
20778
20794
|
let session;
|
|
20779
20795
|
try {
|
|
20780
|
-
const s = await this.settings();
|
|
20796
|
+
const s = candidate ?? await this.settings();
|
|
20781
20797
|
session = await SshSession.open(await targetOf(s, this.keys, this.config.localKeys), signal);
|
|
20782
20798
|
const asRoot = s.username === "root";
|
|
20783
20799
|
const cmd = `echo "uid=$(${asRoot ? "" : `sudo -S -p '' `}id -u)"; echo "sys=$(uname -srm)"`;
|
|
@@ -20785,12 +20801,21 @@ var VpnService = class {
|
|
|
20785
20801
|
`, signal);
|
|
20786
20802
|
const uid = /^uid=(.*)$/m.exec(res.stdout)?.[1];
|
|
20787
20803
|
const system = /^sys=(.*)$/m.exec(res.stdout)?.[1] ?? "";
|
|
20788
|
-
await this.pin(s, session.hostKey);
|
|
20789
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
|
+
}
|
|
20790
20815
|
return {
|
|
20791
20816
|
ok: root,
|
|
20792
20817
|
...root ? {} : { error: "sudo_failed", message: `cannot become root as ${s.username}: ${res.stderr.trim()}` },
|
|
20793
|
-
result: { hostKey
|
|
20818
|
+
result: { hostKey, system, root, ...session.keyLabel ? { key: session.keyLabel } : {} }
|
|
20794
20819
|
};
|
|
20795
20820
|
} catch (err) {
|
|
20796
20821
|
return failure(err);
|
|
@@ -20822,7 +20847,9 @@ function parse(payload) {
|
|
|
20822
20847
|
protocols: protocols.length > 0 ? protocols : ["reality", "hy2"],
|
|
20823
20848
|
certMode: p.certMode === "ip" ? "ip" : "self",
|
|
20824
20849
|
linkAddress: typeof p.linkAddress === "string" ? p.linkAddress : void 0,
|
|
20825
|
-
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
|
|
20826
20853
|
};
|
|
20827
20854
|
}
|
|
20828
20855
|
var toPayload = (s) => JSON.parse(JSON.stringify(s));
|
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",
|