refactor: address CommanderStorm's review feedback
- Use i18n-t for description with code tag and RFC 8446 spec link - Add comment that TLS alert names are from spec (not translatable) - Refactor TCP monitor into smaller functions: - checkTcp() for standard TCP connectivity check - performStartTls() for STARTTLS handshake - checkTlsCertificate() for TLS certificate validation - attemptTlsConnection() for TLS connection with alert capture - Improve error messages with more context
This commit is contained in:
parent
327b51f304
commit
dc1e96f7d1
@ -113,6 +113,16 @@ class TCPMonitorType extends MonitorType {
|
||||
}
|
||||
|
||||
// Standard TCP check
|
||||
await this.checkTcp(monitor, heartbeat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard TCP connectivity check
|
||||
* @param {object} monitor Monitor object
|
||||
* @param {object} heartbeat Heartbeat object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async checkTcp(monitor, heartbeat) {
|
||||
try {
|
||||
const resp = await tcping(monitor.hostname, monitor.port);
|
||||
heartbeat.ping = resp;
|
||||
@ -124,124 +134,11 @@ class TCPMonitorType extends MonitorType {
|
||||
|
||||
let socket_;
|
||||
|
||||
const preTLS = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
let dialogTimeout;
|
||||
let bannerTimeout;
|
||||
socket_ = net.connect(monitor.port, monitor.hostname);
|
||||
|
||||
const onTimeout = () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`);
|
||||
doReject("Connection timed out");
|
||||
};
|
||||
|
||||
const onBannerTimeout = () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS timed out waiting for banner`);
|
||||
// No banner. Could be a XMPP server?
|
||||
socket_.write(`<stream:stream to='${monitor.hostname}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>`);
|
||||
};
|
||||
|
||||
const doResolve = () => {
|
||||
dialogTimeout && clearTimeout(dialogTimeout);
|
||||
bannerTimeout && clearTimeout(bannerTimeout);
|
||||
resolve({ socket: socket_ });
|
||||
};
|
||||
|
||||
const doReject = (error) => {
|
||||
dialogTimeout && clearTimeout(dialogTimeout);
|
||||
bannerTimeout && clearTimeout(bannerTimeout);
|
||||
socket_.end();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
socket_.on("connect", () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS connection: ${JSON.stringify(socket_)}`);
|
||||
});
|
||||
|
||||
socket_.on("data", data => {
|
||||
const response = data.toString();
|
||||
const response_ = response.toLowerCase();
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS response: ${response}`);
|
||||
clearTimeout(bannerTimeout);
|
||||
switch (true) {
|
||||
case response_.includes("start tls") || response_.includes("begin tls"):
|
||||
doResolve();
|
||||
break;
|
||||
case response.startsWith("* OK") || response.match(/CAPABILITY.+STARTTLS/):
|
||||
socket_.write("a001 STARTTLS\r\n");
|
||||
break;
|
||||
case response.startsWith("220") || response.includes("ESMTP"):
|
||||
socket_.write(`EHLO ${monitor.hostname}\r\n`);
|
||||
break;
|
||||
case response.includes("250-STARTTLS"):
|
||||
socket_.write("STARTTLS\r\n");
|
||||
break;
|
||||
case response_.includes("<proceed"):
|
||||
doResolve();
|
||||
break;
|
||||
case response_.includes("<starttls"):
|
||||
socket_.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
|
||||
break;
|
||||
case response_.includes("<stream:stream") || response_.includes("</stream:stream>"):
|
||||
break;
|
||||
default:
|
||||
doReject(`Unexpected response: ${response}`);
|
||||
}
|
||||
});
|
||||
socket_.on("error", error => {
|
||||
log.debug(this.name, `[${monitor.name}] ${error.toString()}`);
|
||||
reject(error);
|
||||
});
|
||||
socket_.setTimeout(1000 * TIMEOUT, onTimeout);
|
||||
dialogTimeout = setTimeout(onTimeout, 1000 * TIMEOUT);
|
||||
bannerTimeout = setTimeout(onBannerTimeout, 1000 * 1.5);
|
||||
});
|
||||
|
||||
const reuseSocket = monitor.smtpSecurity === "starttls" ? await preTLS() : {};
|
||||
|
||||
// Handle TLS certificate checking for secure/starttls connections
|
||||
if ([ "secure", "starttls" ].includes(monitor.smtpSecurity) && monitor.isEnabledExpiryNotification()) {
|
||||
let socket = null;
|
||||
try {
|
||||
const options = {
|
||||
host: monitor.hostname,
|
||||
port: monitor.port,
|
||||
servername: monitor.hostname,
|
||||
...reuseSocket,
|
||||
};
|
||||
|
||||
const tlsInfoObject = await new Promise((resolve, reject) => {
|
||||
socket = tls.connect(options);
|
||||
|
||||
socket.on("secureConnect", () => {
|
||||
try {
|
||||
const info = checkCertificate(socket);
|
||||
resolve(info);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("error", error => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
socket.setTimeout(1000 * TIMEOUT, () => {
|
||||
reject(new Error("Connection timed out"));
|
||||
});
|
||||
});
|
||||
|
||||
await monitor.handleTlsInfo(tlsInfoObject);
|
||||
if (!tlsInfoObject.valid) {
|
||||
throw new Error("Certificate is invalid");
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`TLS Connection failed: ${message}`);
|
||||
} finally {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
const reuseSocket = monitor.smtpSecurity === "starttls" ? await this.performStartTls(monitor) : {};
|
||||
socket_ = reuseSocket.socket;
|
||||
await this.checkTlsCertificate(monitor, reuseSocket);
|
||||
}
|
||||
|
||||
if (socket_ && !socket_.destroyed) {
|
||||
@ -249,6 +146,136 @@ class TCPMonitorType extends MonitorType {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform STARTTLS handshake for various protocols (SMTP, IMAP, XMPP)
|
||||
* @param {object} monitor Monitor object
|
||||
* @returns {Promise<{socket: net.Socket}>} Object containing the socket
|
||||
*/
|
||||
performStartTls(monitor) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let dialogTimeout;
|
||||
let bannerTimeout;
|
||||
const socket_ = net.connect(monitor.port, monitor.hostname);
|
||||
|
||||
const onTimeout = () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`);
|
||||
doReject("Connection timed out");
|
||||
};
|
||||
|
||||
const onBannerTimeout = () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS timed out waiting for banner`);
|
||||
// No banner. Could be a XMPP server?
|
||||
socket_.write(`<stream:stream to='${monitor.hostname}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>`);
|
||||
};
|
||||
|
||||
const doResolve = () => {
|
||||
dialogTimeout && clearTimeout(dialogTimeout);
|
||||
bannerTimeout && clearTimeout(bannerTimeout);
|
||||
resolve({ socket: socket_ });
|
||||
};
|
||||
|
||||
const doReject = (error) => {
|
||||
dialogTimeout && clearTimeout(dialogTimeout);
|
||||
bannerTimeout && clearTimeout(bannerTimeout);
|
||||
socket_.end();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
socket_.on("connect", () => {
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS connection: ${JSON.stringify(socket_)}`);
|
||||
});
|
||||
|
||||
socket_.on("data", data => {
|
||||
const response = data.toString();
|
||||
const response_ = response.toLowerCase();
|
||||
log.debug(this.name, `[${monitor.name}] Pre-TLS response: ${response}`);
|
||||
clearTimeout(bannerTimeout);
|
||||
switch (true) {
|
||||
case response_.includes("start tls") || response_.includes("begin tls"):
|
||||
doResolve();
|
||||
break;
|
||||
case response.startsWith("* OK") || response.match(/CAPABILITY.+STARTTLS/):
|
||||
socket_.write("a001 STARTTLS\r\n");
|
||||
break;
|
||||
case response.startsWith("220") || response.includes("ESMTP"):
|
||||
socket_.write(`EHLO ${monitor.hostname}\r\n`);
|
||||
break;
|
||||
case response.includes("250-STARTTLS"):
|
||||
socket_.write("STARTTLS\r\n");
|
||||
break;
|
||||
case response_.includes("<proceed"):
|
||||
doResolve();
|
||||
break;
|
||||
case response_.includes("<starttls"):
|
||||
socket_.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
|
||||
break;
|
||||
case response_.includes("<stream:stream") || response_.includes("</stream:stream>"):
|
||||
break;
|
||||
default:
|
||||
doReject(`Unexpected response: ${response}`);
|
||||
}
|
||||
});
|
||||
socket_.on("error", error => {
|
||||
log.debug(this.name, `[${monitor.name}] ${error.toString()}`);
|
||||
reject(error);
|
||||
});
|
||||
socket_.setTimeout(1000 * TIMEOUT, onTimeout);
|
||||
dialogTimeout = setTimeout(onTimeout, 1000 * TIMEOUT);
|
||||
bannerTimeout = setTimeout(onBannerTimeout, 1000 * 1.5);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check TLS certificate validity
|
||||
* @param {object} monitor Monitor object
|
||||
* @param {object} reuseSocket Socket to reuse for STARTTLS
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async checkTlsCertificate(monitor, reuseSocket) {
|
||||
let socket = null;
|
||||
try {
|
||||
const options = {
|
||||
host: monitor.hostname,
|
||||
port: monitor.port,
|
||||
servername: monitor.hostname,
|
||||
...reuseSocket,
|
||||
};
|
||||
|
||||
const tlsInfoObject = await new Promise((resolve, reject) => {
|
||||
socket = tls.connect(options);
|
||||
|
||||
socket.on("secureConnect", () => {
|
||||
try {
|
||||
const info = checkCertificate(socket);
|
||||
resolve(info);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("error", error => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
socket.setTimeout(1000 * TIMEOUT, () => {
|
||||
reject(new Error("Connection timed out"));
|
||||
});
|
||||
});
|
||||
|
||||
await monitor.handleTlsInfo(tlsInfoObject);
|
||||
if (!tlsInfoObject.valid) {
|
||||
throw new Error("Certificate is invalid");
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
throw new Error(`TLS Connection failed: ${message}`);
|
||||
} finally {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for expected TLS alert (for mTLS verification)
|
||||
* @param {object} monitor Monitor object
|
||||
@ -277,12 +304,43 @@ class TCPMonitorType extends MonitorType {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const result = await this.attemptTlsConnection(monitor, options, startTime, timeout);
|
||||
|
||||
heartbeat.ping = result.responseTime;
|
||||
|
||||
// Handle TLS info for certificate expiry monitoring
|
||||
if (result.tlsInfo && monitor.isEnabledExpiryNotification()) {
|
||||
await monitor.handleTlsInfo(result.tlsInfo);
|
||||
}
|
||||
|
||||
// Check if we got the expected alert
|
||||
if (result.alertName === expectedTlsAlert) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = `TLS alert received as expected: ${result.alertName} (${result.alertNumber})`;
|
||||
} else if (result.success) {
|
||||
throw new Error(`Expected TLS alert '${expectedTlsAlert}' but connection succeeded. The server accepted the connection without requiring a client certificate.`);
|
||||
} else if (result.alertNumber !== null) {
|
||||
throw new Error(`Expected TLS alert '${expectedTlsAlert}' but received '${result.alertName}' (${result.alertNumber})`);
|
||||
} else {
|
||||
throw new Error(`Expected TLS alert '${expectedTlsAlert}' but got unexpected error: ${result.errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt TLS connection and capture result/alert
|
||||
* @param {object} monitor Monitor object
|
||||
* @param {object} options TLS connection options
|
||||
* @param {number} startTime Connection start timestamp
|
||||
* @param {number} timeout Connection timeout in ms
|
||||
* @returns {Promise<object>} Connection result with success, responseTime, tlsInfo, alertNumber, alertName, errorMessage
|
||||
*/
|
||||
attemptTlsConnection(monitor, options, startTime, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = tls.connect(options);
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
socket.destroy();
|
||||
reject(new Error("Connection timed out"));
|
||||
reject(new Error("TLS connection timed out"));
|
||||
}, timeout);
|
||||
|
||||
socket.on("secureConnect", () => {
|
||||
@ -331,28 +389,9 @@ class TCPMonitorType extends MonitorType {
|
||||
socket.on("timeout", () => {
|
||||
clearTimeout(timeoutId);
|
||||
socket.destroy();
|
||||
reject(new Error("Connection timed out"));
|
||||
reject(new Error("TLS connection timed out"));
|
||||
});
|
||||
});
|
||||
|
||||
heartbeat.ping = result.responseTime;
|
||||
|
||||
// Handle TLS info for certificate expiry monitoring
|
||||
if (result.tlsInfo && monitor.isEnabledExpiryNotification()) {
|
||||
await monitor.handleTlsInfo(result.tlsInfo);
|
||||
}
|
||||
|
||||
// Check if we got the expected alert
|
||||
if (result.alertName === expectedTlsAlert) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = `TLS alert received as expected: ${result.alertName} (${result.alertNumber})`;
|
||||
} else if (result.success) {
|
||||
throw new Error(`Expected TLS alert '${expectedTlsAlert}' but connection succeeded`);
|
||||
} else if (result.alertNumber !== null) {
|
||||
throw new Error(`Expected TLS alert '${expectedTlsAlert}' but got '${result.alertName}' (${result.alertNumber})`);
|
||||
} else {
|
||||
throw new Error(`Expected TLS alert '${expectedTlsAlert}' but got error: ${result.errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -406,6 +406,7 @@
|
||||
<label for="expected_tls_alert" class="form-label">{{ $t("Expected TLS Alert") }}</label>
|
||||
<select id="expected_tls_alert" v-model="monitor.expectedTlsAlert" class="form-select">
|
||||
<option value="none">{{ $t("None (Successful Connection)") }}</option>
|
||||
<!-- TLS alert names are from RFC 8446 spec and should NOT be translated -->
|
||||
<optgroup :label="$t('TLS Alerts')">
|
||||
<option value="certificate_required">certificate_required (116)</option>
|
||||
<option value="bad_certificate">bad_certificate (42)</option>
|
||||
@ -417,9 +418,14 @@
|
||||
<option value="certificate_revoked">certificate_revoked (44)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
{{ $t("expectedTlsAlertDescription") }}
|
||||
</div>
|
||||
<i18n-t tag="div" class="form-text" keypath="expectedTlsAlertDescription">
|
||||
<template #code>
|
||||
<code>certificate_required</code>
|
||||
</template>
|
||||
<template #link>
|
||||
<a href="https://www.rfc-editor.org/rfc/rfc8446#section-6.2" target="_blank" rel="noopener noreferrer">{{ $t("TLS Alert Spec") }}</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user