Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"use strict";2
const test = require("node:test");3
const assert = require("node:assert/strict");4
const dgram = require("node:dgram");5
const net = require("node:net");6
const { once } = require("node:events");7
const { DnsDiagnostics, TYPES, parseResponse, readName, reverseName } = require("../src/main/diagnostics/dnsDiagnostics");8
const { pingCommand, tracerouteCommand, parsePingReply, parseHop, ping, traceroute, runProcess } = require("../src/main/diagnostics/processTools");9
const { normalizeOptions } = require("../src/main/diagnostics/common");10
function name(value) { return Buffer.concat([...value.split(".").map((part) => Buffer.concat([Buffer.from([part.length]), Buffer.from(part)])), Buffer.from([0])]); }11
function answer(packet, code = 0, records = []) {12
const header = Buffer.from(packet.subarray(0, 12)); header.writeUInt16BE(0x8180 | code, 2); header.writeUInt16BE(records.length, 6);13
return Buffer.concat([header, packet.subarray(12), ...records.map(([type, data]) => {14
const fields = Buffer.alloc(12); fields.writeUInt16BE(0xc00c); fields.writeUInt16BE(TYPES[type], 2); fields.writeUInt16BE(1, 4); fields.writeUInt32BE(60, 6); fields.writeUInt16BE(data.length, 10);15
return Buffer.concat([fields, data]);16
})]);17
}18
const options = normalizeOptions({ target: "example.test", tools: ["dns"], timeoutMs: 250, resolver: "custom", customDns: "127.0.0.1" });19
const signal = () => new AbortController().signal;20
test("DNS wire decoding retains TTL, NXDOMAIN/NODATA/SERVFAIL, DNSSEC records and never claims validation", () => {21
const header = Buffer.alloc(12); header.writeUInt16BE(1234); header.writeUInt16BE(1, 4);22
const packet = Buffer.concat([header, name("example.test"), Buffer.from([0, 1, 0, 1])]);23
const decoded = parseResponse(answer(packet, 0, [["A", Buffer.from([127,0,0,1])], ["DS", Buffer.from([0,1,8,2,0xab,0xcd])], ["DNSKEY", Buffer.from([1,1,3,8,1,2,3])]]), 1234, "A");24
assert.equal(decoded.records[0].ttl, 60); assert.equal(decoded.questionName, "example.test"); assert.equal(decoded.records[1].value.digest, "abcd"); assert.equal(decoded.dnssec, "not-validated");25
assert.equal(parseResponse(answer(packet, 3), 1234, "A").status, "nxdomain");26
assert.equal(parseResponse(answer(packet, 2), 1234, "A").status, "servfail");27
assert.equal(parseResponse(answer(packet), 1234, "A").status, "nodata");28
assert.throws(() => parseResponse(answer(packet), 999, "A")); assert.throws(() => readName(Buffer.from([0xc0,0]), 0));29
assert.equal(reverseName("127.0.0.1"), "1.0.0.127.in-addr.arpa"); assert.ok(reverseName("::1").endsWith("ip6.arpa")); assert.equal(reverseName("::ffff:127.0.0.1").split(".").length, 34);30
});31
test("local DNS handles UDP, truncated-response TCP fallback, IPv4-only/IPv6-only/dual-stack and timeout", async (t) => {32
const udp = dgram.createSocket("udp4"), tcp = net.createServer(); let tcpQueries = 0;33
udp.bind(0, "127.0.0.1"); await once(udp, "listening"); const port = udp.address().port;34
await new Promise((resolve) => tcp.listen(port, "127.0.0.1", resolve));35
t.after(async () => { udp.close(); await new Promise((resolve) => tcp.close(resolve)); });36
const respond = (packet) => {37
const query = readName(packet, 12), type = packet.readUInt16BE(query.end), host = query.name;38
if (host === "missing.test") return answer(packet, 3);39
const records = [];40
if (type === TYPES.A && host !== "v6.test") records.push(["A", Buffer.from([127,0,0,1])]);41
if (type === TYPES.AAAA && host !== "v4.test") { const data = Buffer.alloc(16); data[15] = 1; records.push(["AAAA", data]); }42
return answer(packet, 0, records);43
};44
udp.on("message", (packet, remote) => {45
const host = readName(packet, 12).name; if (host === "timeout.test") return;46
const response = host === "tcp.test" ? answer(packet) : respond(packet); if (host === "tcp.test") response[2] |= 2;47
udp.send(response, remote.port, remote.address);48
});49
tcp.on("connection", (socket) => { socket.once("data", (data) => { tcpQueries++; const response = respond(data.subarray(2)), size = Buffer.alloc(2); size.writeUInt16BE(response.length); socket.end(Buffer.concat([size, response])); }); });50
const dns = new DnsDiagnostics(); dns.servers = () => [`127.0.0.1:${port}`];51
assert.equal((await dns.resolve("v4.test", options, signal())).length, 1);52
assert.equal((await dns.resolve("v6.test", options, signal()))[0].family, 6);53
assert.equal((await dns.resolve("dual.test", options, signal())).length, 2);54
await assert.rejects(dns.resolve("missing.test", options, signal()), { code: "DNS_NO_ADDRESS" });55
assert.equal((await dns.query("tcp.test", "A", options, signal())).records[0].value, "127.0.0.1"); assert.equal(tcpQueries, 1);56
await assert.rejects(dns.query("timeout.test", "A", options, signal()), { code: "TIMEOUT" });57
const controller = new AbortController(); controller.abort(); await assert.rejects(dns.query("timeout.test", "A", options, controller.signal), { code: "CANCELLED" });58
});59
test("DNS detects CNAME chains and compares repeated answers from the chosen resolver", async () => {60
const dns = new DnsDiagnostics(); let aQueries = 0;61
dns.query = async (host, type) => ({ name: host, type, status: "success", durationMs: 1, records: host === "alias.test" && ["A", "CNAME"].includes(type) ? [{ name: host, type: "CNAME", value: "target.test", section: "answer", ttl: 60 }] : host === "target.test" && type === "A" ? [{ name: host, type, value: "127.0.0.1", section: "answer", ttl: 60 }] : type === "AAAA" ? [{ name: host, type, value: (++aQueries > 1 ? "::2" : "::1"), section: "answer" }] : [] });62
assert.equal((await dns.resolve("alias.test", { ...options, mode: "ipv4" }, signal()))[0].address, "127.0.0.1");63
const result = await dns.run("alias.test", options, signal(), () => {}); assert.deepEqual(result.cnameChain, ["alias.test → target.test"]); assert.equal(result.consistency.find((r) => r.type === "AAAA").status, "changed");64
});65
test("OS command arguments and localized ping/traceroute parsers handle both IP families", () => {66
for (const platform of ["win32", "darwin", "linux"]) for (const address of ["127.0.0.1", "::1"]) {67
const ping = pingCommand(address, 500, platform), trace = tracerouteCommand(address, { timeoutMs: 500, maxHops: 3 }, platform);68
assert.equal(ping.args.at(-1), address); assert.equal(trace.args.at(-1), address); assert.ok(trace.args.includes("3"));69
}70
assert.throws(() => pingCommand("localhost;bad", 500, "linux"));71
assert.equal(parsePingReply("Antwort von 127.0.0.1: Bytes=32 Zeit=1,25ms TTL=64").rttMs, 1.25);72
assert.equal(parsePingReply("Reply: time<1ms TTL=128").rttMs, null); assert.equal(parsePingReply("Reply: time<1ms TTL=128").received, true);73
assert.equal(parsePingReply("64 bytes from ::1: icmp_seq=0 hlim=64 time=0.032 ms").ttl, 64);74
const hop = parseHop(" 2 192.0.2.1 1.20 ms * 1.40 ms"); assert.equal(hop.hop, 2); assert.equal(hop.probes.length, 3); assert.ok(Math.abs(hop.averageMs - 1.3) < 1e-12);75
assert.equal(parseHop(" 3 <1 ms 2 ms * 2001:db8::1").addresses[0], "2001:db8::1");76
assert.equal(parseHop("traceroute to example"), null);77
});78
test("ICMP absence and missing tools do not claim a server is offline; traceroute updates are progressive", async () => {79
const settings = { ...options, count: 1, repetitions: 1, continuous: false, maxHops: 3 }, events = [];80
const blocked = await ping("127.0.0.1", settings, signal(), (event) => events.push(event), async () => ({ code: 1, stdout: Buffer.from("Request timed out."), stderr: "" }));81
assert.equal(blocked.status, "warning"); assert.equal(blocked.packets[0].status, "timeout"); assert.equal(blocked.lossPercent, 100); assert.equal(blocked.packets[0].rttMs, null); assert.ok(blocked.packets[0].measuredAt);82
const missing = await ping("127.0.0.1", settings, signal(), () => {}, async () => { throw Object.assign(new Error("missing"), { code: "ENOENT" }); }); assert.equal(missing.status, "unavailable");83
const trace = await traceroute("127.0.0.1", settings, signal(), (event) => events.push(event), async (_exe, _args, { onLine }) => { onLine("1 * * *"); onLine("2 127.0.0.1 0.1 ms 0.2 ms 0.3 ms"); return { code: 0, stdout: Buffer.from("trace"), stderr: "" }; });84
assert.equal(trace.status, "success"); assert.equal(trace.routes[0].hops.length, 2); assert.equal(events.filter((e) => e.kind === "trace-hop").length, 2);85
});86
test("process runner uses argument arrays with shell disabled and terminates on timeout, abort and output limit", async () => {87
const { spawn } = require("node:child_process"); let config;88
const result = await runProcess(process.execPath, ["-e", "process.stdout.write('ok')"], { spawnImpl: (exe, args, options) => { config = options; return spawn(exe, args, options); } });89
assert.equal(result.stdout.toString(), "ok"); assert.equal(config.shell, false);90
await assert.rejects(runProcess(process.execPath, ["-e", "setInterval(()=>{},1000)"], { timeoutMs: 30 }), { code: "TIMEOUT" });91
await assert.rejects(runProcess(process.execPath, ["-e", "process.stdout.write('x'.repeat(10000))"], { maxBytes: 50 }), { code: "OUTPUT_LIMIT" });92
const controller = new AbortController(), work = runProcess(process.execPath, ["-e", "setInterval(()=>{},1000)"], { signal: controller.signal }); controller.abort(); await assert.rejects(work, { code: "CANCELLED" });93
await assert.rejects(runProcess("chj-nonexistent-diagnostic-tool", []), { code: "ENOENT" });94
});96
test("system resolution honors localhost/hosts-file entries and remains timeout/cancel bounded", async () => {97
const dns = new DnsDiagnostics(); const addresses = await dns.resolve("localhost", { ...options, resolver: "system", timeoutMs: 1000 }, signal());98
assert.ok(addresses.some((a) => ["127.0.0.1", "::1"].includes(a.address)));99
const hanging = new DnsDiagnostics({ lookup() {} });100
await assert.rejects(hanging.resolve("localhost", { ...options, resolver: "system", timeoutMs: 20 }, signal()), { code: "TIMEOUT" });101
const controller = new AbortController(), work = hanging.resolve("localhost", { ...options, resolver: "system" }, controller.signal); controller.abort(); await assert.rejects(work);102
});SHA-256: 74705216e71251bfd578d0bf831f581f0c5cf30457a231fe9f957deab8467f76
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0