tls: server: Improve heartbeat and log messages

This commit is contained in:
Martin Rubli 2024-06-03 18:15:10 +02:00
parent 84bbda21ab
commit 00c0563d55

View file

@ -4,6 +4,26 @@ const net = require("net");
const tls = require("tls"); const tls = require("tls");
const unescape = require("unescape-js"); const unescape = require("unescape-js");
/**
* Encloses a string in double quotes.
* @param {string} s String to quote
* @returns {string} Quoted string
*/
function quote(s) {
return JSON.stringify(s);
}
/**
* Truncates and appends an ellipsis () to a string if it is longer than a threshold, and encloses
* the resulting string in double quotes.
* @param {string} s String to ellipsize and/or quote
* @returns {string} Ellipsized and/or quoted string
*/
function ellipsize(s) {
const maxLen = 32;
return quote(s.length <= maxLen ? s : `${s.substring(0, maxLen)}`);
}
class TlsMonitorType extends MonitorType { class TlsMonitorType extends MonitorType {
name = "port-tls"; name = "port-tls";
@ -79,17 +99,17 @@ class TlsMonitorType extends MonitorType {
*/ */
processResponse(response, monitor, heartbeat) { processResponse(response, monitor, heartbeat) {
if (response instanceof Error) { if (response instanceof Error) {
log.info(this.name, "check failed: " + response.message); log.info(this.name, "ERROR: " + response.message);
throw response; throw response;
} }
const [ success, message ] = this.checkResponseSuccess(response, monitor); const [ success, message ] = this.checkResponseSuccess(response, monitor);
if (success) { if (success) {
log.info(this.name, "check successful: " + message); log.info(this.name, "SUCCESS: " + message);
heartbeat.msg = message; heartbeat.msg = message;
heartbeat.status = UP; heartbeat.status = UP;
} else { } else {
log.info(this.name, "check failed: " + message); log.info(this.name, "FAILURE: " + message);
throw new Error(message); throw new Error(message);
} }
} }
@ -105,7 +125,7 @@ class TlsMonitorType extends MonitorType {
// Monitor keyword present => Check if keyword is present/absent in response, depending // Monitor keyword present => Check if keyword is present/absent in response, depending
// on whether the 'Invert Keyword' option is enabled. // on whether the 'Invert Keyword' option is enabled.
const keywordContained = response.includes(monitor.keyword); const keywordContained = response.includes(monitor.keyword);
const message = keywordContained ? "Data contains keyword" : "Data does not contain keyword"; const message = `Keyword ${quote(monitor.keyword)} ${keywordContained ? "" : "not "}contained in response ${ellipsize(response)}`;
return [ keywordContained === !monitor.invertKeyword, message ]; return [ keywordContained === !monitor.invertKeyword, message ];
} else { } else {
// No monitor keyword => Any response is considered a success. // No monitor keyword => Any response is considered a success.
@ -122,11 +142,11 @@ class TlsMonitorType extends MonitorType {
*/ */
async getResponseFromTlsPort(aborter, tlsSocket, request) { async getResponseFromTlsPort(aborter, tlsSocket, request) {
if (request) { if (request) {
log.debug(this.name, `sending request: ${JSON.stringify(request)}`); log.debug(this.name, `sending request: ${quote(request)}`);
tlsSocket.write(request); tlsSocket.write(request);
} }
return await this.readData(aborter, tlsSocket); return await this.readData(aborter, tlsSocket, "request response");
} }
/** /**
@ -166,9 +186,10 @@ class TlsMonitorType extends MonitorType {
* Reads available data from the given socket. * Reads available data from the given socket.
* @param {AbortController} aborter Abort controller used to abort the read * @param {AbortController} aborter Abort controller used to abort the read
* @param {net.Socket | tls.TLSSocket} socket Socket instance to use * @param {net.Socket | tls.TLSSocket} socket Socket instance to use
* @param {string} what Human-readable name of the data we're waiting for
* @returns {Promise<string>} Data read from the socket or rejected promise on error * @returns {Promise<string>} Data read from the socket or rejected promise on error
*/ */
readData(aborter, socket) { readData(aborter, socket, what) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const cleanup = function () { const cleanup = function () {
// Pause reading of data (i.e. emission of 'data' events), so that we don't lose // Pause reading of data (i.e. emission of 'data' events), so that we don't lose
@ -181,20 +202,20 @@ class TlsMonitorType extends MonitorType {
}; };
const onAbort = (_) => { const onAbort = (_) => {
log.debug(this.name, "read aborted"); log.debug(this.name, `reading of ${what} aborted`);
cleanup(); cleanup();
reject(new Error("Timeout")); reject(new Error(`Timeout while reading ${what}`));
}; };
const onError = (error) => { const onError = (error) => {
log.debug(this.name, `unable to read data: ${error}`); log.debug(this.name, `unable to read ${what} data: ${error.message}`);
cleanup(); cleanup();
reject(new Error(`Failed to read data: ${error}`)); reject(new Error(`Failed to read ${what}: ${error.message}`));
}; };
const onData = (data) => { const onData = (data) => {
const dataString = data.toString().trim(); const dataString = data.toString().trim();
log.debug(this.name, `read data: '${dataString}'`); log.debug(this.name, `read ${what} data: ${quote(dataString)}`);
cleanup(); cleanup();
resolve(dataString); resolve(dataString);
}; };
@ -212,15 +233,16 @@ class TlsMonitorType extends MonitorType {
* Reads available data from the given socket if it starts with a given prefix. * Reads available data from the given socket if it starts with a given prefix.
* @param {AbortController} aborter Abort controller used to abort the read * @param {AbortController} aborter Abort controller used to abort the read
* @param {net.Socket | tls.TLSSocket} socket Socket instance to use * @param {net.Socket | tls.TLSSocket} socket Socket instance to use
* @param {string} what Human-readable name of the data we're waiting for
* @param {string} expected Prefix the response is expected to start with * @param {string} expected Prefix the response is expected to start with
* @returns {Promise<string>} Data read from the socket or rejected promise if the response does * @returns {Promise<string>} Data read from the socket or rejected promise if the response does
* not start with the prefix * not start with the prefix
*/ */
async expectDataStartsWith(aborter, socket, expected) { async expectDataStartsWith(aborter, socket, what, expected) {
const data = await this.readData(aborter, socket); const data = await this.readData(aborter, socket, what);
log.debug(this.name, `response data: '${data}', expected: '${expected}'…`); log.debug(this.name, `${what} data: ${quote(data)}, expected prefix: ${quote(expected)}`);
if (!data.startsWith(expected)) { if (!data.startsWith(expected)) {
throw new Error(`Unexpected response. Data: '${data}', Expected: '${expected}'…`); throw new Error(`Unexpected ${what}: ${ellipsize(data)} does not start with ${quote(expected)}`);
} }
} }
@ -232,13 +254,13 @@ class TlsMonitorType extends MonitorType {
* @returns {Promise<void>} Rejected promise if the STARTTLS process failed * @returns {Promise<void>} Rejected promise if the STARTTLS process failed
*/ */
async startTls(aborter, socket, tlsOptions) { async startTls(aborter, socket, tlsOptions) {
log.debug(this.name, `waiting for prompt ${JSON.stringify(tlsOptions.prompt)}`); log.debug(this.name, `starttls: waiting for prompt ${quote(tlsOptions.prompt)}`);
await this.expectDataStartsWith(aborter, socket, tlsOptions.prompt); await this.expectDataStartsWith(aborter, socket, "STARTTLS prompt", tlsOptions.prompt);
log.debug(this.name, `got prompt. sending STARTTLS command ${JSON.stringify(tlsOptions.command)}`); log.debug(this.name, `starttls: got prompt. sending command ${quote(tlsOptions.command)}`);
socket.write(tlsOptions.command); socket.write(tlsOptions.command);
log.debug(this.name, `sent command. waiting for response ${JSON.stringify(tlsOptions.response)}`); log.debug(this.name, `starttls: sent command. waiting for response ${quote(tlsOptions.response)}`);
await this.expectDataStartsWith(aborter, socket, tlsOptions.response); await this.expectDataStartsWith(aborter, socket, "STARTTLS response", tlsOptions.response);
log.debug(this.name, "got response"); log.debug(this.name, "starttls: got response");
} }
/** /**