195 lines
6.3 KiB
TypeScript
195 lines
6.3 KiB
TypeScript
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 });
|
|
}
|
|
});
|