benchgo-plugin 0.0.0-stage → 0.0.3
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/DEEPSEEK-LICENSE +21 -0
- package/LICENSE +21 -0
- package/NOTICE +17 -0
- package/PASEO-LICENSE +211 -0
- package/README.md +256 -2
- package/TOOL-CONFIG.md +33 -0
- package/VERIFICATION.md +297 -0
- package/assets/fonts/README.md +13 -0
- package/assets/fonts/manifest.json +437 -0
- package/assets/fonts/web/fangyuan-latin-238bc49130b1.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u30-0fe8d99641c3.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u32-202a573131a1.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u4e-cc2f8b20be49.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u4f-9189b5f2d321.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u50-e2560e003872.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u51-a9c5e7a8c7c4.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u52-0cfd00b473c8.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u53-18122a07f60a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u54-8ca41aa9cff2.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u55-7bd55e14647e.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u56-26f093e8a810.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u57-36bde83a8378.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u58-d1df14217098.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u59-4e41ab22c51a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5a-746a367174e6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5b-3d1dd404d70c.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5c-fe0a273f69e7.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5d-af1f0a1c8d2d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5e-598c65420e17.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u5f-a92eaaa5265a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u60-48ee3d26462d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u61-7a57f35bc077.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u62-d2d0b7b36631.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u63-cade7c6635e6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u64-ed34e40d7f65.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u65-e13e83e69c38.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u66-1dbcfa5da147.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u67-6b0a26b82da6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u68-1b8217cfceed.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u69-673e63627790.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6a-3c94008a4640.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6b-44572c410fa0.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6c-1b323b366e88.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6d-cbc376d6fe3f.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6e-7f7e211e2fee.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u6f-ff280ce052fa.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u70-cb727eaf73b6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u71-0bb1747439fe.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u72-cccd39590d69.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u73-e67705133584.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u74-03a4e7ac9df5.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u75-a2d30b327e65.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u76-c08fe029c988.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u77-76f03be1f4cb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u78-5b721deaf582.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u79-db7713c2eda4.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7a-3e40d3726a09.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7b-a8204d1c9dba.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7c-89bcdae046de.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7d-bcc45fa6351a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7e-49fab96d9269.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u7f-dc110e1ecbf3.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u80-c5ce9be0335e.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u81-b026091b3779.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u82-a7c6960e875d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u83-4b7d1c2c50d4.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u84-4119128100c8.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u85-ca43dd4c5411.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u86-54fc0e3158fb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u87-b64f5eb55720.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u88-00feb1251f08.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u89-db419435b316.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8a-06c11d45fb87.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8b-5c2f6fda436a.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8c-2b46d2d1f4cc.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8d-1a67d064000d.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8e-eb87baf0091c.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u8f-96c1f0632da6.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u90-86752dba8cbe.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u91-0573b8548552.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u92-f85ecf735974.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u93-e7e0451735db.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u94-f3957ff98bcd.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u95-b31129d29013.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u96-ca7f779c2c13.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u97-100a25198b2c.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u98-59e4b784a94f.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u99-d35af6e30868.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9a-1eb6f7cdfcfe.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9b-8f428d81dcbb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9c-e10f7f8f0274.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9e-2be65003b759.woff2 +0 -0
- package/assets/fonts/web/fangyuan-u9f-02c3d5224fdb.woff2 +0 -0
- package/assets/fonts/web/fangyuan-ufb-3eaee54bc476.woff2 +0 -0
- package/assets/fonts/web/fangyuan-ufe-f64da8a3acce.woff2 +0 -0
- package/assets/fonts/web/fangyuan-uff-0a6dcfb689fd.woff2 +0 -0
- package/client/assets/iconsax/LICENSE +16 -0
- package/client/assets/iconsax/SOURCE.md +14 -0
- package/config/routergo-deepseek.example.yml +25 -0
- package/config.example.json +24 -0
- package/cordis.patch.yml +5 -0
- package/lib/LINK-THIRD-PARTY-LICENSES.txt +1197 -0
- package/lib/client.js +3577 -0
- package/lib/link-web/app.css +92 -0
- package/lib/link-web/app.js +599 -0
- package/lib/link-web/index.html +1 -0
- package/lib/link-web/workbench.js +118 -0
- package/lib/link.mjs +244 -0
- package/lib/relay/ip-certificate.mjs +559 -0
- package/lib/relay/relay.example.json +8 -0
- package/lib/relay/run-relay-server.cjs +4100 -0
- package/lib/server/bootstrap.mjs +147 -0
- package/lib/server/bootstrap.sh +39 -0
- package/lib/server/manifest.json +8 -0
- package/lib/server/server-seed.tar.gz +0 -0
- package/locale/en.json +6 -0
- package/locale/zh-CN.json +6 -0
- package/openapi.json +629 -0
- package/package.json +104 -4
- package/shared/plugin-install.mjs +198 -0
- package/shared/tool-quick-settings.mjs +70 -0
- package/shared/update-feed.mjs +191 -0
- package/shared/update-public-key.pem +3 -0
- package/src/cli.mjs +102 -0
- package/src/commands.mjs +17 -0
- package/src/delegation.mjs +47 -0
- package/src/fonts.mjs +17 -0
- package/src/harness-adapter.mjs +228 -0
- package/src/harness-commands.mjs +39 -0
- package/src/harness-selection.mjs +5 -0
- package/src/harness-usage.mjs +31 -0
- package/src/isolated-launch.mjs +22 -0
- package/src/line-process.mjs +114 -0
- package/src/link-plugin.mjs +21 -0
- package/src/mcp.mjs +21 -0
- package/src/model-providers.mjs +125 -0
- package/src/models.mjs +37 -0
- package/src/native-auth.mjs +32 -0
- package/src/picker-catalog.mjs +82 -0
- package/src/plugin.mjs +138 -0
- package/src/projects.mjs +96 -0
- package/src/provider-access.mjs +11 -0
- package/src/providers/acp.mjs +138 -0
- package/src/providers/claude.mjs +115 -0
- package/src/providers/codex.mjs +134 -0
- package/src/providers/opencode.mjs +138 -0
- package/src/providers/pi-permissions.ts +24 -0
- package/src/providers/pi.mjs +97 -0
- package/src/questions.mjs +34 -0
- package/src/recent-workspace.mjs +36 -0
- package/src/runtime.mjs +421 -0
- package/src/session-routes.mjs +89 -0
- package/src/store.mjs +98 -0
- package/src/tool-catalog.mjs +22 -0
- package/src/tool-config.mjs +94 -0
- package/src/tool-install.mjs +31 -0
- package/src/tool-profiles.mjs +114 -0
- package/src/tooling.mjs +90 -0
- package/src/updates.mjs +156 -0
- package/src/usage-import-parsers.mjs +37 -0
- package/src/usage-import.mjs +114 -0
- package/src/usage-timeline.mjs +36 -0
- package/src/usage.mjs +142 -0
- package/src/web.mjs +274 -0
- package/voice/README.md +49 -0
- package/voice/STREAMING.md +141 -0
- package/voice/VERIFICATION.md +36 -0
- package/voice/client/audio-worklet.js +35 -0
- package/voice/locale/en.json +1 -0
- package/voice/locale/zh-CN.json +1 -0
- package/voice/src/asr.mjs +44 -0
- package/voice/src/calls.mjs +37 -0
- package/voice/src/config.mjs +139 -0
- package/voice/src/media.mjs +70 -0
- package/voice/src/plugin.mjs +10 -0
- package/voice/src/realtime-protocols.mjs +30 -0
- package/voice/src/realtime-socket.mjs +86 -0
- package/voice/src/realtime.mjs +130 -0
- package/voice/src/tts.mjs +16 -0
- package/voice/src/web.mjs +171 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// plugins/link/scripts/ip-certificate.mjs
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash, randomUUID, X509Certificate, createPrivateKey } from "node:crypto";
|
|
6
|
+
import { access, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { isIP } from "node:net";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
var MIN_CERTBOT = [5, 4, 0];
|
|
12
|
+
var CERT_NAME = "dsh-link-ip";
|
|
13
|
+
var TOOL_DIR = "dsh-link-ip-certificate";
|
|
14
|
+
var TIMER_SERVICE = "benchgo-relay-cert-renew.service";
|
|
15
|
+
var TIMER_UNIT = "benchgo-relay-cert-renew.timer";
|
|
16
|
+
var RELAY_UNIT = "benchgo-relay.service";
|
|
17
|
+
var UNIT_MARKER = "# Managed by DSH Link IP certificate tool.";
|
|
18
|
+
var TIMER_MANIFEST = ".dsh-link-ip-certificate-renew.json";
|
|
19
|
+
var fail = (message) => {
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
};
|
|
22
|
+
function options(argv) {
|
|
23
|
+
const [operation, ...rest] = argv;
|
|
24
|
+
if (!["issue", "renew", "dry-run", "status", "install-timer", "uninstall-timer"].includes(operation)) fail("\u7528\u6CD5\uFF1Aip-certificate.mjs <issue|renew|dry-run|status|install-timer|uninstall-timer> [\u9009\u9879]");
|
|
25
|
+
const allowed = /* @__PURE__ */ new Set(["--ip", "--domain", "--email", "--webroot", "--state-dir", "--challenge-port", "--relay-port", "--confirm-public-ip", "--certbot", "--restart-relay-unit"]);
|
|
26
|
+
const values = {};
|
|
27
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
28
|
+
const key = rest[i], value = rest[i + 1];
|
|
29
|
+
if (!allowed.has(key) || key in values || !value || value.startsWith("--")) fail("\u8BC1\u4E66\u53C2\u6570\u65E0\u6548\uFF1A" + key);
|
|
30
|
+
values[key] = value;
|
|
31
|
+
}
|
|
32
|
+
if (operation === "uninstall-timer") {
|
|
33
|
+
if (Object.keys(values).length) fail("uninstall-timer \u4E0D\u63A5\u53D7\u53C2\u6570");
|
|
34
|
+
return { operation, values };
|
|
35
|
+
}
|
|
36
|
+
if (!values["--state-dir"]) fail("\u5FC5\u987B\u6307\u5B9A --state-dir");
|
|
37
|
+
if (["status", "install-timer"].includes(operation) && Object.keys(values).some((key) => key !== "--state-dir")) fail(operation + " \u53EA\u63A5\u53D7 --state-dir");
|
|
38
|
+
if (!["status", "install-timer"].includes(operation)) {
|
|
39
|
+
for (const key of ["--webroot", "--challenge-port", "--relay-port", "--confirm-public-ip"]) if (!values[key]) fail("\u5FC5\u987B\u6307\u5B9A " + key);
|
|
40
|
+
}
|
|
41
|
+
if (!["status", "install-timer"].includes(operation) && (!values["--ip"] && !values["--domain"] || values["--ip"] && values["--domain"])) fail("\u5FC5\u987B\u6307\u5B9A IP \u6216\u57DF\u540D");
|
|
42
|
+
if (values["--restart-relay-unit"] && (operation !== "renew" || values["--restart-relay-unit"] !== RELAY_UNIT)) fail("\u7EED\u671F\u53EA\u5141\u8BB8\u91CD\u542F\u672C\u5DE5\u5177\u7BA1\u7406\u7684 " + RELAY_UNIT);
|
|
43
|
+
return { operation, values };
|
|
44
|
+
}
|
|
45
|
+
function portNumber(value) {
|
|
46
|
+
if (!/^\d{1,5}$/.test(value ?? "")) fail("\u7AEF\u53E3\u5FC5\u987B\u4F7F\u7528\u5341\u8FDB\u5236\u6570\u5B57");
|
|
47
|
+
return Number(value);
|
|
48
|
+
}
|
|
49
|
+
function ipv4Number(ip) {
|
|
50
|
+
return ip.split(".").reduce((value, item) => value << 8n | BigInt(item), 0n);
|
|
51
|
+
}
|
|
52
|
+
function inCidr(value, base, bits, width) {
|
|
53
|
+
return value >> BigInt(width - bits) === base >> BigInt(width - bits);
|
|
54
|
+
}
|
|
55
|
+
function ipv6Number(ip) {
|
|
56
|
+
let value = ip.toLowerCase();
|
|
57
|
+
if (value.includes(".")) {
|
|
58
|
+
const index = value.lastIndexOf(":");
|
|
59
|
+
const v4 = value.slice(index + 1);
|
|
60
|
+
if (isIP(v4) !== 4) return null;
|
|
61
|
+
const n = ipv4Number(v4);
|
|
62
|
+
value = value.slice(0, index) + ":" + (n >> 16n).toString(16) + ":" + (n & 0xffffn).toString(16);
|
|
63
|
+
}
|
|
64
|
+
const halves = value.split("::");
|
|
65
|
+
const left = halves[0] ? halves[0].split(":") : [];
|
|
66
|
+
const right = halves.length > 1 && halves[1] ? halves[1].split(":") : [];
|
|
67
|
+
if (halves.length > 2) return null;
|
|
68
|
+
const groups = halves.length === 1 ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
69
|
+
if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/.test(group))) return null;
|
|
70
|
+
return groups.reduce((number, group) => number << 16n | BigInt("0x" + group), 0n);
|
|
71
|
+
}
|
|
72
|
+
function validateConfig(input) {
|
|
73
|
+
const ip = input.ip;
|
|
74
|
+
const domain = input.domain;
|
|
75
|
+
if (domain) {
|
|
76
|
+
if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(domain)) fail("\u57DF\u540D\u683C\u5F0F\u65E0\u6548");
|
|
77
|
+
if (input.confirmPublicIp !== true) fail("\u8BF7\u786E\u8BA4\u57DF\u540D\u5DF2\u89E3\u6790\u5230\u6B64\u670D\u52A1\u5668");
|
|
78
|
+
}
|
|
79
|
+
const family = typeof ip === "string" ? isIP(ip) : 0;
|
|
80
|
+
if (!family && !domain) fail("\u5FC5\u987B\u586B\u5199\u6709\u6548\u7684 IP \u5730\u5740\u6216\u57DF\u540D");
|
|
81
|
+
if (input.confirmPublicIp !== true) fail("\u8BF7\u786E\u8BA4\u8BE5 IP \u662F\u6B64\u670D\u52A1\u5668\u5B9E\u9645\u4F7F\u7528\u7684\u516C\u7F51 IP");
|
|
82
|
+
if (domain) {
|
|
83
|
+
} else if (family === 4) {
|
|
84
|
+
const value = ipv4Number(ip);
|
|
85
|
+
const blocked = [
|
|
86
|
+
["0.0.0.0", 8],
|
|
87
|
+
["10.0.0.0", 8],
|
|
88
|
+
["100.64.0.0", 10],
|
|
89
|
+
["127.0.0.0", 8],
|
|
90
|
+
["169.254.0.0", 16],
|
|
91
|
+
["172.16.0.0", 12],
|
|
92
|
+
["192.0.0.0", 24],
|
|
93
|
+
["192.0.2.0", 24],
|
|
94
|
+
["192.88.99.0", 24],
|
|
95
|
+
["192.168.0.0", 16],
|
|
96
|
+
["198.18.0.0", 15],
|
|
97
|
+
["198.51.100.0", 24],
|
|
98
|
+
["203.0.113.0", 24],
|
|
99
|
+
["224.0.0.0", 4],
|
|
100
|
+
["240.0.0.0", 4]
|
|
101
|
+
];
|
|
102
|
+
if (blocked.some(([base, bits]) => inCidr(value, ipv4Number(base), bits, 32))) fail("\u8BE5 IPv4 \u5730\u5740\u4E0D\u662F\u53EF\u7B7E\u53D1\u8BC1\u4E66\u7684\u516C\u7F51\u5355\u64AD\u5730\u5740");
|
|
103
|
+
} else {
|
|
104
|
+
const value = ipv6Number(ip);
|
|
105
|
+
if (value === null || value >> 125n !== 1n || inCidr(value, ipv6Number("2001::"), 23, 128) || inCidr(value, ipv6Number("2001:db8::"), 32, 128) || inCidr(value, ipv6Number("2001:20::"), 28, 128) || inCidr(value, ipv6Number("3fff::"), 20, 128)) fail("\u8BE5 IPv6 \u5730\u5740\u4E0D\u662F\u53EF\u7B7E\u53D1\u8BC1\u4E66\u7684\u516C\u7F51\u5355\u64AD\u5730\u5740");
|
|
106
|
+
}
|
|
107
|
+
const email = input.email ?? "";
|
|
108
|
+
if (email && !/^[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,253}\.[A-Za-z]{2,63}$/.test(email)) fail("\u8BF7\u8F93\u5165\u6709\u6548\u7684\u8054\u7CFB\u90AE\u7BB1");
|
|
109
|
+
if (input.challengePort !== 80) fail("Let\u2019s Encrypt HTTP-01 \u56FA\u5B9A\u4F7F\u7528\u516C\u7F51 80 \u7AEF\u53E3\uFF1B\u8BF7\u8BA9\u73B0\u6709\u53CD\u5411\u4EE3\u7406\u5C06\u6311\u6218\u76EE\u5F55\u6620\u5C04\u5230 webroot");
|
|
110
|
+
if (!Number.isInteger(input.relayPort) || input.relayPort < 2e4 || input.relayPort > 49151 || [22, 80, 443, 8080, 8443].includes(input.relayPort)) fail("\u4E2D\u7EE7\u4E1A\u52A1\u7AEF\u53E3\u987B\u4E3A 20000\u201349151 \u7684\u4E13\u7528\u9AD8\u4F4D\u7AEF\u53E3");
|
|
111
|
+
if (typeof input.webroot !== "string" || !input.webroot || !input.stateDir) fail("\u5FC5\u987B\u6307\u5B9A\u5DF2\u5B58\u5728\u7684 webroot \u548C\u72B6\u6001\u76EE\u5F55");
|
|
112
|
+
if (input.certbot !== void 0 && (typeof input.certbot !== "string" || !input.certbot || input.certbot.includes("\0"))) fail("Certbot \u8DEF\u5F84\u65E0\u6548");
|
|
113
|
+
return { ip, domain, family, email, challengePort: 80, relayPort: input.relayPort, webroot: resolve(input.webroot), stateDir: resolve(input.stateDir), certbot: input.certbot ?? "certbot" };
|
|
114
|
+
}
|
|
115
|
+
async function existingDirectory(path, label, writable = false) {
|
|
116
|
+
const resolved = await realpath(path).catch(() => fail(label + " \u5FC5\u987B\u662F\u5DF2\u5B58\u5728\u7684\u76EE\u5F55"));
|
|
117
|
+
const stat = await lstat(resolved);
|
|
118
|
+
if (!stat.isDirectory()) fail(label + " \u5FC5\u987B\u662F\u76EE\u5F55");
|
|
119
|
+
await access(resolved, writable ? 2 : 4).catch(() => fail(label + (writable ? " \u4E0D\u53EF\u5199" : " \u4E0D\u53EF\u8BFB")));
|
|
120
|
+
return resolved;
|
|
121
|
+
}
|
|
122
|
+
async function stateDirectory(path, { create = false } = {}) {
|
|
123
|
+
const requested = resolve(path);
|
|
124
|
+
if (create) await mkdir(requested, { recursive: true, mode: 448 });
|
|
125
|
+
const resolved = await realpath(requested).catch((error) => error.code === "ENOENT" && !create ? null : fail("\u72B6\u6001\u76EE\u5F55\u5FC5\u987B\u662F\u53EF\u8BBF\u95EE\u7684\u76EE\u5F55"));
|
|
126
|
+
if (!resolved) return null;
|
|
127
|
+
const stat = await lstat(resolved);
|
|
128
|
+
if (!stat.isDirectory()) fail("\u72B6\u6001\u76EE\u5F55\u5FC5\u987B\u662F\u76EE\u5F55");
|
|
129
|
+
await access(resolved, create ? 2 : 4).catch(() => fail(create ? "\u72B6\u6001\u76EE\u5F55\u4E0D\u53EF\u5199" : "\u72B6\u6001\u76EE\u5F55\u4E0D\u53EF\u8BFB"));
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
function digest(value) {
|
|
133
|
+
return createHash("sha256").update(value).digest("hex");
|
|
134
|
+
}
|
|
135
|
+
function systemdArg(value) {
|
|
136
|
+
const text = String(value);
|
|
137
|
+
if (/[\0\r\n]/.test(text)) fail("systemd \u53C2\u6570\u4E0D\u80FD\u5305\u542B\u6362\u884C\u6216\u7A7A\u5B57\u7B26");
|
|
138
|
+
return '"' + text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "$$").replace(/%/g, "%%") + '"';
|
|
139
|
+
}
|
|
140
|
+
function unitDirectory() {
|
|
141
|
+
const configHome = process.env.XDG_CONFIG_HOME ? resolve(process.env.XDG_CONFIG_HOME) : join(homedir(), ".config");
|
|
142
|
+
return join(configHome, "systemd", "user");
|
|
143
|
+
}
|
|
144
|
+
function timerContents(config, stateDir) {
|
|
145
|
+
const script = fileURLToPath(import.meta.url);
|
|
146
|
+
const args = [
|
|
147
|
+
process.execPath,
|
|
148
|
+
script,
|
|
149
|
+
"renew",
|
|
150
|
+
"--state-dir",
|
|
151
|
+
stateDir,
|
|
152
|
+
...config.domain ? ["--domain", config.domain] : ["--ip", config.ip],
|
|
153
|
+
...config.email ? ["--email", config.email] : [],
|
|
154
|
+
"--webroot",
|
|
155
|
+
config.webroot,
|
|
156
|
+
"--challenge-port",
|
|
157
|
+
"80",
|
|
158
|
+
"--relay-port",
|
|
159
|
+
String(config.relayPort),
|
|
160
|
+
"--confirm-public-ip",
|
|
161
|
+
"true",
|
|
162
|
+
"--certbot",
|
|
163
|
+
config.certbot ?? "certbot",
|
|
164
|
+
"--restart-relay-unit",
|
|
165
|
+
RELAY_UNIT
|
|
166
|
+
];
|
|
167
|
+
const service = [
|
|
168
|
+
UNIT_MARKER,
|
|
169
|
+
"[Unit]",
|
|
170
|
+
"Description=Renew the DSH Link IP certificate",
|
|
171
|
+
"After=network-online.target",
|
|
172
|
+
"",
|
|
173
|
+
"[Service]",
|
|
174
|
+
"Type=oneshot",
|
|
175
|
+
"UMask=0077",
|
|
176
|
+
"TimeoutStartSec=25min",
|
|
177
|
+
"ExecStart=" + args.map(systemdArg).join(" "),
|
|
178
|
+
""
|
|
179
|
+
].join("\n");
|
|
180
|
+
const timer = [
|
|
181
|
+
UNIT_MARKER,
|
|
182
|
+
"[Unit]",
|
|
183
|
+
"Description=Daily renewal of the DSH Link IP certificate",
|
|
184
|
+
"",
|
|
185
|
+
"[Timer]",
|
|
186
|
+
"OnCalendar=daily",
|
|
187
|
+
"RandomizedDelaySec=1h",
|
|
188
|
+
"Persistent=true",
|
|
189
|
+
"Unit=" + TIMER_SERVICE,
|
|
190
|
+
"",
|
|
191
|
+
"[Install]",
|
|
192
|
+
"WantedBy=timers.target",
|
|
193
|
+
""
|
|
194
|
+
].join("\n");
|
|
195
|
+
return { [TIMER_SERVICE]: service, [TIMER_UNIT]: timer };
|
|
196
|
+
}
|
|
197
|
+
async function ownedFile(path, expectedHash, label) {
|
|
198
|
+
let stat;
|
|
199
|
+
try {
|
|
200
|
+
stat = await lstat(path);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error.code === "ENOENT") return null;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
if (!stat.isFile() || stat.isSymbolicLink() || typeof process.getuid === "function" && stat.uid !== process.getuid()) fail(label + " \u4E0D\u662F\u672C\u5DE5\u5177\u62E5\u6709\u7684\u666E\u901A\u6587\u4EF6");
|
|
206
|
+
const contents = await readFile(path, "utf8");
|
|
207
|
+
if (!contents.startsWith(UNIT_MARKER) || digest(contents) !== expectedHash) fail(label + " \u5DF2\u88AB\u4FEE\u6539\uFF0C\u62D2\u7EDD\u8986\u76D6\u6216\u5220\u9664");
|
|
208
|
+
return contents;
|
|
209
|
+
}
|
|
210
|
+
async function readTimerManifest(directory) {
|
|
211
|
+
const path = join(directory, TIMER_MANIFEST);
|
|
212
|
+
let stat;
|
|
213
|
+
try {
|
|
214
|
+
stat = await lstat(path);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error.code === "ENOENT") return null;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()) fail("\u7EED\u671F timer \u5F52\u5C5E\u6E05\u5355\u6743\u9650\u6216\u6587\u4EF6\u7C7B\u578B\u65E0\u6548");
|
|
220
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
221
|
+
if (manifest.schemaVersion !== 1 || manifest.tool !== "dsh-link-ip-certificate" || !manifest.units || typeof manifest.units[TIMER_SERVICE] !== "string" || typeof manifest.units[TIMER_UNIT] !== "string") fail("\u7EED\u671F timer \u5F52\u5C5E\u6E05\u5355\u65E0\u6548");
|
|
222
|
+
if (![manifest.units[TIMER_SERVICE], manifest.units[TIMER_UNIT]].every((value) => /^[a-f0-9]{64}$/.test(value))) fail("\u7EED\u671F timer \u5F52\u5C5E\u6E05\u5355\u54C8\u5E0C\u65E0\u6548");
|
|
223
|
+
return manifest;
|
|
224
|
+
}
|
|
225
|
+
async function createOwnedFile(path, contents) {
|
|
226
|
+
const temporary = path + "." + randomUUID() + ".tmp";
|
|
227
|
+
await writeFile(temporary, contents, { mode: 384, flag: "wx" });
|
|
228
|
+
try {
|
|
229
|
+
await link(temporary, path);
|
|
230
|
+
} finally {
|
|
231
|
+
await rm(temporary, { force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function systemd(args) {
|
|
235
|
+
return command("systemctl", ["--user", ...args], { timeout: 3e4, label: "systemctl" });
|
|
236
|
+
}
|
|
237
|
+
async function requireRelayUnit() {
|
|
238
|
+
const { stdout } = await systemd(["show", "--property=LoadState", "--value", RELAY_UNIT]);
|
|
239
|
+
if (stdout.trim() !== "loaded") fail("\u5F53\u524D\u7528\u6237\u7684 systemd \u5C1A\u672A\u52A0\u8F7D " + RELAY_UNIT + "\uFF0C\u8BF7\u5148\u914D\u7F6E\u5E76\u542F\u52A8\u4E2D\u7EE7\u670D\u52A1");
|
|
240
|
+
}
|
|
241
|
+
async function installTimer(config, stateDir) {
|
|
242
|
+
await requireRelayUnit();
|
|
243
|
+
const directory = unitDirectory();
|
|
244
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
245
|
+
const contents = timerContents(config, stateDir), hashes = Object.fromEntries(Object.entries(contents).map(([name, value]) => [name, digest(value)]));
|
|
246
|
+
const manifestPath = join(directory, TIMER_MANIFEST), manifest = await readTimerManifest(directory);
|
|
247
|
+
if (manifest) {
|
|
248
|
+
if (manifest.units[TIMER_SERVICE] !== hashes[TIMER_SERVICE] || manifest.units[TIMER_UNIT] !== hashes[TIMER_UNIT]) fail("\u7EED\u671F timer \u5DF2\u5B89\u88C5\u4F46\u914D\u7F6E\u4E0D\u540C\uFF0C\u8BF7\u5148\u5378\u8F7D\u518D\u5B89\u88C5");
|
|
249
|
+
for (const name of [TIMER_SERVICE, TIMER_UNIT]) {
|
|
250
|
+
const current = await ownedFile(join(directory, name), manifest.units[name], name);
|
|
251
|
+
if (!current) await createOwnedFile(join(directory, name), contents[name]);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
for (const name of [TIMER_SERVICE, TIMER_UNIT]) {
|
|
255
|
+
try {
|
|
256
|
+
await lstat(join(directory, name));
|
|
257
|
+
fail(name + " \u5DF2\u5B58\u5728\u4E14\u4E0D\u5C5E\u4E8E\u672C\u5DE5\u5177\uFF0C\u62D2\u7EDD\u8986\u76D6");
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error.code !== "ENOENT") throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let serviceCreated = false, timerCreated = false;
|
|
263
|
+
try {
|
|
264
|
+
await createOwnedFile(join(directory, TIMER_SERVICE), contents[TIMER_SERVICE]);
|
|
265
|
+
serviceCreated = true;
|
|
266
|
+
await createOwnedFile(join(directory, TIMER_UNIT), contents[TIMER_UNIT]);
|
|
267
|
+
timerCreated = true;
|
|
268
|
+
await writeFile(manifestPath, JSON.stringify({ schemaVersion: 1, tool: "dsh-link-ip-certificate", units: hashes }) + "\n", { mode: 384, flag: "wx" });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (timerCreated) await rm(join(directory, TIMER_UNIT), { force: true });
|
|
271
|
+
if (serviceCreated) await rm(join(directory, TIMER_SERVICE), { force: true });
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
await systemd(["daemon-reload"]);
|
|
276
|
+
await systemd(["enable", "--now", TIMER_UNIT]);
|
|
277
|
+
}
|
|
278
|
+
async function uninstallTimer() {
|
|
279
|
+
const directory = unitDirectory(), manifest = await readTimerManifest(directory);
|
|
280
|
+
if (!manifest) {
|
|
281
|
+
for (const name of [TIMER_SERVICE, TIMER_UNIT]) {
|
|
282
|
+
try {
|
|
283
|
+
await lstat(join(directory, name));
|
|
284
|
+
fail(name + " \u5B58\u5728\u4F46\u6CA1\u6709\u672C\u5DE5\u5177\u5F52\u5C5E\u6E05\u5355\uFF0C\u62D2\u7EDD\u5220\u9664");
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error.code !== "ENOENT") throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
for (const name of [TIMER_SERVICE, TIMER_UNIT]) await ownedFile(join(directory, name), manifest.units[name], name);
|
|
292
|
+
await systemd(["disable", "--now", TIMER_UNIT]);
|
|
293
|
+
for (const name of [TIMER_SERVICE, TIMER_UNIT]) await rm(join(directory, name), { force: true });
|
|
294
|
+
await rm(join(directory, TIMER_MANIFEST));
|
|
295
|
+
await systemd(["daemon-reload"]);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
async function markRelayReloadPending(root) {
|
|
299
|
+
const path = join(root, "relay-reload-pending");
|
|
300
|
+
try {
|
|
301
|
+
await writeFile(path, "pending\n", { mode: 384, flag: "wx" });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error.code !== "EEXIST") throw error;
|
|
304
|
+
const stat = await lstat(path);
|
|
305
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0) fail("\u4E2D\u7EE7 reload \u6807\u8BB0\u6587\u4EF6\u6743\u9650\u6216\u7C7B\u578B\u65E0\u6548");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function reloadRelayIfPending(root) {
|
|
309
|
+
const path = join(root, "relay-reload-pending");
|
|
310
|
+
let stat;
|
|
311
|
+
try {
|
|
312
|
+
stat = await lstat(path);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error.code === "ENOENT") return false;
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0) fail("\u4E2D\u7EE7 reload \u6807\u8BB0\u6587\u4EF6\u6743\u9650\u6216\u7C7B\u578B\u65E0\u6548");
|
|
318
|
+
await systemd(["try-restart", RELAY_UNIT]);
|
|
319
|
+
await rm(path);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
function command(binary, args, { timeout = 20 * 60 * 1e3, env = process.env, label = "Certbot" } = {}) {
|
|
323
|
+
return new Promise((resolvePromise, reject) => {
|
|
324
|
+
const child = spawn(binary, args, { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], env });
|
|
325
|
+
let stdout = "", stderr = "", settled = false;
|
|
326
|
+
const finish = (error, value) => {
|
|
327
|
+
if (settled) return;
|
|
328
|
+
settled = true;
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
error ? reject(error) : resolvePromise(value);
|
|
331
|
+
};
|
|
332
|
+
const append = (current, data) => (current + data.toString()).slice(-16e3);
|
|
333
|
+
child.stdout.on("data", (data) => {
|
|
334
|
+
stdout = append(stdout, data);
|
|
335
|
+
});
|
|
336
|
+
child.stderr.on("data", (data) => {
|
|
337
|
+
stderr = append(stderr, data);
|
|
338
|
+
});
|
|
339
|
+
child.once("error", (error) => finish(new Error(error.code === "ENOENT" ? label === "Certbot" ? "\u672A\u627E\u5230 Certbot\uFF0C\u8BF7\u5148\u624B\u52A8\u5B89\u88C5 Certbot 5.4 \u6216\u66F4\u9AD8\u7248\u672C" : "\u672A\u627E\u5230 " + label : label + " \u65E0\u6CD5\u542F\u52A8\uFF1A" + error.message)));
|
|
340
|
+
child.once("close", (code) => code === 0 ? finish(null, { stdout, stderr }) : finish(new Error(label + " \u6267\u884C\u5931\u8D25\uFF08" + code + "\uFF09\uFF1A" + (stderr.trim() || stdout.trim()).slice(-1200))));
|
|
341
|
+
const timer = setTimeout(() => {
|
|
342
|
+
child.kill("SIGTERM");
|
|
343
|
+
finish(new Error(label + " \u64CD\u4F5C\u8D85\u65F6"));
|
|
344
|
+
}, timeout);
|
|
345
|
+
timer.unref?.();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async function certbotVersion(binary) {
|
|
349
|
+
const { stdout, stderr } = await command(binary, ["--version"], { timeout: 15e3 });
|
|
350
|
+
const match = (stdout + stderr).match(/certbot\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
351
|
+
if (!match || compareVersion(match.slice(1).map(Number), MIN_CERTBOT) < 0) fail("IP webroot \u8BC1\u4E66\u9700\u8981 Certbot 5.4.0 \u6216\u66F4\u9AD8\u7248\u672C");
|
|
352
|
+
return match.slice(1).join(".");
|
|
353
|
+
}
|
|
354
|
+
function compareVersion(a, b) {
|
|
355
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
var owned = (dir) => join(dir, TOOL_DIR);
|
|
359
|
+
var markerPath = (dir) => join(owned(dir), "managed.json");
|
|
360
|
+
async function readMarker(dir) {
|
|
361
|
+
try {
|
|
362
|
+
const path = markerPath(dir), info = await lstat(path);
|
|
363
|
+
if (!info.isFile() || info.isSymbolicLink() || (info.mode & 63) !== 0) fail("\u8BC1\u4E66\u72B6\u6001\u5F52\u5C5E\u6807\u8BB0\u6743\u9650\u65E0\u6548");
|
|
364
|
+
const marker = JSON.parse(await readFile(path, "utf8"));
|
|
365
|
+
if (marker.schemaVersion !== 1 || marker.tool !== "dsh-link-ip-certificate") fail("\u8BC1\u4E66\u72B6\u6001\u76EE\u5F55\u5F52\u5C5E\u6807\u8BB0\u65E0\u6548");
|
|
366
|
+
return marker;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error.code === "ENOENT") return null;
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function ensureOwnedDir(dir) {
|
|
373
|
+
const path = owned(dir);
|
|
374
|
+
try {
|
|
375
|
+
await mkdir(path, { mode: 448 });
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (error.code !== "EEXIST") throw error;
|
|
378
|
+
}
|
|
379
|
+
const info = await lstat(path);
|
|
380
|
+
if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 63) !== 0) fail("\u8BC1\u4E66\u72B6\u6001\u76EE\u5F55\u5FC5\u987B\u662F\u4EC5\u5F53\u524D\u7528\u6237\u53EF\u8BBF\u95EE\u7684\u771F\u5B9E\u76EE\u5F55");
|
|
381
|
+
let marker = await readMarker(dir);
|
|
382
|
+
if (!marker) {
|
|
383
|
+
if ((await readdir(path)).length) fail("\u8BC1\u4E66\u72B6\u6001\u76EE\u5F55\u5DF2\u6709\u975E\u672C\u5DE5\u5177\u6570\u636E\uFF0C\u62D2\u7EDD\u63A5\u7BA1");
|
|
384
|
+
marker = { schemaVersion: 1, tool: "dsh-link-ip-certificate" };
|
|
385
|
+
await writeFile(markerPath(dir), JSON.stringify(marker) + "\n", { mode: 384, flag: "wx" });
|
|
386
|
+
}
|
|
387
|
+
return path;
|
|
388
|
+
}
|
|
389
|
+
async function saveMarker(stateDir, value) {
|
|
390
|
+
const path = markerPath(stateDir), temp = path + "." + randomUUID() + ".tmp";
|
|
391
|
+
await writeFile(temp, JSON.stringify(value, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
392
|
+
try {
|
|
393
|
+
await rename(temp, path);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
await rm(temp, { force: true });
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function certbotArgs(operation, config, root, hasLineage = false) {
|
|
400
|
+
const registration = config.email ? ["--no-eff-email", "--email", config.email] : ["--register-unsafely-without-email"];
|
|
401
|
+
const common = ["--non-interactive", "--agree-tos", ...registration, "--config-dir", join(root, "certbot"), "--work-dir", join(root, "work"), "--logs-dir", join(root, "logs")];
|
|
402
|
+
const subject = config.domain ? ["--domains", config.domain] : ["--ip-address", config.ip, "--preferred-profile", "shortlived"];
|
|
403
|
+
if (operation === "issue") return ["certonly", "--authenticator", "webroot", "--webroot-path", config.webroot, "--preferred-challenges", "http", ...subject, "--cert-name", CERT_NAME, ...common];
|
|
404
|
+
const profile = config.domain ? [] : ["--preferred-profile", "shortlived"];
|
|
405
|
+
if (operation === "dry-run") return hasLineage ? ["renew", "--cert-name", CERT_NAME, "--webroot-path", config.webroot, ...profile, "--dry-run", ...common] : ["certonly", "--authenticator", "webroot", "--webroot-path", config.webroot, "--preferred-challenges", "http", ...subject, "--cert-name", CERT_NAME, "--dry-run", ...common];
|
|
406
|
+
return ["renew", "--cert-name", CERT_NAME, "--webroot-path", config.webroot, ...profile, ...common];
|
|
407
|
+
}
|
|
408
|
+
async function readCertificate(certPath, keyPath, subject, { allowExpired = false } = {}) {
|
|
409
|
+
const [cert, key] = await Promise.all([readFile(certPath, "utf8"), readFile(keyPath, "utf8")]);
|
|
410
|
+
let certificate;
|
|
411
|
+
try {
|
|
412
|
+
certificate = new X509Certificate(cert);
|
|
413
|
+
if (!certificate.checkPrivateKey(createPrivateKey(key))) fail("\u8BC1\u4E66\u4E0E\u79C1\u94A5\u4E0D\u5339\u914D");
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (/证书与私钥/.test(error.message)) throw error;
|
|
416
|
+
fail("Certbot \u8FD4\u56DE\u7684\u8BC1\u4E66\u6216\u79C1\u94A5\u65E0\u6548");
|
|
417
|
+
}
|
|
418
|
+
if (subject && (isIP(subject) ? !certificate.checkIP(subject) : !certificate.checkHost(subject))) fail("\u7B7E\u53D1\u8BC1\u4E66\u7684\u6807\u8BC6\u4E0E\u914D\u7F6E\u4E0D\u5339\u914D");
|
|
419
|
+
const notBefore = Date.parse(certificate.validFrom), notAfter = Date.parse(certificate.validTo), now = Date.now();
|
|
420
|
+
if (!Number.isFinite(notBefore) || !Number.isFinite(notAfter) || notBefore > now + 5 * 6e4 || !allowExpired && notAfter <= now) fail("\u8BC1\u4E66\u5C1A\u672A\u751F\u6548\u6216\u5DF2\u7ECF\u8FC7\u671F");
|
|
421
|
+
return { cert, key, notBefore: new Date(notBefore).toISOString(), notAfter: new Date(notAfter).toISOString(), daysRemaining: Math.floor((notAfter - now) / 864e5), expired: notAfter <= now };
|
|
422
|
+
}
|
|
423
|
+
async function activePaths(root) {
|
|
424
|
+
const active = join(root, "active");
|
|
425
|
+
let target;
|
|
426
|
+
try {
|
|
427
|
+
target = await readlink(active);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (error.code === "ENOENT") return null;
|
|
430
|
+
throw new Error("\u6D3B\u52A8\u8BC1\u4E66\u8DEF\u5F84\u4E0D\u662F\u672C\u5DE5\u5177\u521B\u5EFA\u7684\u94FE\u63A5");
|
|
431
|
+
}
|
|
432
|
+
if (!/^releases\/[A-Za-z0-9-]+$/.test(target)) fail("\u6D3B\u52A8\u8BC1\u4E66\u94FE\u63A5\u6307\u5411\u72B6\u6001\u76EE\u5F55\u4E4B\u5916");
|
|
433
|
+
const release = join(root, target);
|
|
434
|
+
const actual = await realpath(release).catch(() => fail("\u6D3B\u52A8\u8BC1\u4E66\u7248\u672C\u76EE\u5F55\u7F3A\u5931"));
|
|
435
|
+
if (!actual.startsWith(join(root, "releases") + "/")) fail("\u6D3B\u52A8\u8BC1\u4E66\u7248\u672C\u76EE\u5F55\u8D8A\u754C");
|
|
436
|
+
return { cert: join(actual, "fullchain.pem"), key: join(actual, "privkey.pem"), link: target };
|
|
437
|
+
}
|
|
438
|
+
async function publish(root, data, subject) {
|
|
439
|
+
const releases = join(root, "releases");
|
|
440
|
+
await mkdir(releases, { recursive: true, mode: 448 });
|
|
441
|
+
const id = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-") + "-" + process.pid;
|
|
442
|
+
const release = join(releases, id);
|
|
443
|
+
await mkdir(release, { mode: 448 });
|
|
444
|
+
try {
|
|
445
|
+
await writeFile(join(release, "fullchain.pem"), data.cert, { mode: 420, flag: "wx" });
|
|
446
|
+
await writeFile(join(release, "privkey.pem"), data.key, { mode: 384, flag: "wx" });
|
|
447
|
+
await writeFile(join(release, "metadata.json"), JSON.stringify({ subject, notBefore: data.notBefore, notAfter: data.notAfter }) + "\n", { mode: 384, flag: "wx" });
|
|
448
|
+
const active = join(root, "active"), current = await activePaths(root), temp = join(root, ".active-" + process.pid + "-" + Date.now());
|
|
449
|
+
await symlink("releases/" + id, temp);
|
|
450
|
+
try {
|
|
451
|
+
await rename(temp, active);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
await rm(temp, { force: true });
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
return { cert: join(active, "fullchain.pem"), key: join(active, "privkey.pem"), release: "releases/" + id, previous: current?.link };
|
|
457
|
+
} catch (error) {
|
|
458
|
+
await rm(release, { recursive: true, force: true });
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function output(value) {
|
|
463
|
+
process.stdout.write(JSON.stringify(value) + "\n");
|
|
464
|
+
}
|
|
465
|
+
async function main() {
|
|
466
|
+
const parsed = options(process.argv.slice(2));
|
|
467
|
+
const { operation, values } = parsed;
|
|
468
|
+
if (operation === "uninstall-timer") {
|
|
469
|
+
output({ state: await uninstallTimer() ? "timer-removed" : "timer-not-installed" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const stateDir = await stateDirectory(values["--state-dir"], { create: !["status", "install-timer"].includes(operation) });
|
|
473
|
+
if (!stateDir) {
|
|
474
|
+
if (operation === "install-timer") fail("\u8BF7\u5148\u6210\u529F\u7B7E\u53D1\u8BC1\u4E66\uFF0C\u518D\u5B89\u88C5\u81EA\u52A8\u7EED\u671F");
|
|
475
|
+
output({ state: "not-configured" });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const root = owned(stateDir), marker = await readMarker(stateDir);
|
|
479
|
+
if (operation === "install-timer") {
|
|
480
|
+
if (!marker?.config) fail("\u8BF7\u5148\u6210\u529F\u7B7E\u53D1\u8BC1\u4E66\uFF0C\u518D\u5B89\u88C5\u81EA\u52A8\u7EED\u671F");
|
|
481
|
+
const saved = marker.config;
|
|
482
|
+
const config2 = validateConfig({ ip: saved.ip, domain: saved.domain, email: saved.email, webroot: saved.webroot, stateDir, challengePort: 80, relayPort: saved.relayPort, confirmPublicIp: true, certbot: saved.certbot });
|
|
483
|
+
config2.webroot = await existingDirectory(config2.webroot, "webroot", true);
|
|
484
|
+
const paths = await activePaths(root);
|
|
485
|
+
if (!paths) fail("\u5C1A\u65E0\u672C\u5DE5\u5177\u7BA1\u7406\u7684\u6709\u6548\u8BC1\u4E66\uFF0C\u8BF7\u5148\u8FD0\u884C issue");
|
|
486
|
+
await readCertificate(paths.cert, paths.key, config2.domain || config2.ip, { allowExpired: true });
|
|
487
|
+
await installTimer(config2, stateDir);
|
|
488
|
+
output({ state: "timer-installed", timer: TIMER_UNIT, relayUnit: RELAY_UNIT });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (operation === "status") {
|
|
492
|
+
if (!marker) {
|
|
493
|
+
output({ state: "not-configured" });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const config2 = marker.config;
|
|
497
|
+
if (!config2) {
|
|
498
|
+
output({ state: "not-configured" });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const paths = await activePaths(root);
|
|
502
|
+
if (!paths) {
|
|
503
|
+
output({ state: "certificate-missing", config: config2 });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const data = await readCertificate(paths.cert, paths.key, config2.domain || config2.ip, { allowExpired: true });
|
|
507
|
+
output({ state: data.expired ? "expired" : "ready", ...config2, notBefore: data.notBefore, notAfter: data.notAfter, daysRemaining: data.daysRemaining, cert: paths.cert, key: paths.key, renewalRecommended: data.daysRemaining <= 2 });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const config = validateConfig({ ip: values["--ip"], domain: values["--domain"], email: values["--email"], webroot: values["--webroot"], stateDir, challengePort: portNumber(values["--challenge-port"]), relayPort: portNumber(values["--relay-port"]), confirmPublicIp: values["--confirm-public-ip"] === "true", certbot: values["--certbot"] });
|
|
511
|
+
config.webroot = await existingDirectory(config.webroot, "webroot", true);
|
|
512
|
+
if (operation === "renew" && (!marker?.config || !await activePaths(root))) fail("\u5C1A\u65E0\u672C\u5DE5\u5177\u7BA1\u7406\u7684\u6709\u6548\u8BC1\u4E66\uFF0C\u8BF7\u5148\u8FD0\u884C issue");
|
|
513
|
+
if (marker?.config && (marker.config.ip !== config.ip || marker.config.domain !== config.domain)) fail("\u540C\u4E00\u8BC1\u4E66\u72B6\u6001\u76EE\u5F55\u4E0D\u80FD\u66F4\u6362\u8BC1\u4E66\u6807\u8BC6\uFF0C\u8BF7\u4F7F\u7528\u72EC\u7ACB\u72B6\u6001\u76EE\u5F55");
|
|
514
|
+
if (marker?.config && operation !== "issue" && (marker.config.domain !== config.domain || marker.config.webroot !== config.webroot || marker.config.relayPort !== config.relayPort)) fail("\u7EED\u671F\u914D\u7F6E\u5FC5\u987B\u4E0E\u5F53\u524D\u8BC1\u4E66\u7684 webroot \u548C\u4E2D\u7EE7\u7AEF\u53E3\u4E00\u81F4");
|
|
515
|
+
const version = await certbotVersion(config.certbot);
|
|
516
|
+
const managed = await ensureOwnedDir(stateDir);
|
|
517
|
+
if (operation === "dry-run") {
|
|
518
|
+
let hasLineage = false;
|
|
519
|
+
try {
|
|
520
|
+
await access(join(managed, "certbot/live", CERT_NAME, "fullchain.pem"));
|
|
521
|
+
hasLineage = true;
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
await command(config.certbot, certbotArgs("dry-run", config, managed, hasLineage));
|
|
525
|
+
await saveMarker(stateDir, { schemaVersion: 1, tool: "dsh-link-ip-certificate", certbot: version, config: { ip: config.ip, domain: config.domain, email: config.email, webroot: config.webroot, challengePort: 80, relayPort: config.relayPort, certbot: config.certbot } });
|
|
526
|
+
output({ state: "dry-run-passed", certbot: version, challenge: "http-01", challengePort: 80, relayPort: config.relayPort });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const previousPaths = operation === "renew" ? await activePaths(root) : null;
|
|
530
|
+
const previous = previousPaths ? await readCertificate(previousPaths.cert, previousPaths.key, config.domain || config.ip, { allowExpired: true }) : null;
|
|
531
|
+
await mkdir(join(managed, "certbot"), { recursive: true, mode: 448 });
|
|
532
|
+
await mkdir(join(managed, "work"), { recursive: true, mode: 448 });
|
|
533
|
+
await mkdir(join(managed, "logs"), { recursive: true, mode: 448 });
|
|
534
|
+
await command(config.certbot, certbotArgs(operation, config, managed));
|
|
535
|
+
const raw = await readCertificate(join(managed, "certbot/live", CERT_NAME, "fullchain.pem"), join(managed, "certbot/live", CERT_NAME, "privkey.pem"), config.domain || config.ip);
|
|
536
|
+
const changed = operation === "issue" || !previous || previous.cert !== raw.cert || previous.key !== raw.key;
|
|
537
|
+
if (!changed) {
|
|
538
|
+
if (values["--restart-relay-unit"]) await reloadRelayIfPending(root);
|
|
539
|
+
output({ state: "ready", changed: false, certbot: version, challenge: "http-01", challengePort: 80, relayPort: config.relayPort, ip: config.ip, domain: config.domain, notBefore: raw.notBefore, notAfter: raw.notAfter, daysRemaining: raw.daysRemaining, cert: previousPaths.cert, key: previousPaths.key });
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const nextMarker = { schemaVersion: 1, tool: "dsh-link-ip-certificate", certbot: version, config: { ip: config.ip, domain: config.domain, email: config.email, webroot: config.webroot, challengePort: 80, relayPort: config.relayPort, certbot: config.certbot } };
|
|
543
|
+
await saveMarker(stateDir, nextMarker);
|
|
544
|
+
const published = await publish(managed, raw, config.domain || config.ip);
|
|
545
|
+
if (values["--restart-relay-unit"]) {
|
|
546
|
+
await markRelayReloadPending(root);
|
|
547
|
+
await reloadRelayIfPending(root);
|
|
548
|
+
}
|
|
549
|
+
output({ state: "ready", changed: true, certbot: version, challenge: "http-01", challengePort: 80, relayPort: config.relayPort, ip: config.ip, domain: config.domain, notBefore: raw.notBefore, notAfter: raw.notAfter, daysRemaining: raw.daysRemaining, cert: published.cert, key: published.key });
|
|
550
|
+
}
|
|
551
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
552
|
+
main().catch((error) => {
|
|
553
|
+
process.stderr.write((error?.message ?? "\u8BC1\u4E66\u64CD\u4F5C\u5931\u8D25") + "\n");
|
|
554
|
+
process.exitCode = 1;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
export {
|
|
558
|
+
validateConfig
|
|
559
|
+
};
|