mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-09 16:33:58 +08:00
Merge branch 'master' into snmp-monitor
This commit is contained in:
@@ -250,12 +250,12 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Encode user and password to Base64 encoding
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
* @param {string} user Username to encode
|
||||
* @param {string} pass Password to encode
|
||||
* @returns {string} Encoded username:password
|
||||
* @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)
|
||||
* @returns {string} Encoded Base64 string
|
||||
*/
|
||||
encodeBase64(user, pass) {
|
||||
return Buffer.from(user + ":" + pass).toString("base64");
|
||||
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -538,6 +538,18 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
|
||||
@@ -547,31 +559,19 @@ class Monitor extends BeanModel {
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
// Check certificate if https is used
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
if (this.getUrl()?.protocol === "https:") {
|
||||
log.debug("monitor", `[${this.name}] Check cert`);
|
||||
try {
|
||||
let tlsInfoObject = checkCertificate(res);
|
||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||
// 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;
|
||||
|
||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
||||
await this.checkCertExpiryNotifications(tlsInfoObject);
|
||||
}
|
||||
if (tlsSocket) {
|
||||
tlsInfo = checkCertificate(tlsSocket);
|
||||
tlsInfo.valid = tlsSocket.authorized || false;
|
||||
|
||||
} catch (e) {
|
||||
if (e.message !== "No TLS certificate in response") {
|
||||
log.error("monitor", "Caught error");
|
||||
log.error("monitor", e.message);
|
||||
}
|
||||
await this.handleTlsInfo(tlsInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.TIMELOGGER === "1") {
|
||||
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
}
|
||||
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
|
||||
log.info("monitor", res.data);
|
||||
}
|
||||
@@ -604,8 +604,12 @@ class Monitor extends BeanModel {
|
||||
let data = res.data;
|
||||
|
||||
// convert data to object
|
||||
if (typeof data === "string") {
|
||||
data = JSON.parse(data);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
let expression = jsonata(this.jsonPath);
|
||||
@@ -1620,6 +1624,20 @@ class Monitor extends BeanModel {
|
||||
return oAuthAccessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store TLS certificate information and check for expiry
|
||||
* @param {object} tlsInfo Information about the TLS connection
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
||||
|
@@ -10,6 +10,10 @@ const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
const { RemoteBrowser } = require("../remote-browser");
|
||||
|
||||
/**
|
||||
* Cached instance of a browser
|
||||
* @type {import ("playwright-core").Browser}
|
||||
*/
|
||||
let browser = null;
|
||||
|
||||
let allowedList = [];
|
||||
@@ -71,10 +75,12 @@ async function isAllowedChromeExecutable(executablePath) {
|
||||
/**
|
||||
* Get the current instance of the browser. If there isn't one, create
|
||||
* it.
|
||||
* @returns {Promise<Browser>} The browser
|
||||
* @returns {Promise<import ("playwright-core").Browser>} The browser
|
||||
*/
|
||||
async function getBrowser() {
|
||||
if (!browser) {
|
||||
if (browser && browser.isConnected()) {
|
||||
return browser;
|
||||
} else {
|
||||
let executablePath = await Settings.get("chromeExecutable");
|
||||
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
@@ -83,8 +89,9 @@ async function getBrowser() {
|
||||
//headless: false,
|
||||
executablePath,
|
||||
});
|
||||
|
||||
return browser;
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -19,6 +19,9 @@ class DingDing extends NotificationProvider {
|
||||
markdown: {
|
||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
"at": {
|
||||
"isAtAll": notification.mentioning === "everyone"
|
||||
}
|
||||
};
|
||||
if (await this.sendToDingDing(notification, params)) {
|
||||
@@ -62,7 +65,7 @@ class DingDing extends NotificationProvider {
|
||||
if (result.data.errmsg === "ok") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
throw new Error(result.data.errmsg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
42
server/notification-providers/keep.js
Normal file
42
server/notification-providers/keep.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Keep extends NotificationProvider {
|
||||
name = "Keep";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let config = {
|
||||
headers: {
|
||||
"x-api-key": notification.webhookAPIKey,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
let url = notification.webhookURL;
|
||||
|
||||
if (url.endsWith("/")) {
|
||||
url = url.slice(0, -1);
|
||||
}
|
||||
|
||||
let webhookURL = url + "/alerts/event/uptimekuma";
|
||||
|
||||
await axios.post(webhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Keep;
|
78
server/notification-providers/sevenio.js
Normal file
78
server/notification-providers/sevenio.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class SevenIO extends NotificationProvider {
|
||||
name = "SevenIO";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const data = {
|
||||
to: notification.sevenioTo,
|
||||
from: notification.sevenioSender || "Uptime Kuma",
|
||||
text: msg,
|
||||
};
|
||||
|
||||
const config = {
|
||||
baseURL: "https://gateway.seven.io/api/",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": notification.sevenioApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// testing or certificate expiry notification
|
||||
if (heartbeatJSON == null) {
|
||||
await axios.post("sms", data, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let address = "";
|
||||
|
||||
switch (monitorJSON["type"]) {
|
||||
case "ping":
|
||||
address = monitorJSON["hostname"];
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "gamedig":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
address += ":" + monitorJSON["port"];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
|
||||
address = monitorJSON["url"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (address !== "") {
|
||||
address = `(${address}) `;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
data.text = `Your service ${monitorJSON["name"]} ${address}went down at ${heartbeatJSON["localDateTime"]} ` +
|
||||
`(${heartbeatJSON["timezone"]}). Error: ${heartbeatJSON["msg"]}`;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
data.text = `Your service ${monitorJSON["name"]} ${address}went back up at ${heartbeatJSON["localDateTime"]} ` +
|
||||
`(${heartbeatJSON["timezone"]}).`;
|
||||
}
|
||||
await axios.post("sms", data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = SevenIO;
|
@@ -18,6 +18,7 @@ const Gotify = require("./notification-providers/gotify");
|
||||
const GrafanaOncall = require("./notification-providers/grafana-oncall");
|
||||
const HomeAssistant = require("./notification-providers/home-assistant");
|
||||
const HeiiOnCall = require("./notification-providers/heii-oncall");
|
||||
const Keep = require("./notification-providers/keep");
|
||||
const Kook = require("./notification-providers/kook");
|
||||
const Line = require("./notification-providers/line");
|
||||
const LineNotify = require("./notification-providers/linenotify");
|
||||
@@ -56,6 +57,7 @@ const GoAlert = require("./notification-providers/goalert");
|
||||
const SMSManager = require("./notification-providers/smsmanager");
|
||||
const ServerChan = require("./notification-providers/serverchan");
|
||||
const ZohoCliq = require("./notification-providers/zoho-cliq");
|
||||
const SevenIO = require("./notification-providers/sevenio");
|
||||
const Whapi = require("./notification-providers/whapi");
|
||||
const GtxMessaging = require("./notification-providers/gtx-messaging");
|
||||
const Cellsynt = require("./notification-providers/cellsynt");
|
||||
@@ -94,6 +96,7 @@ class Notification {
|
||||
new GrafanaOncall(),
|
||||
new HomeAssistant(),
|
||||
new HeiiOnCall(),
|
||||
new Keep(),
|
||||
new Kook(),
|
||||
new Line(),
|
||||
new LineNotify(),
|
||||
@@ -132,6 +135,7 @@ class Notification {
|
||||
new WeCom(),
|
||||
new GoAlert(),
|
||||
new ZohoCliq(),
|
||||
new SevenIO(),
|
||||
new Whapi(),
|
||||
new GtxMessaging(),
|
||||
new Cellsynt(),
|
||||
|
@@ -80,23 +80,25 @@ class Prometheus {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
||||
} else {
|
||||
// Is it good?
|
||||
monitorResponseTime.set(this.monitorLabelValues, -1);
|
||||
if (heartbeat) {
|
||||
try {
|
||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
||||
} else {
|
||||
// Is it good?
|
||||
monitorResponseTime.set(this.monitorLabelValues, -1);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1324,6 +1324,12 @@ let needSetup = false;
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
// Log out all clients if enabling auth
|
||||
// GHSA-23q2-5gf8-gjpp
|
||||
if (currentDisabledAuth && !data.disableAuth) {
|
||||
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||
}
|
||||
|
||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||
const previousNSCDStatus = await Settings.get("nscd");
|
||||
|
||||
|
@@ -653,21 +653,27 @@ const parseCertificateInfo = function (info) {
|
||||
|
||||
/**
|
||||
* Check if certificate is valid
|
||||
* @param {object} res Response object from axios
|
||||
* @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected
|
||||
* @returns {object} Object containing certificate information
|
||||
* @throws No socket was found to check certificate for
|
||||
*/
|
||||
exports.checkCertificate = function (res) {
|
||||
if (!res.request.res.socket) {
|
||||
throw new Error("No socket found");
|
||||
exports.checkCertificate = function (socket) {
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
|
||||
// Return null if there is no socket
|
||||
if (socket === undefined || socket == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = res.request.res.socket.getPeerCertificate(true);
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
const info = socket.getPeerCertificate(true);
|
||||
const valid = socket.authorized || false;
|
||||
|
||||
log.debug("cert", "Parsing Certificate Info");
|
||||
const parsedInfo = parseCertificateInfo(info);
|
||||
|
||||
if (process.env.TIMELOGGER === "1") {
|
||||
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
}
|
||||
|
||||
return {
|
||||
valid: valid,
|
||||
certInfo: parsedInfo
|
||||
|
Reference in New Issue
Block a user