'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { entries, nodeClientUri, formatSubscription, makeSingBox, clashProxy } = require('../lib/subscriptions'); // 最小可用数据集:节点 + 服务器 + 分配,entries() 现场生成客户端链接。 function makeData() { const servers = [ { id: 'srv_1', publicAddress: '2001:db8::1', tlsDomain: null }, { id: 'srv_2', publicAddress: 'example.com', tlsDomain: 'entry.example.com' }, ]; const nodes = [ { id: 'nod_reality', name: 'Reality', protocol: 'vless-reality-vision', port: 443, serverId: 'srv_1', networkMode: 'ipv4', topology: 'direct', status: 'active', reality: { keyId: 'k1', serverName: 'www.tesla.com', destPort: 443 }, artifacts: { realityPublicKey: 'public-key' } }, { id: 'nod_ss', name: 'SS', protocol: 'shadowsocks-2022-aes128', port: 1443, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', artifacts: {} }, { id: 'nod_down', name: '故障节点', protocol: 'shadowsocks-2022-aes128', port: 1444, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'failed', artifacts: {} }, ]; const nodeClients = [ { nodeId: 'nod_reality', customerId: 'cus-1', status: 'active', credentials: { clientId: '00000000-0000-4000-8000-000000000001', relayPassword: 'secret', shortId: 'abcdef' } }, { nodeId: 'nod_ss', customerId: 'cus-1', status: 'active', credentials: { clientId: '00000000-0000-4000-8000-000000000002', relayPassword: 'c2VjcmV0', shortId: '123456' } }, { nodeId: 'nod_down', customerId: 'cus-1', status: 'active', credentials: { clientId: '00000000-0000-4000-8000-000000000003', relayPassword: 'c2VjcmV0', shortId: '123456' } }, { nodeId: 'nod_ss', customerId: 'cus-1', status: 'suspended', credentials: { clientId: '00000000-0000-4000-8000-000000000004', relayPassword: 'c2VjcmV0', shortId: '123456' } }, ]; return { servers, nodes, nodeClients }; } test('formats active Reality and Shadowsocks entries, excluding failed nodes and suspended assignments', () => { const data = makeData(); const items = entries(data, 'cus-1'); assert.equal(items.length, 2); assert.ok(items[0].clientUri.startsWith('vless://')); assert.ok(items[1].clientUri.startsWith('ss://')); const proxy = clashProxy(items[0]); assert.equal(proxy.server, '2001:db8::1'); assert.equal(proxy['reality-opts']['public-key'], 'public-key'); const config = JSON.parse(makeSingBox(items)); assert.equal(config.outbounds[1].tls.reality.short_id, 'abcdef'); assert.equal(config.outbounds[2].method, '2022-blake3-aes-128-gcm'); assert.equal(formatSubscription(data, { id: 'cus-1' }, 'auto', 'Mihomo').contentType, 'text/yaml; charset=utf-8'); assert.equal(formatSubscription(data, { id: 'cus-1' }, 'auto', 'Other').contentType, 'text/plain; charset=utf-8'); assert.doesNotMatch(formatSubscription(data, { id: 'cus-1' }, 'raw').body, /故障节点/); }); test('nodeClientUri returns null for inactive nodes', () => { const data = makeData(); const nc = data.nodeClients.find((item) => item.nodeId === 'nod_down'); assert.equal(nodeClientUri(data, nc), null); }); test('TLS WebSocket exports valid Mihomo and sing-box TLS settings', () => { const data = makeData(); data.nodes.push({ id: 'nod_tls', name: 'TLS', protocol: 'vless-ws-tls', port: 24444, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', wsPath: '/test', artifacts: {} }); data.nodeClients.push({ nodeId: 'nod_tls', customerId: 'cus-tls', status: 'active', credentials: { clientId: '00000000-0000-4000-8000-000000000001', relayPassword: 'secret', shortId: 'abcdef' } }); const items = entries(data, 'cus-tls'); assert.equal(items.length, 1); const proxy = clashProxy(items[0]); assert.equal(proxy.tls, true); assert.equal(proxy.servername, 'entry.example.com'); assert.equal(proxy['reality-opts'], undefined); const outbound = JSON.parse(makeSingBox(items)).outbounds[1]; assert.equal(outbound.tls.enabled, true); assert.equal(outbound.tls.reality, undefined); assert.equal(formatSubscription(data, { id: 'cus-tls' }, 'surge'), null); }); test('adding HY2 to an AnyTLS subscription keeps ALPN on one valid YAML line and node names clean', () => { const data = makeData(); data.nodes.push( { id: 'nod_any', name: 'US-rak-CN2', protocol: 'anytls', port: 48924, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', artifacts: {} }, { id: 'nod_hy', name: 'hy2 cn2', protocol: 'hysteria2', port: 40003, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', artifacts: {} }); for (const [nodeId, password] of [['nod_any', 'secret'], ['nod_hy', 'secret']]) { data.nodeClients.push({ nodeId, customerId: '55', status: 'active', credentials: { clientId: '00000000-0000-4000-8000-000000000001', relayPassword: password, shortId: 'abcdef' } }); } const customer = { id: '55' }; const raw = formatSubscription(data, customer, 'raw').body; assert.match(raw, /#US-rak-CN2\n/); assert.match(raw, /#hy2%20cn2\n/); const yaml = formatSubscription(data, customer, 'clash').body; assert.match(yaml, /^ alpn: \["h3"\]$/m); assert.match(yaml, /^proxy-groups:$/m); assert.doesNotMatch(yaml, /^"h3"$/m); assert.match(yaml, /^ - name: "US-rak-CN2"$/m); assert.match(yaml, /^ - name: "hy2 cn2"$/m); }); test('Clash aliases only node names that collide with built-in groups or policies', () => { const data = makeData(); data.nodes.push( { id: 'nod_a', name: '节点选择', protocol: 'anytls', port: 443, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', artifacts: {} }, { id: 'nod_b', name: 'DIRECT', protocol: 'hysteria2', port: 444, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', artifacts: {} }); for (const nodeId of ['nod_a', 'nod_b']) { data.nodeClients.push({ nodeId, customerId: 'one', status: 'active', credentials: { clientId: '00000000-0000-4000-8000-000000000001', relayPassword: 'password', shortId: 'abcdef' } }); } const yaml = formatSubscription(data, { id: 'one' }, 'clash-smart').body; assert.match(yaml, /^ - name: "节点选择 \(节点\)"$/m); assert.match(yaml, /^ - name: "DIRECT \(节点\)"$/m); assert.match(yaml, /^ - "节点选择 \(节点\)"$/m); assert.match(formatSubscription(data, { id: 'one' }, 'raw').body, /#%E8%8A%82%E7%82%B9%E9%80%89%E6%8B%A9/); }); test('SS2022 client links carry ":" for the EIH identity header', () => { const data = makeData(); // 2022 node with a server PSK: link password must be colon-joined so the // sing-box multi-user server can attribute the connection (EIH). data.nodes.push({ id: 'nod_ss2022', name: 'SS2022', protocol: 'shadowsocks-2022-aes128', port: 1445, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', ssServerKey: 'c2VydmVyLW1haW4tcHNr', artifacts: {} }); // Classic AEAD multi-user needs no EIH: user password alone. data.nodes.push({ id: 'nod_ssold', name: 'SSold', protocol: 'shadowsocks-aes128-gcm', port: 1446, serverId: 'srv_2', networkMode: 'ipv4', topology: 'direct', status: 'active', ssServerKey: 'c2VydmVyLW1haW4tcHNr', artifacts: {} }); for (const nodeId of ['nod_ss2022', 'nod_ssold']) { data.nodeClients.push({ nodeId, customerId: 'cus-eih', status: 'active', credentials: { clientId: '00000000-0000-4000-8000-000000000001', relayPassword: 'dXNlci1wYXNzd29yZA', shortId: 'abcdef' } }); } const decodeSs = (uri) => { const payload = Buffer.from(new URL(uri).username, 'base64url').toString('utf8'); return payload.slice(payload.indexOf(':') + 1); }; const items = entries(data, 'cus-eih'); assert.equal(items.length, 2); assert.equal(decodeSs(items[0].clientUri), 'c2VydmVyLW1haW4tcHNr:dXNlci1wYXNzd29yZA'); assert.equal(decodeSs(items[1].clientUri), 'dXNlci1wYXNzd29yZA'); // The sing-box subscription outbound inherits the joined password verbatim. const config = JSON.parse(makeSingBox(items)); assert.equal(config.outbounds[1].password, 'c2VydmVyLW1haW4tcHNr:dXNlci1wYXNzd29yZA'); });