feat: 接入 AnyTLS 节点托管和用户流量同步
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// Optional integration check: SING_BOX_TEST_BINARY=/path/to/build npx tsx tests/anytls-runtime.ts
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
||||
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createServer } from "node:http";
|
||||
import { certificateFixture } from "./certificate-fixture";
|
||||
|
||||
async function main() {
|
||||
const binary = process.env.SING_BOX_TEST_BINARY;
|
||||
const python = process.env.AGENT_TEST_PYTHON;
|
||||
if (!binary || !python)
|
||||
throw new Error("Set SING_BOX_TEST_BINARY and AGENT_TEST_PYTHON");
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "hy2-anytls-runtime-"));
|
||||
process.env.DATABASE_PATH = path.join(dir, "db.sqlite");
|
||||
process.env.APP_URL = "https://panel.example.com";
|
||||
const { saveCertificate } = await import("../lib/certificates");
|
||||
const { createInstallation, installationForDownload, provisionNode } =
|
||||
await import("../lib/provisioning");
|
||||
const { decryptText } = await import("../lib/crypto");
|
||||
const { syncAnyTLSNode } = await import("../lib/node-agent");
|
||||
const { ensureDb } = await import("../lib/db");
|
||||
const fixture = certificateFixture();
|
||||
const certId = Number(
|
||||
await saveCertificate("Runtime", fixture.certificate, fixture.privateKey),
|
||||
);
|
||||
const id = await createInstallation({
|
||||
slug: "runtime",
|
||||
name: "Runtime",
|
||||
certificateId: certId,
|
||||
protocol: "anytls",
|
||||
});
|
||||
const token = decryptText(
|
||||
(await installationForDownload(id)).token_encrypted,
|
||||
)!;
|
||||
const bundle = await provisionNode(token, "prepare", {
|
||||
domain: "runtime.p.xn--876a.net",
|
||||
port: 39194,
|
||||
listenPort: 39194,
|
||||
statsPort: 39196,
|
||||
controlPort: 39197,
|
||||
});
|
||||
assert.ok(bundle.agent && bundle.config && bundle.statsProto);
|
||||
writeFileSync(path.join(dir, "server.crt"), fixture.certificate);
|
||||
writeFileSync(path.join(dir, "server.key"), fixture.privateKey, {
|
||||
mode: 0o600,
|
||||
});
|
||||
const config = JSON.parse(
|
||||
bundle.config.replaceAll("/etc/hy2-panel-anytls", dir),
|
||||
);
|
||||
const authId = config.inbounds[0].users[0].name;
|
||||
writeFileSync(path.join(dir, "server.json"), JSON.stringify(config), {
|
||||
mode: 0o600,
|
||||
});
|
||||
writeFileSync(
|
||||
path.join(dir, "client.json"),
|
||||
JSON.stringify({
|
||||
log: { level: "error" },
|
||||
inbounds: [{ type: "mixed", listen: "127.0.0.1", listen_port: 39193 }],
|
||||
outbounds: [
|
||||
{
|
||||
type: "anytls",
|
||||
server: "127.0.0.1",
|
||||
server_port: 39194,
|
||||
password: config.inbounds[0].users[0].password,
|
||||
tls: {
|
||||
enabled: true,
|
||||
server_name: "runtime.p.xn--876a.net",
|
||||
certificate_path: path.join(dir, "server.crt"),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
const target = createServer((_req, res) => res.end("A".repeat(65536)));
|
||||
await new Promise<void>((resolve) =>
|
||||
target.listen(39195, "127.0.0.1", resolve),
|
||||
);
|
||||
const start = (file: string) =>
|
||||
spawn(binary, ["run", "-c", path.join(dir, file)], { stdio: "ignore" });
|
||||
let server: ChildProcess | undefined, client: ChildProcess | undefined;
|
||||
const stop = async (p?: ChildProcess) => {
|
||||
if (!p || p.exitCode !== null) return;
|
||||
p.kill();
|
||||
await new Promise<void>((resolve) => p.once("exit", () => resolve()));
|
||||
};
|
||||
const transfer = () =>
|
||||
new Promise<number>((resolve) => {
|
||||
const p = spawn(
|
||||
"curl",
|
||||
[
|
||||
"--noproxy",
|
||||
"",
|
||||
"-fsS",
|
||||
"--max-time",
|
||||
"4",
|
||||
"--socks5-hostname",
|
||||
"127.0.0.1:39193",
|
||||
"http://127.0.0.1:39195",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
p.once("exit", (code) => resolve(code ?? 1));
|
||||
});
|
||||
try {
|
||||
execFileSync(binary, ["check", "-c", path.join(dir, "server.json")], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
server = start("server.json");
|
||||
client = start("client.json");
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
assert.equal(await transfer(), 0, "real AnyTLS transfer");
|
||||
writeFileSync(path.join(dir, "stats.proto"), bundle.statsProto);
|
||||
execFileSync(python, [
|
||||
"-m",
|
||||
"grpc_tools.protoc",
|
||||
"-I",
|
||||
dir,
|
||||
`--python_out=${dir}`,
|
||||
path.join(dir, "stats.proto"),
|
||||
]);
|
||||
writeFileSync(
|
||||
path.join(dir, "query.py"),
|
||||
`import grpc,json,stats_pb2\nwith grpc.insecure_channel('127.0.0.1:39196') as c:\n q=c.unary_unary('/v2ray.core.app.stats.command.StatsService/QueryStats',request_serializer=stats_pb2.QueryStatsRequest.SerializeToString,response_deserializer=stats_pb2.QueryStatsResponse.FromString)\n print(json.dumps({s.name:s.value for s in q(stats_pb2.QueryStatsRequest(pattern='user>>>'),timeout=5).stat}))\n`,
|
||||
);
|
||||
const counters = JSON.parse(
|
||||
execFileSync(python, [path.join(dir, "query.py")], { encoding: "utf8" }),
|
||||
);
|
||||
const tx = counters[`user>>>${authId}>>>traffic>>>downlink`],
|
||||
rx = counters[`user>>>${authId}>>>traffic>>>uplink`];
|
||||
assert.ok(tx >= 65536 && rx > 0);
|
||||
await provisionNode(token, "complete");
|
||||
await syncAnyTLSNode("runtime", bundle.agent.token, {
|
||||
epoch: "native:1",
|
||||
traffic: { [authId]: { tx, rx } },
|
||||
streamCount: 0,
|
||||
});
|
||||
const db = await ensureDb();
|
||||
db.prepare("UPDATE hy2_users SET enabled=0 WHERE auth_id=?").run(authId);
|
||||
const disabled = await syncAnyTLSNode("runtime", bundle.agent.token, {
|
||||
epoch: "native:1",
|
||||
traffic: { [authId]: { tx, rx } },
|
||||
streamCount: 0,
|
||||
});
|
||||
await stop(server);
|
||||
writeFileSync(
|
||||
path.join(dir, "server.json"),
|
||||
JSON.stringify(disabled.config).replaceAll("/etc/hy2-panel-anytls", dir),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
server = start("server.json");
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
assert.notEqual(await transfer(), 0, "disabled credentials must fail");
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
anytlsTransfer: true,
|
||||
tlsVerified: true,
|
||||
perUserTx: tx,
|
||||
perUserRx: rx,
|
||||
disabledUserRejected: true,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await stop(client);
|
||||
await stop(server);
|
||||
await new Promise<void>((resolve) => target.close(() => resolve()));
|
||||
(await ensureDb()).close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.error(e.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+179
-144
@@ -3,148 +3,183 @@ import { certificateFixture } from "./certificate-fixture";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("upload wildcard certificate, download script and complete NAT node registration", async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
const fixture = certificateFixture();
|
||||
await page.goto("/login");
|
||||
await page.getByLabel("用户名", { exact: true }).fill("ui-review");
|
||||
await page.getByLabel("密码", { exact: true }).fill("ui-review-local-2026");
|
||||
await page.getByRole("button", { name: "登录", exact: true }).click();
|
||||
await expect(page).toHaveURL(/dashboard/);
|
||||
await page.getByRole("link", { name: "证书与安装", exact: true }).click();
|
||||
const upload = page.locator("details").filter({
|
||||
has: page.getByRole("heading", { name: "上传证书", exact: true }),
|
||||
for (const protocol of ["hy2", "anytls"] as const)
|
||||
test(`upload wildcard certificate and register ${protocol} NAT node`, async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
const fixture = certificateFixture();
|
||||
await page.goto("/login");
|
||||
await page.getByLabel("用户名", { exact: true }).fill("ui-review");
|
||||
await page.getByLabel("密码", { exact: true }).fill("ui-review-local-2026");
|
||||
await page.getByRole("button", { name: "登录", exact: true }).click();
|
||||
await expect(page).toHaveURL(/dashboard/);
|
||||
await page.getByRole("link", { name: "证书与安装", exact: true }).click();
|
||||
const upload = page.locator("details").filter({
|
||||
has: page.getByRole("heading", { name: "上传证书", exact: true }),
|
||||
});
|
||||
await upload.getByLabel("证书名称").fill("测试通配符证书");
|
||||
await upload.getByLabel("CRT / 完整证书链").setInputFiles({
|
||||
name: "test.crt",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from(fixture.certificate),
|
||||
});
|
||||
await upload.getByLabel("KEY / 私钥").setInputFiles({
|
||||
name: "test.key",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from("invalid"),
|
||||
});
|
||||
await upload.getByRole("button", { name: "上传证书", exact: true }).click();
|
||||
await expect(upload.getByRole("alert")).toContainText("无法解析");
|
||||
await upload.getByLabel("KEY / 私钥").setInputFiles({
|
||||
name: "test.key",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from(fixture.privateKey),
|
||||
});
|
||||
await upload.getByRole("button", { name: "上传证书", exact: true }).click();
|
||||
await expect(
|
||||
page.getByText("测试通配符证书", { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
await page.reload();
|
||||
const create = page.locator("details").filter({
|
||||
has: page.getByRole("heading", {
|
||||
name: "生成服务器安装脚本",
|
||||
exact: true,
|
||||
}),
|
||||
});
|
||||
if ((await create.getAttribute("open")) === null)
|
||||
await create.locator("summary").click();
|
||||
await create.getByLabel("节点名称", { exact: true }).fill("NAT 浏览器测试");
|
||||
await create.getByLabel("节点 Slug", { exact: true }).fill("nat-browser");
|
||||
await create.getByLabel("代理协议", { exact: true }).selectOption(protocol);
|
||||
await create
|
||||
.getByLabel("托管证书", { exact: true })
|
||||
.selectOption({ label: "测试通配符证书" });
|
||||
await create
|
||||
.getByRole("button", { name: "生成安装脚本", exact: true })
|
||||
.click();
|
||||
const row = page
|
||||
.locator(".provision-row")
|
||||
.filter({ hasText: "nat-browser" });
|
||||
await expect(row).toContainText("等待安装");
|
||||
const downloadPath = await row
|
||||
.getByRole("link", { name: "下载 SH" })
|
||||
.getAttribute("href");
|
||||
expect(
|
||||
(await request.get(downloadPath!, { maxRedirects: 0 })).status(),
|
||||
).toBe(307);
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
row.getByRole("link", { name: "下载 SH" }).click(),
|
||||
]);
|
||||
const script = readFileSync((await download.path())!, "utf8");
|
||||
expect(script).not.toContain("PRIVATE KEY");
|
||||
expect(script).toContain("--domain");
|
||||
const token = script.match(/^TOKEN='([a-f0-9]+)'$/m)![1];
|
||||
const ports =
|
||||
protocol === "anytls"
|
||||
? {
|
||||
domain: "nat.p.xn--876a.net",
|
||||
port: 23456,
|
||||
listenPort: 23456,
|
||||
statsPort: 19090,
|
||||
controlPort: 19091,
|
||||
}
|
||||
: {
|
||||
domain: "nat.p.xn--876a.net",
|
||||
hy2Port: 23456,
|
||||
statsPort: 23457,
|
||||
hy2ListenPort: 443,
|
||||
statsListenPort: 9999,
|
||||
};
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", {
|
||||
data: { action: "prepare", ports },
|
||||
})
|
||||
).status(),
|
||||
).toBe(400);
|
||||
expect(
|
||||
(await request.post("/api/install", { data: "x".repeat(5000) })).status(),
|
||||
).toBe(413);
|
||||
const prepare = await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "prepare", ports },
|
||||
});
|
||||
expect(prepare.status()).toBe(200);
|
||||
expect(prepare.headers()["cache-control"]).toBe("no-store");
|
||||
const bundle = await prepare.json();
|
||||
expect(bundle.privateKey).toBe(fixture.privateKey);
|
||||
const config = parse(bundle.config);
|
||||
if (protocol === "hy2") {
|
||||
expect(config.listen).toBe(":443");
|
||||
expect(config.trafficStats.listen).toBe(":9999");
|
||||
} else {
|
||||
expect(config.inbounds[0].type).toBe("anytls");
|
||||
expect(config.experimental.v2ray_api.listen).toBe("127.0.0.1:19090");
|
||||
}
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "complete" },
|
||||
})
|
||||
).status(),
|
||||
).toBe(200);
|
||||
await page.reload();
|
||||
await expect(row).toContainText("已安装");
|
||||
await expect(row.getByRole("link", { name: "下载 SH" })).toHaveCount(0);
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "prepare", ports },
|
||||
})
|
||||
).status(),
|
||||
).toBe(400);
|
||||
await page.screenshot({
|
||||
path: "test-results/certificates-desktop.png",
|
||||
fullPage: true,
|
||||
});
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.screenshot({
|
||||
path: "test-results/certificates-mobile.png",
|
||||
fullPage: true,
|
||||
});
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth + 1,
|
||||
),
|
||||
).toBe(true);
|
||||
await page.getByRole("link", { name: "节点管理", exact: true }).click();
|
||||
const node = page
|
||||
.locator(".entity-table__item")
|
||||
.filter({ hasText: "nat-browser" });
|
||||
await expect(node).toContainText("nat.p.xn--876a.net");
|
||||
await node.locator(":scope > summary").click();
|
||||
if (protocol === "hy2") {
|
||||
await expect(node.getByLabel("对外端口", { exact: true })).toHaveValue(
|
||||
"23456",
|
||||
);
|
||||
await expect(node.getByLabel("SNI", { exact: true })).toHaveValue(
|
||||
"nat.p.xn--876a.net",
|
||||
);
|
||||
} else {
|
||||
await expect(node).toContainText("节点主动上报");
|
||||
await expect(node.getByLabel("节点名称 / 订阅名")).toHaveValue(
|
||||
"NAT 浏览器测试",
|
||||
);
|
||||
const sync = await request.post("/api/agent/nat-browser/sync", {
|
||||
headers: { Authorization: `Bearer ${bundle.agent.token}` },
|
||||
data: { epoch: "test:1", traffic: {}, streamCount: 1 },
|
||||
});
|
||||
expect(sync.status()).toBe(200);
|
||||
expect(
|
||||
(await sync.json()).config.inbounds[0].users.length,
|
||||
).toBeGreaterThan(0);
|
||||
}
|
||||
await expect(node.getByLabel("启用节点", { exact: true })).toBeChecked();
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
await upload.getByLabel("证书名称").fill("测试通配符证书");
|
||||
await upload.getByLabel("CRT / 完整证书链").setInputFiles({
|
||||
name: "test.crt",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from(fixture.certificate),
|
||||
});
|
||||
await upload.getByLabel("KEY / 私钥").setInputFiles({
|
||||
name: "test.key",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from("invalid"),
|
||||
});
|
||||
await upload.getByRole("button", { name: "上传证书", exact: true }).click();
|
||||
await expect(upload.getByRole("alert")).toContainText("无法解析");
|
||||
await upload.getByLabel("KEY / 私钥").setInputFiles({
|
||||
name: "test.key",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from(fixture.privateKey),
|
||||
});
|
||||
await upload.getByRole("button", { name: "上传证书", exact: true }).click();
|
||||
await expect(
|
||||
page.getByText("测试通配符证书", { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
await page.reload();
|
||||
const create = page.locator("details").filter({
|
||||
has: page.getByRole("heading", {
|
||||
name: "生成服务器安装脚本",
|
||||
exact: true,
|
||||
}),
|
||||
});
|
||||
if ((await create.getAttribute("open")) === null)
|
||||
await create.locator("summary").click();
|
||||
await create.getByLabel("节点名称", { exact: true }).fill("NAT 浏览器测试");
|
||||
await create.getByLabel("节点 Slug", { exact: true }).fill("nat-browser");
|
||||
await create
|
||||
.getByLabel("托管证书", { exact: true })
|
||||
.selectOption({ label: "测试通配符证书" });
|
||||
await create
|
||||
.getByRole("button", { name: "生成安装脚本", exact: true })
|
||||
.click();
|
||||
const row = page.locator(".provision-row").filter({ hasText: "nat-browser" });
|
||||
await expect(row).toContainText("等待安装");
|
||||
const downloadPath = await row
|
||||
.getByRole("link", { name: "下载 SH" })
|
||||
.getAttribute("href");
|
||||
expect((await request.get(downloadPath!, { maxRedirects: 0 })).status()).toBe(
|
||||
307,
|
||||
);
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
row.getByRole("link", { name: "下载 SH" }).click(),
|
||||
]);
|
||||
const script = readFileSync((await download.path())!, "utf8");
|
||||
expect(script).not.toContain("PRIVATE KEY");
|
||||
expect(script).toContain("--domain");
|
||||
const token = script.match(/^TOKEN='([a-f0-9]+)'$/m)![1];
|
||||
const ports = {
|
||||
domain: "nat.p.xn--876a.net",
|
||||
hy2Port: 23456,
|
||||
statsPort: 23457,
|
||||
hy2ListenPort: 443,
|
||||
statsListenPort: 9999,
|
||||
};
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", { data: { action: "prepare", ports } })
|
||||
).status(),
|
||||
).toBe(400);
|
||||
expect(
|
||||
(await request.post("/api/install", { data: "x".repeat(5000) })).status(),
|
||||
).toBe(413);
|
||||
const prepare = await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "prepare", ports },
|
||||
});
|
||||
expect(prepare.status()).toBe(200);
|
||||
expect(prepare.headers()["cache-control"]).toBe("no-store");
|
||||
const bundle = await prepare.json();
|
||||
expect(bundle.privateKey).toBe(fixture.privateKey);
|
||||
const config = parse(bundle.config);
|
||||
expect(config.listen).toBe(":443");
|
||||
expect(config.trafficStats.listen).toBe(":9999");
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "complete" },
|
||||
})
|
||||
).status(),
|
||||
).toBe(200);
|
||||
await page.reload();
|
||||
await expect(row).toContainText("已安装");
|
||||
await expect(row.getByRole("link", { name: "下载 SH" })).toHaveCount(0);
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "prepare", ports },
|
||||
})
|
||||
).status(),
|
||||
).toBe(400);
|
||||
await page.screenshot({
|
||||
path: "test-results/certificates-desktop.png",
|
||||
fullPage: true,
|
||||
});
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.screenshot({
|
||||
path: "test-results/certificates-mobile.png",
|
||||
fullPage: true,
|
||||
});
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth + 1,
|
||||
),
|
||||
).toBe(true);
|
||||
await page.getByRole("link", { name: "节点管理", exact: true }).click();
|
||||
const node = page
|
||||
.locator(".entity-table__item")
|
||||
.filter({ hasText: "nat-browser" });
|
||||
await expect(node).toContainText("nat.p.xn--876a.net");
|
||||
await node.locator(":scope > summary").click();
|
||||
await expect(node.getByLabel("对外端口", { exact: true })).toHaveValue(
|
||||
"23456",
|
||||
);
|
||||
await expect(node.getByLabel("SNI", { exact: true })).toHaveValue(
|
||||
"nat.p.xn--876a.net",
|
||||
);
|
||||
await expect(node.getByLabel("启用节点", { exact: true })).toBeChecked();
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
@@ -25,7 +25,11 @@ function snapshot(db: Database.Database) {
|
||||
)
|
||||
.filter(
|
||||
({ name }) =>
|
||||
!["managed_certificates", "node_installations"].includes(name),
|
||||
![
|
||||
"managed_certificates",
|
||||
"node_installations",
|
||||
"node_agent_counters",
|
||||
].includes(name),
|
||||
)
|
||||
.map(({ name }) => [name, db.prepare(`SELECT * FROM "${name}"`).all()]),
|
||||
);
|
||||
@@ -45,7 +49,7 @@ test("fresh database exactly matches the inspected remote v4 column definitions"
|
||||
const fresh = new Database(":memory:");
|
||||
const remote = remoteDb();
|
||||
migrateDatabase(fresh);
|
||||
assert.equal(fresh.pragma("user_version", { simple: true }), 5);
|
||||
assert.equal(fresh.pragma("user_version", { simple: true }), 6);
|
||||
for (const { name } of remote
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.all() as { name: string }[]) {
|
||||
@@ -98,7 +102,7 @@ for (const existing of [
|
||||
if (!existing.includes(field)) delete row[field];
|
||||
}
|
||||
assert.deepEqual(after, before);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 5);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 6);
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
@@ -128,8 +132,8 @@ test("failed migrations roll back all statements and future schemas are not down
|
||||
db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(),
|
||||
[{ name: "hy2_users" }],
|
||||
);
|
||||
db.pragma("user_version = 6");
|
||||
db.pragma("user_version = 7");
|
||||
assert.throws(() => migrateDatabase(db), /版本/);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 6);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 7);
|
||||
db.close();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user