Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Server Manager
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
161 Dateien
core-20261011T114645Z-f5edc312d101
1
"use strict";2
const http = require("node:http");3
const https = require("node:https");4
const http2 = require("node:http2");5
const net = require("node:net");6
const zlib = require("node:zlib");7
const crypto = require("node:crypto");8
const { verifyPeerIdentity, DiagnosticError, parseTarget, checkAbort, boundedSignal, now, stats, errorResult } = require("./common");9
const { runProcess } = require("./processTools");10
const MAX_BODY = 2 * 1024 * 1024;11
const PRIVATE_HEADERS = /^(?:set-cookie|cookie|authorization|proxy-authorization|www-authenticate|proxy-authenticate|authentication-info|proxy-authentication-info|set-cookie2)$/i;12
function 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;19
}20
function 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." }));23
}24
function 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);29
}30
function 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
});93
}94
let curlCapability;95
async 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;107
}108
async 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 };146
}147
function 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
});159
}160
class 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
}232
}233
module.exports = { HttpDiagnostics, nativeRequest, http3Request, getCurlCapability, safeHeaders, securityHeaders, decodeBody, MAX_BODY };SHA-256
ffa3bb4ff864a7b70d904488761749f23361b1acad8ab85168faa1dca4818b99CH-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 Archivs
5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0