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 http = require("node:http");5
const http2 = require("node:http2");6
const tls = require("node:tls");7
const net = require("node:net");8
const crypto = require("node:crypto");9
const zlib = require("node:zlib");10
const fs = require("node:fs");11
const path = require("node:path");12
const { once } = require("node:events");13
const { Server } = require("ssh2");14
const { parseTarget, normalizeOptions } = require("../src/main/diagnostics/common");15
const { HttpDiagnostics, nativeRequest, http3Request, getCurlCapability, MAX_BODY, securityHeaders } = require("../src/main/diagnostics/httpDiagnostics");16
const { tlsProbe, tcpProbe, websocketProbe, sshIdentification } = require("../src/main/diagnostics/socketDiagnostics");17
const fixtures = path.join(__dirname, "fixtures", "diagnostics");18
const key = fs.readFileSync(path.join(fixtures, "server-key.pem")), cert = fs.readFileSync(path.join(fixtures, "server.pem")), ca = fs.readFileSync(path.join(fixtures, "ca.pem"));19
const signal = () => new AbortController().signal;20
const settings = { timeoutMs: 1000 };21
async function listen(t, server, host = "127.0.0.1") {22
const sockets = new Set(); server.on("connection", (socket) => { sockets.add(socket); socket.once("close", () => sockets.delete(socket)); });23
server.listen(0, host); await once(server, "listening");24
t.after(async () => { for (const socket of sockets) { if (socket.destroy) socket.destroy(); else socket.end(); } await new Promise((resolve) => server.close(resolve)); }); return server.address().port;25
}26
const dns = { resolve: async () => [{ address: "127.0.0.1", family: 4 }] };27
test("HTTP/1.1 measures actual bytes, headers, cold/warm reuse and bounded redirect chains", async (t) => {28
const server = http.createServer((req, res) => {29
if (req.url === "/loop") { res.writeHead(302, { location: "/loop" }); return res.end(); }30
if (req.url === "/redirect") { res.writeHead(301, { location: "/ok" }); return res.end(); }31
res.writeHead(200, { "content-type": "text/plain", "set-cookie": "password=private", "content-security-policy": "frame-ancestors 'none'" }); res.end("measured body");32
});33
const port = await listen(t, server), service = new HttpDiagnostics(dns), target = parseTarget(`http://localhost:${port}/redirect`);34
const options = normalizeOptions({ target: target.url, tools: ["http"], protocols: ["1.1"], repetitions: 3, warm: true });35
const result = await service.run(target, "127.0.0.1", options, signal(), () => {}), samples = result.comparisons[0].samples;36
assert.equal(samples.length, 3); assert.equal(samples[0].negotiated, "1.1"); assert.equal(samples[0].downloadedBytes, 13);37
assert.equal(samples[0].redirectCount, 1); assert.equal(samples[0].headers["set-cookie"], "[redacted]");38
assert.ok(samples[1].reused); assert.equal(samples[1].timings.tcpMs, null); assert.ok(samples[0].timings.totalMs > 0);39
assert.ok(!("_body" in samples[0])); assert.equal(result.comparisons[0].errorRate, 0);40
const resources = service.resources();41
try { await assert.rejects(service.request(parseTarget(`http://localhost:${port}/loop`), "127.0.0.1", "1.1", options, signal(), resources), { code: "REDIRECT_LOOP" }); }42
finally { service.dispose(resources); }43
assert.equal(securityHeaders({ "content-security-policy": "frame-ancestors 'none'" }, true).find((h) => h.name === "x-frame-options").status, "superseded-by-csp-frame-ancestors");44
});45
test("HTTP/2 uses real h2 TLS ALPN, verifies certificates and reuses the negotiated session", async (t) => {46
const server = http2.createSecureServer({ key, cert }); server.on("sessionError", () => {}); server.on("stream", (stream) => { stream.respond({ ":status": 200, "content-type": "text/plain" }); stream.end("h2 payload"); });47
const port = await listen(t, server), service = new HttpDiagnostics(dns, { ca }), target = parseTarget(`https://localhost:${port}/`);48
const options = normalizeOptions({ target: target.url, tools: ["http"], protocols: ["2"], repetitions: 3, warm: true });49
const result = await service.run(target, "127.0.0.1", options, signal(), () => {}), samples = result.comparisons[0].samples;50
assert.equal(samples[0].negotiated, "2"); assert.equal(samples[0].alpn, "h2"); assert.equal(samples[0].downloadedBytes, 10);51
assert.equal(samples[0].reused, false); assert.equal(samples[1].reused, true); assert.equal(samples[2].timings.tlsMs, null);52
await assert.rejects(nativeRequest(new URL(target.url), "127.0.0.1", "2", { ...settings, signal: signal() }));53
});54
test("HTTP timeout, cancellation and body cap terminate local requests", async (t) => {55
const server = http.createServer((req, res) => { if (req.url === "/large") res.end(Buffer.alloc(MAX_BODY + 1)); });56
const port = await listen(t, server);57
await assert.rejects(nativeRequest(new URL(`http://127.0.0.1:${port}/large`), "127.0.0.1", "1.1", { ...settings, signal: signal() }), { code: "BODY_LIMIT" });58
await assert.rejects(nativeRequest(new URL(`http://127.0.0.1:${port}/hang`), "127.0.0.1", "1.1", { timeoutMs: 30, signal: signal() }), { code: "TIMEOUT" });59
const controller = new AbortController(), request = nativeRequest(new URL(`http://127.0.0.1:${port}/hang`), "127.0.0.1", "1.1", { ...settings, signal: controller.signal });60
controller.abort(); await assert.rejects(request, { code: "CANCELLED" });61
});62
test("compression confirms gzip/deflate/Brotli using real decompression and rejects ignored Accept-Encoding", async (t) => {63
const body = Buffer.from("compression test ".repeat(100));64
const server = http.createServer((req, res) => {65
const encoding = req.headers["accept-encoding"];66
const compress = { gzip: zlib.gzipSync, deflate: zlib.deflateSync, br: zlib.brotliCompressSync, zstd: zlib.zstdCompressSync }[encoding];67
if (req.url !== "/ignored" && compress) { res.setHeader("content-encoding", encoding); res.end(compress(body)); } else res.end(body);68
});69
const port = await listen(t, server), service = new HttpDiagnostics(dns);70
for (const ignored of [false, true]) {71
const target = parseTarget(`http://localhost:${port}/${ignored ? "ignored" : "ok"}`), options = normalizeOptions({ target: target.url, tools: ["compression"] });72
const result = await service.compression(target, "127.0.0.1", options, signal(), () => {}), br = result.results.find((r) => r.encoding === "br");73
assert.equal(br.status, ignored ? "unsupported" : "success"); if (!ignored) { assert.equal(br.decodedBytes, body.length); assert.ok(br.ratio < 1); assert.equal(br.comparable, true); }74
}75
});76
test("HTTP/3 requires HTTP3-enabled curl, --http3-only and an actual version 3 result; fallback is rejected", async () => {77
const url = new URL("https://localhost/"), calls = [];78
const metrics = { http_version: "3", response_code: 200, remote_ip: "127.0.0.1", remote_port: 443, size_download: 4, content_type: "text/plain", time_appconnect: 0.02, time_starttransfer: 0.03, time_total: 0.04, speed_download: 100 };79
const runner = async (_exe, args) => { calls.push(args); return args.includes("--version") ? { stdout: Buffer.from("curl 8.10.0 test\nFeatures: SSL HTTP2 HTTP3\n") } : { code: 0, stdout: Buffer.from("HTTP/3 103\r\nlink: </early.css>\r\n\r\nHTTP/3 200\r\ncontent-type: text/plain\r\n\r\nbody\nCHJ_METRICS:" + JSON.stringify(metrics)), stderr: "" }; };80
const result = await http3Request(url, "127.0.0.1", settings, signal(), runner);81
assert.equal(result.negotiated, "3"); assert.equal(result.headers["content-type"], "text/plain"); assert.equal(result.headers.link, undefined); assert.equal(result.timings.tcpMs, null); assert.equal(result.timings.quicHandshakeMs, 20);82
assert.ok(calls[1].includes("--http3-only")); assert.ok(!calls[1].includes("--http3")); assert.ok(!calls[1].includes("--insecure")); assert.equal(calls[1][0], "--disable");83
metrics.http_version = "2"; assert.equal((await http3Request(url, "127.0.0.1", settings, signal(), runner)).code, "HTTP3_FALLBACK_REJECTED");84
assert.equal((await http3Request(url, "127.0.0.1", settings, signal(), async () => ({ stdout: Buffer.from("curl 8.7.1\nFeatures: SSL HTTP2\n") }))).code, "HTTP3_UNAVAILABLE");85
assert.equal((await http3Request(new URL("https://localhost:444/"), "127.0.0.1", settings, signal(), runner)).code, "HTTP3_REQUIRES_UDP443");86
assert.equal((await getCurlCapability(async () => { throw Object.assign(new Error("missing"), { code: "ENOENT" }); })).available, false);87
});88
test("TLS inspection differentiates trust, hostname mismatch and expiration without changing global verification", async (t) => {89
const server = tls.createServer({ key, cert }), expiredServer = tls.createServer({ key, cert: fs.readFileSync(path.join(fixtures, "expired.pem")) });90
const port = await listen(t, server), expiredPort = await listen(t, expiredServer), target = parseTarget(`https://localhost:${port}/`), prior = process.env.NODE_TLS_REJECT_UNAUTHORIZED;91
const valid = await tlsProbe(target, "127.0.0.1", settings, signal(), "TLSv1.3", ca); assert.equal(valid.valid, true); assert.equal(valid.chain[0].publicKeyBits, 2048); assert.ok(valid.chain[0].signatureAlgorithm); assert.ok(valid.chain[0].fingerprint256);92
const mismatch = await tlsProbe({ ...target, host: "mismatch.test" }, "127.0.0.1", settings, signal(), "TLSv1.2", ca); assert.equal(mismatch.status, "invalid"); assert.ok(mismatch.errors.includes("ERR_TLS_CERT_ALTNAME_INVALID"));93
const untrusted = await tlsProbe(target, "127.0.0.1", settings, signal(), "TLSv1.3"); assert.equal(untrusted.valid, false);94
const expired = await tlsProbe({ ...target, port: expiredPort }, "127.0.0.1", settings, signal(), "TLSv1.3", ca); assert.equal(expired.valid, false); assert.ok(expired.errors.includes("CERT_HAS_EXPIRED")); assert.ok(expired.chain[0].daysUntilExpiration < 0);95
assert.equal(process.env.NODE_TLS_REJECT_UNAUTHORIZED, prior);96
});97
test("TCP reports actual connected/refused states and WebSocket validates the Upgrade accept hash", async (t) => {98
const tcp = net.createServer(), tcpPort = await listen(t, tcp); assert.equal((await tcpProbe("127.0.0.1", tcpPort, settings, signal())).status, "connected");99
const closed = net.createServer(); closed.listen(0, "127.0.0.1"); await once(closed, "listening"); const closedPort = closed.address().port; await new Promise((r) => closed.close(r));100
assert.equal((await tcpProbe("127.0.0.1", closedPort, settings, signal())).status, "refused");101
const ws = http.createServer((_req, res) => res.end());102
ws.on("upgrade", (req, socket) => { const accept = crypto.createHash("sha1").update(req.headers["sec-websocket-key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64"); socket.write("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Accept: " + (req.url === "/bad" ? "invalid" : accept) + "\r\n\r\n"); });103
const port = await listen(t, ws);104
assert.equal((await websocketProbe(parseTarget(`ws://localhost:${port}/ok`), "127.0.0.1", settings, signal())).valid, true);105
assert.equal((await websocketProbe(parseTarget(`ws://localhost:${port}/bad`), "127.0.0.1", settings, signal())).valid, false);106
});107
test("SSH fingerprint is observed from a real handshake before any authentication request", async (t) => {108
let authentications = 0;109
const server = new Server({ hostKeys: [crypto.createPrivateKey(key).export({ type: "pkcs1", format: "pem" })] }, (client) => { client.on("error", () => {}); client.on("authentication", (ctx) => { authentications++; ctx.reject(); }); });110
server.on("error", () => {}); const port = await listen(t, server);111
const result = await sshIdentification("127.0.0.1", { timeoutMs: 2000, sshPort: port }, signal());112
assert.match(result.fingerprint, /^SHA256:/); assert.equal(result.authenticated, false); assert.equal(authentications, 0);113
});114
test("IPv6 HTTP and HTTPS are measured over a real local IPv6 socket", async (t) => {115
const server = http.createServer((_req, res) => res.end("v6")); let port;116
try { port = await listen(t, server, "::1"); } catch (error) { if (["EAFNOSUPPORT", "EADDRNOTAVAIL"].includes(error.code)) return t.skip("IPv6 loopback unavailable"); throw error; }117
const response = await nativeRequest(new URL(`http://[::1]:${port}/`), "::1", "1.1", { ...settings, signal: signal() });118
assert.equal(response.serverIp, "::1"); assert.equal(response.downloadedBytes, 2); assert.equal(response.negotiated, "1.1");119
const secureServer = require("node:https").createServer({ key, cert }, (_req, res) => res.end("v6 TLS"));120
const securePort = await listen(t, secureServer, "::1");121
const secure = await nativeRequest(new URL(`https://[::1]:${securePort}/`), "::1", "1.1", { ...settings, ca, signal: signal() });122
assert.equal(secure.status, "success"); assert.equal(secure.serverIp, "::1"); assert.ok(secure.timings.tlsMs > 0);123
});SHA-256: 861b9099b1a6a85e51d1eaa015cd9a6f80f46758d5144208b40a88fab4fa6b52
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0