diff --git a/server/model/monitor.js b/server/model/monitor.js index b3ed31d6c..1d69e5d8b 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -4,7 +4,7 @@ const { Prometheus } = require("../prometheus"); const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, SQL_DATETIME_FORMAT } = require("../../src/util"); -const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, +const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal } = require("../util-server"); const { R } = require("redbean-node"); @@ -773,37 +773,6 @@ class Monitor extends BeanModel { bean.msg = ""; bean.status = UP; bean.ping = dayjs().valueOf() - startTime; - } 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) { - responseData = responseData.toString().substring(0, 47) + "..."; - } - if (response.code !== 1) { - bean.status = DOWN; - bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`; - } else { - let keywordFound = response.data.toString().includes(this.keyword); - if (keywordFound === !this.isInvertKeyword()) { - bean.status = UP; - bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`; - } else { - log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`); - bean.status = DOWN; - bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`; - } - } } else if (this.type === "postgres") { let startTime = dayjs().valueOf(); diff --git a/server/monitor-types/grpc.js b/server/monitor-types/grpc.js new file mode 100644 index 000000000..57dd0002f --- /dev/null +++ b/server/monitor-types/grpc.js @@ -0,0 +1,86 @@ +const { MonitorType } = require("./monitor-type"); +const { UP, log } = require("../../src/util"); +const dayjs = require("dayjs"); +const grpc = require("@grpc/grpc-js"); +const protojs = require("protobufjs"); + +export class GrpcKeywordMonitorType extends MonitorType { + name = "grpc-keyword"; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, _server) { + const startTime = dayjs().valueOf(); + const service = this.constructGrpcService(this.grpcUrl, this.grpcProtobuf, this.grpcServiceName, this.grpcEnableTls); + let response = await this.grpcQuery(service, this.grpcMethod, this.grpcBody); + heartbeat.ping = dayjs().valueOf() - startTime; + log.debug(this.name, `gRPC response: ${response}`); + if (response.length > 50) { + response = response.toString().substring(0, 47) + "..."; + } + let keywordFound = response.toString().includes(this.keyword); + if (keywordFound !== !this.isInvertKeyword()) { + log.debug(this.name, `GRPC response [${response}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response} + "]"`); + throw new Error(`keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response} + "]`); + } + heartbeat.status = UP; + heartbeat.msg = `${response}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`; + } + + /** + * Create gRPC client + * @param {string} url grpc Url + * @param {string} protobufData grpc ProtobufData + * @param {string} serviceName grpc ServiceName + * @param {string} enableTls grpc EnableTls + * @returns {grpc.Service} grpc Service + */ + constructGrpcService(url, protobufData, serviceName, enableTls) { + const protocObject = protojs.parse(protobufData); + const protoServiceObject = protocObject.root.lookupService(serviceName); + const Client = grpc.makeGenericClientConstructor({}); + const credentials = enableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure(); + const client = new Client(url, credentials); + return protoServiceObject.create((method, requestData, cb) => { + const fullServiceName = method.fullName; + const serviceFQDN = fullServiceName.split("."); + const serviceMethod = serviceFQDN.pop(); + const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`; + log.debug(this.name, `gRPC method ${serviceMethodClientImpl}`); + client.makeUnaryRequest( + serviceMethodClientImpl, + arg => arg, + arg => arg, + requestData, + cb); + }, false, false); + } + + /** + * Create gRPC client stib + * @param {grpc.Service} service grpc Url + * @param {string} method grpc Method + * @param {string} body grpc Body + * @returns {Promise} Result of gRPC query + */ + async grpcQuery(service, method, body) { + return new Promise((resolve, reject) => { + try { + service[`${method}`](JSON.parse(body), (err, response) => { + if (err) { + if (err.code !== 1) { + reject(`Error in send gRPC ${err.code} ${err.details}`); + } + log.debug(this.name, `ignoring ${err.code} ${err.details}, as code=1 is considered OK`); + resolve(`${err.code} is considered OK because ${err.details}`); + } + resolve(JSON.stringify(response)); + }); + } catch (err) { + reject(`Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`); + } + + }); + } +} diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 6ab5f6c26..e27c3b8c5 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -113,6 +113,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType(); UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType(); + UptimeKumaServer.monitorTypeList["grpc-keyword"] = new GrpcKeywordMonitorType(); UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType(); // Allow all CORS origins (polling) in development @@ -517,4 +518,5 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor const { TailscalePing } = require("./monitor-types/tailscale-ping"); const { DnsMonitorType } = require("./monitor-types/dns"); const { MqttMonitorType } = require("./monitor-types/mqtt"); +const { GrpcKeywordMonitorType } = require("./monitor-types/grpc"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); diff --git a/server/util-server.js b/server/util-server.js index f425ac7f4..6d8060391 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -13,8 +13,6 @@ const postgresConParse = require("pg-connection-string").parse; const mysql = require("mysql2"); const { NtlmClient } = require("axios-ntlm"); const { Settings } = require("./settings"); -const grpc = require("@grpc/grpc-js"); -const protojs = require("protobufjs"); const radiusClient = require("node-radius-client"); const redis = require("redis"); const oidc = require("openid-client"); @@ -919,64 +917,6 @@ module.exports.timeObjectToLocal = (obj, timezone = undefined) => { return timeObjectConvertTimezone(obj, timezone, false); }; -/** - * Create gRPC client stib - * @param {object} options from gRPC client - * @returns {Promise} Result of gRPC query - */ -module.exports.grpcQuery = async (options) => { - const { grpcUrl, grpcProtobufData, grpcServiceName, grpcEnableTls, grpcMethod, grpcBody } = options; - const protocObject = protojs.parse(grpcProtobufData); - const protoServiceObject = protocObject.root.lookupService(grpcServiceName); - const Client = grpc.makeGenericClientConstructor({}); - const credentials = grpcEnableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure(); - const client = new Client( - grpcUrl, - credentials - ); - const grpcService = protoServiceObject.create(function (method, requestData, cb) { - const fullServiceName = method.fullName; - const serviceFQDN = fullServiceName.split("."); - const serviceMethod = serviceFQDN.pop(); - const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`; - log.debug("monitor", `gRPC method ${serviceMethodClientImpl}`); - client.makeUnaryRequest( - serviceMethodClientImpl, - arg => arg, - arg => arg, - requestData, - cb); - }, false, false); - return new Promise((resolve, _) => { - try { - return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) { - const responseData = JSON.stringify(response); - if (err) { - return resolve({ - code: err.code, - errorMessage: err.details, - data: "" - }); - } else { - log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`); - return resolve({ - code: 1, - errorMessage: "", - data: responseData - }); - } - }); - } catch (err) { - return resolve({ - code: -1, - errorMessage: `Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`, - data: "" - }); - } - - }); -}; - /** * Returns an array of SHA256 fingerprints for all known root certificates. * @returns {Set} A set of SHA256 fingerprints.