feat: 接入 AnyTLS 节点托管和用户流量同步
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user