Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"use strict";2
const net = require("node:net");3
const tls = require("node:tls");4
const crypto = require("node:crypto");5
const { Client } = require("ssh2");6
const { verifyPeerIdentity, SERVICES, DiagnosticError, checkAbort, now, mapLimit, errorResult } = require("./common");7
function 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; }16
}17
function 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 };25
}26
function 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 require31
// 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
});57
}58
async 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 };66
}67
function 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
});81
}82
function 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
});95
}96
async 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 };103
}104
function 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
});124
}125
module.exports = { certificateInfo, algorithmOid, tlsProbe, runTls, tcpProbe, runTcp, sshIdentification, websocketProbe };SHA-256: 28f277745c6d378ed33dc40596774a96d70cace387f4137e4b18e816a35913f1
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0