@ -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,128 +134,145 @@ 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 ) ;
} ;
// Handle TLS certificate checking for secure/starttls connections
if ( [ "secure" , "starttls" ] . includes ( monitor . smtpSecurity ) && monitor . isEnabledExpiryNotification ( ) ) {
const reuseSocket = monitor . smtpSecurity === "starttls" ? await this . performStartTls ( monitor ) : { } ;
socket _ = reuseSocket . socket ;
await this . checkTlsCertificate ( monitor , reuseSocket ) ;
}
socket _ . on ( "connect" , ( ) => {
log . debug ( this . name , ` [ ${ monitor . name } ] Pre-TLS connection: ${ JSON . stringify ( socket _ ) } ` ) ;
} ) ;
if ( socket _ && ! socket _ . destroyed ) {
socket _ . end ( ) ;
}
}
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 ) ;
/ * *
* 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 _ ) } ` ) ;
} ) ;
const reuseSocket = monitor . smtpSecurity === "starttls" ? await preTLS ( ) : { } ;
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 ) ;
} ) ;
}
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 => {
/ * *
* 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 . setTimeout ( 1000 * TIMEOUT , ( ) => {
reject ( new Error ( "Connection timed out" ) ) ;
} ) ;
socket . on ( "error" , error => {
reject ( error ) ;
} ) ;
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 ( ) ;
}
}
}
socket . setTimeout ( 1000 * TIMEOUT , ( ) => {
reject ( new Error ( "Connection timed out" ) ) ;
} ) ;
} ) ;
if ( socket _ && ! socket _ . destroyed ) {
socket _ . end ( ) ;
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 ( ) ;
}
}
}
@ -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 c onnection timed out") ) ;
} , timeout ) ;
socket . on ( "secureConnect" , ( ) => {
@ -331,28 +389,9 @@ class TCPMonitorType extends MonitorType {
socket . on ( "timeout" , ( ) => {
clearTimeout ( timeoutId ) ;
socket . destroy ( ) ;
reject ( new Error ( " C onnection timed out") ) ;
reject ( new Error ( " TLS c onnection 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 } ` ) ;
}
}
}