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 { EventEmitter } = require("node:events");3
const crypto = require("node:crypto");4
const net = require("node:net");5
const { normalizeOptions, DiagnosticError, checkAbort, errorResult, boundedSignal, now } = require("./common");6
const { DnsDiagnostics, reverseName } = require("./dnsDiagnostics");7
const { ping, traceroute } = require("./processTools");8
const { HttpDiagnostics } = require("./httpDiagnostics");9
const { runTls, runTcp, websocketProbe } = require("./socketDiagnostics");10
const { DiagnosticsHistory, exportReport, scrub } = require("./historyStore");11
function sameIp(a, b) {12
if (net.isIP(a) !== net.isIP(b)) return false;13
return a === b || net.isIP(a) === 6 && new URL(`http://[${a}]/`).hostname === new URL(`http://[${b}]/`).hostname;14
}15
class DiagnosticsService extends EventEmitter {16
constructor(options = {}) {17
super();18
this.dns = options.dns || new DnsDiagnostics();19
this.http = options.http || new HttpDiagnostics(this.dns);20
this.history = options.history || new DiagnosticsHistory(options.storageRoot);21
this.jobs = new Map(); this.resolvers = new Set();22
this.adapters = { ping, traceroute, tls: runTls, tcp: runTcp,23
websocket: (target, address, settings, signal) => websocketProbe(target, address, settings, signal), ...options.adapters };24
}25
async resolve(source) {26
if (this.resolvers.size >= 2) throw new DiagnosticError("DIAGNOSTICS_BUSY");27
const options = normalizeOptions({ ...source, tools: ["dns"] });28
const controller = new AbortController(), bounded = boundedSignal(controller.signal, 30000);29
this.resolvers.add(controller);30
try { return { target: options.target, addresses: await this.dns.resolve(options.target.host, options, bounded.signal) }; }31
finally { bounded.close(); this.resolvers.delete(controller); }32
}33
start(source) {34
if ([...this.jobs.values()].some((job) => job.report.status === "running")) throw new DiagnosticError("DIAGNOSTICS_BUSY");35
const options = normalizeOptions(source), id = crypto.randomBytes(16).toString("hex"), controller = new AbortController();36
const report = { id, createdAt: new Date().toISOString(), status: "running", options, results: {}, progress: [], addresses: [] };37
const job = { report, controller, tools: new Map(), cancelledTools: new Set() };38
this.jobs.set(id, job);39
while (this.jobs.size > 5) this.jobs.delete(this.jobs.keys().next().value);40
setImmediate(() => { job.promise = this._run(job).catch((error) => this._finish(job, errorResult(error))); });41
return { id, report: scrub(report) };42
}43
cancel(id, tool) {44
const job = this.jobs.get(String(id)); if (!job) throw new DiagnosticError("JOB_NOT_FOUND");45
if (tool) {46
if (!job.report.options.tools.includes(tool)) throw new DiagnosticError("INVALID_TOOLS");47
job.cancelledTools.add(tool); job.tools.get(tool)?.abort();48
} else job.controller.abort();49
return { cancelling: true };50
}51
stopAll() {52
for (const job of this.jobs.values()) if (job.report.status === "running") job.controller.abort();53
for (const controller of this.resolvers) controller.abort();54
}55
get(id) { const job = this.jobs.get(id); return job ? scrub(job.report) : this.history.get(id); }56
_emit(job, event) {57
const value = { jobId: job.report.id, ...event };58
if (event.kind !== "snapshot" && event.kind !== "complete") {59
job.report.progress.push(scrub(event));60
if (job.report.progress.length > 500) job.report.progress.shift();61
}62
this.emit("progress", value);63
}64
_finish(job, failure) {65
if (job.report.status !== "running") return;66
job.report.status = job.controller.signal.aborted ? "cancelled" : failure ? "failed" : "completed";67
job.report.finishedAt = new Date().toISOString();68
if (failure) job.report.error = failure;69
if (job.controller.signal.reason?.code) job.report.error = errorResult(job.controller.signal.reason);70
try { this.history.add(job.report); } catch (error) { job.report.historyError = error.code || "HISTORY_WRITE_FAILED"; }71
this._emit(job, { kind: "complete", report: scrub(job.report) });72
}73
async _address(job, tool, item, signal, progress) {74
const { report } = job, { options } = report;75
try {76
checkAbort(signal); let result;77
if (tool === "http") result = await this.http.run(options.target, item.address, options, signal, progress);78
else if (tool === "compression") result = await this.http.compression(options.target, item.address, options, signal, progress);79
else if (tool === "tls" || tool === "websocket") result = await this.adapters[tool](options.target, item.address, options, signal, progress);80
else result = await this.adapters[tool](item.address, options, signal, progress);81
if (tool === "traceroute") {82
const addresses = [...new Set(result.routes?.flatMap((route) => route.hops.flatMap((hop) => hop.addresses)) || [])].slice(0, options.maxHops);83
result.reverseDns = {};84
for (const address of addresses) {85
checkAbort(signal);86
try { const query = await this.dns.query(reverseName(address), "PTR", options, signal); result.reverseDns[address] = query.records.filter((r) => r.type === "PTR").map((r) => r.value); }87
catch (error) { if (signal.aborted) throw error; result.reverseDns[address] = []; }88
}89
}90
return { address: item.address, family: item.family, ...result };91
} catch (error) {92
const partial = { address: item.address, family: item.family, ...errorResult(error) };93
if (tool === "ping") {94
const latest = report.livePing?.[item.address];95
if (latest) Object.assign(partial, { sent: latest.sent, received: latest.received, lossPercent: latest.lossPercent, statistics: latest.statistics });96
partial.packets = report.progress.filter((e) => e.kind === "ping-packet" && e.address === item.address);97
}98
if (tool === "traceroute") partial.routes = [{ hops: report.progress.filter((e) => e.kind === "trace-hop" && e.address === item.address) }];99
if (tool === "http") partial.comparisons = options.protocols.map((protocol) => ({ protocol, address: item.address, samples: report.progress.filter((e) => e.kind === "http-sample" && e.address === item.address && e.protocol === protocol) }));100
return partial;101
}102
}103
async _run(job) {104
const { report, controller } = job, { options } = report, signal = controller.signal;105
const deadline = setTimeout(() => controller.abort(new DiagnosticError("JOB_TIME_LIMIT")), options.continuous ? 30 * 60 * 1000 : 5 * 60 * 1000);106
try {107
checkAbort(signal); const started = now();108
try { report.addresses = await this.dns.resolve(options.target.host, options, signal); }109
catch (error) { if (signal.aborted) throw error; report.resolutionError = errorResult(error); }110
report.resolvedAddresses = [...report.addresses]; report.resolutionMs = now() - started;111
if (options.selectedIp) {112
const selected = report.addresses.filter((item) => sameIp(item.address, options.selectedIp));113
if (!selected.length) throw new DiagnosticError("SELECTED_IP_NOT_RESOLVED");114
report.addresses = selected;115
}116
if (options.mode === "ipv4") report.addresses = report.addresses.filter((a) => a.family === 4);117
if (options.mode === "ipv6") report.addresses = report.addresses.filter((a) => a.family === 6);118
if (options.mode === "auto") report.addresses = [report.addresses.find((a) => a.family === 4) || report.addresses[0]].filter(Boolean);119
if (options.mode === "both") report.addresses = [report.addresses.find((a) => a.family === 4), report.addresses.find((a) => a.family === 6)].filter(Boolean);120
this._emit(job, { kind: "resolved", addresses: report.addresses, resolvedAddresses: report.resolvedAddresses, resolutionMs: report.resolutionMs, error: report.resolutionError });121
// Two tools at a time; at most two IP families per tool. All workers settle before completion.122
const queue = [...options.tools]; let cursor = 0;123
await Promise.all(Array.from({ length: Math.min(2, queue.length) }, async () => {124
while (cursor < queue.length && !signal.aborted) {125
const tool = queue[cursor++], sub = new AbortController(); job.tools.set(tool, sub);126
const abort = () => sub.abort(); signal.addEventListener("abort", abort, { once: true });127
if (signal.aborted || job.cancelledTools.has(tool)) sub.abort();128
this._emit(job, { kind: "tool-start", tool });129
const progress = (event) => {130
if (event.kind === "ping-packet") { report.livePing ||= {}; report.livePing[event.address] = event; }131
this._emit(job, { tool, ...event });132
};133
try {134
checkAbort(sub.signal);135
if (tool === "dns") report.results.dns = await this.dns.run(options.target.host, options, sub.signal, progress);136
else if (!report.addresses.length) report.results[tool] = [{ status: "not-tested", code: "NO_ADDRESS" }];137
else report.results[tool] = await Promise.all(report.addresses.map((item) => this._address(job, tool, item, sub.signal, progress)));138
} catch (error) { report.results[tool] = [{ ...errorResult(error) }]; }139
finally {140
this._emit(job, { kind: "tool-complete", tool, result: scrub(report.results[tool]) });141
signal.removeEventListener("abort", abort); job.tools.delete(tool);142
}143
}144
}));145
this._finish(job);146
} catch (error) { this._finish(job, errorResult(error)); }147
finally { clearTimeout(deadline); }148
}149
export(id, format) { return exportReport(this.get(id), format); }150
}151
module.exports = { DiagnosticsService, sameIp };SHA-256: abcc87d72e54bbff5583c62d1ac0a754c0fe4b9f3f4137059bffd13f96c97779
SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0