CH-J Server Managerupravljanje poslužiteljima preko SSH
Meni
Objavljen izvor

CH-J Server Manager

Pregledaj direktorije i datoteke za određeni programski izdanje.

Preuzmi ZIP izvora
CH-J / Server Manager 196 datoteke
core-20261013T133631Z-faa4ab45100e Dodano: Datum nije zabilježen · Izgrađeno:
Datoteke
httpDiagnostics.js Dodano: Datum nije zabilježen 23,0 KB · 263 linije
Preuzmi datoteku
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 });
94const { runtimePaths, validateRuntime, runtimeEnvironment } = require("./httpRuntime");
95async function getCurlCapability(runner = runProcess, signal) {
96 checkAbort(signal);
97 try {
98 const runtime = runner === runProcess ? validateRuntime() : runtimePaths();
99 const result = await runner(runtime.executable, ["--disable", "--version"], { timeoutMs: 3000, signal, env: runtimeEnvironment() });
100 const output = result.stdout.toString(), version = output.match(/^curl (\d+)\.(\d+)\.(\d+)/);
101 const http2 = /Features:.*\bHTTP2\b/.test(output), http3 = /Features:.*\bHTTP3\b/.test(output), tls = /Features:.*\bSSL\b/.test(output);
102 const available = Boolean(version) && Number(version[1]) >= 8 && http2 && http3 && tls && (result.code === undefined || result.code === 0);
103 return { available, version: version?.[0] || null, http2, http3, tls, platform: process.platform, arch: process.arch,
104 tlsBackend: output.split("\n")[0].match(/(?:OpenSSL|LibreSSL|BoringSSL|AWS-LC)\/[^ ]+/)?.[0] || null,
105 code: available ? null : "HTTP_RUNTIME_INCOMPATIBLE", raw: output.slice(0, 3000), executable: runtime.executable, caPath: runtime.caPath };
106 } catch (error) {
107 if (signal?.aborted) throw new DiagnosticError("CANCELLED");
108 return { available: false, code: error.code?.startsWith("HTTP_RUNTIME_") ? error.code : error.code === "ENOENT" ? "HTTP_RUNTIME_MISSING" : "HTTP_RUNTIME_INVALID", platform: process.platform, arch: process.arch };
109 }
111function curlFailure(code, protocol) {
112 return ({ 1: "HTTP_PROTOCOL_INCOMPATIBLE", 2: "HTTP_RUNTIME_INCOMPATIBLE", 4: "HTTP_RUNTIME_INCOMPATIBLE", 5: "DNS_FAILED", 6: "DNS_FAILED", 7: "CONNECTION_REFUSED", 28: protocol === "3" ? "HTTP3_UDP_TIMEOUT" : "TIMEOUT", 35: "TLS_HANDSHAKE_FAILED", 51: "TLS_CERT_INVALID", 58: "TLS_CERT_INVALID", 60: "TLS_CERT_INVALID", 63: "BODY_LIMIT", 77: "HTTP_RUNTIME_INVALID", 92: "HTTP_PROTOCOL_INCOMPATIBLE", 95: "HTTP3_UNVERIFIED", 96: "HTTP3_UNVERIFIED" })[code] || "HTTP_TRANSFER_FAILED";
114async function curlRequest(url, address, protocol, options, signal, runner = runProcess) {
115 checkAbort(signal);
116 if (!net.isIP(address)) throw new DiagnosticError("INVALID_IP");
117 parseTarget(url.href);
118 if (protocol === "3" && url.protocol !== "https:") return { status: "unavailable", code: "HTTP3_REQUIRES_HTTPS" };
119 const capability = options.capability || await getCurlCapability(runner, signal);
120 const client = { version: capability.version, http2: capability.http2, http3: capability.http3, tlsBackend: capability.tlsBackend, platform: capability.platform, arch: capability.arch };
121 if (!capability.available) return { status: "unavailable", code: capability.code, capability: client };
122 const port = Number(url.port || (url.protocol === "https:" ? 443 : 80)), host = url.hostname.replace(/^\[|\]$/g, "");
123 const args = ["--disable", "--silent", "--show-error", { "1.1": "--http1.1", "2": "--http2-prior-knowledge", "3": "--http3-only" }[protocol],
124 "--ipv" + net.isIP(address), "--noproxy", "*", "--proto", "=http,https", "--cacert", options.caPath || capability.caPath,
125 "--max-time", String(options.timeoutMs / 1000), "--connect-timeout", String(options.timeoutMs / 1000), "--max-filesize", String(MAX_BODY),
126 "--output", "-", "--dump-header", "-", "--header", "Accept-Encoding: " + (options.encoding || "identity"), "--user-agent", "CH-J-Server-Diagnostics/1",
127 ...(net.isIP(host) ? [] : ["--resolve", `${host}:${port}:${net.isIP(address) === 6 ? `[${address}]` : address}`]),
128 "--write-out", "\nCHJ_METRICS:%{json}", "--url", url.href];
129 let result;
130 try { result = await runner(capability.executable, args, { signal, timeoutMs: options.timeoutMs + 1000, maxBytes: MAX_BODY + 131072, env: runtimeEnvironment() }); }
131 catch (error) { if (signal?.aborted) throw new DiagnosticError("CANCELLED"); if (error.code === "TIMEOUT" && protocol === "3") return { status: "error", code: "HTTP3_UDP_TIMEOUT", capability: client }; throw error; }
132 if (result.code !== 0) return { status: "error", code: curlFailure(result.code, protocol), curlCode: result.code, capability: client };
133 const output = result.stdout, marker = output.lastIndexOf("\nCHJ_METRICS:");
134 if (marker < 0) throw new DiagnosticError("HTTP_INVALID_RESULT");
135 let metrics; try { metrics = JSON.parse(output.subarray(marker + 13).toString()); } catch { throw new DiagnosticError("HTTP_INVALID_RESULT"); }
136 if (String(metrics.http_version) !== protocol) return { status: "unsupported", code: protocol === "3" ? "HTTP3_FALLBACK_REJECTED" : "HTTP_PROTOCOL_INCOMPATIBLE", negotiated: String(metrics.http_version), capability: client };
137 const required = ["time_total", "time_starttransfer", "time_appconnect", "size_download", "speed_download", "response_code", "remote_port"];
138 if (required.some((key) => !Number.isFinite(metrics[key]) || metrics[key] < 0) || metrics.response_code < 200) throw new DiagnosticError("HTTP_INVALID_RESULT");
139 if (url.protocol === "https:" && metrics.ssl_verify_result !== 0) throw new DiagnosticError("TLS_CERT_INVALID");
140 if (metrics.size_download > MAX_BODY) throw new DiagnosticError("BODY_LIMIT");
141 let head, cursor = 0, bodyStart;
142 for (let index = 0; index < 8; index++) {
143 const end = output.indexOf("\r\n\r\n", cursor);
144 if (end < 0 || end > 32768 || end >= marker) throw new DiagnosticError("HTTP_INVALID_RESULT");
145 const candidate = output.subarray(cursor, end).toString(), status = candidate.match(/^HTTP\/(1\.1|2|3)(?:\.0)?\s+(\d{3})\b/);
146 if (!status) throw new DiagnosticError("HTTP_INVALID_RESULT");
147 if (Number(status[2]) >= 200) { if (Number(status[2]) !== metrics.response_code) throw new DiagnosticError("HTTP_INVALID_RESULT"); head = candidate; bodyStart = end + 4; break; }
148 cursor = end + 4;
149 }
150 if (!head) throw new DiagnosticError("HTTP_INVALID_RESULT");
151 const headers = {};
152 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(); });
153 const body = output.subarray(bodyStart, marker);
154 return { status: "success", code: protocol === "3" ? "HTTP3_CONFIRMED" : undefined, negotiated: protocol, alpn: { "1.1": "http/1.1", "2": "h2", "3": "h3" }[protocol], statusCode: metrics.response_code,
155 serverIp: metrics.remote_ip, port: metrics.remote_port, headers: safeHeaders(headers), altSvc: headers["alt-svc"] || null,
156 downloadedBytes: metrics.size_download, contentType: metrics.content_type, contentLength: /^\d+$/.test(headers["content-length"]) ? Number(headers["content-length"]) : null,
157 _body: body, bodyHash: crypto.createHash("sha256").update(body).digest("hex"), capability: client,
158 tls: url.protocol === "https:" ? { verified: true, version: protocol === "3" ? "TLSv1.3" : null, backend: capability.tlsBackend } : null,
159 reused: false, reuseReason: "Independent bundled-client process for every sample; connection reuse is not measured.",
160 timings: { dnsMs: null, tcpMs: protocol === "3" ? null : Number(metrics.time_connect) * 1000 || null,
161 tlsMs: protocol === "3" || url.protocol !== "https:" ? null : (metrics.time_appconnect - (metrics.time_connect || 0)) * 1000,
162 quicHandshakeMs: protocol === "3" ? metrics.time_appconnect * 1000 : null, ttfbMs: metrics.time_starttransfer * 1000,
163 downloadMs: (metrics.time_total - metrics.time_starttransfer) * 1000, totalMs: metrics.time_total * 1000 }, bytesPerSecond: metrics.speed_download };
165async function http3Request(url, address, options, signal, runner = runProcess) { return curlRequest(url, address, "3", options, signal, runner); }
166function decodeBody(body, encoding, signal) {
167 checkAbort(signal);
168 const methods = { gzip: "createGunzip", deflate: "createInflate", br: "createBrotliDecompress", zstd: "createZstdDecompress" };
169 if (encoding === "identity") return Promise.resolve(body);
170 if (typeof zlib[methods[encoding]] !== "function") throw new DiagnosticError("COMPRESSION_CLIENT_UNAVAILABLE");
171 return new Promise((resolve, reject) => {
172 const decoder = zlib[methods[encoding]](); const pieces = []; let size = 0;
173 const abort = () => decoder.destroy(new DiagnosticError("CANCELLED")); signal?.addEventListener("abort", abort, { once: true });
174 decoder.on("data", (piece) => { size += piece.length; if (size > MAX_BODY * 4) decoder.destroy(new DiagnosticError("DECOMPRESSION_LIMIT")); else pieces.push(piece); });
175 decoder.once("error", (error) => { signal?.removeEventListener("abort", abort); reject(error); });
176 decoder.once("end", () => { signal?.removeEventListener("abort", abort); resolve(Buffer.concat(pieces)); }); decoder.end(body);
177 });
179class HttpDiagnostics {
180 constructor(dnsService, options = {}) { this.dns = dnsService; this.ca = options.ca; this.h3 = options.h3 || ((url, ip, settings, signal) => this.curlRequest(url, ip, "3", { ...settings, signal })); this.native = Boolean(options.native); this.caPath = options.caPath; }
181 async request(target, address, protocol, options, signal, resources, encoding = "identity") {
182 let url = new URL(target.url); if (url.protocol === "ws:") url.protocol = "http:"; if (url.protocol === "wss:") url.protocol = "https:";
183 const visited = new Set(), redirects = []; const started = now(); let redirectMs = 0, dnsMs = 0, hadDns = false;
184 for (let hop = 0; hop <= 8; hop++) {
185 checkAbort(signal); parseTarget(url.href);
186 if (visited.has(url.href)) throw new DiagnosticError("REDIRECT_LOOP"); visited.add(url.href);
187 const host = url.hostname.replace(/^\[|\]$/g, ""); let ip = address;
188 if (host !== target.host || !options.selectedIp && !net.isIP(host)) {
189 const dnsStart = now(); const list = await this.dns.resolve(host, { ...options, mode: net.isIP(address) === 6 ? "ipv6" : "ipv4" }, signal); dnsMs += now() - dnsStart; hadDns = true;
190 ip = host === target.host && list.some((entry) => entry.address === address) ? address : list[0].address;
191 }
192 const beforeRequestElapsed = now() - started;
193 const result = protocol === "3" ? await this.h3(url, ip, options, signal) : await (this.native ? nativeRequest : this.curlRequest.bind(this))(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 });
194 if (result.status !== "success") return { ...result, redirects };
195 if ([301, 302, 303, 307, 308].includes(result.statusCode) && result.headers.location) {
196 if (hop === 8) throw new DiagnosticError("REDIRECT_LIMIT");
197 const next = new URL(result.headers.location, url); parseTarget(next.href);
198 redirects.push({ from: url.href, to: next.href, statusCode: result.statusCode, crossDomain: next.hostname !== url.hostname, downgrade: url.protocol === "https:" && next.protocol === "http:" });
199 redirectMs += result.timings.totalMs; url = next; continue;
200 }
201 return { ...result, url: url.href, redirects, redirectCount: redirects.length, securityHeaders: securityHeaders(result.headers, url.protocol === "https:"),
202 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." };
203 }
204 }
205 async curlRequest(url, ip, protocol, options) {
206 let temp;
207 try {
208 let caPath = this.caPath;
209 if (this.ca) {
210 temp = require("node:fs").mkdtempSync(require("node:path").join(require("node:os").tmpdir(), "chj-test-ca-"));
211 caPath = require("node:path").join(temp, "ca.pem"); require("node:fs").writeFileSync(caPath, this.ca, { mode: 0o600 });
212 }
213 return await curlRequest(url, ip, protocol, { ...options, caPath }, options.signal);
214 } finally { if (temp) require("node:fs").rmSync(temp, { recursive: true, force: true }); }
215 }
216 resources() { return { http: new http.Agent({ keepAlive: true, maxSockets: 1 }), https: new https.Agent({ keepAlive: true, maxSockets: 1 }), h2: new Map() }; }
217 dispose(resources) { resources.http.destroy(); resources.https.destroy(); resources.h2.forEach((session) => session.destroy()); }
218 async run(target, address, options, signal, progress) {
219 const resources = this.resources(), comparisons = [];
220 try {
221 const targets = [target];
222 if (options.compareSchemes) { const url = new URL(target.url); url.protocol = url.protocol === "http:" ? "https:" : "http:"; url.port = ""; targets.push(parseTarget(url.href)); }
223 for (const tested of targets) for (const protocol of options.protocols) {
224 const samples = [];
225 for (let iteration = 0; iteration < options.repetitions; iteration++) {
226 checkAbort(signal); let sample;
227 try { sample = await this.request(tested, address, protocol, options, signal, resources); }
228 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"; }
229 delete sample._body;
230 samples.push({ iteration, ...sample }); progress({ kind: "http-sample", protocol, address, scheme: new URL(tested.url).protocol, iteration, ...sample });
231 if (["unavailable", "unsupported"].includes(sample.status)) break;
232 }
233 comparisons.push({ protocol, url: tested.url, address, connection: this.native && 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 });
234 }
235 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." };
236 } finally { this.dispose(resources); }
237 }
238 async compression(target, address, options, signal, progress) {
239 const resources = this.resources(), results = []; let identity;
240 try {
241 for (const encoding of ["identity", "gzip", "deflate", "br", "zstd"]) {
242 checkAbort(signal); let result;
243 if (encoding === "zstd" && !zlib.createZstdDecompress) { results.push({ encoding, status: "unavailable", code: "COMPRESSION_CLIENT_UNAVAILABLE" }); continue; }
244 try {
245 const response = await this.request(target, address, "1.1", { ...options, warm: false }, signal, resources, encoding);
246 const actual = String(response.headers?.["content-encoding"] || "identity").toLowerCase().trim();
247 if (response.status !== "success") result = response;
248 else if (actual !== encoding) result = { status: "unsupported", actualEncoding: actual, downloadedBytes: response.downloadedBytes, reason: "Server did not use the requested content encoding." };
249 else {
250 const decoded = await decodeBody(response._body, encoding, signal);
251 const decodedHash = crypto.createHash("sha256").update(decoded).digest("hex");
252 if (encoding === "identity") identity = { hash: decodedHash, bytes: decoded.length, url: response.url, statusCode: response.statusCode };
253 const comparable = identity && identity.hash === decodedHash && identity.url === response.url && identity.statusCode === response.statusCode;
254 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 };
255 }
256 } catch (error) { if (signal.aborted) throw error; result = errorResult(error); }
257 results.push({ encoding, ...result }); progress({ kind: "compression", address, encoding, ...result });
258 }
259 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." };
260 } finally { this.dispose(resources); }
261 }
263module.exports = { HttpDiagnostics, nativeRequest, http3Request, curlRequest, getCurlCapability, safeHeaders, securityHeaders, decodeBody, MAX_BODY };
SHA-2563125caaf30865d74cb78a51379f75c58971bdfed18f411781dbfd5fd73b4530b

CH-J Proprietary Software License 1.14 · Izvor je osiguran pod CH-J Proprietary Software License 1.14. Njena dostupnost ne mijenja uvjete licenciranja niti daje dodatna prava.

Arhiv SHA-256de757268dbc4f0226549f8b4f41f9b7e585c856ee97fa0d43b17a0e95c281449