dsh-vpn 0.1.8 → 0.1.10
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 +30 -15
- 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; }
|
|
@@ -1268,6 +1274,7 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1268
1274
|
const [form, setForm] = (0, import_react.useState)(null);
|
|
1269
1275
|
const [busy, setBusy] = (0, import_react.useState)(null);
|
|
1270
1276
|
const [notice, setNotice] = (0, import_react.useState)(null);
|
|
1277
|
+
const [noticeFrom, setNoticeFrom] = (0, import_react.useState)(null);
|
|
1271
1278
|
const [links, setLinks] = (0, import_react.useState)(null);
|
|
1272
1279
|
const [refreshedAt, setRefreshedAt] = (0, import_react.useState)(null);
|
|
1273
1280
|
const [regenerate, setRegenerate] = (0, import_react.useState)(false);
|
|
@@ -1297,6 +1304,7 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1297
1304
|
async function op(name, call, onOk) {
|
|
1298
1305
|
setBusy(name);
|
|
1299
1306
|
setNotice(null);
|
|
1307
|
+
setNoticeFrom(name);
|
|
1300
1308
|
try {
|
|
1301
1309
|
const r = await call();
|
|
1302
1310
|
setNotice(r.ok ? onOk(r) : { tone: "error", text: errorText(t, r) });
|
|
@@ -1305,12 +1313,12 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1305
1313
|
setBusy(null);
|
|
1306
1314
|
}
|
|
1307
1315
|
}
|
|
1308
|
-
const detect = async () => {
|
|
1309
|
-
await op("detect", api.detect, (
|
|
1310
|
-
const res =
|
|
1316
|
+
const detect = async (fromForm = true) => {
|
|
1317
|
+
const r = await op("detect", () => api.detect(fromForm ? form : void 0), (r2) => {
|
|
1318
|
+
const res = r2.result ?? {};
|
|
1311
1319
|
if (res.installed !== true) return { tone: "info", text: t("detectNone") };
|
|
1312
|
-
if (
|
|
1313
|
-
setLinks(
|
|
1320
|
+
if (r2.links) {
|
|
1321
|
+
setLinks(r2.links);
|
|
1314
1322
|
setRefreshedAt(/* @__PURE__ */ new Date());
|
|
1315
1323
|
}
|
|
1316
1324
|
const protocols = (res.protocols ?? []).map((id) => PROTOCOLS.find((p) => p.id === id)?.name ?? id).join("\u3001");
|
|
@@ -1319,10 +1327,12 @@ function VpnSettingsPage({ view, t, api }) {
|
|
|
1319
1327
|
return { tone: "success", text: res.synced === true ? `${text}
|
|
1320
1328
|
${t("detectSynced")}` : text };
|
|
1321
1329
|
});
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1330
|
+
if (r.ok || r.links) {
|
|
1331
|
+
const s = await api.getSettings();
|
|
1332
|
+
if (s.ok) {
|
|
1333
|
+
setSaved(s.settings);
|
|
1334
|
+
setForm(formOf(s.settings));
|
|
1335
|
+
}
|
|
1326
1336
|
}
|
|
1327
1337
|
};
|
|
1328
1338
|
const save = async () => {
|
|
@@ -1332,7 +1342,7 @@ ${t("detectSynced")}` : text };
|
|
|
1332
1342
|
setForm(formOf(r2.settings));
|
|
1333
1343
|
return { tone: "success", text: t("saved") };
|
|
1334
1344
|
});
|
|
1335
|
-
if (r.ok && first) await detect();
|
|
1345
|
+
if (r.ok && first) await detect(false);
|
|
1336
1346
|
};
|
|
1337
1347
|
const refreshSettings = async () => {
|
|
1338
1348
|
const r = await api.getSettings();
|
|
@@ -1368,6 +1378,7 @@ ${t("cloudHint")}`;
|
|
|
1368
1378
|
return null;
|
|
1369
1379
|
});
|
|
1370
1380
|
const disabled = busy !== null;
|
|
1381
|
+
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 });
|
|
1371
1382
|
const needsSave = dirty || !saved.configured;
|
|
1372
1383
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn", children: [
|
|
1373
1384
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
|
|
@@ -1418,6 +1429,13 @@ ${t("cloudHint")}`;
|
|
|
1418
1429
|
] }, m)) })
|
|
1419
1430
|
] }),
|
|
1420
1431
|
form.auth === "key" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyPanel, { t, api, form, saved, set, setForm }),
|
|
1432
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-wide dshvpn-detect", children: [
|
|
1433
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: disabled || form.host.trim() === "", onClick: () => {
|
|
1434
|
+
void detect();
|
|
1435
|
+
}, children: busy === "detect" ? t("detecting") : t("detect") }),
|
|
1436
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-muted", children: t("detectHint") })
|
|
1437
|
+
] }),
|
|
1438
|
+
noticeFrom === "detect" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dshvpn-wide", children: noticeBox }),
|
|
1421
1439
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dshvpn-wide", children: [
|
|
1422
1440
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("linkAddress") }),
|
|
1423
1441
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Input, { value: form.linkAddress, placeholder: form.host, onChange: (e) => {
|
|
@@ -1506,9 +1524,6 @@ ${t("cloudHint")}`;
|
|
|
1506
1524
|
children: t("test")
|
|
1507
1525
|
}
|
|
1508
1526
|
),
|
|
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
1527
|
dirty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "dshvpn-warn", children: t("unsaved") })
|
|
1513
1528
|
] }),
|
|
1514
1529
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { children: [
|
|
@@ -1588,7 +1603,7 @@ ${fw.backend === "none" ? t("fwNone") : t("fwStatus", { backend: fw.backend, cou
|
|
|
1588
1603
|
}, children: t("uninstall") })
|
|
1589
1604
|
] })
|
|
1590
1605
|
] }),
|
|
1591
|
-
|
|
1606
|
+
noticeFrom !== "detect" && noticeBox,
|
|
1592
1607
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dshvpn-qr-section", children: [
|
|
1593
1608
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshvpn-row", children: [
|
|
1594
1609
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("qrSection") }),
|
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