CH-J Server ManagerSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Server Manager

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J Proprietary Software License 1.14

Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

12,6 KB · 123 ZeilenDatei herunterladen
1"use strict";
2const test = require("node:test");
3const assert = require("node:assert/strict");
4const http = require("node:http");
5const http2 = require("node:http2");
6const tls = require("node:tls");
7const net = require("node:net");
8const crypto = require("node:crypto");
9const zlib = require("node:zlib");
10const fs = require("node:fs");
11const path = require("node:path");
12const { once } = require("node:events");
13const { Server } = require("ssh2");
14const { parseTarget, normalizeOptions } = require("../src/main/diagnostics/common");
15const { HttpDiagnostics, nativeRequest, http3Request, getCurlCapability, MAX_BODY, securityHeaders } = require("../src/main/diagnostics/httpDiagnostics");
16const { tlsProbe, tcpProbe, websocketProbe, sshIdentification } = require("../src/main/diagnostics/socketDiagnostics");
17const fixtures = path.join(__dirname, "fixtures", "diagnostics");
18const 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"));
19const signal = () => new AbortController().signal;
20const settings = { timeoutMs: 1000 };
21async 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;
26const dns = { resolve: async () => [{ address: "127.0.0.1", family: 4 }] };
27test("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});
45test("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});
54test("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});
62test("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});
76test("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});
88test("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});
97test("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});
107test("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});
114test("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

SHA-256 des Archivs: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0