Merge branch '1.23.X' into 1.23.13-to-2.0.0

# Conflicts:
#	.github/workflows/auto-test.yml
#	package-lock.json
#	package.json
#	server/database.js
#	server/model/monitor.js
#	server/monitor-types/real-browser-monitor-type.js
#	server/util-server.js
#	test/cypress/unit/i18n.spec.js
This commit is contained in:
Louis Lam
2024-04-25 15:42:53 +08:00
13 changed files with 152 additions and 66 deletions

View File

@@ -245,12 +245,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}
*/
encodeBase64(user, pass) {
return Buffer.from(user + ":" + pass).toString("base64");
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
}
/**
@@ -533,6 +533,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`);
@@ -542,31 +554,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);
}
@@ -599,8 +599,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);
@@ -1615,6 +1619,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;

View File

@@ -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;
}
/**

View File

@@ -62,7 +62,7 @@ class DingDing extends NotificationProvider {
if (result.data.errmsg === "ok") {
return true;
}
return false;
throw new Error(result.data.errmsg);
}
/**

View File

@@ -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);
}
}

View File

@@ -1319,6 +1319,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");

View File

@@ -653,21 +653,27 @@ const parseCertificateInfo = function (info) {
/**
* Check if certificate is valid
* @param {object} res Response object from axios
* @returns {object} Object containing certificate information
* @throws No socket was found to check certificate for
* @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected
* @returns {Object} Object containing certificate information
*/
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