dsh-vpn 0.1.8 → 0.1.9
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 +25 -14
- package/lib/index.js +73 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ dsh plugin --profile web add dsh-vpn
|
|
|
45
45
|
|
|
46
46
|
放好后点「保存」和「测试连接」。已经有自己的私钥、服务器上也放好了对应公钥的,展开「改用我自己已有的私钥」粘贴进去即可(支持带密码短语的私钥)。用非 root 用户时,「sudo 密码」一栏填该用户的 sudo 密码;root 或免密 sudo 不用填。
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
**找回已有节点**:重装过 dsh 或 dsh-vpn 后,本地保存的设置会丢失,但 VPS 上的节点还在运行。在 VPN 设置里填好服务器 IP、端口、用户名和密码(或选密钥登录),**不用先保存**,直接点登录方式下面的「检查已有安装」:插件登录服务器读取仍在运行的节点,显示原来的二维码(链接和以前完全一样,已经导入手机或电脑客户端的配置继续可用),按服务器上的实际情况勾选协议和证书类型,并自动保存设置。登录失败时什么都不保存,填的内容也不会被清掉。
|
|
49
49
|
|
|
50
50
|
页面第一行显示插件版本,有新版本时旁边会提示,点「更新」即可升级插件,重启 dsh 后生效(需要 dsh 0.2 及以上;更早的版本请在「设置 → 插件」中更新)。为了供应链安全,发布不足 24 小时的新版本会等满 24 小时再提供更新。
|
|
51
51
|
|
package/lib/client.js
CHANGED
|
@@ -57,7 +57,7 @@ var vpnApi = {
|
|
|
57
57
|
status: () => post("/status"),
|
|
58
58
|
links: () => post("/links"),
|
|
59
59
|
/** Find an earlier installation on the server and adopt its protocols and certificate mode. */
|
|
60
|
-
detect: () => post("/detect"),
|
|
60
|
+
detect: (form) => post("/detect", form ?? {}),
|
|
61
61
|
install: (regenerate) => post("/install", { regenerate }),
|
|
62
62
|
uninstall: () => post("/uninstall"),
|
|
63
63
|
/** Installed version; with `check`, also the newest release on npm. */
|
|
@@ -157,6 +157,8 @@ var zh = {
|
|
|
157
157
|
qrSection: "\u8282\u70B9\u4E8C\u7EF4\u7801",
|
|
158
158
|
qrHint: "\u7528 v2rayN\u3001NekoBox\u3001Shadowrocket\u3001sing-box\u3001Clash Meta \u7B49\u5BA2\u6237\u7AEF\u626B\u63CF\u5BFC\u5165\u3002\u4E8C\u7EF4\u7801\u7B49\u540C\u4E8E\u5BC6\u7801\uFF0C\u8BF7\u52FF\u5916\u4F20\u3002",
|
|
159
159
|
detect: "\u68C0\u67E5\u5DF2\u6709\u5B89\u88C5",
|
|
160
|
+
detecting: "\u6B63\u5728\u8BFB\u53D6\u670D\u52A1\u5668\u2026",
|
|
161
|
+
detectHint: "\u91CD\u88C5\u8FC7 dsh \u6216\u672C\u63D2\u4EF6\uFF1F\u586B\u597D\u4E0A\u9762\u7684\u767B\u5F55\u4FE1\u606F\u76F4\u63A5\u70B9\u8FD9\u91CC\uFF08\u4E0D\u7528\u5148\u4FDD\u5B58\uFF09\uFF0C\u4F1A\u767B\u5F55\u670D\u52A1\u5668\u8BFB\u53D6\u4ECD\u5728\u8FD0\u884C\u7684\u8282\u70B9\uFF0C\u663E\u793A\u539F\u6765\u7684\u4E8C\u7EF4\u7801\uFF0C\u5E76\u81EA\u52A8\u4FDD\u5B58\u8BBE\u7F6E\u3002",
|
|
160
162
|
detectFound: "\u5DF2\u627E\u5230\u4E4B\u524D\u7684\u5B89\u88C5\uFF1Asing-box {version}\uFF0C\u534F\u8BAE\uFF1A{protocols}\uFF08{cert}\uFF09\u3002\u8282\u70B9\u4E8C\u7EF4\u7801\u5DF2\u663E\u793A\u5728\u4E0B\u65B9\uFF0C\u539F\u6709\u7684\u5BA2\u6237\u7AEF\u914D\u7F6E\u53EF\u4EE5\u7EE7\u7EED\u4F7F\u7528\u3002",
|
|
161
163
|
detectSynced: "\u5DF2\u6309\u670D\u52A1\u5668\u4E0A\u7684\u5B9E\u9645\u60C5\u51B5\u66F4\u65B0\u4E86\u534F\u8BAE\u52FE\u9009\u548C\u8BC1\u4E66\u7C7B\u578B\u3002",
|
|
162
164
|
detectNone: "\u8FD9\u53F0\u670D\u52A1\u5668\u4E0A\u6CA1\u6709\u627E\u5230\u4E4B\u524D\u7684\u5B89\u88C5\uFF0C\u53EF\u4EE5\u76F4\u63A5\u70B9\u300C\u5B89\u88C5 / \u66F4\u65B0\u300D\u3002",
|
|
@@ -286,6 +288,8 @@ var en = {
|
|
|
286
288
|
qrSection: "Node QR codes",
|
|
287
289
|
qrHint: "Scan with v2rayN, NekoBox, Shadowrocket, sing-box, Clash Meta and similar apps. A QR code is as secret as a password.",
|
|
288
290
|
detect: "Find existing install",
|
|
291
|
+
detecting: "Reading the server\u2026",
|
|
292
|
+
detectHint: "Reinstalled dsh or this plugin? Fill in the login details above and click here (no need to save first): it logs in, reads the node that is still running, shows its QR codes and saves the settings.",
|
|
289
293
|
detectFound: "Found an earlier installation: sing-box {version}, protocols: {protocols} ({cert}). Its QR codes are shown below; clients you already set up keep working.",
|
|
290
294
|
detectSynced: "The protocol selection and certificate type now match the server.",
|
|
291
295
|
detectNone: "No earlier installation was found on this server. Click Install / Update to set one up.",
|
|
@@ -379,6 +383,8 @@ var STYLES = `
|
|
|
379
383
|
.dshvpn-disclosure { align-self: flex-start; border: none; background: transparent; color: inherit; padding: 0; cursor: pointer; font-size: 12.5px; opacity: .75; }
|
|
380
384
|
.dshvpn-textarea { width: 100%; box-sizing: border-box; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; padding: 8px; border-radius: 8px; resize: vertical;
|
|
381
385
|
color: inherit; background: transparent; border: 1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3)); }
|
|
386
|
+
.dshvpn-detect { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding-top: 4px; }
|
|
387
|
+
.dshvpn-detect small { flex: 1; min-width: 220px; }
|
|
382
388
|
.dshvpn-section { display: flex; flex-direction: column; gap: 16px; }
|
|
383
389
|
.dshvpn-section-head h2 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
384
390
|
.dshvpn-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 4px; }
|
|
@@ -1305,12 +1311,12 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1305
1311
|
setBusy(null);
|
|
1306
1312
|
}
|
|
1307
1313
|
}
|
|
1308
|
-
const detect = async () => {
|
|
1309
|
-
await op("detect", api.detect, (
|
|
1310
|
-
const res =
|
|
1314
|
+
const detect = async (fromForm = true) => {
|
|
1315
|
+
const r = await op("detect", () => api.detect(fromForm ? form : void 0), (r2) => {
|
|
1316
|
+
const res = r2.result ?? {};
|
|
1311
1317
|
if (res.installed !== true) return { tone: "info", text: t("detectNone") };
|
|
1312
|
-
if (
|
|
1313
|
-
setLinks(
|
|
1318
|
+
if (r2.links) {
|
|
1319
|
+
setLinks(r2.links);
|
|
1314
1320
|
setRefreshedAt(/* @__PURE__ */ new Date());
|
|
1315
1321
|
}
|
|
1316
1322
|
const protocols = (res.protocols ?? []).map((id) => PROTOCOLS.find((p) => p.id === id)?.name ?? id).join("\u3001");
|
|
@@ -1319,10 +1325,12 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1319
1325
|
return { tone: "success", text: res.synced === true ? `${text}
|
|
1320
1326
|
${t("detectSynced")}` : text };
|
|
1321
1327
|
});
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1328
|
+
if (r.ok || r.links) {
|
|
1329
|
+
const s = await api.getSettings();
|
|
1330
|
+
if (s.ok) {
|
|
1331
|
+
setSaved(s.settings);
|
|
1332
|
+
setForm(formOf(s.settings));
|
|
1333
|
+
}
|
|
1326
1334
|
}
|
|
1327
1335
|
};
|
|
1328
1336
|
const save = async () => {
|
|
@@ -1332,7 +1340,7 @@ ${t("detectSynced")}` : text };
|
|
|
1332
1340
|
setForm(formOf(r2.settings));
|
|
1333
1341
|
return { tone: "success", text: t("saved") };
|
|
1334
1342
|
});
|
|
1335
|
-
if (r.ok && first) await detect();
|
|
1343
|
+
if (r.ok && first) await detect(false);
|
|
1336
1344
|
};
|
|
1337
1345
|
const refreshSettings = async () => {
|
|
1338
1346
|
const r = await api.getSettings();
|
|
@@ -1418,6 +1426,12 @@ ${t("cloudHint")}`;
|
|
|
1418
1426
|
] }, m)) })
|
|
1419
1427
|
] }),
|
|
1420
1428
|
form.auth === "key" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyPanel, { t, api, form, saved, set, setForm }),
|
|
1429
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-wide dshvpn-detect", children: [
|
|
1430
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: disabled || form.host.trim() === "", onClick: () => {
|
|
1431
|
+
void detect();
|
|
1432
|
+
}, children: busy === "detect" ? t("detecting") : t("detect") }),
|
|
1433
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("detectHint") })
|
|
1434
|
+
] }),
|
|
1421
1435
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-wide", children: [
|
|
1422
1436
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("linkAddress") }),
|
|
1423
1437
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: form.linkAddress, placeholder: form.host, onChange: (e) => {
|
|
@@ -1506,9 +1520,6 @@ ${t("cloudHint")}`;
|
|
|
1506
1520
|
children: t("test")
|
|
1507
1521
|
}
|
|
1508
1522
|
),
|
|
1509
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", disabled: disabled || needsSave, onClick: () => {
|
|
1510
|
-
void detect();
|
|
1511
|
-
}, children: busy === "detect" ? t("working") : t("detect") }),
|
|
1512
1523
|
dirty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-warn", children: t("unsaved") })
|
|
1513
1524
|
] }),
|
|
1514
1525
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
|
package/lib/index.js
CHANGED
|
@@ -20483,7 +20483,12 @@ function apiHandlers(service, uiLang, onUiLang, updater) {
|
|
|
20483
20483
|
"/test": (_body, signal) => service.test(signal),
|
|
20484
20484
|
"/status": (_body, signal) => service.status(signal),
|
|
20485
20485
|
"/links": (_body, signal) => service.links(signal),
|
|
20486
|
-
|
|
20486
|
+
// With form fields: try them as they are (no save needed first); they are
|
|
20487
|
+
// stored only once the login works.
|
|
20488
|
+
"/detect": async (body, signal) => {
|
|
20489
|
+
if (typeof body.host !== "string") return service.detect(signal);
|
|
20490
|
+
return service.detect(signal, applyInput(await service.store.get(), body));
|
|
20491
|
+
},
|
|
20487
20492
|
"/install": (body, signal) => service.install({ regenerate: body.regenerate === true }, signal),
|
|
20488
20493
|
"/uninstall": (_body, signal) => service.uninstall(signal)
|
|
20489
20494
|
};
|
|
@@ -20630,31 +20635,42 @@ var VpnService = class {
|
|
|
20630
20635
|
return this.exclusive(async () => {
|
|
20631
20636
|
try {
|
|
20632
20637
|
const s = await this.settings();
|
|
20633
|
-
const
|
|
20634
|
-
await this.pin(s,
|
|
20635
|
-
|
|
20636
|
-
const sudo = /incorrect password|a password is required|not in the sudoers|may not run sudo/i.test(res.log);
|
|
20637
|
-
return {
|
|
20638
|
-
ok: false,
|
|
20639
|
-
error: sudo ? "sudo_failed" : "no_result",
|
|
20640
|
-
message: sudo ? `sudo refused for ${s.username}: ${res.log}` : `script did not report a result (exit ${res.exitCode}): ${res.log}`,
|
|
20641
|
-
log: res.log
|
|
20642
|
-
};
|
|
20643
|
-
}
|
|
20644
|
-
const r = res.result;
|
|
20645
|
-
return {
|
|
20646
|
-
ok: r.ok === true,
|
|
20647
|
-
...typeof r.error === "string" ? { error: r.error } : {},
|
|
20648
|
-
...typeof r.message === "string" ? { message: r.message } : {},
|
|
20649
|
-
result: r,
|
|
20650
|
-
...r.links ? { links: r.links } : {},
|
|
20651
|
-
log: res.log
|
|
20652
|
-
};
|
|
20638
|
+
const { op, hostKey } = await this.runOn(s, action, args(s), signal);
|
|
20639
|
+
await this.pin(s, hostKey);
|
|
20640
|
+
return op;
|
|
20653
20641
|
} catch (err) {
|
|
20654
20642
|
return failure(err);
|
|
20655
20643
|
}
|
|
20656
20644
|
});
|
|
20657
20645
|
}
|
|
20646
|
+
/** Run the script against `s` (stored or not); throws SSH and config errors. */
|
|
20647
|
+
async runOn(s, action, args, signal) {
|
|
20648
|
+
const res = await runVpnScript(action, args, await targetOf(s, this.keys), signal, this.config.timeoutMs);
|
|
20649
|
+
if (!res.result) {
|
|
20650
|
+
const sudo = /incorrect password|a password is required|not in the sudoers|may not run sudo/i.test(res.log);
|
|
20651
|
+
return {
|
|
20652
|
+
hostKey: res.hostKey,
|
|
20653
|
+
op: {
|
|
20654
|
+
ok: false,
|
|
20655
|
+
error: sudo ? "sudo_failed" : "no_result",
|
|
20656
|
+
message: sudo ? `sudo refused for ${s.username}: ${res.log}` : `script did not report a result (exit ${res.exitCode}): ${res.log}`,
|
|
20657
|
+
log: res.log
|
|
20658
|
+
}
|
|
20659
|
+
};
|
|
20660
|
+
}
|
|
20661
|
+
const r = res.result;
|
|
20662
|
+
return {
|
|
20663
|
+
hostKey: res.hostKey,
|
|
20664
|
+
op: {
|
|
20665
|
+
ok: r.ok === true,
|
|
20666
|
+
...typeof r.error === "string" ? { error: r.error } : {},
|
|
20667
|
+
...typeof r.message === "string" ? { message: r.message } : {},
|
|
20668
|
+
result: r,
|
|
20669
|
+
...r.links ? { links: r.links } : {},
|
|
20670
|
+
log: res.log
|
|
20671
|
+
}
|
|
20672
|
+
};
|
|
20673
|
+
}
|
|
20658
20674
|
install(opts, signal) {
|
|
20659
20675
|
return this.run("install", (s) => {
|
|
20660
20676
|
const protocols = resolveProtocols(opts.protocols, s.protocols);
|
|
@@ -20678,32 +20694,44 @@ var VpnService = class {
|
|
|
20678
20694
|
return this.run("links", (s) => ["--server", s.linkAddress ?? s.host], signal);
|
|
20679
20695
|
}
|
|
20680
20696
|
/**
|
|
20681
|
-
* Find an existing installation (e.g.
|
|
20682
|
-
* settings
|
|
20683
|
-
*
|
|
20684
|
-
*
|
|
20697
|
+
* Find an existing installation (e.g. dsh or dsh-vpn was reinstalled and the
|
|
20698
|
+
* local settings are gone, but the node still runs). `candidate` is what the
|
|
20699
|
+
* settings page holds, saved or not: it is stored only once login works, with
|
|
20700
|
+
* the server's protocols and certificate mode adopted so that a later
|
|
20701
|
+
* 安装 / 更新 keeps the node, its keys and its links as they are.
|
|
20685
20702
|
*/
|
|
20686
|
-
|
|
20687
|
-
|
|
20688
|
-
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
20692
|
-
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
|
|
20696
|
-
|
|
20697
|
-
|
|
20698
|
-
|
|
20703
|
+
detect(signal, candidate) {
|
|
20704
|
+
return this.exclusive(async () => {
|
|
20705
|
+
try {
|
|
20706
|
+
const s = candidate ?? await this.settings();
|
|
20707
|
+
const status = await this.runOn(s, "status", [], signal);
|
|
20708
|
+
if (!status.op.ok) return status.op;
|
|
20709
|
+
const r = status.op.result ?? {};
|
|
20710
|
+
const installed = r.installed === true;
|
|
20711
|
+
const protocols = installed ? String(r.protocols ?? "").split(",").filter(isProtocolId) : [];
|
|
20712
|
+
const mode = r.cert?.mode;
|
|
20713
|
+
const certMode = installed && (mode === "ip" || mode === "self") ? mode : void 0;
|
|
20714
|
+
const before = candidate ?? s;
|
|
20715
|
+
const next = {
|
|
20716
|
+
...before,
|
|
20717
|
+
...protocols.length > 0 ? { protocols } : {},
|
|
20718
|
+
...certMode ? { certMode } : {},
|
|
20719
|
+
hostKey: before.hostKey ?? (status.hostKey || void 0)
|
|
20720
|
+
};
|
|
20721
|
+
const synced = next.certMode !== before.certMode || next.protocols.join() !== before.protocols.join();
|
|
20722
|
+
await this.store.update((cur) => candidate || cur ? next : void 0);
|
|
20723
|
+
if (!installed) return { ok: true, result: { installed: false } };
|
|
20724
|
+
const links = await this.runOn(next, "links", ["--server", next.linkAddress ?? next.host], signal);
|
|
20725
|
+
return {
|
|
20726
|
+
ok: links.op.ok,
|
|
20727
|
+
...links.op.error ? { error: links.op.error, message: links.op.message } : {},
|
|
20728
|
+
result: { installed: true, active: r.active, version: r.version, protocols, certMode, synced },
|
|
20729
|
+
...links.op.links ? { links: links.op.links } : {}
|
|
20730
|
+
};
|
|
20731
|
+
} catch (err) {
|
|
20732
|
+
return failure(err);
|
|
20733
|
+
}
|
|
20699
20734
|
});
|
|
20700
|
-
const links = await this.links(signal);
|
|
20701
|
-
return {
|
|
20702
|
-
ok: links.ok,
|
|
20703
|
-
...links.error ? { error: links.error, message: links.message } : {},
|
|
20704
|
-
result: { installed: true, active: r.active, version: r.version, protocols, certMode, synced },
|
|
20705
|
-
...links.links ? { links: links.links } : {}
|
|
20706
|
-
};
|
|
20707
20735
|
}
|
|
20708
20736
|
uninstall(signal) {
|
|
20709
20737
|
return this.run("uninstall", () => [], signal);
|
package/package.json
CHANGED