CH-J Server Managerserver management over SSH
Menu
Published source

CH-J Server Manager

Browse directories and files for a specific application release.

Download source ZIP
CH-J Proprietary Software License 1.14

Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.

14,7 KB · 193 linesDownload file
1"use strict";
2const dns = require("node:dns");
3const dgram = require("node:dgram");
4const net = require("node:net");
5const crypto = require("node:crypto");
6const { hostname, DiagnosticError, boundedSignal, checkAbort, now, mapLimit, errorResult } = require("./common");
7const TYPES = { A: 1, NS: 2, CNAME: 5, SOA: 6, PTR: 12, MX: 15, TXT: 16, AAAA: 28, SRV: 33, DS: 43, DNSKEY: 48, CAA: 257 };
8const PROVIDERS = { cloudflare: ["1.1.1.1", "2606:4700:4700::1111"], google: ["8.8.8.8", "2001:4860:4860::8888"], quad9: ["9.9.9.9", "2620:fe::fe"] };
9function readName(buffer, position) {
10 let cursor = position, end, labels = [], seen = new Set();
11 for (let count = 0; count < 128; count++) {
12 if (cursor >= buffer.length || seen.has(cursor)) throw new DiagnosticError("DNS_MALFORMED");
13 seen.add(cursor); const length = buffer[cursor++];
14 if ((length & 0xc0) === 0xc0) { if (cursor >= buffer.length) throw new DiagnosticError("DNS_MALFORMED"); end ??= cursor + 1; cursor = ((length & 0x3f) << 8) | buffer[cursor]; continue; }
15 if (length & 0xc0 || length > 63 || cursor + length > buffer.length) throw new DiagnosticError("DNS_MALFORMED");
16 if (!length) return { name: labels.join("."), end: end ?? cursor };
17 labels.push(buffer.subarray(cursor, cursor + length).toString("ascii")); cursor += length;
18 }
19 throw new DiagnosticError("DNS_MALFORMED");
21function parseResponse(buffer, id, type) {
22 if (buffer.length < 12 || buffer.readUInt16BE(0) !== id || !(buffer[2] & 0x80)) throw new DiagnosticError("DNS_MALFORMED");
23 const code = buffer[3] & 15; const status = { 0: "success", 2: "servfail", 3: "nxdomain", 5: "refused" }[code] || "error";
24 let cursor = 12;
25 const questions = buffer.readUInt16BE(4), total = buffer.readUInt16BE(6) + buffer.readUInt16BE(8) + buffer.readUInt16BE(10);
26 if (questions !== 1 || total > 1024) throw new DiagnosticError("DNS_MALFORMED");
27 const question = readName(buffer, cursor); cursor = question.end;
28 if (cursor + 4 > buffer.length || buffer.readUInt16BE(cursor) !== TYPES[type] || buffer.readUInt16BE(cursor + 2) !== 1) throw new DiagnosticError("DNS_MALFORMED");
29 cursor += 4; const records = [];
30 for (let index = 0; index < total; index++) {
31 const owner = readName(buffer, cursor); cursor = owner.end;
32 if (cursor + 10 > buffer.length) throw new DiagnosticError("DNS_MALFORMED");
33 const number = buffer.readUInt16BE(cursor), cls = buffer.readUInt16BE(cursor + 2), ttl = buffer.readUInt32BE(cursor + 4), length = buffer.readUInt16BE(cursor + 8);
34 cursor += 10; const end = cursor + length;
35 if (end > buffer.length) throw new DiagnosticError("DNS_MALFORMED");
36 const recordType = Object.keys(TYPES).find((key) => TYPES[key] === number);
37 let value;
38 if (number === 1 && length === 4) value = [...buffer.subarray(cursor, end)].join(".");
39 else if (number === 28 && length === 16) value = Array.from({ length: 8 }, (_, n) => buffer.readUInt16BE(cursor + n * 2).toString(16)).join(":");
40 else if ([2, 5, 12].includes(number)) value = readName(buffer, cursor).name;
41 else if (number === 15 && length >= 3) value = { priority: buffer.readUInt16BE(cursor), exchange: readName(buffer, cursor + 2).name };
42 else if (number === 33 && length >= 7) value = { priority: buffer.readUInt16BE(cursor), weight: buffer.readUInt16BE(cursor + 2), port: buffer.readUInt16BE(cursor + 4), name: readName(buffer, cursor + 6).name };
43 else if (number === 16) { value = []; let p = cursor; while (p < end) { const size = buffer[p++]; if (p + size > end) throw new DiagnosticError("DNS_MALFORMED"); value.push(buffer.subarray(p, p + size).toString("utf8")); p += size; } }
44 else if (number === 6) { const first = readName(buffer, cursor), second = readName(buffer, first.end); if (second.end + 20 > end) throw new DiagnosticError("DNS_MALFORMED"); value = { nsname: first.name, hostmaster: second.name }; ["serial", "refresh", "retry", "expire", "minttl"].forEach((key, n) => { value[key] = buffer.readUInt32BE(second.end + n * 4); }); }
45 else if (number === 43 && length >= 4) value = { keyTag: buffer.readUInt16BE(cursor), algorithm: buffer[cursor + 2], digestType: buffer[cursor + 3], digest: buffer.subarray(cursor + 4, end).toString("hex") };
46 else if (number === 48 && length >= 4) value = { flags: buffer.readUInt16BE(cursor), protocol: buffer[cursor + 2], algorithm: buffer[cursor + 3], publicKey: buffer.subarray(cursor + 4, end).toString("base64") };
47 else if (number === 257 && length >= 2 && cursor + 2 + buffer[cursor + 1] <= end) value = { flags: buffer[cursor], tag: buffer.subarray(cursor + 2, cursor + 2 + buffer[cursor + 1]).toString(), value: buffer.subarray(cursor + 2 + buffer[cursor + 1], end).toString() };
48 if (recordType && cls === 1 && value !== undefined) records.push({ name: owner.name, type: recordType, ttl, value, section: index < buffer.readUInt16BE(6) ? "answer" : "authority/additional" });
49 cursor = end;
50 }
51 return { questionName: question.name, status: status === "success" && !records.some((r) => r.type === type && r.section === "answer") ? "nodata" : status, records, truncated: Boolean(buffer[2] & 2), dnssec: "not-validated", resolverAdFlag: Boolean(buffer[3] & 32) };
53function reverseName(address) {
54 if (net.isIP(address) === 4) return address.split(".").reverse().join(".") + ".in-addr.arpa";
55 if (net.isIP(address) !== 6) throw new DiagnosticError("INVALID_IP");
56 // URL canonicalization expands embedded IPv4 to two hexadecimal groups.
57 const value = new URL(`http://[${address}]/`).hostname.slice(1, -1), halves = value.split("::"), left = halves[0] ? halves[0].split(":") : [], right = halves[1] ? halves[1].split(":") : [];
58 return [...left, ...Array(8 - left.length - right.length).fill("0"), ...right].map((part) => part.padStart(4, "0")).join("").split("").reverse().join(".") + ".ip6.arpa";
60function serverAddress(value) {
61 const v6 = String(value).match(/^\[([^\]]+)\](?::(\d+))?$/);
62 if (v6) return { host: v6[1], port: Number(v6[2] || 53) };
63 if (net.isIP(value)) return { host: value, port: 53 };
64 const v4 = String(value).match(/^([\d.]+):(\d+)$/);
65 if (v4 && net.isIP(v4[1]) === 4) return { host: v4[1], port: Number(v4[2]) };
66 throw new DiagnosticError("INVALID_DNS_SERVER");
68function exchange(packet, server, signal, tcp = false) {
69 checkAbort(signal);
70 checkAbort(signal);
71 return new Promise((resolve, reject) => {
72 const address = serverAddress(server), socket = tcp ? net.createConnection({ host: address.host, port: address.port }) : dgram.createSocket(net.isIP(address.host) === 6 ? "udp6" : "udp4");
73 let done = false, data = Buffer.alloc(0);
74 const finish = (error, response) => { if (done) return; done = true; signal?.removeEventListener("abort", abort); if (tcp) socket.destroy(); else { try { socket.close(); } catch {} } error ? reject(error) : resolve(response); };
75 const abort = () => finish(signal.reason || new DiagnosticError("CANCELLED"));
76 signal?.addEventListener("abort", abort, { once: true }); socket.once("error", finish);
77 if (tcp) {
78 socket.once("connect", () => { const size = Buffer.alloc(2); size.writeUInt16BE(packet.length); socket.write(Buffer.concat([size, packet])); });
79 socket.on("data", (chunk) => { data = Buffer.concat([data, chunk]); if (data.length > 65537) return finish(new DiagnosticError("DNS_TOO_LARGE")); if (data.length >= 2 && data.length >= data.readUInt16BE(0) + 2) finish(null, data.subarray(2, data.readUInt16BE(0) + 2)); });
80 socket.once("end", () => finish(new DiagnosticError("DNS_INCOMPLETE")));
81 } else {
82 socket.once("message", (response) => finish(null, response));
83 socket.connect(address.port, address.host, () => socket.send(packet, (error) => { if (error) finish(error); }));
84 }
85 });
87class DnsDiagnostics {
88 constructor({ lookup = dns.lookup } = {}) { this.lookup = lookup; }
89 systemLookup(host, options, signal) {
90 checkAbort(signal);
91 const started = now(), limit = boundedSignal(signal, options.timeoutMs);
92 return new Promise((resolve, reject) => {
93 let done = false;
94 const finish = (error, addresses) => {
95 if (done) return; done = true; limit.signal.removeEventListener("abort", abort); limit.close();
96 error ? reject(error) : resolve(addresses.map((a) => ({ ...a, dnsMs: now() - started, method: "OS resolver (hosts file / system DNS); TTL unavailable" })));
97 };
98 const abort = () => finish(limit.signal.reason || new DiagnosticError("CANCELLED"));
99 limit.signal.addEventListener("abort", abort, { once: true });
100 try { this.lookup(host, { all: true, verbatim: true, family: options.mode === "ipv4" ? 4 : options.mode === "ipv6" ? 6 : 0 }, finish); }
101 catch (error) { finish(error); }
102 });
103 }
104 servers(options) { return options.resolver === "custom" ? [options.customDns] : PROVIDERS[options.resolver] || dns.getServers(); }
105 async query(name, type, options, signal) {
106 const started = now(), queryName = hostname(name), id = crypto.randomBytes(2).readUInt16BE();
107 const header = Buffer.alloc(12); header.writeUInt16BE(id); header.writeUInt16BE(0x0100, 2); header.writeUInt16BE(1, 4);
108 const question = Buffer.concat([...queryName.split(".").map((part) => Buffer.concat([Buffer.from([Buffer.byteLength(part)]), Buffer.from(part)])), Buffer.from([0, TYPES[type] >> 8, TYPES[type] & 255, 0, 1])]);
109 const packet = Buffer.concat([header, question]), limit = boundedSignal(signal, options.timeoutMs);
110 let failure;
111 try {
112 for (const server of this.servers(options)) {
113 try {
114 let response = await exchange(packet, server, limit.signal);
115 if (response[2] & 2) response = await exchange(packet, server, limit.signal, true);
116 const result = parseResponse(response, id, type);
117 if (result.questionName && result.questionName !== queryName) throw new DiagnosticError("DNS_MALFORMED");
118 return { name: queryName, type, server, durationMs: now() - started, ...result };
119 } catch (error) { failure = error; if (limit.signal.aborted) break; }
120 }
121 throw failure || new DiagnosticError("DNS_UNAVAILABLE");
122 } finally { limit.close(); }
123 }
124 async resolve(host, options, signal) {
125 checkAbort(signal);
126 if (net.isIP(host)) return [{ address: host, family: net.isIP(host), dnsMs: 0 }];
127 if (options.resolver === "system") { const addresses = await this.systemLookup(host, options, signal); if (!addresses.length) throw new DiagnosticError("DNS_NO_ADDRESS"); return addresses; }
128 const types = options.mode === "ipv4" ? ["A"] : options.mode === "ipv6" ? ["AAAA"] : ["A", "AAAA"];
129 const results = await Promise.all(types.map(async (type) => {
130 let name = host, duration = 0, seen = new Set();
131 for (let i = 0; i < 8; i++) {
132 if (seen.has(name)) throw new DiagnosticError("DNS_CNAME_LOOP"); seen.add(name);
133 const result = await this.query(name, type, options, signal); duration += result.durationMs;
134 const addresses = result.records.filter((r) => r.type === type && r.section === "answer");
135 if (addresses.length) return addresses.map((r) => ({ address: r.value, family: type === "A" ? 4 : 6, dnsMs: duration, ttl: r.ttl }));
136 const cname = result.records.find((r) => r.type === "CNAME" && r.section === "answer");
137 if (!cname) return [];
138 name = cname.value;
139 }
140 throw new DiagnosticError("DNS_CNAME_LIMIT");
141 }).map((promise) => promise.catch((error) => { if (signal?.aborted) throw error; return []; })));
142 const addresses = results.flat();
143 if (!addresses.length) throw new DiagnosticError("DNS_NO_ADDRESS");
144 return addresses;
145 }
146 async run(host, options, signal, progress) {
147 const name = net.isIP(host) ? reverseName(host) : host;
148 const types = net.isIP(host) ? ["PTR"] : Object.keys(TYPES);
149 const queries = await mapLimit(types, 3, async (type) => {
150 checkAbort(signal);
151 let result; try { result = await this.query(name, type, options, signal); } catch (error) { result = { type, name, ...errorResult(error) }; }
152 progress?.({ kind: "dns-record", ...result }); return result;
153 });
154 const cnames = queries.flatMap((q) => q.records || []).filter((r) => r.type === "CNAME" && r.section === "answer");
155 const seen = new Set([host]); let next = cnames.find((r) => r.name === host)?.value, cnameError;
156 for (let index = 0; next && index < 8; index++) {
157 checkAbort(signal);
158 if (seen.has(next)) { cnameError = "DNS_CNAME_LOOP"; break; }
159 seen.add(next);
160 try {
161 const query = await this.query(next, "CNAME", options, signal);
162 const record = query.records.find((r) => r.type === "CNAME" && r.section === "answer" && r.name === next);
163 if (!record) { next = null; break; }
164 cnames.push(record); next = record.value;
165 } catch (error) { if (signal.aborted) throw error; cnameError = error.code; break; }
166 }
167 if (next && !cnameError) cnameError = "DNS_CNAME_LIMIT";
168 const consistency = [];
169 if (!net.isIP(host)) {
170 for (const type of ["A", "AAAA"]) {
171 checkAbort(signal);
172 const first = queries.find((q) => q.type === type);
173 try {
174 const second = await this.query(host, type, options, signal);
175 const values = (q) => (q.records || []).filter((r) => [type, "CNAME"].includes(r.type) && r.section === "answer").map((r) => JSON.stringify([r.name, r.type, r.value])).sort();
176 consistency.push({ type, status: first?.status === second.status && JSON.stringify(values(first)) === JSON.stringify(values(second)) ? "consistent" : "changed", first: values(first), second: values(second) });
177 } catch (error) { if (signal.aborted) throw error; consistency.push({ type, ...errorResult(error) }); }
178 }
179 } else {
180 for (const record of (queries[0]?.records || []).filter((r) => r.type === "PTR").slice(0, 4)) {
181 try {
182 const answers = await this.resolve(record.value, { ...options, mode: net.isIP(host) === 4 ? "ipv4" : "ipv6" }, signal);
183 const canonical = (address) => net.isIP(address) === 6 ? new URL(`http://[${address}]/`).hostname : address;
184 consistency.push({ type: "forward-confirmed-PTR", name: record.value, status: answers.some((a) => canonical(a.address) === canonical(host)) ? "consistent" : "changed" });
185 } catch (error) { if (signal.aborted) throw error; consistency.push({ type: "forward-confirmed-PTR", name: record.value, ...errorResult(error) }); }
186 }
187 }
188 return { status: queries.some((q) => q.status === "success") ? "success" : "warning", queries,
189 cnameChain: [...new Set(cnames.map((r) => `${r.name} → ${r.value}`))], cnameError, consistency,
190 dnssec: "not-validated", note: "Repeated answers / forward-confirmed PTR from the selected resolver. Changes can reflect load balancing; this is not authoritative consistency or DNSSEC validation." };
191 }
193module.exports = { TYPES, PROVIDERS, DnsDiagnostics, parseResponse, readName, reverseName };

SHA-256: 41fc390764513e013f94aa827f051433aeea02bf7b07b289983f7eb8dcbe2179

Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0