feat: the option to expect a certain TLS error for the TCP monitor (#6587)
This commit is contained in:
commit
adec2a7307
11
db/knex_migrations/2026-01-05-0000-add-tls-monitor.js
Normal file
11
db/knex_migrations/2026-01-05-0000-add-tls-monitor.js
Normal file
@ -0,0 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.string("expected_tls_alert", 50).defaultTo(null);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("expected_tls_alert");
|
||||
});
|
||||
};
|
||||
@ -165,6 +165,7 @@ class Monitor extends BeanModel {
|
||||
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
|
||||
conditions: JSON.parse(this.conditions),
|
||||
ipFamily: this.ipFamily,
|
||||
expectedTlsAlert: this.expected_tls_alert,
|
||||
|
||||
// ping advanced options
|
||||
ping_numeric: this.isPingNumeric(),
|
||||
|
||||
@ -5,6 +5,69 @@ const tls = require("tls");
|
||||
const net = require("net");
|
||||
const tcpp = require("tcp-ping");
|
||||
|
||||
/**
|
||||
* TLS Alert codes as defined in RFC 5246 and RFC 8446
|
||||
* @see https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-6
|
||||
*/
|
||||
const TLS_ALERT_CODES = {
|
||||
0: "close_notify",
|
||||
10: "unexpected_message",
|
||||
20: "bad_record_mac",
|
||||
21: "decryption_failed",
|
||||
22: "record_overflow",
|
||||
30: "decompression_failure",
|
||||
40: "handshake_failure",
|
||||
41: "no_certificate",
|
||||
42: "bad_certificate",
|
||||
43: "unsupported_certificate",
|
||||
44: "certificate_revoked",
|
||||
45: "certificate_expired",
|
||||
46: "certificate_unknown",
|
||||
47: "illegal_parameter",
|
||||
48: "unknown_ca",
|
||||
49: "access_denied",
|
||||
50: "decode_error",
|
||||
51: "decrypt_error",
|
||||
60: "export_restriction",
|
||||
70: "protocol_version",
|
||||
71: "insufficient_security",
|
||||
80: "internal_error",
|
||||
86: "inappropriate_fallback",
|
||||
90: "user_canceled",
|
||||
100: "no_renegotiation",
|
||||
109: "missing_extension",
|
||||
110: "unsupported_extension",
|
||||
111: "certificate_unobtainable",
|
||||
112: "unrecognized_name",
|
||||
113: "bad_certificate_status_response",
|
||||
114: "bad_certificate_hash_value",
|
||||
115: "unknown_psk_identity",
|
||||
116: "certificate_required",
|
||||
120: "no_application_protocol",
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse TLS alert number from error message
|
||||
* @param {string} errorMessage Error message from TLS connection
|
||||
* @returns {number|null} TLS alert number or null if not found
|
||||
*/
|
||||
function parseTlsAlertNumber(errorMessage) {
|
||||
const match = errorMessage.match(/alert number (\d+)/i);
|
||||
if (match) {
|
||||
return parseInt(match[1], 10);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TLS alert name from alert number
|
||||
* @param {number} alertNumber TLS alert number
|
||||
* @returns {string} TLS alert name or "unknown_alert"
|
||||
*/
|
||||
function getTlsAlertName(alertNumber) {
|
||||
return TLS_ALERT_CODES[alertNumber] || `unknown_alert_${alertNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send TCP request to specified hostname and port
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
@ -41,6 +104,25 @@ class TCPMonitorType extends MonitorType {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
const expectedTlsAlert = monitor.expected_tls_alert;
|
||||
|
||||
// If expecting a TLS alert, use TLS connection with alert detection
|
||||
if (expectedTlsAlert && expectedTlsAlert !== "none") {
|
||||
await this.checkTlsAlert(monitor, heartbeat, expectedTlsAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@ -52,132 +134,272 @@ 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) {
|
||||
socket_.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {object} heartbeat Heartbeat object
|
||||
* @param {string} expectedTlsAlert Expected TLS alert name
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async checkTlsAlert(monitor, heartbeat, expectedTlsAlert) {
|
||||
const timeout = monitor.timeout * 1000 || 30000;
|
||||
const startTime = Date.now();
|
||||
|
||||
const options = {
|
||||
host: monitor.hostname,
|
||||
port: monitor.port || 443,
|
||||
servername: monitor.hostname,
|
||||
rejectUnauthorized: !monitor.getIgnoreTls(),
|
||||
timeout: timeout,
|
||||
};
|
||||
|
||||
// Add client certificate if provided (for mTLS testing with cert)
|
||||
if (monitor.tlsCert && monitor.tlsKey) {
|
||||
options.cert = monitor.tlsCert;
|
||||
options.key = monitor.tlsKey;
|
||||
if (monitor.tlsCa) {
|
||||
options.ca = monitor.tlsCa;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// Note: Error messages below could be translated, but alert names (e.g., certificate_required)
|
||||
// are from RFC 8446 spec and should remain in English for consistency with the spec.
|
||||
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("TLS connection timed out"));
|
||||
}, timeout);
|
||||
|
||||
socket.on("secureConnect", () => {
|
||||
clearTimeout(timeoutId);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
let tlsInfo = null;
|
||||
if (monitor.isEnabledExpiryNotification()) {
|
||||
try {
|
||||
tlsInfo = checkCertificate(socket);
|
||||
} catch (e) {
|
||||
log.debug(this.name, `[${monitor.name}] Error checking certificate: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
socket.end();
|
||||
resolve({
|
||||
success: true,
|
||||
responseTime,
|
||||
tlsInfo,
|
||||
alertNumber: null,
|
||||
alertName: null,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("error", (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
const responseTime = Date.now() - startTime;
|
||||
const errorMessage = error.message || error.toString();
|
||||
|
||||
const alertNumber = parseTlsAlertNumber(errorMessage);
|
||||
const alertName = alertNumber !== null ? getTlsAlertName(alertNumber) : null;
|
||||
|
||||
log.debug(this.name, `[${monitor.name}] TLS error: ${errorMessage}, alert: ${alertNumber} (${alertName})`);
|
||||
|
||||
resolve({
|
||||
success: false,
|
||||
responseTime,
|
||||
tlsInfo: null,
|
||||
alertNumber,
|
||||
alertName,
|
||||
errorMessage,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
clearTimeout(timeoutId);
|
||||
socket.destroy();
|
||||
reject(new Error("TLS connection timed out"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TCPMonitorType,
|
||||
TLS_ALERT_CODES,
|
||||
parseTlsAlertNumber,
|
||||
getTlsAlertName,
|
||||
};
|
||||
|
||||
@ -902,6 +902,7 @@ let needSetup = false;
|
||||
bean.conditions = JSON.stringify(monitor.conditions);
|
||||
bean.manual_status = monitor.manual_status;
|
||||
bean.system_service_name = monitor.system_service_name;
|
||||
bean.expected_tls_alert = monitor.expectedTlsAlert;
|
||||
|
||||
// ping advanced options
|
||||
bean.ping_numeric = monitor.ping_numeric;
|
||||
|
||||
@ -1295,5 +1295,10 @@
|
||||
"End": "End",
|
||||
"Show this Maintenance Message on which Status Pages": "Show this Maintenance Message on which Status Pages",
|
||||
"Endpoint": "Endpoint",
|
||||
"Details": "Details"
|
||||
"Details": "Details",
|
||||
"TLS Alerts": "TLS Alerts",
|
||||
"Expected TLS Alert": "Expected TLS Alert",
|
||||
"None (Successful Connection)": "None (Successful Connection)",
|
||||
"expectedTlsAlertDescription": "Select the TLS alert you expect the server to return. Use {code} to verify mTLS endpoints reject connections without client certificates. See {link} for details.",
|
||||
"TLS Alert Spec": "RFC 8446"
|
||||
}
|
||||
|
||||
@ -400,6 +400,35 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Expected TLS Alert (for TCP monitor mTLS verification) -->
|
||||
<template v-if="monitor.type === 'port'">
|
||||
<div class="my-3">
|
||||
<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>
|
||||
<option value="certificate_unknown">certificate_unknown (46)</option>
|
||||
<option value="unknown_ca">unknown_ca (48)</option>
|
||||
<option value="access_denied">access_denied (49)</option>
|
||||
<option value="handshake_failure">handshake_failure (40)</option>
|
||||
<option value="certificate_expired">certificate_expired (45)</option>
|
||||
<option value="certificate_revoked">certificate_revoked (44)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<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>
|
||||
|
||||
<!-- Json Query -->
|
||||
<!-- For Json Query / SNMP -->
|
||||
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
||||
|
||||
@ -220,4 +220,52 @@ describe("TCP Monitor", () => {
|
||||
}, heartbeat);
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
});
|
||||
|
||||
// TLS Alert checking tests
|
||||
test("check() rejects when expecting TLS alert but connection succeeds", async () => {
|
||||
const tcpMonitor = new TCPMonitorType();
|
||||
|
||||
const monitor = {
|
||||
hostname: "google.com",
|
||||
port: 443,
|
||||
expected_tls_alert: "certificate_required",
|
||||
timeout: 10,
|
||||
isEnabledExpiryNotification: () => false,
|
||||
getIgnoreTls: () => false,
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
// Retry with backoff for external service reliability, expecting rejection
|
||||
await retryExternalService(async () => {
|
||||
await assert.rejects(
|
||||
tcpMonitor.check(monitor, heartbeat, {}),
|
||||
/Expected TLS alert 'certificate_required' but connection succeeded/
|
||||
);
|
||||
}, heartbeat);
|
||||
});
|
||||
|
||||
test("parseTlsAlertNumber() extracts alert number from error message", async () => {
|
||||
const { parseTlsAlertNumber } = require("../../../server/monitor-types/tcp");
|
||||
|
||||
// Test various error message formats
|
||||
assert.strictEqual(parseTlsAlertNumber("alert number 116"), 116);
|
||||
assert.strictEqual(parseTlsAlertNumber("SSL alert number 42"), 42);
|
||||
assert.strictEqual(parseTlsAlertNumber("TLS alert number 48"), 48);
|
||||
assert.strictEqual(parseTlsAlertNumber("no alert here"), null);
|
||||
assert.strictEqual(parseTlsAlertNumber(""), null);
|
||||
});
|
||||
|
||||
test("getTlsAlertName() returns correct alert name for known codes", async () => {
|
||||
const { getTlsAlertName } = require("../../../server/monitor-types/tcp");
|
||||
|
||||
assert.strictEqual(getTlsAlertName(116), "certificate_required");
|
||||
assert.strictEqual(getTlsAlertName(42), "bad_certificate");
|
||||
assert.strictEqual(getTlsAlertName(48), "unknown_ca");
|
||||
assert.strictEqual(getTlsAlertName(40), "handshake_failure");
|
||||
assert.strictEqual(getTlsAlertName(999), "unknown_alert_999");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user