Merge branch 'master' into async-fs

This commit is contained in:
Frank Elsinga
2024-05-23 20:32:23 +02:00
committed by GitHub
235 changed files with 14186 additions and 7984 deletions

View File

@@ -9,7 +9,7 @@ class Group extends BeanModel {
* @param {boolean} showTags Should the JSON include monitor tags
* @param {boolean} certExpiry Should JSON include info about
* certificate expiry?
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
async toPublicJSON(showTags = false, certExpiry = false) {
let monitorBeanList = await this.getMonitorList();
@@ -29,7 +29,7 @@ class Group extends BeanModel {
/**
* Get all monitors
* @returns {Bean[]} List of monitors
* @returns {Promise<Bean[]>} List of monitors
*/
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`

View File

@@ -11,7 +11,7 @@ class Maintenance extends BeanModel {
/**
* Return an object that ready to parse to JSON for public
* Only show necessary data to public
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
async toPublicJSON() {
@@ -98,7 +98,7 @@ class Maintenance extends BeanModel {
/**
* Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
async toJSON(timezone = null) {
return this.toPublicJSON(timezone);
@@ -143,7 +143,7 @@ class Maintenance extends BeanModel {
* Convert data from socket to bean
* @param {Bean} bean Bean to fill in
* @param {object} obj Data to fill bean with
* @returns {Bean} Filled bean
* @returns {Promise<Bean>} Filled bean
*/
static async jsonToBean(bean, obj) {
if (obj.id) {
@@ -189,9 +189,9 @@ class Maintenance extends BeanModel {
/**
* Throw error if cron is invalid
* @param {string|Date} cron Pattern or date
* @returns {Promise<void>}
* @returns {void}
*/
static async validateCron(cron) {
static validateCron(cron) {
let job = new Cron(cron, () => {});
job.stop();
}
@@ -324,7 +324,7 @@ class Maintenance extends BeanModel {
/**
* Is this maintenance currently active
* @returns {boolean} The maintenance is active?
* @returns {Promise<boolean>} The maintenance is active?
*/
async isUnderMaintenance() {
return (await this.getStatus()) === "under-maintenance";
@@ -332,7 +332,7 @@ class Maintenance extends BeanModel {
/**
* Get the timezone of the maintenance
* @returns {string} timezone
* @returns {Promise<string>} timezone
*/
async getTimezone() {
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
@@ -343,7 +343,7 @@ class Maintenance extends BeanModel {
/**
* Get offset for timezone
* @returns {string} offset
* @returns {Promise<string>} offset
*/
async getTimezoneOffset() {
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
@@ -351,7 +351,7 @@ class Maintenance extends BeanModel {
/**
* Get the current status of the maintenance
* @returns {string} Current status
* @returns {Promise<string>} Current status
*/
async getStatus() {
if (!this.active) {

View File

@@ -5,7 +5,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -15,7 +15,6 @@ const { demoMode } = require("../config");
const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
@@ -24,6 +23,8 @@ const crypto = require("crypto");
const { UptimeCalculator } = require("../uptime-calculator");
const { CookieJar } = require("tough-cookie");
const { HttpsCookieAgent } = require("http-cookie-agent/http");
const https = require("https");
const http = require("http");
const rootCertificates = rootCertificatesFingerprints();
@@ -42,7 +43,7 @@ class Monitor extends BeanModel {
* @param {boolean} showTags Include tags in JSON
* @param {boolean} certExpiry Include certificate expiry info in
* JSON
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
async toPublicJSON(showTags = false, certExpiry = false) {
let obj = {
@@ -73,7 +74,7 @@ class Monitor extends BeanModel {
* Return an object that ready to parse to JSON
* @param {boolean} includeSensitiveData Include sensitive data in
* JSON
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
async toJSON(includeSensitiveData = true) {
@@ -95,11 +96,15 @@ class Monitor extends BeanModel {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
const path = await this.getPath();
const pathName = path.join(" / ");
let data = {
id: this.id,
name: this.name,
description: this.description,
pathName: await this.getPathName(),
path,
pathName,
parent: this.parent,
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
url: this.url,
@@ -240,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} Encoded Base64 string
*/
encodeBase64(user, pass) {
return Buffer.from(user + ":" + pass).toString("base64");
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
}
/**
@@ -323,9 +328,9 @@ class Monitor extends BeanModel {
/**
* Start monitor
* @param {Server} io Socket server instance
* @returns {void}
* @returns {Promise<void>}
*/
start(io) {
async start(io) {
let previousBeat = null;
let retries = 0;
@@ -432,9 +437,7 @@ class Monitor extends BeanModel {
if (this.auth_method === "oauth2-cc") {
try {
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
this.oauthAccessToken = await this.makeOidcTokenClientCredentialsRequest();
}
oauth2AuthHeader = {
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
@@ -530,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`);
@@ -539,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);
}
@@ -596,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);
@@ -667,12 +674,12 @@ class Monitor extends BeanModel {
headers: {
"Accept": "*/*",
},
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
httpAgent: new http.Agent({
maxCachedSessions: 0,
}),
maxRedirects: this.maxredirects,
@@ -719,12 +726,12 @@ class Monitor extends BeanModel {
headers: {
"Accept": "*/*",
},
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
httpAgent: new http.Agent({
maxCachedSessions: 0,
}),
};
@@ -739,7 +746,7 @@ class Monitor extends BeanModel {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
options.httpsAgent = new https.Agent(
await DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
);
}
@@ -815,15 +822,6 @@ class Monitor extends BeanModel {
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
let startTime = dayjs().valueOf();
await mongodbPing(this.databaseConnectionString);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
@@ -854,7 +852,7 @@ class Monitor extends BeanModel {
} else if (this.type === "redis") {
let startTime = dayjs().valueOf();
bean.msg = await redisPingAsync(this.databaseConnectionString);
bean.msg = await redisPingAsync(this.databaseConnectionString, !this.ignoreTls);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
@@ -944,7 +942,7 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] apicache clear`);
apicache.clear();
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
await UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
} else {
bean.important = false;
@@ -1064,27 +1062,44 @@ class Monitor extends BeanModel {
}
return res;
} catch (e) {
} 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);
}
// Fix #2253
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
if (!finalCall && typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
if (!finalCall && typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) {
log.debug("monitor", "makeAxiosRequest with gzip");
options.headers["Accept-Encoding"] = "gzip, deflate";
return this.makeAxiosRequest(options, true);
} else {
if (typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
e.message = "response timeout: incomplete response within a interval";
if (typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) {
error.message = "response timeout: incomplete response within a interval";
}
throw e;
throw error;
}
}
}
/**
* Stop monitor
* @returns {void}
* @returns {Promise<void>}
*/
stop() {
async stop() {
clearTimeout(this.heartbeatInterval);
this.isStop = true;
@@ -1357,7 +1372,7 @@ class Monitor extends BeanModel {
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
}
@@ -1511,11 +1526,11 @@ class Monitor extends BeanModel {
}
/**
* Gets Full Path-Name (Groups and Name)
* @returns {Promise<string>} Full path name of this monitor
* Gets the full path
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
*/
async getPathName() {
let path = this.name;
async getPath() {
const path = [ this.name ];
if (this.parent === null) {
return path;
@@ -1523,7 +1538,7 @@ class Monitor extends BeanModel {
let parent = await Monitor.getParent(this.id);
while (parent !== null) {
path = `${parent.name} / ${path}`;
path.unshift(parent.name);
parent = await Monitor.getParent(parent.id);
}
@@ -1553,7 +1568,7 @@ class Monitor extends BeanModel {
}
/**
* Unlinks all children of the the group monitor
* Unlinks all children of the group monitor
* @param {number} groupID ID of group to remove children of
* @returns {Promise<void>}
*/
@@ -1578,6 +1593,37 @@ class Monitor extends BeanModel {
const parentActive = await Monitor.isParentActive(parent.id);
return parent.active && parentActive;
}
/**
* 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;
}
/**
* 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

@@ -18,9 +18,15 @@ class StatusPage extends BeanModel {
* @param {Response} response Response object
* @param {string} indexHTML HTML to render
* @param {string} slug Status page slug
* @returns {void}
* @returns {Promise<void>}
*/
static async handleStatusPageResponse(response, indexHTML, slug) {
// Handle url with trailing slash (http://localhost:3001/status/)
// The slug comes from the route "/status/:slug". If the slug is empty, express converts it to "index.html"
if (slug === "index.html") {
slug = "default";
}
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
@@ -36,7 +42,7 @@ class StatusPage extends BeanModel {
* SSR for status pages
* @param {string} indexHTML HTML page to render
* @param {StatusPage} statusPage Status page populate HTML with
* @returns {void}
* @returns {Promise<string>} the rendered html
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
@@ -232,6 +238,7 @@ class StatusPage extends BeanModel {
description: this.description,
icon: this.getIcon(),
theme: this.theme,
autoRefreshInterval: this.autoRefreshInterval,
published: !!this.published,
showTags: !!this.show_tags,
domainNameList: this.getDomainNameList(),
@@ -254,6 +261,7 @@ class StatusPage extends BeanModel {
title: this.title,
description: this.description,
icon: this.getIcon(),
autoRefreshInterval: this.autoRefreshInterval,
theme: this.theme,
published: !!this.published,
showTags: !!this.show_tags,

View File

@@ -25,8 +25,14 @@ class User extends BeanModel {
* @returns {Promise<void>}
*/
async resetPassword(newPassword) {
await User.resetPassword(this.id, newPassword);
this.password = newPassword;
const hashedPassword = passwordHash.generate(newPassword);
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
hashedPassword,
this.id
]);
this.password = hashedPassword;
}
/**