Vydané zdroje
Stáhnout zdroje ZIP
CH-J Server Manager
Procházejte adresáře a soubory konkrétního vydání aplikace.
Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.
1
"use strict";2
const fs = require("node:fs");3
const path = require("node:path");4
const { atomicWriteJson } = require("../storage/atomicFile");5
const net = require("node:net");6
const { DiagnosticError, normalizeOptions, TOOLS } = require("./common");7
// History contains measured metadata, never response bodies, credentials or URL queries.8
function scrub(value, key = "", depth = 0) {9
if (depth > 24) throw new DiagnosticError("IMPORT_INVALID");10
if (/password|passphrase|privatekey|cookie|authoriz|authenticat|api.?key|secret|token/i.test(key)) return "[redacted]";11
if (typeof value === "string") {12
if (key === "input" || /^(?:https?|wss?):\/\//i.test(value)) {13
try { const url = new URL(value.includes("://") ? value : `https://${value}`); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; return url.href; } catch {}14
}15
if (key.toLowerCase() === "location") return value.split(/[?#]/)[0].slice(0, 16000);16
return value.slice(0, 16000);17
}18
if (Array.isArray(value)) return value.slice(0, 500).map((v) => scrub(v, "", depth + 1));19
if (value && typeof value === "object") {20
const result = {};21
for (const [k, v] of Object.entries(value)) {22
if (["_body", "raw", "__proto__", "constructor", "prototype"].includes(k)) continue;23
result[k] = scrub(v, k, depth + 1);24
}25
return result;26
}27
return typeof value === "number" && !Number.isFinite(value) ? null : value;28
}29
function validateReport(report) {30
const invalid = () => { throw new DiagnosticError("IMPORT_INVALID"); };31
const object = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);32
const objects = (v) => Array.isArray(v) && v.length <= 500 && v.every(object);33
if (!object(report) || !/^[a-f0-9]{32}$/.test(report.id) || !Number.isFinite(Date.parse(report.createdAt)) ||34
!["completed", "cancelled", "failed"].includes(report.status) || !object(report.options) || !object(report.options.target) || !object(report.results)) invalid();35
let options;36
try { options = normalizeOptions({ ...report.options, target: report.options.target.url, ports: report.options.ports?.join(",") }); } catch { invalid(); }37
if (options.target.host !== report.options.target.host || !objects(report.addresses) || report.addresses.some((a) => net.isIP(a.address) !== a.family)) invalid();38
let nodes = 0;39
const arrayKeys = new Set(["comparisons", "samples", "routes", "hops", "probes", "queries", "records", "packets", "ports", "redirects", "securityHeaders", "chain"]);40
function walk(value, depth = 0) {41
if (++nodes > 50000 || depth > 24) invalid();42
if (Array.isArray(value)) { if (value.length > 500) invalid(); for (const child of value) walk(child, depth + 1); }43
else if (object(value)) {44
for (const [key, child] of Object.entries(value)) {45
if (["__proto__", "constructor", "prototype"].includes(key)) invalid();46
if (arrayKeys.has(key) && !(key === "ports" && Array.isArray(child) && child.every(Number.isInteger)) && !objects(child)) invalid();47
if (["addresses", "errors"].includes(key) && !Array.isArray(child)) invalid();48
walk(child, depth + 1);49
}50
if ("comparisons" in value && value.comparisons.some((g) => !objects(g.samples))) invalid();51
if ("routes" in value && value.routes.some((r) => !objects(r.hops))) invalid();52
if ("hop" in value && (!Array.isArray(value.addresses) || !value.addresses.every((a) => typeof a === "string") || !objects(value.probes))) invalid();53
}54
}55
walk(report);56
for (const [tool, result] of Object.entries(report.results)) {57
if (!TOOLS.includes(tool)) invalid();58
if (tool === "dns" ? !object(result) && !objects(result) : !objects(result)) invalid();59
if (tool === "tcp" && result.some((entry) => entry.ports !== undefined && !objects(entry.ports))) invalid();60
if (["tls", "compression"].includes(tool) && result.some((entry) => entry.results !== undefined && !objects(entry.results))) invalid();61
}62
if (report.progress !== undefined && !objects(report.progress)) invalid();63
return scrub({ ...report, options });64
}65
function rows(report) {66
const result = [];67
for (const [tool, entries] of Object.entries(report.results || {})) for (const entry of Array.isArray(entries) ? entries : [entries]) {68
if (tool === "ping") for (const packet of entry.packets || []) result.push({ tool, address: entry.address, sequence: packet.sequence, status: packet.status, rttMs: packet.rttMs });69
else if (tool === "http") for (const group of entry.comparisons || []) for (const sample of group.samples || []) result.push({ tool, address: group.address, protocol: group.protocol, sequence: sample.iteration, status: sample.status, httpStatus: sample.statusCode, dnsMs: sample.timings?.dnsMs, tcpMs: sample.timings?.tcpMs, tlsMs: sample.timings?.tlsMs, ttfbMs: sample.timings?.ttfbMs, totalMs: sample.timings?.totalMs, bytes: sample.downloadedBytes });70
else if (tool === "tcp") for (const port of entry.ports || []) result.push({ tool, address: port.address, port: port.port, status: port.status, tcpMs: port.connectionMs });71
else result.push({ tool, address: entry.address, status: entry.status, detail: JSON.stringify(entry) });72
}73
return result;74
}75
function exportReport(report, format) {76
const safe = scrub(report);77
if (format === "json") return JSON.stringify({ schema: 1, report: safe }, null, 2) + "\n";78
if (format === "txt") return `CH-J Server Diagnostics\n${safe.createdAt}\n${safe.options?.target?.host || ""}\n\n${JSON.stringify(safe, null, 2)}\n`;79
if (format !== "csv") throw new DiagnosticError("INVALID_EXPORT_FORMAT");80
const table = rows(safe), keys = [...new Set(table.flatMap((row) => Object.keys(row)))];81
const cell = (value) => { let text = value === null || value === undefined ? "" : String(value); if (/^\s*[=+@-]|^[\t\r\n]/.test(text)) text = "'" + text; return '"' + text.replace(/"/g, '""') + '"'; };82
return [keys.map(cell).join(","), ...table.map((row) => keys.map((key) => cell(row[key])).join(","))].join("\r\n") + "\r\n";83
}84
class DiagnosticsHistory {85
constructor(root) { this.file = root ? path.join(root, "diagnostics", "history.json") : null; this.items = []; this.loaded = false; }86
load() {87
if (this.loaded) return;88
if (!this.file || !fs.existsSync(this.file)) { this.loaded = true; return; }89
const attrs = fs.lstatSync(this.file); if (!attrs.isFile() || attrs.isSymbolicLink() || attrs.size > 12 * 1024 * 1024) throw new DiagnosticError("HISTORY_INVALID");90
const document = JSON.parse(fs.readFileSync(this.file, "utf8")); if (document.schema !== 1 || !Array.isArray(document.items)) throw new DiagnosticError("HISTORY_INVALID");91
this.items = document.items.slice(0, 30).map(validateReport); this.loaded = true;92
}93
add(report) { this.load(); this.items = this.items.filter((item) => item.id !== report.id); this.items.unshift(scrub(report)); this.items = this.items.slice(0, 30); while (Buffer.byteLength(JSON.stringify(this.items)) > 10 * 1024 * 1024) this.items.pop(); this.persist(); }94
persist() { if (this.file) atomicWriteJson(this.file, { schema: 1, items: this.items }); }95
list() { this.load(); return this.items.map((item) => ({ id: item.id, createdAt: item.createdAt, target: item.options.target, mode: item.options.mode, tools: item.options.tools, status: item.status })); }96
get(id) { this.load(); const report = this.items.find((item) => item.id === id); if (!report) throw new DiagnosticError("HISTORY_NOT_FOUND"); return scrub(report); }97
remove(id) { this.load(); this.items = id ? this.items.filter((item) => item.id !== id) : []; this.persist(); return this.list(); }98
import(text) {99
if (typeof text !== "string" || Buffer.byteLength(text) > 2 * 1024 * 1024) throw new DiagnosticError("IMPORT_LIMIT");100
let value;101
try { value = JSON.parse(text); } catch { throw new DiagnosticError("IMPORT_INVALID"); }102
if (value?.schema !== 1) throw new DiagnosticError("IMPORT_INVALID");103
const report = validateReport(value.report); this.add(report); return report.id;104
}105
}106
module.exports = { DiagnosticsHistory, scrub, exportReport, rows, validateReport };SHA-256: bdd88081167f6505851c95eb939ba95089da0acd3cbc5d6981df5608a8addc62
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0