feat: 支持证书托管和节点自动安装

This commit is contained in:
2026-09-29 13:06:56 +08:00
parent ab7d1c937a
commit 586c2399d1
20 changed files with 1600 additions and 20 deletions
+194
View File
@@ -0,0 +1,194 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { parse } from "yaml";
import { certificateFixture } from "../certificate-fixture";
test("certificate provisioning preserves credentials, binds domain/ports, supports retry and consumes capability", async () => {
const dir = mkdtempSync(path.join(tmpdir(), "hy2-provision-test-"));
process.env.DATABASE_PATH = path.join(dir, "test.db");
process.env.APP_URL = "https://panel.example.com";
process.env.POLLER_ENABLED = "false";
const { ensureDb } = await import("../../lib/db");
const { decryptText } = await import("../../lib/crypto");
const {
saveCertificate,
validateCertificate,
deleteCertificate,
listCertificates,
} = await import("../../lib/certificates");
const {
createInstallation,
installationForDownload,
provisionNode,
revokeInstallation,
} = await import("../../lib/provisioning");
const { buildInstallScript } = await import("../../lib/install-script");
const db = await ensureDb();
const fixture = certificateFixture();
try {
assert.throws(
() =>
validateCertificate(
fixture.certificate,
certificateFixture().privateKey,
),
/匹配/,
);
assert.throws(
() =>
validateCertificate(
fixture.certificate,
fixture.privateKey,
"a.b.p.xn--876a.net",
),
/SNI/,
);
assert.throws(
() => validateCertificate("invalid", fixture.privateKey),
/解析/,
);
const certId = Number(
await saveCertificate(
"Wildcard",
fixture.certificate,
fixture.privateKey,
),
);
assert.equal(
JSON.stringify(await listCertificates()).includes("PRIVATE KEY"),
false,
);
const stored = db
.prepare("SELECT private_key_encrypted FROM managed_certificates")
.get() as { private_key_encrypted: string };
assert.notEqual(stored.private_key_encrypted, fixture.privateKey);
assert.equal(decryptText(stored.private_key_encrypted), fixture.privateKey);
const id = await createInstallation({
name: "New server",
slug: "new-server",
certificateId: certId,
});
await assert.rejects(
() =>
createInstallation({
name: "Duplicate",
slug: "new-server",
certificateId: certId,
}),
/存在/,
);
await assert.rejects(() => deleteCertificate(certId), /关联/);
const job = await installationForDownload(id);
const script = buildInstallScript(job);
assert.ok(script.includes("--domain"));
assert.ok(!script.includes("PRIVATE KEY"));
assert.ok(!script.includes("__TOKEN__"));
const token = decryptText(job.token_encrypted)!;
assert.equal(job.token_hash.includes(token), false);
const ports = {
domain: "hk.p.xn--876a.net",
hy2Port: 23456,
statsPort: 23457,
hy2ListenPort: 443,
statsListenPort: 9999,
};
await assert.rejects(
() => provisionNode("f".repeat(64), "prepare", ports),
/无效/,
);
await assert.rejects(() => provisionNode(token, "complete"), /先获取/);
await assert.rejects(
() =>
provisionNode(token, "prepare", { ...ports, domain: "evil.example" }),
/域名/,
);
await assert.rejects(
() => provisionNode(token, "prepare", { ...ports, hy2Port: 65536 }),
/端口/,
);
const bundle = await provisionNode(token, "prepare", ports);
assert.ok("config" in bundle);
assert.equal(bundle.privateKey, fixture.privateKey);
const config = parse(bundle.config);
assert.equal(config.listen, ":443");
assert.equal(config.trafficStats.listen, ":9999");
assert.match(
config.auth.http.url,
/^https:\/\/panel.example.com\/api\/hy2\/nodes\/new-server\/auth\?token=/,
);
assert.deepEqual(await provisionNode(token, "prepare", ports), bundle);
await assert.rejects(
() => provisionNode(token, "prepare", { ...ports, statsPort: 12345 }),
/绑定/,
);
const node = db
.prepare("SELECT * FROM hy2_nodes WHERE slug='new-server'")
.get() as Record<string, unknown>;
assert.equal(node.server_port, 23456);
assert.equal(node.traffic_stats_url, "http://hk.p.xn--876a.net:23457");
assert.equal(node.sni, ports.domain);
assert.equal(node.enabled, 0);
await provisionNode(token, "complete");
await provisionNode(token, "complete");
assert.equal(
(
db.prepare("SELECT enabled FROM hy2_nodes WHERE id=?").get(node.id) as {
enabled: number;
}
).enabled,
1,
);
await assert.rejects(() => provisionNode(token, "prepare", ports), /完成/);
await assert.rejects(() => installationForDownload(id), /完成/);
assert.equal(
(db.prepare("SELECT COUNT(*) AS n FROM hy2_nodes").get() as { n: number })
.n,
1,
);
const retryId = await createInstallation({
name: "Retry",
slug: "retry",
certificateId: certId,
});
const retryToken = decryptText(
(await installationForDownload(retryId)).token_encrypted,
)!;
await revokeInstallation(retryId);
await assert.rejects(
() => provisionNode(retryToken, "prepare", ports),
/无效/,
);
const expiredId = await createInstallation({
name: "Expired",
slug: "expired",
certificateId: certId,
});
const expiredToken = decryptText(
(await installationForDownload(expiredId)).token_encrypted,
)!;
db.prepare(
"UPDATE node_installations SET expires_at='2000-01-01' WHERE id=?",
).run(expiredId);
await assert.rejects(
() => provisionNode(expiredToken, "prepare", ports),
/过期/,
);
await revokeInstallation(id, true);
assert.equal(
(db.prepare("SELECT COUNT(*) AS n FROM hy2_nodes").get() as { n: number })
.n,
1,
);
await revokeInstallation(retryId, true);
await revokeInstallation(expiredId, true);
await deleteCertificate(certId);
assert.equal((await listCertificates()).length, 0);
assert.deepEqual(db.pragma("foreign_key_check"), []);
} finally {
db.close();
rmSync(dir, { recursive: true, force: true });
}
});