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.

11,1 KB · 125 linesDownload file
1"use strict";
2const net = require("node:net");
3const tls = require("node:tls");
4const crypto = require("node:crypto");
5const { Client } = require("ssh2");
6const { verifyPeerIdentity, SERVICES, DiagnosticError, checkAbort, now, mapLimit, errorResult } = require("./common");
7function algorithmOid(raw) {
8 try {
9 function element(at) { const tag = raw[at++]; let length = raw[at++]; if (length & 128) { const count = length & 127; if (count > 4) throw new Error(); length = 0; for (let n = 0; n < count; n++) length = length * 256 + raw[at++]; } if (at + length > raw.length) throw new Error(); return { tag, start: at, end: at + length }; }
10 const root = element(0), tbs = element(root.start), algorithm = element(tbs.end), oid = element(algorithm.start);
11 if (oid.tag !== 6) return null;
12 const bytes = raw.subarray(oid.start, oid.end), arcs = [Math.floor(bytes[0] / 40), bytes[0] % 40]; let value = 0;
13 for (const byte of bytes.subarray(1)) { value = value * 128 + (byte & 127); if (!(byte & 128)) { arcs.push(value); value = 0; } }
14 return arcs.join(".");
15 } catch { return null; }
17function certificateInfo(cert, host) {
18 const x509 = new crypto.X509Certificate(cert.raw), key = x509.publicKey, details = key.asymmetricKeyDetails || {};
19 return { subject: x509.subject, issuer: x509.issuer, san: x509.subjectAltName || null, validFrom: x509.validFrom, validUntil: x509.validTo,
20 daysUntilExpiration: Math.floor((new Date(x509.validTo).getTime() - Date.now()) / 86400000), fingerprint256: x509.fingerprint256,
21 publicKeyType: key.asymmetricKeyType, publicKeyBits: details.modulusLength || ({ prime256v1: 256, secp384r1: 384, secp521r1: 521 }[details.namedCurve]) || null,
22 publicKeyCurve: details.namedCurve || null, signatureAlgorithm: x509.signatureAlgorithm || algorithmOid(cert.raw),
23 hostnameMatches: Boolean(net.isIP(host) ? x509.checkIP(host) : x509.checkHost(host)),
24 selfSigned: x509.checkIssued(x509) && x509.verify(x509.publicKey), serialNumber: x509.serialNumber };
26function tlsProbe(target, address, options, signal, version, ca) {
27 checkAbort(signal);
28 return new Promise((resolve, reject) => {
29 const started = now(), host = target.host, port = target.port;
30 // Inspection-only socket: collect invalid chains too, then explicitly require
31 // both OpenSSL chain authorization AND hostname verification for a valid result.
32 // No HTTP/authentication/application payload is sent on this socket.
33 const socket = tls.connect({ host: address, port, servername: net.isIP(host) ? undefined : host, minVersion: version, maxVersion: version, rejectUnauthorized: false, ca, ALPNProtocols: ["h2", "http/1.1"] });
34 let done = false;
35 const finish = (error, result) => { if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener("abort", abort); socket.destroy(); error ? reject(error) : resolve(result); };
36 const abort = () => finish(new DiagnosticError("CANCELLED"));
37 const timer = setTimeout(() => finish(new DiagnosticError("TIMEOUT")), options.timeoutMs);
38 signal?.addEventListener("abort", abort, { once: true });
39 socket.once("error", (error) => finish(error));
40 socket.once("secureConnect", () => {
41 try {
42 const chain = [], seen = new Set(); let cert = socket.getPeerCertificate(true);
43 while (cert?.raw && chain.length < 12) {
44 const fingerprint = crypto.createHash("sha256").update(cert.raw).digest("hex"); if (seen.has(fingerprint)) break; seen.add(fingerprint);
45 chain.push(certificateInfo(cert, host)); cert = cert.issuerCertificate;
46 }
47 const hostnameError = verifyPeerIdentity(host, socket.getPeerCertificate());
48 const errors = []; if (!socket.authorized) errors.push(String(socket.authorizationError || "UNTRUSTED_CHAIN")); if (hostnameError) errors.push(hostnameError.code || "HOSTNAME_MISMATCH");
49 if (!chain.length) errors.push("CERTIFICATE_MISSING");
50 finish(null, { status: errors.length ? "invalid" : chain[0].daysUntilExpiration < 30 ? "warning" : "valid", valid: errors.length === 0,
51 requestedVersion: version, negotiatedVersion: socket.getProtocol(), address, port, sni: net.isIP(host) ? null : host,
52 durationMs: now() - started, cipher: socket.getCipher(), alpn: socket.alpnProtocol || null, chain, errors,
53 validation: "OpenSSL trust/date/signature verification plus explicit hostname verification; revocation/CT not independently audited." });
54 } catch (error) { finish(error); }
55 });
56 });
58async function runTls(target, address, options, signal, progress, ca) {
59 const results = [];
60 for (const version of ["TLSv1.2", "TLSv1.3"]) {
61 let result; try { result = await tlsProbe(target, address, options, signal, version, ca); }
62 catch (error) { if (signal.aborted) throw error; result = { ...errorResult(error), requestedVersion: version }; if (/protocol version|unsupported protocol/i.test(error.message)) result.status = "unsupported"; }
63 results.push(result); progress({ kind: "tls", address, ...result });
64 }
65 return { status: results.some((r) => r.status === "invalid") ? results.some((r) => r.valid) ? "warning" : "failed" : results.some((r) => r.valid) ? "success" : "warning", results };
67function tcpProbe(address, port, options, signal, connect = net.createConnection) {
68 checkAbort(signal);
69 return new Promise((resolve, reject) => {
70 const started = now(), socket = connect({ host: address, port }); let done = false, connectedAt, banner = Buffer.alloc(0), bannerTimer;
71 const finish = (error) => { if (done) return; done = true; clearTimeout(timer); clearTimeout(bannerTimer); signal?.removeEventListener("abort", abort); socket.destroy(); if (signal.aborted) return reject(new DiagnosticError("CANCELLED"));
72 resolve({ address, family: net.isIP(address), port, standardService: SERVICES[port] || null, status: error ? error.code === "ECONNREFUSED" ? "refused" : error.code === "TIMEOUT" || error.code === "ETIMEDOUT" ? "timeout" : "error" : "connected",
73 code: error?.code || null, connectionMs: connectedAt ? connectedAt - started : null, banner: banner.length ? banner.toString("utf8").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "").slice(0, 512) : null,
74 serviceNote: "Standard port assignment is a label, not service verification." }); };
75 const abort = () => finish(new DiagnosticError("CANCELLED")); const timer = setTimeout(() => finish(new DiagnosticError("TIMEOUT")), options.timeoutMs);
76 signal?.addEventListener("abort", abort, { once: true }); socket.once("error", finish);
77 socket.once("connect", () => { connectedAt = now(); if ([21, 22, 25, 110, 143, 587].includes(port)) bannerTimer = setTimeout(() => finish(), Math.min(500, options.timeoutMs)); else finish(); });
78 socket.on("data", (chunk) => { banner = Buffer.concat([banner, chunk]).subarray(0, 512); finish(); });
79 socket.once("end", () => finish(connectedAt ? undefined : new DiagnosticError("TCP_CLOSED")));
80 });
82function sshIdentification(address, options, signal) {
83 checkAbort(signal);
84 return new Promise((resolve, reject) => {
85 const client = new Client(); let done = false, identification = null;
86 const finish = (error, value) => { if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener("abort", abort); client.destroy(); error ? reject(error) : resolve(value); };
87 const abort = () => finish(new DiagnosticError("CANCELLED")), timer = setTimeout(() => finish(new DiagnosticError("TIMEOUT")), options.timeoutMs);
88 signal?.addEventListener("abort", abort, { once: true });
89 client.on("greeting", (value) => { identification = String(value).slice(0, 512); });
90 client.on("handshake", (negotiated) => { identification ||= negotiated?.serverIdent || null; });
91 client.on("error", (error) => { if (!done) finish(error); });
92 try { client.connect({ host: address, port: options.sshPort || 22, username: "diagnostics-no-auth", readyTimeout: options.timeoutMs, authHandler: () => false,
93 hostVerifier(key) { const fingerprint = "SHA256:" + crypto.createHash("sha256").update(key).digest("base64").replace(/=+$/, ""); finish(null, { status: "success", fingerprint, identification, authenticated: false, note: "Observed handshake key; not trusted or added to SSH known hosts." }); return false; } }); } catch (error) { finish(error); }
94 });
96async function runTcp(address, options, signal, progress) {
97 const ports = await mapLimit(options.ports, 4, async (port) => { const result = await tcpProbe(address, port, options, signal); progress({ kind: "tcp", ...result }); return result; });
98 let ssh = null;
99 if (ports.some((p) => p.port === 22 && p.status === "connected")) {
100 try { ssh = await sshIdentification(address, options, signal); ssh.identification ||= ports.find((p) => p.port === 22)?.banner || null; } catch (error) { if (signal.aborted) throw error; ssh = errorResult(error); }
101 }
102 return { status: ports.some((p) => p.status === "connected") ? "success" : "warning", ports, ssh };
104function websocketProbe(target, address, options, signal, ca) {
105 checkAbort(signal);
106 return new Promise((resolve, reject) => {
107 const url = new URL(target.url), secure = ["wss:", "https:"].includes(url.protocol), key = crypto.randomBytes(16).toString("base64"), started = now();
108 const transport = secure ? require("node:https") : require("node:http");
109 url.protocol = secure ? "https:" : "http:";
110 const request = transport.request(url, { method: "GET", agent: false, rejectUnauthorized: true, ca, servername: net.isIP(target.host) ? "" : target.host, checkServerIdentity: (_name, cert) => verifyPeerIdentity(target.host, cert), maxHeaderSize: 16384,
111 lookup: (_host, config, callback) => config?.all ? callback(null, [{ address, family: net.isIP(address) }]) : callback(null, address, net.isIP(address)),
112 headers: { Connection: "Upgrade", Upgrade: "websocket", "Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": key } });
113 let done = false;
114 const finish = (error, value) => { if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener("abort", abort); request.destroy(); error ? reject(error) : resolve(value); };
115 const abort = () => finish(new DiagnosticError("CANCELLED")), timer = setTimeout(() => finish(new DiagnosticError("TIMEOUT")), options.timeoutMs);
116 signal?.addEventListener("abort", abort, { once: true }); request.once("error", (error) => finish(error));
117 request.once("upgrade", (response, socket) => {
118 const expected = crypto.createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
119 const valid = response.statusCode === 101 && String(response.headers.upgrade).toLowerCase() === "websocket" && /\bupgrade\b/i.test(response.headers.connection || "") && response.headers["sec-websocket-accept"] === expected;
120 socket.destroy(); finish(null, { status: valid ? "success" : "failed", valid, statusCode: response.statusCode, durationMs: now() - started, address, secure });
121 });
122 request.once("response", (response) => { response.destroy(); finish(null, { status: "unsupported", statusCode: response.statusCode, durationMs: now() - started, address }); }); request.end();
123 });
125module.exports = { certificateInfo, algorithmOid, tlsProbe, runTls, tcpProbe, runTcp, sshIdentification, websocketProbe };

SHA-256: 28f277745c6d378ed33dc40596774a96d70cace387f4137e4b18e816a35913f1

Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0