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 / Server Manager 161 Dateien
core-20261011T114645Z-f5edc312d101
Dateien
httpDiagnostics.js 19,7 KB · 233 Zeilen
Datei herunterladen
1"use strict";
2const http = require("node:http");
3const https = require("node:https");
4const http2 = require("node:http2");
5const net = require("node:net");
6const zlib = require("node:zlib");
7const crypto = require("node:crypto");
8const { verifyPeerIdentity, DiagnosticError, parseTarget, checkAbort, boundedSignal, now, stats, errorResult } = require("./common");
9const { runProcess } = require("./processTools");
10const MAX_BODY = 2 * 1024 * 1024;
11const PRIVATE_HEADERS = /^(?:set-cookie|cookie|authorization|proxy-authorization|www-authenticate|proxy-authenticate|authentication-info|proxy-authentication-info|set-cookie2)$/i;
12function safeHeaders(headers) {
13 const result = {};
14 for (const [key, value] of Object.entries(headers)) {
15 if (key.startsWith(":")) continue;
16 result[key.toLowerCase()] = PRIVATE_HEADERS.test(key) ? "[redacted]" : String(value).slice(0, 8192);
17 }
18 return result;
20function securityHeaders(headers, httpsTarget) {
21 const keys = ["strict-transport-security", "content-security-policy", "x-content-type-options", "referrer-policy", "permissions-policy", "x-frame-options"];
22 return keys.map((name) => ({ name, value: headers[name] || null, status: headers[name] ? "present" : name === "strict-transport-security" && !httpsTarget ? "not-applicable" : name === "x-frame-options" && /(?:^|;)\s*frame-ancestors\s/i.test(headers["content-security-policy"] || "") ? "superseded-by-csp-frame-ancestors" : "missing", note: "Presence alone does not establish correct policy or website security." }));
24function consume(stream, onEnd, fail) {
25 let bytes = 0, chunks = [];
26 stream.on("data", (chunk) => { bytes += chunk.length; if (bytes > MAX_BODY) { stream.destroy(); fail(new DiagnosticError("BODY_LIMIT")); return; } chunks.push(chunk); });
27 stream.once("end", () => onEnd(Buffer.concat(chunks), bytes));
28 stream.once("error", fail);
30function nativeRequest(url, address, protocol, { signal, timeoutMs, encoding = "identity", agent, sessionCache, ca } = {}) {
31 checkAbort(signal);
32 return new Promise((resolve, reject) => {
33 let request, session, socket, finished = false, connectAt = null, secureAt = null, firstAt = null, tcpStart = now(), reused = false;
34 const start = tcpStart;
35 const sessionError = (error) => finish(error);
36 const finish = (error, result) => { if (finished) return; finished = true; session?.removeListener("error", sessionError); clearTimeout(timer); signal?.removeEventListener("abort", abort); if (error) { request?.destroy(); if (session) session.destroy(); reject(error); } else { if (session && !sessionCache) session.close(); resolve(result); } };
37 const abort = () => finish(new DiagnosticError("CANCELLED"));
38 const timer = setTimeout(() => finish(new DiagnosticError("TIMEOUT")), timeoutMs);
39 signal?.addEventListener("abort", abort, { once: true });
40 const lookup = (_host, options, callback) => options?.all ? callback(null, [{ address, family: net.isIP(address) }]) : callback(null, address, net.isIP(address));
41 const tlsHost = url.hostname.replace(/^\[|\]$/g, "");
42 const bind = (value) => {
43 socket = value;
44 if (!socket.connecting) return;
45 socket.once("connect", () => { connectAt = now(); });
46 socket.once("secureConnect", () => { secureAt = now(); });
47 };
48 const complete = (statusCode, headers, body, bytes, negotiated, alpn) => finish(null, { status: protocol === "1.1" && negotiated !== "1.1" ? "unsupported" : "success", negotiated, statusCode,
49 serverIp: socket?.remoteAddress || address, port: Number(url.port || (url.protocol === "https:" ? 443 : 80)), headers: safeHeaders(headers),
50 contentType: headers["content-type"] || null, contentLength: /^\d+$/.test(String(headers["content-length"])) ? Number(headers["content-length"]) : null,
51 downloadedBytes: bytes, bodyHash: crypto.createHash("sha256").update(body).digest("hex"), _body: body,
52 reused, alpn: alpn || null, timings: { tcpMs: reused ? null : connectAt === null ? null : connectAt - tcpStart,
53 tlsMs: reused || url.protocol !== "https:" || secureAt === null ? null : secureAt - (connectAt ?? start),
54 ttfbMs: firstAt === null ? null : firstAt - start, downloadMs: firstAt === null ? null : now() - firstAt, totalMs: now() - start },
55 bytesPerSecond: bytes / Math.max(0.001, (now() - (firstAt ?? start)) / 1000) });
56 try {
57 if (protocol === "1.1") {
58 const transport = url.protocol === "https:" ? https : http;
59 request = transport.request(url, { method: "GET", lookup, family: net.isIP(address), agent: agent || false,
60 rejectUnauthorized: true, ca, servername: net.isIP(tlsHost) ? "" : tlsHost, checkServerIdentity: (_name, cert) => verifyPeerIdentity(tlsHost, cert), ALPNProtocols: ["http/1.1"], maxHeaderSize: 32768,
61 headers: { "accept-encoding": encoding, "user-agent": "CH-J-Server-Diagnostics/1", accept: "*/*" } }, (response) => {
62 firstAt = now(); reused = Boolean(request.reusedSocket); socket = response.socket;
63 consume(response, (body, bytes) => complete(response.statusCode, response.headers, body, bytes, response.httpVersion, socket?.alpnProtocol), (error) => finish(error));
64 });
65 request.once("socket", bind); request.once("error", (error) => finish(error)); request.end();
66 } else {
67 const key = url.origin + "|" + address;
68 session = sessionCache?.get(key);
69 if (session?.destroyed || session?.closed) session = null;
70 reused = Boolean(session);
71 if (!session) {
72 session = http2.connect(url.origin, { lookup, family: net.isIP(address), rejectUnauthorized: true, ca,
73 servername: net.isIP(tlsHost) ? "" : tlsHost, checkServerIdentity: (_name, cert) => verifyPeerIdentity(tlsHost, cert), ALPNProtocols: ["h2"], maxSessionMemory: 2, settings: { enablePush: false, maxHeaderListSize: 32768 } });
74 session.on("error", () => {});
75 sessionCache?.set(key, session);
76 bind(session.socket);
77 } else socket = session.socket;
78 session.on("error", sessionError);
79 const open = () => {
80 if (finished) return;
81 try {
82 if (url.protocol === "https:" && session.alpnProtocol !== "h2") return finish(new DiagnosticError("HTTP2_NOT_NEGOTIATED"));
83 request = session.request({ ":method": "GET", ":path": url.pathname + url.search, "accept-encoding": encoding, "user-agent": "CH-J-Server-Diagnostics/1" }, { endStream: true });
84 request.once("response", (headers) => { firstAt = now(); consume(request, (body, bytes) => complete(headers[":status"], headers, body, bytes, "2", session.alpnProtocol), (error) => finish(error)); });
85 request.once("error", (error) => finish(error));
86 request.once("aborted", () => finish(new DiagnosticError("HTTP_ABORTED")));
87 } catch (error) { finish(error); }
88 };
89 if (session.connecting) session.once("connect", open); else open();
90 }
91 } catch (error) { finish(error); }
92 });
94let curlCapability;
95async function getCurlCapability(runner = runProcess, signal) {
96 checkAbort(signal);
97 if (runner === runProcess && curlCapability) return curlCapability;
98 let result;
99 try {
100 const output = (await runner(process.platform === "win32" ? "curl.exe" : "curl", ["--disable", "--version"], { timeoutMs: 3000, signal })).stdout.toString();
101 const version = output.match(/^curl (\d+)\.(\d+)\.(\d+)/);
102 result = { available: Boolean(version) && /Features:.*\bHTTP3\b/i.test(output) && (Number(version[1]) > 7 || Number(version[1]) === 7 && Number(version[2]) >= 88), version: version?.[0] || null,
103 reason: /Features:.*\bHTTP3\b/i.test(output) ? "curl must support --http3-only (7.88+)" : "curl was not built with HTTP/3 / QUIC", raw: output.slice(0, 3000) };
104 } catch (error) { if (signal?.aborted) throw new DiagnosticError("CANCELLED"); result = { available: false, reason: error.code === "ENOENT" ? "curl executable is missing" : error.message }; }
105 if (runner === runProcess) curlCapability = result;
106 return result;
108async function http3Request(url, address, options, signal, runner = runProcess) {
109 const capability = await getCurlCapability(runner, signal);
110 if (!capability.available) return { status: "unavailable", code: "HTTP3_UNAVAILABLE", reason: capability.reason, capability };
111 if (url.protocol !== "https:" || Number(url.port || 443) !== 443) return { status: "unavailable", code: "HTTP3_REQUIRES_UDP443", reason: "HTTP/3 comparison uses HTTPS over UDP/443." };
112 // No config files, proxies, cookies, credentials, redirects or protocol fallback.
113 const args = ["--disable", "--silent", "--show-error", "--http3-only", "--noproxy", "*", "--proto", "=https", "--max-time", String(options.timeoutMs / 1000), "--connect-timeout", String(options.timeoutMs / 1000),
114 "--max-filesize", String(MAX_BODY), "--output", "-", "--dump-header", "-", "--header", "Accept-Encoding: identity",
115 ...(net.isIP(url.hostname.replace(/^\[|\]$/g, "")) ? [] : ["--resolve", `${url.hostname}:443:${net.isIP(address) === 6 ? `[${address}]` : address}`]),
116 "--write-out", "\nCHJ_METRICS:%{json}", "--url", url.href];
117 const result = await runner(process.platform === "win32" ? "curl.exe" : "curl", args, { signal, timeoutMs: options.timeoutMs + 1000, maxBytes: MAX_BODY + 131072 });
118 const output = result.stdout, marker = output.lastIndexOf("\nCHJ_METRICS:");
119 if (marker < 0) throw new DiagnosticError("HTTP3_INVALID_RESULT");
120 let metrics; try { metrics = JSON.parse(output.subarray(marker + 13).toString()); } catch { throw new DiagnosticError("HTTP3_INVALID_RESULT"); }
121 if (result.code !== 0) return { status: "error", code: `CURL_${result.code}`, reason: result.stderr.slice(0, 400), negotiated: metrics.http_version || null };
122 if (String(metrics.http_version) !== "3") return { status: "error", code: "HTTP3_FALLBACK_REJECTED", negotiated: metrics.http_version || null };
123 const requiredMetrics = ["time_total", "time_starttransfer", "time_appconnect", "size_download", "speed_download", "response_code", "remote_port"];
124 if (requiredMetrics.some((key) => !Number.isFinite(metrics[key]) || metrics[key] < 0)) throw new DiagnosticError("HTTP3_INVALID_RESULT");
125 if (metrics.size_download > MAX_BODY) throw new DiagnosticError("BODY_LIMIT");
126 let head, headerCursor = 0;
127 const headers = {};
128 // curl can print informational responses (e.g. 103) before the final headers.
129 // Walk only leading header blocks; body contents never select the response headers.
130 for (let index = 0; index < 8; index++) {
131 const end = output.indexOf("\r\n\r\n", headerCursor);
132 if (end < 0 || end > 32768 || end >= marker) throw new DiagnosticError("HTTP3_INVALID_RESULT");
133 const candidate = output.subarray(headerCursor, end).toString(), status = candidate.match(/^HTTP\/3(?:\.0)?\s+(\d{3})\b/);
134 if (!status) throw new DiagnosticError("HTTP3_INVALID_RESULT");
135 if (Number(status[1]) >= 200) {
136 if (Number(status[1]) !== Number(metrics.response_code)) throw new DiagnosticError("HTTP3_INVALID_RESULT");
137 head = candidate; break;
138 }
139 headerCursor = end + 4;
140 }
141 if (!head) throw new DiagnosticError("HTTP3_INVALID_RESULT");
142 head.split("\r\n").slice(1).forEach((line) => { const at = line.indexOf(":"); if (at > 0) headers[line.slice(0, at).toLowerCase()] = line.slice(at + 1).trim(); });
143 return { status: "success", negotiated: "3", alpn: "h3", statusCode: metrics.response_code, serverIp: metrics.remote_ip, port: metrics.remote_port, headers: safeHeaders(headers),
144 downloadedBytes: metrics.size_download, contentType: metrics.content_type, reused: null, reuseReason: "Independent curl process; reuse not asserted", timings: { dnsMs: null, tcpMs: null, tlsMs: null,
145 quicHandshakeMs: metrics.time_appconnect * 1000, ttfbMs: metrics.time_starttransfer * 1000, downloadMs: (metrics.time_total - metrics.time_starttransfer) * 1000, totalMs: metrics.time_total * 1000 }, bytesPerSecond: metrics.speed_download };
147function decodeBody(body, encoding, signal) {
148 checkAbort(signal);
149 const methods = { gzip: "createGunzip", deflate: "createInflate", br: "createBrotliDecompress", zstd: "createZstdDecompress" };
150 if (encoding === "identity") return Promise.resolve(body);
151 if (typeof zlib[methods[encoding]] !== "function") throw new DiagnosticError("COMPRESSION_CLIENT_UNAVAILABLE");
152 return new Promise((resolve, reject) => {
153 const decoder = zlib[methods[encoding]](); const pieces = []; let size = 0;
154 const abort = () => decoder.destroy(new DiagnosticError("CANCELLED")); signal?.addEventListener("abort", abort, { once: true });
155 decoder.on("data", (piece) => { size += piece.length; if (size > MAX_BODY * 4) decoder.destroy(new DiagnosticError("DECOMPRESSION_LIMIT")); else pieces.push(piece); });
156 decoder.once("error", (error) => { signal?.removeEventListener("abort", abort); reject(error); });
157 decoder.once("end", () => { signal?.removeEventListener("abort", abort); resolve(Buffer.concat(pieces)); }); decoder.end(body);
158 });
160class HttpDiagnostics {
161 constructor(dnsService, options = {}) { this.dns = dnsService; this.ca = options.ca; this.h3 = options.h3 || http3Request; }
162 async request(target, address, protocol, options, signal, resources, encoding = "identity") {
163 let url = new URL(target.url); if (url.protocol === "ws:") url.protocol = "http:"; if (url.protocol === "wss:") url.protocol = "https:";
164 const visited = new Set(), redirects = []; const started = now(); let redirectMs = 0, dnsMs = 0, hadDns = false;
165 for (let hop = 0; hop <= 8; hop++) {
166 checkAbort(signal); parseTarget(url.href);
167 if (visited.has(url.href)) throw new DiagnosticError("REDIRECT_LOOP"); visited.add(url.href);
168 const host = url.hostname.replace(/^\[|\]$/g, ""); let ip = address;
169 if (host !== target.host || !options.selectedIp && !net.isIP(host)) {
170 const dnsStart = now(); const list = await this.dns.resolve(host, { ...options, mode: net.isIP(address) === 6 ? "ipv6" : "ipv4" }, signal); dnsMs += now() - dnsStart; hadDns = true;
171 ip = host === target.host && list.some((entry) => entry.address === address) ? address : list[0].address;
172 }
173 const beforeRequestElapsed = now() - started;
174 const result = protocol === "3" ? await this.h3(url, ip, options, signal) : await nativeRequest(url, ip, protocol, { ...options, signal, encoding, ca: this.ca, agent: options.warm ? url.protocol === "https:" ? resources.https : resources.http : null, sessionCache: options.warm ? resources.h2 : null });
175 if (result.status !== "success") return { ...result, redirects };
176 if ([301, 302, 303, 307, 308].includes(result.statusCode) && result.headers.location) {
177 if (hop === 8) throw new DiagnosticError("REDIRECT_LIMIT");
178 const next = new URL(result.headers.location, url); parseTarget(next.href);
179 redirects.push({ from: url.href, to: next.href, statusCode: result.statusCode, crossDomain: next.hostname !== url.hostname, downgrade: url.protocol === "https:" && next.protocol === "http:" });
180 redirectMs += result.timings.totalMs; url = next; continue;
181 }
182 return { ...result, url: url.href, redirects, redirectCount: redirects.length, securityHeaders: securityHeaders(result.headers, url.protocol === "https:"),
183 timings: { ...result.timings, finalResponseTtfbMs: result.timings.ttfbMs, ttfbMs: result.timings.ttfbMs === null ? null : beforeRequestElapsed + result.timings.ttfbMs, dnsMs: hadDns ? dnsMs : null, redirectMs, totalMs: now() - started }, dnsReason: hadDns ? null : "Pinned IP or IP literal; no DNS operation in this request." };
184 }
185 }
186 resources() { return { http: new http.Agent({ keepAlive: true, maxSockets: 1 }), https: new https.Agent({ keepAlive: true, maxSockets: 1 }), h2: new Map() }; }
187 dispose(resources) { resources.http.destroy(); resources.https.destroy(); resources.h2.forEach((session) => session.destroy()); }
188 async run(target, address, options, signal, progress) {
189 const resources = this.resources(), comparisons = [];
190 try {
191 const targets = [target];
192 if (options.compareSchemes) { const url = new URL(target.url); url.protocol = url.protocol === "http:" ? "https:" : "http:"; url.port = ""; targets.push(parseTarget(url.href)); }
193 for (const tested of targets) for (const protocol of options.protocols) {
194 const samples = [];
195 for (let iteration = 0; iteration < options.repetitions; iteration++) {
196 checkAbort(signal); let sample;
197 try { sample = await this.request(tested, address, protocol, options, signal, resources); }
198 catch (error) { if (signal.aborted) throw error; sample = errorResult(error); if (["HTTP2_NOT_NEGOTIATED", "ERR_HTTP2_ERROR", "ERR_HTTP2_SESSION_ERROR"].includes(error.code)) sample.status = "unsupported"; }
199 delete sample._body;
200 samples.push({ iteration, ...sample }); progress({ kind: "http-sample", protocol, address, scheme: new URL(tested.url).protocol, iteration, ...sample });
201 if (["unavailable", "unsupported"].includes(sample.status)) break;
202 }
203 comparisons.push({ protocol, url: tested.url, address, connection: options.warm ? "warm-if-reused" : "cold", samples, statistics: stats(samples.map((s) => s.timings?.totalMs)), errorRate: 100 * samples.filter((s) => s.status !== "success").length / samples.length });
204 }
205 return { status: comparisons.some((c) => c.samples.some((s) => s.status === "success")) ? "success" : "warning", comparisons, note: "Network/server request timings; no page rendering or Lighthouse score is measured." };
206 } finally { this.dispose(resources); }
207 }
208 async compression(target, address, options, signal, progress) {
209 const resources = this.resources(), results = []; let identity;
210 try {
211 for (const encoding of ["identity", "gzip", "deflate", "br", "zstd"]) {
212 checkAbort(signal); let result;
213 if (encoding === "zstd" && !zlib.createZstdDecompress) { results.push({ encoding, status: "unavailable", code: "COMPRESSION_CLIENT_UNAVAILABLE" }); continue; }
214 try {
215 const response = await this.request(target, address, "1.1", { ...options, warm: false }, signal, resources, encoding);
216 const actual = String(response.headers?.["content-encoding"] || "identity").toLowerCase().trim();
217 if (response.status !== "success") result = response;
218 else if (actual !== encoding) result = { status: "unsupported", actualEncoding: actual, downloadedBytes: response.downloadedBytes, reason: "Server did not use the requested content encoding." };
219 else {
220 const decoded = await decodeBody(response._body, encoding, signal);
221 const decodedHash = crypto.createHash("sha256").update(decoded).digest("hex");
222 if (encoding === "identity") identity = { hash: decodedHash, bytes: decoded.length, url: response.url, statusCode: response.statusCode };
223 const comparable = identity && identity.hash === decodedHash && identity.url === response.url && identity.statusCode === response.statusCode;
224 result = { status: "success", actualEncoding: actual, downloadedBytes: response.downloadedBytes, decodedBytes: decoded.length, decodedHash, comparable: Boolean(comparable), ratio: comparable && identity.bytes > 0 ? response.downloadedBytes / identity.bytes : null, statusCode: response.statusCode };
225 }
226 } catch (error) { if (signal.aborted) throw error; result = errorResult(error); }
227 results.push({ encoding, ...result }); progress({ kind: "compression", address, encoding, ...result });
228 }
229 return { status: results.some((r) => r.encoding === "br" && r.status === "success") ? "success" : "warning", results, note: "Compression is confirmed by Content-Encoding and bounded decompression. Ratios require identical decoded content." };
230 } finally { this.dispose(resources); }
231 }
233module.exports = { HttpDiagnostics, nativeRequest, http3Request, getCurlCapability, safeHeaders, securityHeaders, decodeBody, MAX_BODY };
SHA-256ffa3bb4ff864a7b70d904488761749f23361b1acad8ab85168faa1dca4818b99

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.

SHA-256 des Archivs5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0