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
commit 63a380326d
13 changed files with 152 additions and 66 deletions

View file

@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 18, 20.5 ]
node: [ 16, 20.5 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:

View file

@ -0,0 +1,18 @@
BEGIN TRANSACTION;
PRAGMA writable_schema = TRUE;
UPDATE
SQLITE_MASTER
SET
sql = replace(sql,
'monitor_id INTEGER NOT NULL',
'monitor_id INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE'
)
WHERE
name = 'monitor_tls_info'
AND type = 'table';
PRAGMA writable_schema = RESET;
COMMIT;

View file

@ -49,7 +49,7 @@
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
"setup": "git checkout 1.23.11 && npm ci --production && npm run download-dist",
"setup": "git checkout 1.23.13 && npm ci --production && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@ -79,7 +79,7 @@
"@louislam/sqlite3": "15.1.6",
"@vvo/tzdb": "^6.125.0",
"args-parser": "~1.3.0",
"axios": "~0.28.0",
"axios": "~0.28.1",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
@ -117,7 +117,7 @@
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~8.1.4",
"mysql2": "~3.6.2",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
@ -140,14 +140,14 @@
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socks-proxy-agent": "6.1.1",
"tar": "~6.1.11",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
"tough-cookie": "~4.1.3",
"ws": "^8.13.0"
},
"devDependencies": {
"@actions/github": "~5.0.1",
"@actions/github": "~5.1.1",
"@fortawesome/fontawesome-svg-core": "~1.2.36",
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
@ -171,7 +171,7 @@
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~2.4.3",
"dompurify": "~3.0.11",
"eslint": "~8.14.0",
"eslint-plugin-jsdoc": "~46.4.6",
"eslint-plugin-vue": "~8.7.1",
@ -192,7 +192,7 @@
"test": "~3.3.0",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~5.0.10",
"vite": "~5.2.8",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.0.15",
"vue": "~3.4.2",

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,30 +554,18 @@ 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") {
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,9 +89,10 @@ async function getBrowser() {
//headless: false,
executablePath,
});
}
return browser;
}
}
/**
* Get the current instance of the browser. If there isn't one, create it

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,6 +80,7 @@ class Prometheus {
}
}
if (heartbeat) {
try {
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
} catch (e) {
@ -99,6 +100,7 @@ class Prometheus {
log.error("prometheus", e);
}
}
}
/**
* Remove monitor from prometheus

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

View file

@ -5,6 +5,14 @@
<input id="hostname" v-model="$parent.notification.smtpHost" type="text" class="form-control" required>
</div>
<i18n-t tag="div" keypath="Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent" class="form-text">
<template #localhost>
<code>localhost</code>
</template>
<template #local_mta>
<a href="https://wikipedia.org/wiki/Mail_Transfer_Agent" target="_blank">{{ $t("locally configured mail transfer agent") }}</a>
</template>
</i18n-t>
<div class="mb-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="$parent.notification.smtpPort" type="number" class="form-control" required min="0" max="65535" step="1">

View file

@ -59,10 +59,29 @@ for (let lang in languageList) {
const rtlLangs = [ "he-IL", "fa", "ar-SY", "ur" ];
export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language
|| languageList[navigator.language.substring(0, 2)] && navigator.language.substring(0, 2)
|| "en";
/**
* Find the best matching locale to display
* If no locale can be matched, the default is "en"
* @returns {string} the locale that should be displayed
*/
export function currentLocale() {
for (const locale of [ localStorage.locale, navigator.language, ...navigator.languages ]) {
// localstorage might not have a value or there might not be a language in `navigator.language`
if (!locale) {
continue;
}
if (locale in messages) {
return locale;
}
// some locales are further specified such as "en-US".
// If we only have a generic locale for this, we can use it too
const genericLocale = locale.split("-")[0];
if (genericLocale in messages) {
return genericLocale;
}
}
return "en";
}
export const localeDirection = () => {
return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr";

View file

@ -63,6 +63,8 @@
"Friendly Name": "Friendly Name",
"URL": "URL",
"Hostname": "Hostname",
"locally configured mail transfer agent": "locally configured mail transfer agent",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Either enter the hostname of the server you want to connect to or {localhost} if you intend to use a {local_mta}",
"Port": "Port",
"Heartbeat Interval": "Heartbeat Interval",
"Request Timeout": "Request Timeout",

View file