import assert from "node:assert/strict"; import { test } from "node:test"; import { mkdtempSync, rmSync } from "node:fs"; import path from "node:path"; import { tmpdir } from "node:os"; import { certificateFixture } from "../certificate-fixture"; import { parse } from "yaml"; test("AnyTLS provisioning, per-user credentials, cumulative accounting and revocation", async () => { const dir = mkdtempSync(path.join(tmpdir(), "hy2-anytls-")); process.env.DATABASE_PATH = path.join(dir, "db.sqlite"); process.env.APP_URL = "https://panel.example.com"; const { ensureDb } = await import("../../lib/db"); const { encryptText, decryptText } = await import("../../lib/crypto"); const { saveCertificate } = await import("../../lib/certificates"); const { createInstallation, installationForDownload, provisionNode } = await import("../../lib/provisioning"); const { syncAnyTLSNode } = await import("../../lib/node-agent"); const { anytlsPassword } = await import("../../lib/anytls"); const { buildInstallScript } = await import("../../lib/install-script"); const { getNodeBySlug, listUsers, getSubscriptionSettings, listEnabledNodesForPolling, } = await import("../../lib/store"); const { buildSubscriptionConfig } = await import("../../lib/subscription"); const db = await ensureDb(); try { const fixture = certificateFixture(); const certificateId = Number( await saveCertificate( "Wildcard", fixture.certificate, fixture.privateKey, ), ); const id = await createInstallation({ slug: "anytls-test", name: "AnyTLS", certificateId, protocol: "anytls", }); const job = await installationForDownload(id); assert.equal(job.protocol, "anytls"); assert.match(buildInstallScript(job), /--port/); const installToken = decryptText(job.token_encrypted)!; const ports = { domain: "jp2.p.xn--876a.net", port: 51046, listenPort: 51046, statsPort: 19090, controlPort: 19091, }; await assert.rejects( () => provisionNode(installToken, "prepare", { ...ports, controlPort: 19090, }), /端口/, ); const bundle = await provisionNode(installToken, "prepare", ports); assert.ok(bundle.agent && bundle.config && bundle.agentSource); assert.equal(bundle.agent.slug, "anytls-test"); assert.match(bundle.agentSource, /reset=False/); const initial = JSON.parse(bundle.config); assert.equal(initial.inbounds[0].type, "anytls"); assert.equal(initial.inbounds[0].listen_port, 51046); assert.equal(initial.experimental.v2ray_api.listen, "127.0.0.1:19090"); const user = (await listUsers())[0]; const password = anytlsPassword(user); assert.equal(initial.inbounds[0].users[0].password, password); assert.notEqual(password, decryptText(user.auth_secret_encrypted)); await provisionNode(installToken, "complete"); const node = (await getNodeBySlug(job.slug))!; assert.equal(node.node_type, "anytls"); assert.equal((await listEnabledNodesForPolling()).length, 0); const subscription = parse( buildSubscriptionConfig({ user, nodes: [node], settings: await getSubscriptionSettings(), }), ); assert.equal(subscription.proxies[0].type, "anytls"); assert.equal(subscription.proxies[0].password, password); assert.equal(subscription.proxies[0]["skip-cert-verify"], false); const report = { epoch: "boot:123:456", traffic: { [user.auth_id]: { tx: 1000, rx: 500 } }, streamCount: 2, }; await assert.rejects( () => syncAnyTLSNode(job.slug, "0".repeat(64), report), /凭据/, ); await assert.rejects( () => syncAnyTLSNode(job.slug, bundle.agent.token, { ...report, traffic: { [user.auth_id]: { tx: -1, rx: 2 } }, }), /无效/, ); await syncAnyTLSNode(job.slug, bundle.agent.token, report); await syncAnyTLSNode(job.slug, bundle.agent.token, report); let refreshed = (await listUsers())[0]; assert.equal(refreshed.used_tx_bytes, 1000); assert.equal(refreshed.used_rx_bytes, 500); await syncAnyTLSNode(job.slug, bundle.agent.token, { ...report, traffic: { [user.auth_id]: { tx: 900, rx: 400 } }, }); refreshed = (await listUsers())[0]; assert.equal(refreshed.used_tx_bytes, 1000); await syncAnyTLSNode(job.slug, bundle.agent.token, { ...report, epoch: "boot:124:500", traffic: { [user.auth_id]: { tx: 100, rx: 50 } }, }); refreshed = (await listUsers())[0]; assert.equal(refreshed.used_tx_bytes, 1100); // Make this account an ordinary user to exercise quota/expiry/disable rules. db.prepare( "UPDATE hy2_users SET is_admin=0,traffic_limit_bytes=1000 WHERE id=?", ).run(user.id); const capped = await syncAnyTLSNode(job.slug, bundle.agent.token, report); assert.equal(capped.config.inbounds[0].users.length, 0); db.prepare( "UPDATE hy2_users SET traffic_limit_bytes=NULL,expires_at='2000-01-01' WHERE id=?", ).run(user.id); assert.equal( (await syncAnyTLSNode(job.slug, bundle.agent.token, report)).config .inbounds[0].users.length, 0, ); db.prepare("UPDATE hy2_users SET expires_at=NULL,enabled=0 WHERE id=?").run( user.id, ); assert.equal( (await syncAnyTLSNode(job.slug, bundle.agent.token, report)).config .inbounds[0].users.length, 0, ); db.prepare( "UPDATE hy2_users SET enabled=1,auth_secret_encrypted=? WHERE id=?", ).run(encryptText("new-password"), user.id); const updated = await syncAnyTLSNode(job.slug, bundle.agent.token, report); assert.notEqual(updated.config.inbounds[0].users[0].password, password); db.prepare("UPDATE hy2_nodes SET enabled=0 WHERE id=?").run(node.id); assert.equal( (await syncAnyTLSNode(job.slug, bundle.agent.token, report)).config .inbounds[0].users.length, 0, ); assert.deepEqual(db.pragma("foreign_key_check"), []); } finally { db.close(); rmSync(dir, { recursive: true, force: true }); } });