2021-06-25 13:55:49 +00:00
const dayjs = require ( "dayjs" ) ;
2021-06-27 08:10:55 +00:00
const axios = require ( "axios" ) ;
2021-07-30 11:18:26 +00:00
const { Prometheus } = require ( "../prometheus" ) ;
2023-10-02 21:39:17 +00:00
const { log , UP , DOWN , PENDING , MAINTENANCE , flipStatus , MAX _INTERVAL _SECOND , MIN _INTERVAL _SECOND ,
2023-11-23 18:23:38 +00:00
SQL _DATETIME _FORMAT
2023-04-09 08:01:27 +00:00
} = require ( "../../src/util" ) ;
2023-12-02 17:36:19 +00:00
const { tcping , ping , checkCertificate , checkStatusCode , getTotalClientInRoom , setting , mssqlQuery , postgresQuery , mysqlQuery , setSetting , httpNtlm , radius , grpcQuery ,
2024-05-19 19:59:57 +00:00
redisPingAsync , kafkaProducerAsync , getOidcTokenClientCredentials , rootCertificatesFingerprints , axiosAbortSignal
2023-01-05 14:58:24 +00:00
} = require ( "../util-server" ) ;
2021-07-30 11:18:26 +00:00
const { R } = require ( "redbean-node" ) ;
const { BeanModel } = require ( "redbean-node/dist/bean-model" ) ;
2021-09-17 06:42:19 +00:00
const { Notification } = require ( "../notification" ) ;
2021-11-04 11:32:16 +00:00
const { Proxy } = require ( "../proxy" ) ;
2021-10-15 16:57:26 +00:00
const { demoMode } = require ( "../config" ) ;
2021-08-12 16:13:46 +00:00
const version = require ( "../../package.json" ) . version ;
2021-10-09 09:04:51 +00:00
const apicache = require ( "../modules/apicache" ) ;
2022-05-06 06:41:34 +00:00
const { UptimeKumaServer } = require ( "../uptime-kuma-server" ) ;
2022-10-04 08:19:56 +00:00
const { DockerHost } = require ( "../docker" ) ;
2023-01-08 08:22:36 +00:00
const Gamedig = require ( "gamedig" ) ;
2023-07-13 15:37:26 +00:00
const jsonata = require ( "jsonata" ) ;
2023-06-27 07:54:33 +00:00
const jwt = require ( "jsonwebtoken" ) ;
2023-11-18 03:33:34 +00:00
const crypto = require ( "crypto" ) ;
2023-08-31 21:19:21 +00:00
const { UptimeCalculator } = require ( "../uptime-calculator" ) ;
2023-11-29 09:25:33 +00:00
const { CookieJar } = require ( "tough-cookie" ) ;
const { HttpsCookieAgent } = require ( "http-cookie-agent/http" ) ;
2023-12-10 18:01:56 +00:00
const https = require ( "https" ) ;
const http = require ( "http" ) ;
2021-06-27 08:10:55 +00:00
2023-10-15 18:20:38 +00:00
const rootCertificates = rootCertificatesFingerprints ( ) ;
2021-06-27 08:10:55 +00:00
/ * *
* status :
* 0 = DOWN
* 1 = UP
2021-07-27 17:53:59 +00:00
* 2 = PENDING
2022-01-23 14:22:00 +00:00
* 3 = MAINTENANCE
2021-06-27 08:10:55 +00:00
* /
2021-06-25 13:55:49 +00:00
class Monitor extends BeanModel {
2021-09-12 18:26:45 +00:00
/ * *
2023-08-11 07:46:41 +00:00
* Return an object that ready to parse to JSON for public Only show
* necessary data to public
* @ param { boolean } showTags Include tags in JSON
* @ param { boolean } certExpiry Include certificate expiry info in
* JSON
2024-03-15 14:02:55 +00:00
* @ returns { Promise < object > } Object ready to parse
2021-09-12 18:26:45 +00:00
* /
2023-07-04 23:37:45 +00:00
async toPublicJSON ( showTags = false , certExpiry = false ) {
2022-03-18 09:56:46 +00:00
let obj = {
2021-09-19 11:04:51 +00:00
id : this . id ,
name : this . name ,
2022-06-11 16:23:12 +00:00
sendUrl : this . sendUrl ,
2023-07-17 06:01:10 +00:00
type : this . type ,
2021-09-19 11:04:51 +00:00
} ;
2022-06-11 16:23:12 +00:00
if ( this . sendUrl ) {
obj . url = this . url ;
}
2022-03-18 09:56:46 +00:00
if ( showTags ) {
obj . tags = await this . getTags ( ) ;
}
2023-07-04 23:37:45 +00:00
2023-09-23 19:18:18 +00:00
if ( certExpiry && ( this . type === "http" || this . type === "keyword" || this . type === "json-query" ) && this . getURLProtocol ( ) === "https:" ) {
2023-07-04 23:37:45 +00:00
const { certExpiryDaysRemaining , validCert } = await this . getCertExpiry ( this . id ) ;
obj . certExpiryDaysRemaining = certExpiryDaysRemaining ;
obj . validCert = validCert ;
}
2022-03-18 09:56:46 +00:00
return obj ;
2021-09-12 18:26:45 +00:00
}
/ * *
2022-03-18 09:56:46 +00:00
* Return an object that ready to parse to JSON
2023-08-11 07:46:41 +00:00
* @ param { boolean } includeSensitiveData Include sensitive data in
* JSON
2024-03-15 14:02:55 +00:00
* @ returns { Promise < object > } Object ready to parse
2021-09-12 18:26:45 +00:00
* /
2022-04-17 11:30:58 +00:00
async toJSON ( includeSensitiveData = true ) {
2021-07-09 09:55:48 +00:00
let notificationIDList = { } ;
let list = await R . find ( "monitor_notification" , " monitor_id = ? " , [
2021-07-30 11:18:26 +00:00
this . id ,
2021-09-17 06:42:19 +00:00
] ) ;
2021-07-09 09:55:48 +00:00
for ( let bean of list ) {
notificationIDList [ bean . notification _id ] = true ;
}
2022-03-18 09:56:46 +00:00
const tags = await this . getTags ( ) ;
2021-08-26 10:55:19 +00:00
2023-06-27 07:54:33 +00:00
let screenshot = null ;
if ( this . type === "real-browser" ) {
screenshot = "/screenshots/" + jwt . sign ( this . id , UptimeKumaServer . getInstance ( ) . jwtSecret ) + ".png" ;
}
2024-03-03 09:59:38 +00:00
const path = await this . getPath ( ) ;
const pathName = path . join ( " / " ) ;
2022-04-17 11:30:58 +00:00
let data = {
2021-06-25 13:55:49 +00:00
id : this . id ,
name : this . name ,
2021-11-11 23:06:32 +00:00
description : this . description ,
2024-03-03 09:59:38 +00:00
path ,
pathName ,
2023-01-28 01:58:03 +00:00
parent : this . parent ,
childrenIDs : await Monitor . getAllChildrenIDs ( this . id ) ,
2021-06-25 13:55:49 +00:00
url : this . url ,
2021-10-02 14:48:27 +00:00
method : this . method ,
2021-07-01 06:03:06 +00:00
hostname : this . hostname ,
port : this . port ,
2021-07-19 16:23:06 +00:00
maxretries : this . maxretries ,
2021-07-01 05:11:16 +00:00
weight : this . weight ,
2023-01-28 12:39:17 +00:00
active : await this . isActive ( ) ,
forceInactive : ! await Monitor . isParentActive ( this . id ) ,
2021-06-25 13:55:49 +00:00
type : this . type ,
2023-08-06 16:14:56 +00:00
timeout : this . timeout ,
2021-06-25 13:55:49 +00:00
interval : this . interval ,
2021-09-11 16:54:55 +00:00
retryInterval : this . retryInterval ,
2022-01-23 14:22:57 +00:00
resendInterval : this . resendInterval ,
2021-07-01 09:19:28 +00:00
keyword : this . keyword ,
2023-04-06 00:10:21 +00:00
invertKeyword : this . isInvertKeyword ( ) ,
2022-04-05 13:27:50 +00:00
expiryNotification : this . isEnabledExpiryNotification ( ) ,
2021-07-30 14:11:14 +00:00
ignoreTls : this . getIgnoreTls ( ) ,
2021-07-30 16:01:04 +00:00
upsideDown : this . isUpsideDown ( ) ,
2022-07-14 07:32:51 +00:00
packetSize : this . packetSize ,
2021-08-05 11:04:38 +00:00
maxredirects : this . maxredirects ,
accepted _statuscodes : this . getAcceptedStatuscodes ( ) ,
2021-08-22 22:05:48 +00:00
dns _resolve _type : this . dns _resolve _type ,
dns _resolve _server : this . dns _resolve _server ,
2021-08-28 19:20:25 +00:00
dns _last _result : this . dns _last _result ,
2022-01-13 16:17:07 +00:00
docker _container : this . docker _container ,
2022-07-22 15:47:04 +00:00
docker _host : this . docker _host ,
2021-10-30 17:37:15 +00:00
proxyId : this . proxy _id ,
2021-07-30 11:18:26 +00:00
notificationIDList ,
2021-08-26 10:55:19 +00:00
tags : tags ,
2022-01-25 18:07:27 +00:00
maintenance : await Monitor . isUnderMaintenance ( this . id ) ,
2021-12-18 21:35:18 +00:00
mqttTopic : this . mqttTopic ,
2022-05-13 17:58:23 +00:00
mqttSuccessMessage : this . mqttSuccessMessage ,
2023-12-02 17:36:19 +00:00
mqttCheckType : this . mqttCheckType ,
2022-05-13 00:54:02 +00:00
databaseQuery : this . databaseQuery ,
2022-05-13 17:58:23 +00:00
authMethod : this . authMethod ,
2022-08-03 05:00:39 +00:00
grpcUrl : this . grpcUrl ,
grpcProtobuf : this . grpcProtobuf ,
grpcMethod : this . grpcMethod ,
grpcServiceName : this . grpcServiceName ,
2022-08-03 06:39:31 +00:00
grpcEnableTls : this . getGrpcEnableTls ( ) ,
2022-05-12 09:48:38 +00:00
radiusCalledStationId : this . radiusCalledStationId ,
radiusCallingStationId : this . radiusCallingStationId ,
2023-01-08 08:22:36 +00:00
game : this . game ,
2023-08-07 19:14:21 +00:00
gamedigGivenPortOnly : this . getGameDigGivenPortOnly ( ) ,
2023-06-27 07:54:33 +00:00
httpBodyEncoding : this . httpBodyEncoding ,
2023-07-13 15:37:26 +00:00
jsonPath : this . jsonPath ,
expectedValue : this . expectedValue ,
2023-07-17 08:15:44 +00:00
kafkaProducerTopic : this . kafkaProducerTopic ,
kafkaProducerBrokers : JSON . parse ( this . kafkaProducerBrokers ) ,
2023-10-28 06:42:55 +00:00
kafkaProducerSsl : this . getKafkaProducerSsl ( ) ,
kafkaProducerAllowAutoTopicCreation : this . getKafkaProducerAllowAutoTopicCreation ( ) ,
2023-07-17 08:15:44 +00:00
kafkaProducerMessage : this . kafkaProducerMessage ,
2023-06-27 07:54:33 +00:00
screenshot ,
2023-12-01 07:29:10 +00:00
remote _browser : this . remote _browser ,
2021-06-25 13:55:49 +00:00
} ;
2022-04-17 11:30:58 +00:00
if ( includeSensitiveData ) {
data = {
... data ,
headers : this . headers ,
body : this . body ,
2022-08-03 05:00:39 +00:00
grpcBody : this . grpcBody ,
grpcMetadata : this . grpcMetadata ,
2022-04-17 11:30:58 +00:00
basic _auth _user : this . basic _auth _user ,
basic _auth _pass : this . basic _auth _pass ,
2023-08-02 07:40:19 +00:00
oauth _client _id : this . oauth _client _id ,
oauth _client _secret : this . oauth _client _secret ,
oauth _token _url : this . oauth _token _url ,
oauth _scopes : this . oauth _scopes ,
oauth _auth _method : this . oauth _auth _method ,
2022-04-17 11:30:58 +00:00
pushToken : this . pushToken ,
2022-12-05 10:18:19 +00:00
databaseConnectionString : this . databaseConnectionString ,
radiusUsername : this . radiusUsername ,
radiusPassword : this . radiusPassword ,
radiusSecret : this . radiusSecret ,
mqttUsername : this . mqttUsername ,
mqttPassword : this . mqttPassword ,
authWorkstation : this . authWorkstation ,
authDomain : this . authDomain ,
2023-01-04 07:37:30 +00:00
tlsCa : this . tlsCa ,
tlsCert : this . tlsCert ,
tlsKey : this . tlsKey ,
2023-07-17 08:15:44 +00:00
kafkaProducerSaslOptions : JSON . parse ( this . kafkaProducerSaslOptions ) ,
2022-04-17 11:30:58 +00:00
} ;
}
2022-12-05 10:18:19 +00:00
data . includeSensitiveData = includeSensitiveData ;
2022-04-17 11:30:58 +00:00
return data ;
2021-06-25 13:55:49 +00:00
}
2023-01-28 12:39:17 +00:00
/ * *
2023-08-11 07:46:41 +00:00
* Checks if the monitor is active based on itself and its parents
* @ returns { Promise < boolean > } Is the monitor active ?
* /
2023-01-28 12:39:17 +00:00
async isActive ( ) {
const parentActive = await Monitor . isParentActive ( this . id ) ;
2023-06-26 05:21:51 +00:00
return ( this . active === 1 ) && parentActive ;
2023-01-28 12:39:17 +00:00
}
2022-04-21 17:30:04 +00:00
/ * *
* Get all tags applied to this monitor
2023-08-11 07:46:41 +00:00
* @ returns { Promise < LooseObject < any > [ ] > } List of tags on the
* monitor
2022-04-21 17:30:04 +00:00
* /
2022-03-18 09:56:46 +00:00
async getTags ( ) {
2023-02-15 19:06:29 +00:00
return await R . getAll ( "SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name" , [ this . id ] ) ;
2022-03-18 09:56:46 +00:00
}
2023-07-04 23:37:45 +00:00
/ * *
* Gets certificate expiry for this monitor
* @ param { number } monitorID ID of monitor to send
2023-08-11 07:46:41 +00:00
* @ returns { Promise < LooseObject < any >> } Certificate expiry info for
* monitor
2023-07-04 23:37:45 +00:00
* /
async getCertExpiry ( monitorID ) {
let tlsInfoBean = await R . findOne ( "monitor_tls_info" , "monitor_id = ?" , [
monitorID ,
] ) ;
let tlsInfo ;
if ( tlsInfoBean ) {
tlsInfo = JSON . parse ( tlsInfoBean ? . info _json ) ;
if ( tlsInfo ? . valid && tlsInfo ? . certInfo ? . daysRemaining ) {
return {
certExpiryDaysRemaining : tlsInfo . certInfo . daysRemaining ,
validCert : true
} ;
}
}
return {
2023-07-14 00:00:23 +00:00
certExpiryDaysRemaining : "" ,
2023-07-04 23:37:45 +00:00
validCert : false
} ;
}
2021-11-02 11:30:44 +00:00
/ * *
* Encode user and password to Base64 encoding
* for HTTP "basic" auth , as per RFC - 7617
2024-01-06 18:06:06 +00:00
* @ param { string | null } user - The username ( nullable if not changed by a user )
* @ param { string | null } pass - The password ( nullable if not changed by a user )
2024-04-26 07:01:03 +00:00
* @ returns { string } Encoded Base64 string
2021-11-02 11:30:44 +00:00
* /
2021-11-23 04:59:48 +00:00
encodeBase64 ( user , pass ) {
2024-01-06 18:06:06 +00:00
return Buffer . from ( ` ${ user || "" } : ${ pass || "" } ` ) . toString ( "base64" ) ;
2021-11-02 12:11:33 +00:00
}
2021-11-02 11:30:44 +00:00
2022-04-21 17:30:04 +00:00
/ * *
* Is the TLS expiry notification enabled ?
2023-08-11 07:46:41 +00:00
* @ returns { boolean } Enabled ?
2022-04-21 17:30:04 +00:00
* /
2022-04-05 13:27:50 +00:00
isEnabledExpiryNotification ( ) {
return Boolean ( this . expiryNotification ) ;
}
2021-07-30 14:11:14 +00:00
/ * *
* Parse to boolean
2023-08-11 07:46:41 +00:00
* @ returns { boolean } Should TLS errors be ignored ?
2021-07-30 14:11:14 +00:00
* /
getIgnoreTls ( ) {
2021-09-17 06:42:19 +00:00
return Boolean ( this . ignoreTls ) ;
2021-07-30 14:11:14 +00:00
}
/ * *
* Parse to boolean
2023-08-11 07:46:41 +00:00
* @ returns { boolean } Is the monitor in upside down mode ?
2021-07-30 14:11:14 +00:00
* /
2021-07-30 16:01:04 +00:00
isUpsideDown ( ) {
2021-07-30 14:11:14 +00:00
return Boolean ( this . upsideDown ) ;
}
2023-04-06 00:10:21 +00:00
/ * *
* Parse to boolean
2023-08-11 07:46:41 +00:00
* @ returns { boolean } Invert keyword match ?
2023-04-06 00:10:21 +00:00
* /
isInvertKeyword ( ) {
return Boolean ( this . invertKeyword ) ;
}
2022-08-03 06:39:31 +00:00
/ * *
* Parse to boolean
2023-08-11 07:46:41 +00:00
* @ returns { boolean } Enable TLS for gRPC ?
2022-08-03 06:39:31 +00:00
* /
getGrpcEnableTls ( ) {
return Boolean ( this . grpcEnableTls ) ;
}
2022-04-16 20:11:45 +00:00
/ * *
2022-04-22 17:42:47 +00:00
* Get accepted status codes
2023-08-11 07:46:41 +00:00
* @ returns { object } Accepted status codes
2022-04-16 20:11:45 +00:00
* /
2021-08-05 11:04:38 +00:00
getAcceptedStatuscodes ( ) {
return JSON . parse ( this . accepted _statuscodes _json ) ;
}
2023-08-11 14:29:45 +00:00
/ * *
2023-09-07 07:42:44 +00:00
* Get if game dig should only use the port which was provided
* @ returns { boolean } gamedig should only use the provided port
2023-08-11 14:29:45 +00:00
* /
2023-08-07 19:14:21 +00:00
getGameDigGivenPortOnly ( ) {
return Boolean ( this . gamedigGivenPortOnly ) ;
}
2023-10-28 06:42:55 +00:00
/ * *
* Parse to boolean
* @ returns { boolean } Kafka Producer Ssl enabled ?
* /
getKafkaProducerSsl ( ) {
return Boolean ( this . kafkaProducerSsl ) ;
}
/ * *
* Parse to boolean
* @ returns { boolean } Kafka Producer Allow Auto Topic Creation Enabled ?
* /
getKafkaProducerAllowAutoTopicCreation ( ) {
return Boolean ( this . kafkaProducerAllowAutoTopicCreation ) ;
}
2022-04-16 20:11:45 +00:00
/ * *
* Start monitor
* @ param { Server } io Socket server instance
2024-01-30 16:54:04 +00:00
* @ returns { Promise < void > }
2022-04-16 20:11:45 +00:00
* /
2023-02-23 16:16:49 +00:00
async start ( io ) {
2021-06-29 08:06:20 +00:00
let previousBeat = null ;
2021-07-19 16:23:06 +00:00
let retries = 0 ;
2021-06-29 08:06:20 +00:00
2023-01-24 09:14:16 +00:00
this . prometheus = new Prometheus ( this ) ;
2021-07-22 15:00:11 +00:00
2021-06-27 08:10:55 +00:00
const beat = async ( ) => {
2021-09-08 11:54:37 +00:00
2022-02-24 07:11:17 +00:00
let beatInterval = this . interval ;
if ( ! beatInterval ) {
beatInterval = 1 ;
}
if ( demoMode ) {
if ( beatInterval < 20 ) {
console . log ( "beat interval too low, reset to 20s" ) ;
beatInterval = 20 ;
}
}
2021-08-10 09:51:30 +00:00
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined ;
2022-05-09 19:10:12 +00:00
if ( ! previousBeat || this . type === "push" ) {
2021-06-29 08:06:20 +00:00
previousBeat = await R . findOne ( "heartbeat" , " monitor_id = ? ORDER BY time DESC" , [
2021-07-30 11:18:26 +00:00
this . id ,
2021-09-17 06:42:19 +00:00
] ) ;
2023-11-24 10:11:36 +00:00
if ( previousBeat ) {
retries = previousBeat . retries ;
}
2021-06-29 08:06:20 +00:00
}
2021-07-24 03:42:14 +00:00
const isFirstBeat = ! previousBeat ;
2021-09-17 06:42:19 +00:00
let bean = R . dispense ( "heartbeat" ) ;
2021-06-27 08:10:55 +00:00
bean . monitor _id = this . id ;
2022-03-28 21:28:50 +00:00
bean . time = R . isoDateTimeMillis ( dayjs . utc ( ) ) ;
2021-07-24 03:42:14 +00:00
bean . status = DOWN ;
2022-06-15 14:56:26 +00:00
bean . downCount = previousBeat ? . downCount || 0 ;
2021-06-27 08:10:55 +00:00
2021-07-30 16:01:04 +00:00
if ( this . isUpsideDown ( ) ) {
bean . status = flipStatus ( bean . status ) ;
}
2023-11-12 05:50:51 +00:00
// Runtime patch timeout if it is 0
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
2023-11-17 17:17:54 +00:00
if ( ! this . timeout || this . timeout <= 0 ) {
2023-11-12 05:50:51 +00:00
this . timeout = this . interval * 1000 * 0.8 ;
}
2021-06-27 08:10:55 +00:00
try {
2022-01-25 18:07:27 +00:00
if ( await Monitor . isUnderMaintenance ( this . id ) ) {
2022-01-23 14:22:00 +00:00
bean . msg = "Monitor under maintenance" ;
bean . status = MAINTENANCE ;
2023-01-28 01:58:03 +00:00
} else if ( this . type === "group" ) {
const children = await Monitor . getChildren ( this . id ) ;
2023-01-28 12:39:17 +00:00
if ( children . length > 0 ) {
bean . status = UP ;
2023-02-20 12:48:16 +00:00
bean . msg = "All children up and running" ;
2023-01-28 12:39:17 +00:00
for ( const child of children ) {
if ( ! child . active ) {
// Ignore inactive childs
continue ;
}
const lastBeat = await Monitor . getPreviousHeartbeat ( child . id ) ;
// Only change state if the monitor is in worse conditions then the ones before
2023-08-04 06:48:21 +00:00
// lastBeat.status could be null
if ( ! lastBeat ) {
bean . status = PENDING ;
} else if ( bean . status === UP && ( lastBeat . status === PENDING || lastBeat . status === DOWN ) ) {
2023-01-28 12:39:17 +00:00
bean . status = lastBeat . status ;
} else if ( bean . status === PENDING && lastBeat . status === DOWN ) {
bean . status = lastBeat . status ;
}
2023-01-28 01:58:03 +00:00
}
2023-01-28 12:39:17 +00:00
if ( bean . status !== UP ) {
bean . msg = "Child inaccessible" ;
}
} else {
// Set status pending if group is empty
bean . status = PENDING ;
2023-02-01 19:19:47 +00:00
bean . msg = "Group empty" ;
2023-01-28 01:58:03 +00:00
}
2023-01-28 12:39:17 +00:00
2023-07-13 15:37:26 +00:00
} else if ( this . type === "http" || this . type === "keyword" || this . type === "json-query" ) {
2021-08-23 10:52:24 +00:00
// Do not do any queries/high loading things before the "bean.ping"
2021-06-27 08:10:55 +00:00
let startTime = dayjs ( ) . valueOf ( ) ;
2021-07-30 14:11:14 +00:00
2021-11-02 11:30:44 +00:00
// HTTP basic auth
2021-11-04 09:12:06 +00:00
let basicAuthHeader = { } ;
2022-05-13 17:58:23 +00:00
if ( this . auth _method === "basic" ) {
2021-11-04 09:12:06 +00:00
basicAuthHeader = {
2021-11-23 04:59:48 +00:00
"Authorization" : "Basic " + this . encodeBase64 ( this . basic _auth _user , this . basic _auth _pass ) ,
2021-11-02 12:11:33 +00:00
} ;
2021-11-02 11:30:44 +00:00
}
2023-08-02 07:40:19 +00:00
// OIDC: Basic client credential flow.
// Additional grants might be implemented in the future
let oauth2AuthHeader = { } ;
if ( this . auth _method === "oauth2-cc" ) {
try {
if ( this . oauthAccessToken === undefined || new Date ( this . oauthAccessToken . expires _at * 1000 ) <= new Date ( ) ) {
2023-12-17 09:21:07 +00:00
this . oauthAccessToken = await this . makeOidcTokenClientCredentialsRequest ( ) ;
2023-08-02 07:40:19 +00:00
}
oauth2AuthHeader = {
"Authorization" : this . oauthAccessToken . token _type + " " + this . oauthAccessToken . access _token ,
} ;
} catch ( e ) {
throw new Error ( "The oauth config is invalid. " + e . message ) ;
}
}
2021-10-30 17:37:15 +00:00
const httpsAgentOptions = {
maxCachedSessions : 0 , // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized : ! this . getIgnoreTls ( ) ,
2023-11-17 07:21:08 +00:00
secureOptions : crypto . constants . SSL _OP _LEGACY _SERVER _CONNECT ,
2021-10-30 17:37:15 +00:00
} ;
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Prepare Options for axios ` ) ;
2021-11-07 16:15:36 +00:00
2022-09-26 21:09:10 +00:00
let contentType = null ;
2022-09-26 19:52:43 +00:00
let bodyValue = null ;
2023-01-14 13:33:21 +00:00
2023-02-25 09:28:32 +00:00
if ( this . body && ( typeof this . body === "string" && this . body . trim ( ) . length > 0 ) ) {
if ( ! this . httpBodyEncoding || this . httpBodyEncoding === "json" ) {
try {
bodyValue = JSON . parse ( this . body ) ;
contentType = "application/json" ;
} catch ( e ) {
throw new Error ( "Your JSON body is invalid. " + e . message ) ;
}
2023-09-09 10:05:25 +00:00
} else if ( this . httpBodyEncoding === "form" ) {
bodyValue = this . body ;
contentType = "application/x-www-form-urlencoded" ;
2023-02-25 09:28:32 +00:00
} else if ( this . httpBodyEncoding === "xml" ) {
bodyValue = this . body ;
contentType = "text/xml; charset=utf-8" ;
}
2022-08-12 00:57:03 +00:00
}
2022-12-04 14:55:05 +00:00
// Axios Options
2021-10-02 14:48:27 +00:00
const options = {
url : this . url ,
method : ( this . method || "get" ) . toLowerCase ( ) ,
2023-08-08 10:29:48 +00:00
timeout : this . timeout * 1000 ,
2021-07-30 11:18:26 +00:00
headers : {
2021-12-16 07:09:10 +00:00
"Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" ,
2023-01-20 09:29:56 +00:00
... ( contentType ? { "Content-Type" : contentType } : { } ) ,
2021-11-04 09:12:06 +00:00
... ( basicAuthHeader ) ,
2023-08-02 07:40:19 +00:00
... ( oauth2AuthHeader ) ,
2023-01-20 09:29:56 +00:00
... ( this . headers ? JSON . parse ( this . headers ) : { } )
2021-07-30 11:18:26 +00:00
} ,
2021-08-05 11:04:38 +00:00
maxRedirects : this . maxredirects ,
validateStatus : ( status ) => {
return checkStatusCode ( status , this . getAcceptedStatuscodes ( ) ) ;
} ,
2023-11-22 08:03:03 +00:00
signal : axiosAbortSignal ( ( this . timeout + 10 ) * 1000 ) ,
2021-10-02 14:48:27 +00:00
} ;
2021-11-07 13:00:47 +00:00
2023-02-25 09:28:32 +00:00
if ( bodyValue ) {
options . data = bodyValue ;
}
2021-10-30 17:37:15 +00:00
if ( this . proxy _id ) {
const proxy = await R . load ( "proxy" , this . proxy _id ) ;
if ( proxy && proxy . active ) {
2021-11-04 11:32:16 +00:00
const { httpAgent , httpsAgent } = Proxy . createAgents ( proxy , {
httpsAgentOptions : httpsAgentOptions ,
} ) ;
2021-10-30 17:37:15 +00:00
options . proxy = false ;
2021-11-04 11:32:16 +00:00
options . httpAgent = httpAgent ;
options . httpsAgent = httpsAgent ;
2021-10-30 17:37:15 +00:00
}
}
if ( ! options . httpsAgent ) {
2023-11-29 09:25:33 +00:00
let jar = new CookieJar ( ) ;
let httpsCookieAgentOptions = {
... httpsAgentOptions ,
cookies : { jar }
} ;
options . httpsAgent = new HttpsCookieAgent ( httpsCookieAgentOptions ) ;
2021-10-30 17:37:15 +00:00
}
2023-01-04 07:37:30 +00:00
if ( this . auth _method === "mtls" ) {
if ( this . tlsCert !== null && this . tlsCert !== "" ) {
options . httpsAgent . options . cert = Buffer . from ( this . tlsCert ) ;
}
if ( this . tlsCa !== null && this . tlsCa !== "" ) {
options . httpsAgent . options . ca = Buffer . from ( this . tlsCa ) ;
}
if ( this . tlsKey !== null && this . tlsKey !== "" ) {
options . httpsAgent . options . key = Buffer . from ( this . tlsKey ) ;
}
}
2024-04-24 06:37:17 +00:00
let tlsInfo = { } ;
// Store tlsInfo when secureConnect event is emitted
// The keylog event listener is a workaround to access the tlsSocket
options . httpsAgent . once ( "keylog" , async ( line , tlsSocket ) => {
tlsSocket . once ( "secureConnect" , async ( ) => {
tlsInfo = checkCertificate ( tlsSocket ) ;
tlsInfo . valid = tlsSocket . authorized || false ;
await this . handleTlsInfo ( tlsInfo ) ;
} ) ;
2024-04-06 10:43:08 +00:00
} ) ;
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Axios Options: ${ JSON . stringify ( options ) } ` ) ;
log . debug ( "monitor" , ` [ ${ this . name } ] Axios Request ` ) ;
2021-10-30 17:37:15 +00:00
2022-12-26 13:00:46 +00:00
// Make Request
let res = await this . makeAxiosRequest ( options ) ;
2022-05-13 17:58:23 +00:00
2021-09-17 06:42:19 +00:00
bean . msg = ` ${ res . status } - ${ res . statusText } ` ;
2021-06-27 08:10:55 +00:00
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2021-07-01 09:19:28 +00:00
2024-04-24 06:37:17 +00:00
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
// e.g. if the connection is made through a proxy
if ( this . getUrl ( ) ? . protocol === "https:" && tlsInfo . valid === undefined ) {
const tlsSocket = res . request . res . socket ;
2021-10-27 07:33:15 +00:00
2024-04-24 06:37:17 +00:00
if ( tlsSocket ) {
tlsInfo = checkCertificate ( tlsSocket ) ;
tlsInfo . valid = tlsSocket . authorized || false ;
2021-10-27 07:33:15 +00:00
2024-04-24 06:37:17 +00:00
await this . handleTlsInfo ( tlsInfo ) ;
2021-07-23 04:58:05 +00:00
}
2021-07-21 04:09:09 +00:00
}
2021-07-01 09:19:28 +00:00
2022-04-17 07:43:03 +00:00
if ( process . env . UPTIME _KUMA _LOG _RESPONSE _BODY _MONITOR _ID === this . id ) {
2022-04-13 15:33:37 +00:00
log . info ( "monitor" , res . data ) ;
2021-10-15 10:36:40 +00:00
}
2021-07-23 04:58:05 +00:00
2021-07-01 09:19:28 +00:00
if ( this . type === "http" ) {
2021-07-24 03:42:14 +00:00
bean . status = UP ;
2023-07-13 15:37:26 +00:00
} else if ( this . type === "keyword" ) {
2021-07-01 09:19:28 +00:00
2021-07-12 02:52:41 +00:00
let data = res . data ;
// Convert to string for object/array
if ( typeof data !== "string" ) {
2021-09-17 06:42:19 +00:00
data = JSON . stringify ( data ) ;
2021-07-12 02:52:41 +00:00
}
2023-04-06 20:30:38 +00:00
let keywordFound = data . includes ( this . keyword ) ;
if ( keywordFound === ! this . isInvertKeyword ( ) ) {
bean . msg += ", keyword " + ( keywordFound ? "is" : "not" ) + " found" ;
2021-07-24 03:42:14 +00:00
bean . status = UP ;
2021-07-01 09:19:28 +00:00
} else {
2023-04-08 16:23:29 +00:00
data = data . replace ( /<[^>]*>?|[\n\r]|\s+/gm , " " ) . trim ( ) ;
2022-04-01 07:26:50 +00:00
if ( data . length > 50 ) {
2022-01-24 05:31:49 +00:00
data = data . substring ( 0 , 47 ) + "..." ;
}
2023-04-06 00:10:21 +00:00
throw new Error ( bean . msg + ", but keyword is " +
2023-04-06 20:30:38 +00:00
( keywordFound ? "present" : "not" ) + " in [" + data + "]" ) ;
2021-07-01 09:19:28 +00:00
}
2023-07-13 15:37:26 +00:00
} else if ( this . type === "json-query" ) {
let data = res . data ;
// convert data to object
2024-01-27 19:18:24 +00:00
if ( typeof data === "string" && res . headers [ "content-type" ] !== "application/json" ) {
try {
data = JSON . parse ( data ) ;
} catch ( _ ) {
// Failed to parse as JSON, just process it as a string
}
2021-07-01 09:19:28 +00:00
}
2023-07-13 15:37:26 +00:00
let expression = jsonata ( this . jsonPath ) ;
let result = await expression . evaluate ( data ) ;
if ( result . toString ( ) === this . expectedValue ) {
bean . msg += ", expected value is found" ;
bean . status = UP ;
} else {
throw new Error ( bean . msg + ", but value is not equal to expected value, value was: [" + result + "]" ) ;
}
2021-07-01 09:19:28 +00:00
}
2021-07-01 06:03:06 +00:00
} else if ( this . type === "port" ) {
bean . ping = await tcping ( this . hostname , this . port ) ;
2021-09-17 06:42:19 +00:00
bean . msg = "" ;
2021-07-24 03:42:14 +00:00
bean . status = UP ;
2021-07-01 09:00:23 +00:00
} else if ( this . type === "ping" ) {
2022-07-14 07:32:51 +00:00
bean . ping = await ping ( this . hostname , this . packetSize ) ;
2021-09-17 06:42:19 +00:00
bean . msg = "" ;
2021-07-24 03:42:14 +00:00
bean . status = UP ;
2021-09-30 16:09:43 +00:00
} else if ( this . type === "push" ) { // Type: Push
2022-05-27 04:45:56 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Checking monitor at ${ dayjs ( ) . format ( "YYYY-MM-DD HH:mm:ss.SSS" ) } ` ) ;
2022-03-28 21:28:50 +00:00
const bufferTime = 1000 ; // 1s buffer to accommodate clock differences
2022-05-28 15:19:58 +00:00
2022-03-28 21:28:50 +00:00
if ( previousBeat ) {
const msSinceLastBeat = dayjs . utc ( ) . valueOf ( ) - dayjs . utc ( previousBeat . time ) . valueOf ( ) ;
2022-05-28 15:22:44 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] msSinceLastBeat = ${ msSinceLastBeat } ` ) ;
2022-05-29 02:57:45 +00:00
// If the previous beat was down or pending we use the regular
// beatInterval/retryInterval in the setTimeout further below
2022-06-14 05:05:58 +00:00
if ( previousBeat . status !== ( this . isUpsideDown ( ) ? DOWN : UP ) || msSinceLastBeat > beatInterval * 1000 + bufferTime ) {
2023-11-24 10:11:36 +00:00
bean . duration = Math . round ( msSinceLastBeat / 1000 ) ;
2022-03-28 21:28:50 +00:00
throw new Error ( "No heartbeat in the time window" ) ;
} else {
let timeout = beatInterval * 1000 - msSinceLastBeat ;
if ( timeout < 0 ) {
timeout = bufferTime ;
} else {
timeout += bufferTime ;
}
// No need to insert successful heartbeat for push type, so end here
retries = 0 ;
2022-05-27 04:45:56 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] timeout = ${ timeout } ` ) ;
2023-05-19 10:52:00 +00:00
this . heartbeatInterval = setTimeout ( safeBeat , timeout ) ;
2022-03-28 21:28:50 +00:00
return ;
}
2022-05-29 02:57:45 +00:00
} else {
2023-11-24 10:11:36 +00:00
bean . duration = beatInterval ;
2022-05-29 02:57:45 +00:00
throw new Error ( "No heartbeat in the time window" ) ;
2021-09-30 16:09:43 +00:00
}
2021-09-27 09:17:57 +00:00
} else if ( this . type === "steam" ) {
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/" ;
2021-10-18 09:11:41 +00:00
const steamAPIKey = await setting ( "steamAPIKey" ) ;
2021-09-27 09:17:57 +00:00
const filter = ` addr \\ ${ this . hostname } : ${ this . port } ` ;
2021-10-18 09:11:41 +00:00
if ( ! steamAPIKey ) {
throw new Error ( "Steam API Key not found" ) ;
}
2022-06-23 07:54:33 +00:00
let res = await axios . get ( steamApiUrl , {
2023-08-08 10:29:48 +00:00
timeout : this . timeout * 1000 ,
2021-09-27 09:17:57 +00:00
headers : {
"Accept" : "*/*" ,
} ,
2023-12-10 18:01:56 +00:00
httpsAgent : new https . Agent ( {
2021-09-27 09:17:57 +00:00
maxCachedSessions : 0 , // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
2021-11-04 01:46:43 +00:00
rejectUnauthorized : ! this . getIgnoreTls ( ) ,
2023-11-17 07:21:08 +00:00
secureOptions : crypto . constants . SSL _OP _LEGACY _SERVER _CONNECT ,
2021-09-27 09:17:57 +00:00
} ) ,
2023-12-10 18:01:56 +00:00
httpAgent : new http . Agent ( {
2022-07-18 14:33:35 +00:00
maxCachedSessions : 0 ,
} ) ,
2021-09-27 09:17:57 +00:00
maxRedirects : this . maxredirects ,
validateStatus : ( status ) => {
return checkStatusCode ( status , this . getAcceptedStatuscodes ( ) ) ;
} ,
params : {
filter : filter ,
2021-10-18 09:11:41 +00:00
key : steamAPIKey ,
2021-09-27 09:17:57 +00:00
}
} ) ;
2021-10-18 09:02:05 +00:00
if ( res . data . response && res . data . response . servers && res . data . response . servers . length > 0 ) {
bean . status = UP ;
bean . msg = res . data . response . servers [ 0 ] . name ;
2021-09-27 09:17:57 +00:00
2021-10-18 09:02:05 +00:00
try {
2022-07-14 07:32:51 +00:00
bean . ping = await ping ( this . hostname , this . packetSize ) ;
2021-10-18 09:02:05 +00:00
} catch ( _ ) { }
2021-09-27 09:17:57 +00:00
} else {
2021-10-18 09:02:05 +00:00
throw new Error ( "Server not found on Steam" ) ;
2021-09-27 09:17:57 +00:00
}
2023-01-08 08:22:36 +00:00
} else if ( this . type === "gamedig" ) {
try {
const state = await Gamedig . query ( {
type : this . game ,
host : this . hostname ,
2023-01-24 16:19:54 +00:00
port : this . port ,
2023-08-07 19:14:21 +00:00
givenPortOnly : this . getGameDigGivenPortOnly ( ) ,
2023-01-08 08:22:36 +00:00
} ) ;
bean . msg = state . name ;
bean . status = UP ;
bean . ping = state . ping ;
} catch ( e ) {
2023-01-24 15:03:01 +00:00
throw new Error ( e . message ) ;
2023-01-08 08:22:36 +00:00
}
2022-01-13 16:17:07 +00:00
} else if ( this . type === "docker" ) {
2023-01-24 15:40:24 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Prepare Options for Axios ` ) ;
2022-01-13 16:17:07 +00:00
const options = {
url : ` /containers/ ${ this . docker _container } /json ` ,
2023-01-18 01:53:04 +00:00
timeout : this . interval * 1000 * 0.8 ,
2022-01-13 16:17:07 +00:00
headers : {
"Accept" : "*/*" ,
} ,
2023-12-10 18:01:56 +00:00
httpsAgent : new https . Agent ( {
2022-01-13 16:17:07 +00:00
maxCachedSessions : 0 , // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
2023-01-18 01:53:04 +00:00
rejectUnauthorized : ! this . getIgnoreTls ( ) ,
2023-11-17 07:21:08 +00:00
secureOptions : crypto . constants . SSL _OP _LEGACY _SERVER _CONNECT ,
2023-01-18 01:53:04 +00:00
} ) ,
2023-12-10 18:01:56 +00:00
httpAgent : new http . Agent ( {
2023-01-18 01:53:04 +00:00
maxCachedSessions : 0 ,
2022-01-13 16:17:07 +00:00
} ) ,
} ;
2023-11-12 12:32:40 +00:00
const dockerHost = await R . load ( "docker_host" , this . docker _host ) ;
if ( ! dockerHost ) {
throw new Error ( "Failed to load docker host config" ) ;
}
2022-07-22 15:57:40 +00:00
if ( dockerHost . _dockerType === "socket" ) {
options . socketPath = dockerHost . _dockerDaemon ;
} else if ( dockerHost . _dockerType === "tcp" ) {
2022-10-04 08:19:56 +00:00
options . baseURL = DockerHost . patchDockerURL ( dockerHost . _dockerDaemon ) ;
2023-12-10 18:01:56 +00:00
options . httpsAgent = new https . Agent (
2023-08-04 14:49:33 +00:00
DockerHost . getHttpsAgentOptions ( dockerHost . _dockerType , options . baseURL )
) ;
2022-01-22 01:57:37 +00:00
}
2023-01-24 15:40:24 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Axios Request ` ) ;
2022-01-13 16:17:07 +00:00
let res = await axios . request ( options ) ;
2023-05-17 16:52:28 +00:00
2022-01-13 16:17:07 +00:00
if ( res . data . State . Running ) {
2023-05-18 07:55:33 +00:00
if ( res . data . State . Health && res . data . State . Health . Status !== "healthy" ) {
bean . status = PENDING ;
bean . msg = res . data . State . Health . Status ;
} else {
bean . status = UP ;
bean . msg = res . data . State . Health ? res . data . State . Health . Status : res . data . State . Status ;
}
2023-01-03 14:07:14 +00:00
} else {
throw Error ( "Container State is " + res . data . State . Status ) ;
2022-01-13 16:17:07 +00:00
}
2022-05-12 17:48:03 +00:00
} else if ( this . type === "sqlserver" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
2023-10-15 16:38:56 +00:00
await mssqlQuery ( this . databaseConnectionString , this . databaseQuery || "SELECT 1" ) ;
2022-05-12 17:48:03 +00:00
bean . msg = "" ;
bean . status = UP ;
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2022-08-03 05:00:39 +00:00
} else if ( this . type === "grpc-keyword" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
const options = {
grpcUrl : this . grpcUrl ,
grpcProtobufData : this . grpcProtobuf ,
grpcServiceName : this . grpcServiceName ,
grpcEnableTls : this . grpcEnableTls ,
grpcMethod : this . grpcMethod ,
grpcBody : this . grpcBody ,
} ;
const response = await grpcQuery ( options ) ;
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
log . debug ( "monitor:" , ` gRPC response: ${ JSON . stringify ( response ) } ` ) ;
let responseData = response . data ;
if ( responseData . length > 50 ) {
2022-12-28 15:31:33 +00:00
responseData = responseData . toString ( ) . substring ( 0 , 47 ) + "..." ;
2022-08-03 05:00:39 +00:00
}
if ( response . code !== 1 ) {
bean . status = DOWN ;
bean . msg = ` Error in send gRPC ${ response . code } ${ response . errorMessage } ` ;
} else {
2023-04-06 20:30:38 +00:00
let keywordFound = response . data . toString ( ) . includes ( this . keyword ) ;
if ( keywordFound === ! this . isInvertKeyword ( ) ) {
2022-08-03 05:00:39 +00:00
bean . status = UP ;
2023-04-06 20:30:38 +00:00
bean . msg = ` ${ responseData } , keyword [ ${ this . keyword } ] ${ keywordFound ? "is" : "not" } found ` ;
2022-08-03 05:00:39 +00:00
} else {
2023-04-06 20:30:38 +00:00
log . debug ( "monitor:" , ` GRPC response [ ${ response . data } ] + ", but keyword [ ${ this . keyword } ] is ${ keywordFound ? "present" : "not" } in [" + ${ response . data } + "]" ` ) ;
2022-08-03 05:00:39 +00:00
bean . status = DOWN ;
2023-04-06 20:30:38 +00:00
bean . msg = ` , but keyword [ ${ this . keyword } ] is ${ keywordFound ? "present" : "not" } in [" + ${ responseData } + "] ` ;
2022-08-03 05:00:39 +00:00
}
}
2022-06-15 17:12:47 +00:00
} else if ( this . type === "postgres" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
2023-10-15 16:38:56 +00:00
await postgresQuery ( this . databaseConnectionString , this . databaseQuery || "SELECT 1" ) ;
2022-06-15 17:12:47 +00:00
2022-10-02 00:52:53 +00:00
bean . msg = "" ;
bean . status = UP ;
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
} else if ( this . type === "mysql" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
2023-10-15 16:38:56 +00:00
// Use `radius_password` as `password` field, since there are too many unnecessary fields
// TODO: rename `radius_password` to `password` later for general use
let mysqlPassword = this . radiusPassword ;
bean . msg = await mysqlQuery ( this . databaseConnectionString , this . databaseQuery || "SELECT 1" , mysqlPassword ) ;
2022-05-12 17:48:03 +00:00
bean . status = UP ;
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2022-05-12 09:48:38 +00:00
} else if ( this . type === "radius" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
2022-10-12 16:32:05 +00:00
// Handle monitors that were created before the
// update and as such don't have a value for
// this.port.
let port ;
if ( this . port == null ) {
port = 1812 ;
} else {
port = this . port ;
}
2023-07-27 09:42:22 +00:00
const resp = await radius (
this . hostname ,
this . radiusUsername ,
this . radiusPassword ,
this . radiusCalledStationId ,
this . radiusCallingStationId ,
this . radiusSecret ,
port ,
this . interval * 1000 * 0.4 ,
) ;
bean . msg = resp . code ;
bean . status = UP ;
2022-05-12 09:48:38 +00:00
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2023-01-05 14:58:24 +00:00
} else if ( this . type === "redis" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
2024-05-19 21:34:01 +00:00
bean . msg = await redisPingAsync ( this . databaseConnectionString , ! this . ignoreTls ) ;
2023-01-05 14:58:24 +00:00
bean . status = UP ;
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2023-01-27 10:25:57 +00:00
} else if ( this . type in UptimeKumaServer . monitorTypeList ) {
let startTime = dayjs ( ) . valueOf ( ) ;
const monitorType = UptimeKumaServer . monitorTypeList [ this . type ] ;
2023-06-27 07:54:33 +00:00
await monitorType . check ( this , bean , UptimeKumaServer . getInstance ( ) ) ;
2023-01-27 10:25:57 +00:00
if ( ! bean . ping ) {
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
}
2023-07-17 08:15:44 +00:00
} else if ( this . type === "kafka-producer" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
bean . msg = await kafkaProducerAsync (
JSON . parse ( this . kafkaProducerBrokers ) ,
this . kafkaProducerTopic ,
this . kafkaProducerMessage ,
{
allowAutoTopicCreation : this . kafkaProducerAllowAutoTopicCreation ,
ssl : this . kafkaProducerSsl ,
clientId : ` Uptime-Kuma/ ${ version } ` ,
interval : this . interval ,
} ,
JSON . parse ( this . kafkaProducerSaslOptions ) ,
) ;
bean . status = UP ;
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2021-09-30 16:09:43 +00:00
} else {
2023-01-27 10:25:57 +00:00
throw new Error ( "Unknown Monitor Type" ) ;
2021-06-27 08:10:55 +00:00
}
2021-07-30 16:01:04 +00:00
if ( this . isUpsideDown ( ) ) {
bean . status = flipStatus ( bean . status ) ;
if ( bean . status === DOWN ) {
throw new Error ( "Flip UP to DOWN" ) ;
}
}
2021-07-19 16:23:06 +00:00
retries = 0 ;
2021-06-27 08:10:55 +00:00
} catch ( error ) {
2024-06-23 15:46:52 +00:00
2023-11-21 15:56:17 +00:00
if ( error ? . name === "CanceledError" ) {
bean . msg = ` timeout by AbortSignal ( ${ this . timeout } s) ` ;
} else {
bean . msg = error . message ;
}
2021-07-30 16:01:04 +00:00
// If UP come in here, it must be upside down mode
// Just reset the retries
if ( this . isUpsideDown ( ) && bean . status === UP ) {
retries = 0 ;
} else if ( ( this . maxretries > 0 ) && ( retries < this . maxretries ) ) {
2021-07-19 16:23:06 +00:00
retries ++ ;
2021-07-24 03:42:14 +00:00
bean . status = PENDING ;
2023-11-24 10:11:36 +00:00
} else {
// Continue counting retries during DOWN
retries ++ ;
2021-07-19 16:23:06 +00:00
}
2021-06-27 08:10:55 +00:00
}
2023-11-24 10:11:36 +00:00
bean . retries = retries ;
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Check isImportant ` ) ;
2021-10-16 09:28:03 +00:00
let isImportant = Monitor . isImportantBeat ( isFirstBeat , previousBeat ? . status , bean . status ) ;
2021-07-24 03:42:14 +00:00
2021-07-20 09:50:33 +00:00
// Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up
2021-07-24 03:42:14 +00:00
if ( isImportant ) {
2021-06-29 08:06:20 +00:00
bean . important = true ;
2021-11-07 13:00:47 +00:00
2022-01-23 14:22:00 +00:00
if ( Monitor . isImportantForNotification ( isFirstBeat , previousBeat ? . status , bean . status ) ) {
2022-04-30 11:40:34 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] sendNotification ` ) ;
2022-01-23 14:22:00 +00:00
await Monitor . sendNotification ( isFirstBeat , this , bean ) ;
2022-04-30 12:57:08 +00:00
} else {
2022-04-30 11:40:34 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] will not sendNotification because it is (or was) under maintenance ` ) ;
2022-01-23 14:22:00 +00:00
}
2021-10-27 03:39:46 +00:00
2022-06-15 14:56:26 +00:00
// Reset down count
bean . downCount = 0 ;
2022-01-23 14:22:57 +00:00
2021-10-27 03:39:46 +00:00
// Clear Status Page Cache
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] apicache clear ` ) ;
2021-10-27 03:39:46 +00:00
apicache . clear ( ) ;
2024-03-15 14:02:55 +00:00
await UptimeKumaServer . getInstance ( ) . sendMaintenanceListByUserID ( this . user _id ) ;
2022-10-15 12:15:50 +00:00
2021-06-29 08:06:20 +00:00
} else {
bean . important = false ;
2022-01-23 14:22:57 +00:00
if ( bean . status === DOWN && this . resendInterval > 0 ) {
2022-06-15 14:56:26 +00:00
++ bean . downCount ;
if ( bean . downCount >= this . resendInterval ) {
2022-01-23 14:22:57 +00:00
// Send notification again, because we are still DOWN
2022-06-15 14:56:26 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] sendNotification again: Down Count: ${ bean . downCount } | Resend Interval: ${ this . resendInterval } ` ) ;
2022-01-23 14:22:57 +00:00
await Monitor . sendNotification ( isFirstBeat , this , bean ) ;
2022-06-15 14:56:26 +00:00
// Reset down count
bean . downCount = 0 ;
2022-01-23 14:22:57 +00:00
}
}
2021-06-29 08:06:20 +00:00
}
2021-07-24 03:42:14 +00:00
if ( bean . status === UP ) {
2022-06-13 13:15:47 +00:00
log . debug ( "monitor" , ` Monitor # ${ this . id } ' ${ this . name } ': Successful Response: ${ bean . ping } ms | Interval: ${ beatInterval } seconds | Type: ${ this . type } ` ) ;
2021-07-24 03:42:14 +00:00
} else if ( bean . status === PENDING ) {
2021-09-29 09:20:35 +00:00
if ( this . retryInterval > 0 ) {
2021-09-11 16:54:55 +00:00
beatInterval = this . retryInterval ;
}
2022-04-13 15:33:37 +00:00
log . warn ( "monitor" , ` Monitor # ${ this . id } ' ${ this . name } ': Pending: ${ bean . msg } | Max retries: ${ this . maxretries } | Retry: ${ retries } | Retry Interval: ${ beatInterval } seconds | Type: ${ this . type } ` ) ;
2022-01-23 14:22:00 +00:00
} else if ( bean . status === MAINTENANCE ) {
2022-04-30 11:40:34 +00:00
log . warn ( "monitor" , ` Monitor # ${ this . id } ' ${ this . name } ': Under Maintenance | Type: ${ this . type } ` ) ;
2021-07-20 22:41:38 +00:00
} else {
2022-06-15 14:56:26 +00:00
log . warn ( "monitor" , ` Monitor # ${ this . id } ' ${ this . name } ': Failing: ${ bean . msg } | Interval: ${ beatInterval } seconds | Type: ${ this . type } | Down Count: ${ bean . downCount } | Resend Interval: ${ this . resendInterval } ` ) ;
2021-07-20 22:41:38 +00:00
}
2023-08-31 21:19:21 +00:00
// Calculate uptime
let uptimeCalculator = await UptimeCalculator . getUptimeCalculator ( this . id ) ;
let endTimeDayjs = await uptimeCalculator . update ( bean . status , parseFloat ( bean . ping ) ) ;
bean . end _time = R . isoDateTimeMillis ( endTimeDayjs ) ;
// Send to frontend
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Send to socket ` ) ;
2021-06-29 08:06:20 +00:00
io . to ( this . user _id ) . emit ( "heartbeat" , bean . toJSON ( ) ) ;
2021-09-17 06:42:19 +00:00
Monitor . sendStats ( io , this . id , this . user _id ) ;
2021-06-29 08:06:20 +00:00
2023-08-31 21:19:21 +00:00
// Store to database
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] Store ` ) ;
2021-08-23 10:52:24 +00:00
await R . store ( bean ) ;
2021-11-07 13:00:47 +00:00
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] prometheus.update ` ) ;
2023-02-24 09:12:57 +00:00
this . prometheus ? . update ( bean , tlsInfo ) ;
2021-08-23 10:52:24 +00:00
2021-06-29 08:06:20 +00:00
previousBeat = bean ;
2021-08-23 10:52:24 +00:00
2021-09-08 12:00:16 +00:00
if ( ! this . isStop ) {
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , ` [ ${ this . name } ] SetTimeout for next check. ` ) ;
2023-09-21 12:11:04 +00:00
let intervalRemainingMs = Math . max (
1 ,
beatInterval * 1000 - dayjs ( ) . diff ( dayjs . utc ( bean . time ) )
) ;
log . debug ( "monitor" , ` [ ${ this . name } ] Next heartbeat in: ${ intervalRemainingMs } ms ` ) ;
this . heartbeatInterval = setTimeout ( safeBeat , intervalRemainingMs ) ;
2021-11-07 13:00:47 +00:00
} else {
2022-04-13 15:33:37 +00:00
log . info ( "monitor" , ` [ ${ this . name } ] isStop = true, no next check. ` ) ;
2021-09-08 12:00:16 +00:00
}
2021-09-17 06:42:19 +00:00
} ;
2021-06-25 13:55:49 +00:00
2023-08-11 07:46:41 +00:00
/ * *
* Get a heartbeat and handle errors7
* @ returns { void }
* /
2021-10-27 06:08:44 +00:00
const safeBeat = async ( ) => {
try {
await beat ( ) ;
} catch ( e ) {
console . trace ( e ) ;
2022-05-06 06:41:34 +00:00
UptimeKumaServer . errorLog ( e , false ) ;
2022-04-13 15:33:37 +00:00
log . error ( "monitor" , "Please report to https://github.com/louislam/uptime-kuma/issues" ) ;
2021-10-27 06:08:44 +00:00
if ( ! this . isStop ) {
2022-04-13 15:33:37 +00:00
log . info ( "monitor" , "Try to restart the monitor" ) ;
2021-10-27 06:08:44 +00:00
this . heartbeatInterval = setTimeout ( safeBeat , this . interval * 1000 ) ;
}
}
} ;
2021-09-30 16:09:43 +00:00
// Delay Push Type
if ( this . type === "push" ) {
setTimeout ( ( ) => {
2021-10-27 06:08:44 +00:00
safeBeat ( ) ;
2021-09-30 16:09:43 +00:00
} , this . interval * 1000 ) ;
} else {
2021-10-27 06:08:44 +00:00
safeBeat ( ) ;
2021-09-30 16:09:43 +00:00
}
2021-06-25 13:55:49 +00:00
}
2023-01-05 22:19:05 +00:00
/ * *
* Make a request using axios
2023-08-11 07:46:41 +00:00
* @ param { object } options Options for Axios
2023-01-05 22:19:05 +00:00
* @ param { boolean } finalCall Should this be the final call i . e
2023-08-11 07:46:41 +00:00
* don ' t retry on failure
* @ returns { object } Axios response
2023-01-05 22:19:05 +00:00
* /
2022-12-26 13:00:46 +00:00
async makeAxiosRequest ( options , finalCall = false ) {
try {
let res ;
if ( this . auth _method === "ntlm" ) {
options . httpsAgent . keepAlive = true ;
res = await httpNtlm ( options , {
username : this . basic _auth _user ,
password : this . basic _auth _pass ,
domain : this . authDomain ,
workstation : this . authWorkstation ? this . authWorkstation : undefined
} ) ;
} else {
res = await axios . request ( options ) ;
}
return res ;
2023-12-17 09:21:07 +00:00
} catch ( error ) {
/ * *
* Make a single attempt to obtain an new access token in the event that
* the recent api request failed for authentication purposes
* /
if ( this . auth _method === "oauth2-cc" && error . response . status === 401 && ! finalCall ) {
this . oauthAccessToken = await this . makeOidcTokenClientCredentialsRequest ( ) ;
let oauth2AuthHeader = {
"Authorization" : this . oauthAccessToken . token _type + " " + this . oauthAccessToken . access _token ,
} ;
options . headers = { ... ( options . headers ) ,
... ( oauth2AuthHeader )
} ;
return this . makeAxiosRequest ( options , true ) ;
}
2022-12-26 13:00:46 +00:00
// Fix #2253
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
2023-12-17 09:21:07 +00:00
if ( ! finalCall && typeof error . message === "string" && error . message . includes ( "maxContentLength size of -1 exceeded" ) ) {
2022-12-26 13:00:46 +00:00
log . debug ( "monitor" , "makeAxiosRequest with gzip" ) ;
options . headers [ "Accept-Encoding" ] = "gzip, deflate" ;
return this . makeAxiosRequest ( options , true ) ;
} else {
2023-12-17 09:21:07 +00:00
if ( typeof error . message === "string" && error . message . includes ( "maxContentLength size of -1 exceeded" ) ) {
error . message = "response timeout: incomplete response within a interval" ;
2022-12-26 13:00:46 +00:00
}
2023-12-17 09:21:07 +00:00
throw error ;
2022-12-26 13:00:46 +00:00
}
}
}
2023-08-11 07:46:41 +00:00
/ * *
* Stop monitor
2024-01-30 16:53:21 +00:00
* @ returns { Promise < void > }
2023-08-11 07:46:41 +00:00
* /
2023-02-23 16:16:49 +00:00
async stop ( ) {
2021-08-23 10:52:24 +00:00
clearTimeout ( this . heartbeatInterval ) ;
2021-09-08 12:00:16 +00:00
this . isStop = true ;
2022-01-07 04:26:26 +00:00
2023-02-24 09:12:57 +00:00
this . prometheus ? . remove ( ) ;
2021-06-25 13:55:49 +00:00
}
2021-06-30 13:04:58 +00:00
2022-04-21 17:30:04 +00:00
/ * *
2023-01-24 09:14:16 +00:00
* Get prometheus instance
2023-08-11 07:46:41 +00:00
* @ returns { Prometheus | undefined } Current prometheus instance
2022-04-21 17:30:04 +00:00
* /
2023-01-24 09:14:16 +00:00
getPrometheus ( ) {
return this . prometheus ;
2021-06-25 13:55:49 +00:00
}
2021-06-30 13:04:58 +00:00
2021-07-30 03:23:04 +00:00
/ * *
* Helper Method :
* returns URL object for further usage
* returns null if url is invalid
2023-08-11 07:46:41 +00:00
* @ returns { ( null | URL ) } Monitor URL
2021-07-30 03:23:04 +00:00
* /
2021-07-22 08:04:32 +00:00
getUrl ( ) {
try {
return new URL ( this . url ) ;
} catch ( _ ) {
return null ;
}
}
2023-09-16 18:40:08 +00:00
/ * *
* Example : http : or https :
2023-10-02 21:39:17 +00:00
* @ returns { ( null | string ) } URL ' s protocol
2023-09-16 18:40:08 +00:00
* /
getURLProtocol ( ) {
const url = this . getUrl ( ) ;
if ( url ) {
return this . getUrl ( ) . protocol ;
} else {
return null ;
}
}
2021-07-30 03:23:04 +00:00
/ * *
* Store TLS info to database
2023-08-11 07:46:41 +00:00
* @ param { object } checkCertificateResult Certificate to update
* @ returns { Promise < object > } Updated certificate
2021-07-30 03:23:04 +00:00
* /
2021-07-22 08:04:32 +00:00
async updateTlsInfo ( checkCertificateResult ) {
2022-04-13 16:30:32 +00:00
let tlsInfoBean = await R . findOne ( "monitor_tls_info" , "monitor_id = ?" , [
2021-07-30 11:18:26 +00:00
this . id ,
2021-07-22 08:04:32 +00:00
] ) ;
2021-10-27 08:12:18 +00:00
2022-04-13 16:30:32 +00:00
if ( tlsInfoBean == null ) {
tlsInfoBean = R . dispense ( "monitor_tls_info" ) ;
tlsInfoBean . monitor _id = this . id ;
2021-10-27 08:12:18 +00:00
} else {
2021-10-27 08:03:16 +00:00
2021-10-27 08:12:18 +00:00
// Clear sent history if the cert changed.
try {
2022-04-13 16:30:32 +00:00
let oldCertInfo = JSON . parse ( tlsInfoBean . info _json ) ;
2021-10-27 08:03:16 +00:00
2021-10-27 08:12:18 +00:00
let isValidObjects = oldCertInfo && oldCertInfo . certInfo && checkCertificateResult && checkCertificateResult . certInfo ;
if ( isValidObjects ) {
if ( oldCertInfo . certInfo . fingerprint256 !== checkCertificateResult . certInfo . fingerprint256 ) {
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , "Resetting sent_history" ) ;
2021-10-27 08:12:18 +00:00
await R . exec ( "DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?" , [
this . id
] ) ;
} else {
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , "No need to reset sent_history" ) ;
log . debug ( "monitor" , oldCertInfo . certInfo . fingerprint256 ) ;
log . debug ( "monitor" , checkCertificateResult . certInfo . fingerprint256 ) ;
2021-10-27 08:12:18 +00:00
}
} else {
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , "Not valid object" ) ;
2021-10-27 08:12:18 +00:00
}
} catch ( e ) { }
2021-10-27 08:03:16 +00:00
}
2022-04-13 16:30:32 +00:00
tlsInfoBean . info _json = JSON . stringify ( checkCertificateResult ) ;
await R . store ( tlsInfoBean ) ;
2021-08-10 09:51:30 +00:00
return checkCertificateResult ;
2021-07-22 08:04:32 +00:00
}
2022-04-16 20:11:45 +00:00
/ * *
* Send statistics to clients
* @ param { Server } io Socket server instance
* @ param { number } monitorID ID of monitor to send
* @ param { number } userID ID of user to send to
2023-08-11 07:46:41 +00:00
* @ returns { void }
2022-04-16 20:11:45 +00:00
* /
2021-06-30 13:04:58 +00:00
static async sendStats ( io , monitorID , userID ) {
2021-08-30 06:55:33 +00:00
const hasClients = getTotalClientInRoom ( io , userID ) > 0 ;
2023-08-31 21:19:21 +00:00
let uptimeCalculator = await UptimeCalculator . getUptimeCalculator ( monitorID ) ;
2021-08-30 06:55:33 +00:00
if ( hasClients ) {
2023-08-31 21:19:21 +00:00
// Send 24 hour average ping
let data24h = await uptimeCalculator . get24Hour ( ) ;
2023-10-08 18:33:32 +00:00
io . to ( userID ) . emit ( "avgPing" , monitorID , ( data24h . avgPing ) ? Number ( data24h . avgPing . toFixed ( 2 ) ) : null ) ;
2023-08-31 21:19:21 +00:00
// Send 24 hour uptime
io . to ( userID ) . emit ( "uptime" , monitorID , 24 , data24h . uptime ) ;
// Send 30 day uptime
let data30d = await uptimeCalculator . get30Day ( ) ;
io . to ( userID ) . emit ( "uptime" , monitorID , 720 , data30d . uptime ) ;
// Send 1-year uptime
let data1y = await uptimeCalculator . get1Year ( ) ;
io . to ( userID ) . emit ( "uptime" , monitorID , "1y" , data1y . uptime ) ;
// Send Cert Info
2021-08-30 06:55:33 +00:00
await Monitor . sendCertInfo ( io , monitorID , userID ) ;
} else {
2022-04-13 15:33:37 +00:00
log . debug ( "monitor" , "No clients in the room, no need to send stats" ) ;
2021-08-30 06:55:33 +00:00
}
2021-06-30 13:04:58 +00:00
}
2022-04-16 20:11:45 +00:00
/ * *
2022-04-22 17:42:47 +00:00
* Send certificate information to client
2022-04-16 20:11:45 +00:00
* @ param { Server } io Socket server instance
* @ param { number } monitorID ID of monitor to send
* @ param { number } userID ID of user to send to
2023-08-11 07:46:41 +00:00
* @ returns { void }
2022-04-16 20:11:45 +00:00
* /
2021-07-22 08:04:32 +00:00
static async sendCertInfo ( io , monitorID , userID ) {
2022-04-16 17:39:49 +00:00
let tlsInfo = await R . findOne ( "monitor_tls_info" , "monitor_id = ?" , [
2021-07-30 11:18:26 +00:00
monitorID ,
2021-07-22 08:04:32 +00:00
] ) ;
2022-04-16 17:39:49 +00:00
if ( tlsInfo != null ) {
io . to ( userID ) . emit ( "certInfo" , monitorID , tlsInfo . info _json ) ;
2021-07-22 08:04:32 +00:00
}
2021-07-21 04:09:09 +00:00
}
2022-04-16 20:11:45 +00:00
/ * *
* Has status of monitor changed since last beat ?
* @ param { boolean } isFirstBeat Is this the first beat of this monitor ?
* @ param { const } previousBeatStatus Status of the previous beat
* @ param { const } currentBeatStatus Status of the current beat
* @ returns { boolean } True if is an important beat else false
* /
2021-10-14 14:32:15 +00:00
static isImportantBeat ( isFirstBeat , previousBeatStatus , currentBeatStatus ) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
2022-01-23 14:22:00 +00:00
// MAINTENANCE -> MAINTENANCE = not important
// * MAINTENANCE -> UP = important
// * MAINTENANCE -> DOWN = important
// * DOWN -> MAINTENANCE = important
// * UP -> MAINTENANCE = important
return isFirstBeat ||
( previousBeatStatus === DOWN && currentBeatStatus === MAINTENANCE ) ||
( previousBeatStatus === UP && currentBeatStatus === MAINTENANCE ) ||
( previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN ) ||
( previousBeatStatus === MAINTENANCE && currentBeatStatus === UP ) ||
( previousBeatStatus === UP && currentBeatStatus === DOWN ) ||
( previousBeatStatus === DOWN && currentBeatStatus === UP ) ||
( previousBeatStatus === PENDING && currentBeatStatus === DOWN ) ;
}
2022-05-30 13:31:45 +00:00
/ * *
* Is this beat important for notifications ?
* @ param { boolean } isFirstBeat Is this the first beat of this monitor ?
* @ param { const } previousBeatStatus Status of the previous beat
* @ param { const } currentBeatStatus Status of the current beat
* @ returns { boolean } True if is an important beat else false
* /
2022-01-23 14:22:00 +00:00
static isImportantForNotification ( isFirstBeat , previousBeatStatus , currentBeatStatus ) {
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
// MAINTENANCE -> MAINTENANCE = not important
// MAINTENANCE -> UP = not important
// * MAINTENANCE -> DOWN = important
// DOWN -> MAINTENANCE = not important
// UP -> MAINTENANCE = not important
return isFirstBeat ||
( previousBeatStatus === MAINTENANCE && currentBeatStatus === DOWN ) ||
2021-10-14 14:32:15 +00:00
( previousBeatStatus === UP && currentBeatStatus === DOWN ) ||
( previousBeatStatus === DOWN && currentBeatStatus === UP ) ||
( previousBeatStatus === PENDING && currentBeatStatus === DOWN ) ;
}
2022-04-16 20:11:45 +00:00
/ * *
* Send a notification about a monitor
* @ param { boolean } isFirstBeat Is this beat the first of this monitor ?
* @ param { Monitor } monitor The monitor to send a notificaton about
* @ param { Bean } bean Status information about monitor
2023-08-11 07:46:41 +00:00
* @ returns { void }
2022-04-16 20:11:45 +00:00
* /
2021-10-14 14:32:15 +00:00
static async sendNotification ( isFirstBeat , monitor , bean ) {
if ( ! isFirstBeat || bean . status === DOWN ) {
2021-10-27 07:33:15 +00:00
const notificationList = await Monitor . getNotificationList ( monitor ) ;
2021-10-14 14:32:15 +00:00
let text ;
if ( bean . status === UP ) {
text = "✅ Up" ;
} else {
text = "🔴 Down" ;
}
let msg = ` [ ${ monitor . name } ] [ ${ text } ] ${ bean . msg } ` ;
for ( let notification of notificationList ) {
try {
2022-12-24 06:23:50 +00:00
const heartbeatJSON = bean . toJSON ( ) ;
2023-04-09 08:01:27 +00:00
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
2022-12-24 06:23:50 +00:00
if ( ! heartbeatJSON [ "msg" ] ) {
2022-12-30 05:46:34 +00:00
heartbeatJSON [ "msg" ] = "N/A" ;
2022-12-24 06:23:50 +00:00
}
2023-04-09 08:01:27 +00:00
// Also provide the time in server timezone
heartbeatJSON [ "timezone" ] = await UptimeKumaServer . getInstance ( ) . getTimezone ( ) ;
heartbeatJSON [ "timezoneOffset" ] = UptimeKumaServer . getInstance ( ) . getTimezoneOffset ( ) ;
heartbeatJSON [ "localDateTime" ] = dayjs . utc ( heartbeatJSON [ "time" ] ) . tz ( heartbeatJSON [ "timezone" ] ) . format ( SQL _DATETIME _FORMAT ) ;
2022-12-24 06:23:50 +00:00
await Notification . send ( JSON . parse ( notification . config ) , msg , await monitor . toJSON ( false ) , heartbeatJSON ) ;
2021-10-14 14:32:15 +00:00
} catch ( e ) {
2022-04-13 15:33:37 +00:00
log . error ( "monitor" , "Cannot send notification to " + notification . name ) ;
log . error ( "monitor" , e ) ;
2021-10-14 14:32:15 +00:00
}
}
}
}
2022-04-16 20:11:45 +00:00
/ * *
* Get list of notification providers for a given monitor
* @ param { Monitor } monitor Monitor to get notification providers for
2023-08-11 07:46:41 +00:00
* @ returns { Promise < LooseObject < any > [ ] > } List of notifications
2022-04-16 20:11:45 +00:00
* /
2021-10-27 07:33:15 +00:00
static async getNotificationList ( monitor ) {
let notificationList = await R . getAll ( "SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id " , [
monitor . id ,
] ) ;
return notificationList ;
}
2022-04-16 20:11:45 +00:00
/ * *
2023-01-12 10:39:36 +00:00
* checks certificate chain for expiring certificates
2023-08-11 07:46:41 +00:00
* @ param { object } tlsInfoObject Information about certificate
* @ returns { void }
2022-04-16 20:11:45 +00:00
* /
2023-01-12 10:39:36 +00:00
async checkCertExpiryNotifications ( tlsInfoObject ) {
2021-10-27 07:33:15 +00:00
if ( tlsInfoObject && tlsInfoObject . certInfo && tlsInfoObject . certInfo . daysRemaining ) {
const notificationList = await Monitor . getNotificationList ( this ) ;
2023-01-12 10:39:36 +00:00
if ( ! notificationList . length > 0 ) {
// fail fast. If no notification is set, all the following checks can be skipped.
log . debug ( "monitor" , "No notification, no need to send cert notification" ) ;
return ;
}
2022-05-12 10:18:47 +00:00
let notifyDays = await setting ( "tlsExpiryNotifyDays" ) ;
if ( notifyDays == null || ! Array . isArray ( notifyDays ) ) {
// Reset Default
2024-03-15 14:02:55 +00:00
await setSetting ( "tlsExpiryNotifyDays" , [ 7 , 14 , 21 ] , "general" ) ;
2022-05-12 10:18:47 +00:00
notifyDays = [ 7 , 14 , 21 ] ;
}
2023-01-12 10:39:36 +00:00
if ( Array . isArray ( notifyDays ) ) {
for ( const targetDays of notifyDays ) {
let certInfo = tlsInfoObject . certInfo ;
while ( certInfo ) {
let subjectCN = certInfo . subject [ "CN" ] ;
2023-10-15 18:20:38 +00:00
if ( rootCertificates . has ( certInfo . fingerprint256 ) ) {
log . debug ( "monitor" , ` Known root cert: ${ certInfo . certType } certificate " ${ subjectCN } " ( ${ certInfo . daysRemaining } days valid) on ${ targetDays } deadline. ` ) ;
break ;
} else if ( certInfo . daysRemaining > targetDays ) {
2023-01-12 10:39:36 +00:00
log . debug ( "monitor" , ` No need to send cert notification for ${ certInfo . certType } certificate " ${ subjectCN } " ( ${ certInfo . daysRemaining } days valid) on ${ targetDays } deadline. ` ) ;
} else {
log . debug ( "monitor" , ` call sendCertNotificationByTargetDays for ${ targetDays } deadline on certificate ${ subjectCN } . ` ) ;
await this . sendCertNotificationByTargetDays ( subjectCN , certInfo . certType , certInfo . daysRemaining , targetDays , notificationList ) ;
}
certInfo = certInfo . issuerCertificate ;
}
2022-05-12 10:18:47 +00:00
}
}
2021-10-27 07:33:15 +00:00
}
}
2022-04-16 20:11:45 +00:00
/ * *
* Send a certificate notification when certificate expires in less
* than target days
2023-01-12 10:39:36 +00:00
* @ param { string } certCN Common Name attribute from the certificate subject
* @ param { string } certType certificate type
* @ param { number } daysRemaining Number of days remaining on certificate
2022-04-16 20:11:45 +00:00
* @ param { number } targetDays Number of days to alert after
2022-04-21 19:02:18 +00:00
* @ param { LooseObject < any > [ ] } notificationList List of notification providers
2022-04-16 20:11:45 +00:00
* @ returns { Promise < void > }
* /
2023-01-12 10:39:36 +00:00
async sendCertNotificationByTargetDays ( certCN , certType , daysRemaining , targetDays , notificationList ) {
2021-10-27 07:33:15 +00:00
2023-05-26 13:38:51 +00:00
let row = await R . getRow ( "SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?" , [
"certificate" ,
this . id ,
targetDays ,
] ) ;
2021-10-27 07:33:15 +00:00
2023-01-12 10:39:36 +00:00
// Sent already, no need to send again
if ( row ) {
log . debug ( "monitor" , "Sent already, no need to send again" ) ;
2021-10-27 07:33:15 +00:00
return ;
}
2023-01-12 10:39:36 +00:00
let sent = false ;
log . debug ( "monitor" , "Send certificate notification" ) ;
2021-10-27 07:33:15 +00:00
2023-01-12 10:39:36 +00:00
for ( let notification of notificationList ) {
try {
log . debug ( "monitor" , "Sending to " + notification . name ) ;
await Notification . send ( JSON . parse ( notification . config ) , ` [ ${ this . name } ][ ${ this . url } ] ${ certType } certificate ${ certCN } will be expired in ${ daysRemaining } days ` ) ;
sent = true ;
} catch ( e ) {
log . error ( "monitor" , "Cannot send cert notification to " + notification . name ) ;
log . error ( "monitor" , e ) ;
}
}
2021-10-27 07:33:15 +00:00
2023-01-12 10:39:36 +00:00
if ( sent ) {
await R . exec ( "INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)" , [
2021-10-27 07:33:15 +00:00
"certificate" ,
this . id ,
targetDays ,
] ) ;
}
}
2021-12-08 06:59:59 +00:00
2022-04-16 20:11:45 +00:00
/ * *
* Get the status of the previous heartbeat
* @ param { number } monitorID ID of monitor to check
2023-08-11 07:46:41 +00:00
* @ returns { Promise < LooseObject < any >> } Previous heartbeat
2022-04-16 20:11:45 +00:00
* /
2021-12-08 06:59:59 +00:00
static async getPreviousHeartbeat ( monitorID ) {
2023-11-24 10:11:36 +00:00
return await R . findOne ( "heartbeat" , " id = (select MAX(id) from heartbeat where monitor_id = ?)" , [
2021-12-08 06:59:59 +00:00
monitorID
] ) ;
}
2022-01-25 18:07:27 +00:00
2022-04-30 13:50:05 +00:00
/ * *
* Check if monitor is under maintenance
* @ param { number } monitorID ID of monitor to check
2023-08-11 07:46:41 +00:00
* @ returns { Promise < boolean > } Is the monitor under maintenance
2022-04-30 13:50:05 +00:00
* /
2022-01-25 18:07:27 +00:00
static async isUnderMaintenance ( monitorID ) {
2023-03-30 20:04:17 +00:00
const maintenanceIDList = await R . getCol ( `
SELECT maintenance _id FROM monitor _maintenance
WHERE monitor _id = ?
` , [ monitorID ]);
for ( const maintenanceID of maintenanceIDList ) {
const maintenance = await UptimeKumaServer . getInstance ( ) . getMaintenance ( maintenanceID ) ;
if ( maintenance && await maintenance . isUnderMaintenance ( ) ) {
return true ;
}
2023-05-31 19:29:20 +00:00
}
2023-05-31 19:19:46 +00:00
2023-05-31 19:29:20 +00:00
const parent = await Monitor . getParent ( monitorID ) ;
if ( parent != null ) {
return await Monitor . isUnderMaintenance ( parent . id ) ;
2023-01-28 13:02:10 +00:00
}
2023-03-30 20:04:17 +00:00
return false ;
2022-01-25 18:07:27 +00:00
}
2022-12-08 15:21:55 +00:00
2023-08-11 07:46:41 +00:00
/ * *
* Make sure monitor interval is between bounds
* @ returns { void }
* @ throws Interval is outside of range
* /
2022-12-08 15:21:55 +00:00
validate ( ) {
if ( this . interval > MAX _INTERVAL _SECOND ) {
throw new Error ( ` Interval cannot be more than ${ MAX _INTERVAL _SECOND } seconds ` ) ;
}
if ( this . interval < MIN _INTERVAL _SECOND ) {
throw new Error ( ` Interval cannot be less than ${ MIN _INTERVAL _SECOND } seconds ` ) ;
}
}
2023-01-28 01:58:03 +00:00
/ * *
* Gets Parent of the monitor
* @ param { number } monitorID ID of monitor to get
2023-08-11 07:46:41 +00:00
* @ returns { Promise < LooseObject < any >> } Parent
2023-01-28 01:58:03 +00:00
* /
static async getParent ( monitorID ) {
return await R . getRow ( `
SELECT parent . * FROM monitor parent
LEFT JOIN monitor child
ON child . parent = parent . id
WHERE child . id = ?
` , [
monitorID ,
] ) ;
}
/ * *
* Gets all Children of the monitor
* @ param { number } monitorID ID of monitor to get
2023-08-11 07:46:41 +00:00
* @ returns { Promise < LooseObject < any >> } Children
2023-01-28 01:58:03 +00:00
* /
static async getChildren ( monitorID ) {
return await R . getAll ( `
SELECT * FROM monitor
WHERE parent = ?
` , [
monitorID ,
] ) ;
}
/ * *
2024-03-03 09:59:38 +00:00
* Gets the full path
* @ returns { Promise < string [ ] > } Full path ( includes groups and the name ) of the monitor
2023-01-28 01:58:03 +00:00
* /
2024-03-03 09:59:38 +00:00
async getPath ( ) {
const path = [ this . name ] ;
2023-01-28 01:58:03 +00:00
if ( this . parent === null ) {
return path ;
}
let parent = await Monitor . getParent ( this . id ) ;
while ( parent !== null ) {
2024-03-03 09:59:38 +00:00
path . unshift ( parent . name ) ;
2023-01-28 01:58:03 +00:00
parent = await Monitor . getParent ( parent . id ) ;
}
return path ;
}
/ * *
* Gets recursive all child ids
2023-08-11 07:46:41 +00:00
* @ param { number } monitorID ID of the monitor to get
* @ returns { Promise < Array > } IDs of all children
2023-01-28 01:58:03 +00:00
* /
static async getAllChildrenIDs ( monitorID ) {
const childs = await Monitor . getChildren ( monitorID ) ;
if ( childs === null ) {
return [ ] ;
}
let childrenIDs = [ ] ;
for ( const child of childs ) {
childrenIDs . push ( child . id ) ;
childrenIDs = childrenIDs . concat ( await Monitor . getAllChildrenIDs ( child . id ) ) ;
}
return childrenIDs ;
}
2023-01-28 12:39:17 +00:00
2023-06-26 02:44:15 +00:00
/ * *
2024-04-16 14:59:07 +00:00
* Unlinks all children of the group monitor
2023-06-26 02:44:15 +00:00
* @ param { number } groupID ID of group to remove children of
* @ returns { Promise < void > }
* /
static async unlinkAllChildren ( groupID ) {
return await R . exec ( "UPDATE `monitor` SET parent = ? WHERE parent = ? " , [
null , groupID
] ) ;
}
2023-01-28 12:39:17 +00:00
/ * *
2023-08-11 07:46:41 +00:00
* Checks recursive if parent ( ancestors ) are active
* @ param { number } monitorID ID of the monitor to get
* @ returns { Promise < boolean > } Is the parent monitor active ?
* /
2023-01-28 12:39:17 +00:00
static async isParentActive ( monitorID ) {
const parent = await Monitor . getParent ( monitorID ) ;
if ( parent === null ) {
return true ;
}
const parentActive = await Monitor . isParentActive ( parent . id ) ;
return parent . active && parentActive ;
}
2023-12-17 09:21:07 +00:00
/ * *
* Obtains a new Oidc Token
* @ returns { Promise < object > } OAuthProvider client
* /
async makeOidcTokenClientCredentialsRequest ( ) {
log . debug ( "monitor" , ` [ ${ this . name } ] The oauth access-token undefined or expired. Requesting a new token ` ) ;
const oAuthAccessToken = await getOidcTokenClientCredentials ( this . oauth _token _url , this . oauth _client _id , this . oauth _client _secret , this . oauth _scopes , this . oauth _auth _method ) ;
if ( this . oauthAccessToken ? . expires _at ) {
log . debug ( "monitor" , ` [ ${ this . name } ] Obtained oauth access-token. Expires at ${ new Date ( this . oauthAccessToken ? . expires _at * 1000 ) } ` ) ;
} else {
log . debug ( "monitor" , ` [ ${ this . name } ] Obtained oauth access-token. Time until expiry was not provided ` ) ;
}
return oAuthAccessToken ;
}
2024-04-24 06:37:17 +00:00
/ * *
* Store TLS certificate information and check for expiry
2024-04-25 15:45:24 +00:00
* @ param { object } tlsInfo Information about the TLS connection
2024-04-24 06:37:17 +00:00
* @ returns { Promise < void > }
* /
async handleTlsInfo ( tlsInfo ) {
await this . updateTlsInfo ( tlsInfo ) ;
this . prometheus ? . update ( null , tlsInfo ) ;
if ( ! this . getIgnoreTls ( ) && this . isEnabledExpiryNotification ( ) ) {
log . debug ( "monitor" , ` [ ${ this . name } ] call checkCertExpiryNotifications ` ) ;
await this . checkCertExpiryNotifications ( tlsInfo ) ;
}
}
2021-06-25 13:55:49 +00:00
}
module . exports = Monitor ;