Mã nguồn đã công bố
Tải mã nguồn dạng ZIP
CH-J Server Manager
Duyệt thư mục và tệp của một bản phát hành ứng dụng cụ thể.
CH-J / Server Manager
377 tệp
core-20261019T111616Z-160a00bfcddc
Ngày thêm: · Ngày dựng:
1
"use strict";2
const { EventEmitter } = require("node:events");3
const crypto = require("node:crypto");4
const net = require("node:net");5
const { normalizeOptions, parseTarget, PING_DURATION_LIMIT_MS, 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
// A finite ping run may outlast the default 5 min budget; the hard cap stays 30 min.12
function jobTimeLimit(options) {13
if (options.continuous) return PING_DURATION_LIMIT_MS;14
const ping = options.tools.includes("ping") ? options.count * options.repetitions * options.intervalMs + (options.pingTimeoutMs || options.timeoutMs) + 60000 : 0;15
return Math.min(PING_DURATION_LIMIT_MS, Math.max(5 * 60 * 1000, ping));16
}17
function targetForAddress(target, address) {18
const url = new URL(target.url); url.hostname = net.isIP(address) === 6 ? `[${address}]` : address;19
return parseTarget(url.href);20
}21
function sameIp(a, b) {22
if (net.isIP(a) !== net.isIP(b)) return false;23
return a === b || net.isIP(a) === 6 && new URL(`http://[${a}]/`).hostname === new URL(`http://[${b}]/`).hostname;24
}25
class DiagnosticsService extends EventEmitter {26
constructor(options = {}) {27
super();28
this.dns = options.dns || new DnsDiagnostics();29
this.http = options.http || new HttpDiagnostics(this.dns);30
this.history = options.history || new DiagnosticsHistory(options.storageRoot);31
this.jobs = new Map(); this.resolvers = new Set();32
this.adapters = { ping, traceroute, tls: runTls, tcp: runTcp,33
websocket: (target, address, settings, signal) => websocketProbe(target, address, settings, signal), ...options.adapters };34
}35
async resolve(source) {36
if (this.resolvers.size >= 2) throw new DiagnosticError("DIAGNOSTICS_BUSY");37
const options = normalizeOptions({ ...source, tools: ["dns"] });38
const controller = new AbortController(), bounded = boundedSignal(controller.signal, 30000);39
this.resolvers.add(controller);40
try { return { target: options.manualAddresses ? { ...options.target, type: "ipv4+ipv6" } : options.target, addresses: options.manualAddresses || await this.dns.resolve(options.target.host, options, bounded.signal) }; }41
finally { bounded.close(); this.resolvers.delete(controller); }42
}43
start(source) {44
if ([...this.jobs.values()].some((job) => job.report.status === "running")) throw new DiagnosticError("DIAGNOSTICS_BUSY");45
const options = normalizeOptions(source), id = crypto.randomBytes(16).toString("hex"), controller = new AbortController();46
const report = { id, createdAt: new Date().toISOString(), status: "running", options, results: {}, progress: [], addresses: [] };47
const job = { report, controller, tools: new Map(), cancelledTools: new Set() };48
this.jobs.set(id, job);49
while (this.jobs.size > 5) this.jobs.delete(this.jobs.keys().next().value);50
setImmediate(() => { job.promise = this._run(job).catch((error) => this._finish(job, errorResult(error))); });51
return { id, report: scrub(report) };52
}53
cancel(id, tool) {54
const job = this.jobs.get(String(id)); if (!job) throw new DiagnosticError("JOB_NOT_FOUND");55
if (tool) {56
if (!job.report.options.tools.includes(tool)) throw new DiagnosticError("INVALID_TOOLS");57
job.cancelledTools.add(tool); job.tools.get(tool)?.abort();58
} else job.controller.abort();59
return { cancelling: true };60
}61
stopAll() {62
for (const job of this.jobs.values()) if (job.report.status === "running") job.controller.abort();63
for (const controller of this.resolvers) controller.abort();64
}65
get(id) { const job = this.jobs.get(id); return job ? scrub(job.report) : this.history.get(id); }66
_emit(job, event) {67
const value = { jobId: job.report.id, ...event };68
if (event.kind !== "snapshot" && event.kind !== "complete") {69
job.report.progress.push(scrub(event));70
if (job.report.progress.length > 500) job.report.progress.shift();71
}72
this.emit("progress", value);73
}74
_finish(job, failure) {75
if (job.report.status !== "running") return;76
job.report.status = job.controller.signal.aborted ? "cancelled" : failure ? "failed" : "completed";77
job.report.finishedAt = new Date().toISOString();78
if (failure) job.report.error = failure;79
if (job.controller.signal.reason?.code) job.report.error = errorResult(job.controller.signal.reason);80
try { this.history.add(job.report); } catch (error) { job.report.historyError = error.code || "HISTORY_WRITE_FAILED"; }81
this._emit(job, { kind: "complete", report: scrub(job.report) });82
}83
async _address(job, tool, item, signal, progress) {84
const { report } = job, { options: base } = report;85
// A manual IPv4+IPv6 pair gives each address its own literal URL; otherwise HTTP/TLS86
// would connect to the first literal regardless of the address being tested.87
const options = base.manualAddresses ? { ...base, target: targetForAddress(base.target, item.address) } : base;88
try {89
checkAbort(signal); let result;90
if (tool === "http") result = await this.http.run(options.target, item.address, options, signal, progress);91
else if (tool === "compression") result = await this.http.compression(options.target, item.address, options, signal, progress);92
else if (tool === "tls" || tool === "websocket") result = await this.adapters[tool](options.target, item.address, options, signal, progress);93
else result = await this.adapters[tool](item.address, options, signal, progress);94
if (tool === "traceroute") {95
const addresses = [...new Set(result.routes?.flatMap((route) => route.hops.flatMap((hop) => hop.addresses)) || [])].slice(0, options.maxHops);96
result.reverseDns = {};97
for (const address of addresses) {98
checkAbort(signal);99
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); }100
catch (error) { if (signal.aborted) throw error; result.reverseDns[address] = []; }101
}102
}103
return { address: item.address, family: item.family, ...result };104
} catch (error) {105
const partial = { address: item.address, family: item.family, ...errorResult(error) };106
if (tool === "ping") {107
const latest = report.livePing?.[item.address];108
if (latest) Object.assign(partial, { sent: latest.sent, received: latest.received, lossPercent: latest.lossPercent, statistics: latest.statistics });109
partial.packets = report.progress.filter((e) => e.kind === "ping-packet" && e.address === item.address);110
}111
if (tool === "traceroute") partial.routes = [{ hops: report.progress.filter((e) => e.kind === "trace-hop" && e.address === item.address) }];112
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) }));113
return partial;114
}115
}116
async _run(job) {117
const { report, controller } = job, { options } = report, signal = controller.signal;118
const deadline = setTimeout(() => controller.abort(new DiagnosticError("JOB_TIME_LIMIT")), jobTimeLimit(options));119
try {120
checkAbort(signal); const started = now();121
if (options.manualAddresses) report.addresses = options.manualAddresses.map((item) => ({ ...item, source: "manual" }));122
else {123
try { report.addresses = await this.dns.resolve(options.target.host, options, signal); }124
catch (error) { if (signal.aborted) throw error; report.resolutionError = errorResult(error); }125
}126
report.resolvedAddresses = [...report.addresses]; report.resolutionMs = now() - started;127
if (options.selectedIp) {128
const selected = report.addresses.filter((item) => sameIp(item.address, options.selectedIp));129
if (!selected.length) throw new DiagnosticError("SELECTED_IP_NOT_RESOLVED");130
report.addresses = selected;131
}132
if (options.mode === "ipv4") report.addresses = report.addresses.filter((a) => a.family === 4);133
if (options.mode === "ipv6") report.addresses = report.addresses.filter((a) => a.family === 6);134
if (options.mode === "auto" && !options.manualAddresses) report.addresses = [report.addresses.find((a) => a.family === 4) || report.addresses[0]].filter(Boolean);135
if (options.mode === "both") report.addresses = [report.addresses.find((a) => a.family === 4), report.addresses.find((a) => a.family === 6)].filter(Boolean);136
this._emit(job, { kind: "resolved", addresses: report.addresses, resolvedAddresses: report.resolvedAddresses, resolutionMs: report.resolutionMs, error: report.resolutionError });137
// Two tools at a time; at most two IP families per tool. All workers settle before completion.138
const queue = [...options.tools]; let cursor = 0;139
await Promise.all(Array.from({ length: Math.min(2, queue.length) }, async () => {140
while (cursor < queue.length && !signal.aborted) {141
const tool = queue[cursor++], sub = new AbortController(); job.tools.set(tool, sub);142
const abort = () => sub.abort(); signal.addEventListener("abort", abort, { once: true });143
if (signal.aborted || job.cancelledTools.has(tool)) sub.abort();144
this._emit(job, { kind: "tool-start", tool });145
const progress = (event) => {146
if (event.kind === "ping-packet") { report.livePing ||= {}; report.livePing[event.address] = event; }147
this._emit(job, { tool, ...event });148
};149
try {150
checkAbort(sub.signal);151
if (tool === "dns") report.results.dns = await this.dns.run(options.target.host, options, sub.signal, progress);152
else if (!report.addresses.length) report.results[tool] = [{ status: "not-tested", code: "NO_ADDRESS" }];153
else report.results[tool] = await Promise.all(report.addresses.map((item) => this._address(job, tool, item, sub.signal, progress)));154
} catch (error) { report.results[tool] = [{ ...errorResult(error) }]; }155
finally {156
this._emit(job, { kind: "tool-complete", tool, result: scrub(report.results[tool]) });157
signal.removeEventListener("abort", abort); job.tools.delete(tool);158
}159
}160
}));161
this._finish(job);162
} catch (error) { this._finish(job, errorResult(error)); }163
finally { clearTimeout(deadline); }164
}165
export(id, format) { return exportReport(this.get(id), format); }166
}167
module.exports = { DiagnosticsService, sameIp, jobTimeLimit };SHA-256
37f59ff58670243afea618bb6226dff90cc0795b4466deac2f150eec279c1c33CH-J Proprietary Software License 1.14 · Mã nguồn được cung cấp theo CH-J Proprietary Software License 1.14. Việc công khai mã nguồn không thay đổi các điều khoản giấy phép và không cấp thêm quyền nào.
SHA-256 của tệp nén
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a