Compare commits

..

3 Commits

Author SHA1 Message Date
Frank Elsinga
4b0e7de3e4 fixed linting issues 2024-06-04 01:23:51 +02:00
Frank Elsinga
2a556e714f implemented additional changes 2024-06-04 01:11:13 +02:00
mueller-ma
7f0d3a3043 Add custom html code to head
With this PR a new text field will be added that allows setting custom
html code to the `<head>` of a status page.
The implementation will be similar to
https://github.com/louislam/uptime-kuma/pull/2567/files, but with a
multi-line text field and without escaping any special chars.

For security reasons the env var `UPTIME_KUMA_ALLOW_CUSTOM_HTML` must be set to `1` to enable this feature.

This will allow tracking with most analytic platforms and has been
requested several times.

Closes #2818
2024-06-04 00:31:47 +02:00
177 changed files with 6621 additions and 15506 deletions

28
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Codespaces
You can modifiy Uptime Kuma in your browser without setting up a local development.
![image](https://github.com/louislam/uptime-kuma/assets/1336778/31d9f06d-dd0b-4405-8e0d-a96586ee4595)
1. Click `Code` -> `Create codespace on master`
2. Wait a few minutes until you see there are two exposed ports
3. Go to the `3000` url, see if it is working
![image](https://github.com/louislam/uptime-kuma/assets/1336778/909b2eb4-4c5e-44e4-ac26-6d20ed856e7f)
## Frontend
Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
You don't need to restart the frontend, unless you try to add a new frontend dependency.
## Backend
The backend does not automatically hot-reload.
You will need to restart the backend after changing something using these steps:
1. Click `Terminal`
2. Click `Codespaces: server-dev` in the right panel
3. Press `Ctrl + C` to stop the server
4. Press `Up` to run `npm run start-server-dev`
![image](https://github.com/louislam/uptime-kuma/assets/1336778/e0c0a350-fe46-4588-9f37-e053c85834d1)

View File

@@ -0,0 +1,23 @@
{
"image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"updateContentCommand": "npm ci",
"postCreateCommand": "",
"postAttachCommand": {
"frontend-dev": "npm run start-frontend-devcontainer",
"server-dev": "npm run start-server-dev",
"open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
},
"customizations": {
"vscode": {
"extensions": [
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"GitHub.copilot-chat"
]
}
},
"forwardPorts": [3000, 3001]
}

View File

@@ -1,6 +1,7 @@
/.idea
/node_modules
/data*
/cypress
/out
/test
/kubernetes
@@ -17,6 +18,7 @@ README.md
.vscode
.eslint*
.stylelint*
/.devcontainer
/.github
yarn.lock
app.json

View File

@@ -1,7 +1,8 @@
module.exports = {
ignorePatterns: [
"test/*.js",
"server/modules/*",
"test/cypress",
"server/modules/apicache/*",
"src/util.js"
],
root: true,

View File

@@ -15,14 +15,14 @@ on:
jobs:
auto-test:
needs: [ check-linters ]
needs: [ check-linters, e2e-test ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 18, 20 ]
node: [ 18, 20.5 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@@ -42,7 +42,7 @@ jobs:
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
armv7-simple-test:
needs: [ ]
needs: [ check-linters ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: ${{ github.repository == 'louislam/uptime-kuma' }}
@@ -77,7 +77,7 @@ jobs:
- run: npm run lint:prod
e2e-test:
needs: [ ]
needs: [ check-linters ]
runs-on: ARM64
steps:
- run: git config --global core.autocrlf false # Mainly for Windows

View File

@@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v8
with:
stale-issue-message: |-
We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity.
@@ -21,7 +21,7 @@ jobs:
exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,feature-request'
exempt-issue-assignees: 'louislam'
operations-per-run: 200
- uses: actions/stale@v9
- uses: actions/stale@v8
with:
stale-issue-message: |-
This issue was marked as `cannot-reproduce` by a maintainer.

View File

@@ -127,7 +127,7 @@ Different guidelines exist for different types of pull requests (PRs):
- `server/monitor-types/MONITORING_TYPE.js` is the core of each monitor.
the `async check(...)`-function should:
- throw an error for each fault that is detected with an actionable error message
- in the happy-path, you should set `heartbeat.msg` to a successful message and set `heartbeat.status = UP`
- in the happy-path, you should set `heartbeat.msg` to a successfull message and set `heartbeat.status = UP`
- `server/uptime-kuma-server.js` is where the monitoring backend needs to be registered.
*If you have an idea how we can skip this step, we would love to hear about it ^^*
- `src/pages/EditMonitor.vue` is the shared frontend users interact with.
@@ -236,6 +236,12 @@ The goal is to make the Uptime Kuma installation as easy as installing a mobile
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
### GitHub Codespaces
If you don't want to setup an local environment, you can now develop on GitHub Codespaces, read more:
https://github.com/louislam/uptime-kuma/tree/master/.devcontainer
## Git Branches
- `master`: 2.X.X development. If you want to add a new feature, your pull request should base on this.

28
config/cypress.config.js Normal file
View File

@@ -0,0 +1,28 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
projectId: "vyjuem",
e2e: {
experimentalStudio: true,
setupNodeEvents(on, config) {
},
fixturesFolder: "test/cypress/fixtures",
screenshotsFolder: "test/cypress/screenshots",
videosFolder: "test/cypress/videos",
downloadsFolder: "test/cypress/downloads",
supportFile: "test/cypress/support/e2e.js",
baseUrl: "http://localhost:3002",
defaultCommandTimeout: 10000,
pageLoadTimeout: 60000,
viewportWidth: 1920,
viewportHeight: 1080,
specPattern: [
"test/cypress/e2e/setup.cy.js",
"test/cypress/e2e/**/*.js"
],
},
env: {
baseUrl: "http://localhost:3002",
},
});

View File

@@ -0,0 +1,10 @@
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
supportFile: false,
specPattern: [
"test/cypress/unit/**/*.js"
],
}
});

View File

@@ -1,11 +1,11 @@
import { defineConfig, devices } from "@playwright/test";
const port = 30001;
export const url = `http://localhost:${port}`;
const url = `http://localhost:${port}`;
export default defineConfig({
// Look for test files in the "tests" directory, relative to this configuration file.
testDir: "../test/e2e/specs",
testDir: "../test/e2e",
outputDir: "../private/playwright-test-results",
fullyParallel: false,
locale: "en-US",
@@ -40,15 +40,9 @@ export default defineConfig({
// Configure projects for major browsers.
projects: [
{
name: "run-once setup",
testMatch: /setup-process\.once\.js/,
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "specs",
use: { ...devices["Desktop Chrome"] },
dependencies: [ "run-once setup" ],
},
/*
{
name: "firefox",
@@ -58,7 +52,7 @@ export default defineConfig({
// Run your local dev server before starting the tests.
webServer: {
command: `node extra/remove-playwright-test-data.js && cross-env NODE_ENV=development node server/server.js --port=${port} --data-dir=./data/playwright-test`,
command: `node extra/remove-playwright-test-data.js && node server/server.js --port=${port} --data-dir=./data/playwright-test`,
url,
reuseExistingServer: false,
cwd: "../",

View File

@@ -16,7 +16,9 @@ export default defineConfig({
},
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
"process.env": {},
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
},
plugins: [
vue(),

View File

@@ -0,0 +1,14 @@
exports.up = function (knex) {
// Insert column for custom HTML code
return knex.schema
.alterTable("status_page", function (table) {
table.text("custom_html").nullable().defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("status_page", function (table) {
table.dropColumn("custom_html");
});
};

View File

@@ -1,16 +0,0 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("snmp_oid").defaultTo(null);
table.enum("snmp_version", [ "1", "2c", "3" ]).defaultTo("2c");
table.string("json_path_operator").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("snmp_oid");
table.dropColumn("snmp_version");
table.dropColumn("json_path_operator");
});
};

View File

@@ -1,13 +0,0 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.boolean("cache_bust").notNullable().defaultTo(false);
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("cache_bust");
});
};

View File

@@ -1,12 +0,0 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.text("conditions").notNullable().defaultTo("[]");
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("conditions");
});
};

View File

@@ -3,6 +3,7 @@ FROM node:20-bookworm-slim AS base2-slim
ARG TARGETPLATFORM
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
# apprise = for notifications (From testing repo)
# sqlite3 = for debugging
# iputils-ping = for ping
# util-linux = for setpriv (Should be dropped in 2.0.0?)
@@ -11,10 +12,10 @@ ARG TARGETPLATFORM
# ca-certificates = keep the cert up-to-date
# sudo = for start service nscd with non-root user
# nscd = for better DNS caching
RUN apt update && \
apt --yes --no-install-recommends install \
sqlite3 \
ca-certificates \
RUN echo "deb http://deb.debian.org/debian testing main" >> /etc/apt/sources.list && \
apt update && \
apt --yes --no-install-recommends -t testing install apprise sqlite3 ca-certificates && \
apt --yes --no-install-recommends -t stable install \
iputils-ping \
util-linux \
dumb-init \
@@ -24,15 +25,6 @@ RUN apt update && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
# python3-paho-mqtt (#4859)
RUN curl http://ftp.debian.org/debian/pool/main/a/apprise/apprise_1.8.0-2_all.deb --output apprise.deb && \
apt update && \
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
rm -rf /var/lib/apt/lists/* && \
rm -f apprise.deb && \
apt --yes autoremove
# Install cloudflared
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
@@ -50,9 +42,7 @@ COPY ./docker/etc/sudoers /etc/sudoers
# Full Base Image
# MariaDB, Chromium and fonts
# Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch.
# FROM base2-slim AS base2
FROM louislam/uptime-kuma:base2-slim AS base2
FROM base2-slim AS base2
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1
RUN apt update && \
apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \

View File

@@ -4,6 +4,7 @@ const tar = require("tar");
const packageJSON = require("../package.json");
const fs = require("fs");
const rmSync = require("./fs-rmSync.js");
const version = packageJSON.version;
const filename = "dist.tar.gz";
@@ -28,9 +29,8 @@ function download(url) {
if (fs.existsSync("./dist")) {
if (fs.existsSync("./dist-backup")) {
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
rmSync("./dist-backup", {
recursive: true
});
}
@@ -43,9 +43,8 @@ function download(url) {
tarStream.on("close", () => {
if (fs.existsSync("./dist-backup")) {
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
rmSync("./dist-backup", {
recursive: true
});
}
console.log("Done");

23
extra/fs-rmSync.js Normal file
View File

@@ -0,0 +1,23 @@
const fs = require("fs");
/**
* Detect if `fs.rmSync` is available
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
* or the `recursive` property removing completely in the future Node.js version.
* See the link below.
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation information of `fs.rmdirSync`
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
* @param {fs.PathLike} path Valid types for path values in "fs".
* @param {fs.RmDirOptions} options options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
* @returns {void}
*/
const rmSync = (path, options) => {
if (typeof fs.rmSync === "function") {
if (options.recursive) {
options.force = true;
}
return fs.rmSync(path, options);
}
return fs.rmdirSync(path, options);
};
module.exports = rmSync;

View File

@@ -2,6 +2,7 @@
import fs from "fs";
import util from "util";
import rmSync from "../fs-rmSync.js";
/**
* Copy across the required language files
@@ -15,10 +16,7 @@ import util from "util";
*/
function copyFiles(langCode, baseLang) {
if (fs.existsSync("./languages")) {
fs.rmSync("./languages", {
recursive: true,
force: true,
});
rmSync("./languages", { recursive: true });
}
fs.mkdirSync("./languages");
@@ -95,9 +93,6 @@ console.log("Updating: " + langCode);
copyFiles(langCode, baseLangCode);
await updateLanguage(langCode, baseLangCode);
fs.rmSync("./languages", {
recursive: true,
force: true,
});
rmSync("./languages", { recursive: true });
console.log("Done. Fixing formatting by ESLint...");

12450
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,9 @@
"build": "vite build --config ./config/vite.config.js",
"test": "npm run test-backend && npm run test-e2e",
"test-with-build": "npm run build && npm test",
"test-backend": "cross-env TEST_BACKEND=1 node --test test/backend-test",
"test-backend": "node test/backend-test-entry.js",
"test-backend:14": "cross-env TEST_BACKEND=1 NODE_OPTIONS=\"--experimental-abortcontroller --no-warnings\" node--test test/backend-test",
"test-backend:18": "cross-env TEST_BACKEND=1 node --test test/backend-test",
"test-e2e": "playwright test --config ./config/playwright.config.js",
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
@@ -47,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.14 && 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",
@@ -68,15 +70,16 @@
"sort-contributors": "node extra/sort-contributors.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.8.22",
"@grpc/grpc-js": "~1.7.3",
"@louislam/ping": "~0.4.4-mod.1",
"@louislam/sqlite3": "15.1.6",
"@vvo/tzdb": "^6.125.0",
"args-parser": "~1.3.0",
"axios": "~0.28.1",
"axios-ntlm": "1.3.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"chardet": "~1.4.0",
@@ -86,14 +89,12 @@
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "~1.7.4",
"croner": "~8.1.0",
"croner": "~6.0.5",
"dayjs": "~1.11.5",
"dev-null": "^0.1.1",
"dotenv": "~16.0.3",
"express": "~4.21.0",
"express": "~4.19.2",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.7",
"feed": "^4.2.2",
"form-data": "~4.0.0",
"gamedig": "^4.2.0",
"html-escaper": "^3.0.3",
@@ -111,14 +112,12 @@
"knex": "^2.4.2",
"limiter": "~2.1.0",
"liquidjs": "^10.7.0",
"marked": "^14.0.0",
"mitt": "~3.0.1",
"mongodb": "~4.17.1",
"mqtt": "~4.3.7",
"mssql": "~11.0.0",
"mssql": "~8.1.4",
"mysql2": "~3.9.6",
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",
@@ -137,9 +136,10 @@
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"semver": "~7.5.4",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",
"socket.io": "~4.6.1",
"socket.io-client": "~4.6.1",
"socks-proxy-agent": "6.1.1",
"sqlite3": "~5.1.7",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
@@ -171,12 +171,13 @@
"cross-env": "~7.0.3",
"delay": "^5.0.0",
"dns2": "~2.0.1",
"dompurify": "~3.1.7",
"dompurify": "~3.0.11",
"eslint": "~8.14.0",
"eslint-plugin-jsdoc": "~46.4.6",
"eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10",
"get-port-please": "^3.1.1",
"marked": "~4.2.5",
"node-ssh": "~13.1.0",
"postcss-html": "~1.5.0",
"postcss-rtlcss": "~3.7.2",

View File

@@ -1,6 +1,7 @@
const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { log } = require("../src/util");
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
const { Settings } = require("./settings");
@@ -138,7 +139,7 @@ exports.basicAuth = async function (req, res, next) {
challenge: true,
});
const disabledAuth = await Settings.get("disableAuth");
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
middleware(req, res, next);

View File

@@ -1,7 +1,7 @@
const { setSetting, setting } = require("./util-server");
const axios = require("axios");
const compareVersions = require("compare-versions");
const { log } = require("../src/util");
const { Settings } = require("./settings");
exports.version = require("../package.json").version;
exports.latestVersion = null;
@@ -14,7 +14,7 @@ let interval;
exports.startInterval = () => {
let check = async () => {
if (await Settings.get("checkUpdate") === false) {
if (await setting("checkUpdate") === false) {
return;
}
@@ -28,7 +28,7 @@ exports.startInterval = () => {
res.data.slow = "1000.0.0";
}
let checkBeta = await Settings.get("checkBeta");
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
@@ -57,7 +57,7 @@ exports.startInterval = () => {
* @returns {Promise<void>}
*/
exports.enableCheckUpdate = async (value) => {
await Settings.set("checkUpdate", value);
await setSetting("checkUpdate", value);
clearInterval(interval);

View File

@@ -6,8 +6,8 @@ const { R } = require("redbean-node");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance();
const io = server.io;
const { setting } = require("./util-server");
const checkVersion = require("./check-version");
const { Settings } = require("./settings");
const Database = require("./database");
/**
@@ -158,8 +158,8 @@ async function sendInfo(socket, hideVersion = false) {
version,
latestVersion,
isContainer,
primaryBaseURL: await Settings.get("primaryBaseURL"),
dbType,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),
});
@@ -213,32 +213,6 @@ async function sendRemoteBrowserList(socket) {
return list;
}
/**
* Send list of monitor types to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<void>}
*/
async function sendMonitorTypeList(socket) {
const result = Object.entries(UptimeKumaServer.monitorTypeList).map(([ key, type ]) => {
return [ key, {
supportsConditions: type.supportsConditions,
conditionVariables: type.conditionVariables.map(v => {
return {
id: v.id,
operators: v.operators.map(o => {
return {
id: o.id,
caption: o.caption,
};
}),
};
}),
}];
});
io.to(socket.userID).emit("monitorTypeList", Object.fromEntries(result));
}
module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
@@ -248,5 +222,4 @@ module.exports = {
sendInfo,
sendDockerHostList,
sendRemoteBrowserList,
sendMonitorTypeList,
};

View File

@@ -1,11 +1,11 @@
const fs = require("fs");
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
const knex = require("knex");
const path = require("path");
const { EmbeddedMariaDB } = require("./embedded-mariadb");
const mysql = require("mysql2/promise");
const { Settings } = require("./settings");
/**
* Database & App Data Folder
@@ -223,11 +223,8 @@ class Database {
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
config = {
client: Dialect,
client: "sqlite3",
connection: {
filename: Database.sqlitePath,
acquireConnectionTimeout: acquireConnectionTimeout,
@@ -420,7 +417,7 @@ class Database {
* @deprecated
*/
static async patchSqlite() {
let version = parseInt(await Settings.get("database_version"));
let version = parseInt(await setting("database_version"));
if (! version) {
version = 0;
@@ -445,7 +442,7 @@ class Database {
log.info("db", `Patching ${sqlFile}`);
await Database.importSQLFile(sqlFile);
log.info("db", `Patched ${sqlFile}`);
await Settings.set("database_version", i);
await setSetting("database_version", i);
}
} catch (ex) {
await Database.close();
@@ -471,7 +468,7 @@ class Database {
*/
static async patchSqlite2() {
log.debug("db", "Database Patch 2.0 Process");
let databasePatchedFiles = await Settings.get("databasePatchedFiles");
let databasePatchedFiles = await setting("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
@@ -499,7 +496,7 @@ class Database {
process.exit(1);
}
await Settings.set("databasePatchedFiles", databasePatchedFiles);
await setSetting("databasePatchedFiles", databasePatchedFiles);
}
/**
@@ -512,27 +509,27 @@ class Database {
// Fix 1.13.0 empty slug bug
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
let title = await Settings.get("title");
let title = await setting("title");
if (title) {
log.info("database", "Migrating Status Page");
console.log("Migrating Status Page");
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
if (statusPageCheck !== null) {
log.info("database", "Migrating Status Page - Skip, default slug record is already existing");
console.log("Migrating Status Page - Skip, default slug record is already existing");
return;
}
let statusPage = R.dispense("status_page");
statusPage.slug = "default";
statusPage.title = title;
statusPage.description = await Settings.get("description");
statusPage.icon = await Settings.get("icon");
statusPage.theme = await Settings.get("statusPageTheme");
statusPage.published = !!await Settings.get("statusPagePublished");
statusPage.search_engine_index = !!await Settings.get("searchEngineIndex");
statusPage.show_tags = !!await Settings.get("statusPageTags");
statusPage.description = await setting("description");
statusPage.icon = await setting("icon");
statusPage.theme = await setting("statusPageTheme");
statusPage.published = !!await setting("statusPagePublished");
statusPage.search_engine_index = !!await setting("searchEngineIndex");
statusPage.show_tags = !!await setting("statusPageTags");
statusPage.password = null;
if (!statusPage.title) {
@@ -560,13 +557,13 @@ class Database {
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
// Migrate Entry Page if it is status page
let entryPage = await Settings.get("entryPage");
let entryPage = await setting("entryPage");
if (entryPage === "statusPage") {
await Settings.set("entryPage", "statusPage-default", "general");
await setSetting("entryPage", "statusPage-default", "general");
}
log.info("database", "Migrating Status Page - Done");
console.log("Migrating Status Page - Done");
}
}

View File

@@ -1,7 +1,7 @@
const { R } = require("redbean-node");
const { log } = require("../../src/util");
const { setSetting, setting } = require("../util-server");
const Database = require("../database");
const { Settings } = require("../settings");
const DEFAULT_KEEP_PERIOD = 180;
@@ -11,11 +11,11 @@ const DEFAULT_KEEP_PERIOD = 180;
*/
const clearOldData = async () => {
let period = await Settings.get("keepDataPeriodDays");
let period = await setting("keepDataPeriodDays");
// Set Default Period
if (period == null) {
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
period = DEFAULT_KEEP_PERIOD;
}
@@ -25,7 +25,7 @@ const clearOldData = async () => {
parsedPeriod = parseInt(period);
} catch (_) {
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
parsedPeriod = DEFAULT_KEEP_PERIOD;
}

View File

@@ -239,7 +239,19 @@ class Maintenance extends BeanModel {
this.beanMeta.status = "under-maintenance";
clearTimeout(this.beanMeta.durationTimeout);
let duration = this.inferDuration(customDuration);
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
let duration;
if (customDuration > 0) {
duration = customDuration;
} else if (this.end_date) {
let d = dayjs(this.end_date).diff(dayjs(), "second");
if (d < this.duration) {
duration = d * 1000;
}
} else {
duration = this.duration * 1000;
}
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
@@ -251,21 +263,9 @@ class Maintenance extends BeanModel {
};
// Create Cron
if (this.strategy === "recurring-interval") {
// For recurring-interval, Croner needs to have interval and startAt
const startDate = dayjs(this.startDate);
const [ hour, minute ] = this.startTime.split(":");
const startDateTime = startDate.hour(hour).minute(minute);
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
interval: this.interval_day * 24 * 60 * 60,
startAt: startDateTime.toISOString(),
}, startEvent);
} else {
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
}, startEvent);
}
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
}, startEvent);
// Continue if the maintenance is still in the window
let runningTimeslot = this.getRunningTimeslot();
@@ -311,24 +311,6 @@ class Maintenance extends BeanModel {
}
}
/**
* Calculate the maintenance duration
* @param {number} customDuration - The custom duration in milliseconds.
* @returns {number} The inferred duration in milliseconds.
*/
inferDuration(customDuration) {
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
if (customDuration > 0) {
return customDuration;
} else if (this.end_date) {
let d = dayjs(this.end_date).diff(dayjs(), "second");
if (d < this.duration) {
return d * 1000;
}
}
return this.duration * 1000;
}
/**
* Stop the maintenance
* @returns {void}
@@ -413,8 +395,10 @@ class Maintenance extends BeanModel {
} else if (!this.strategy.startsWith("recurring-")) {
this.cron = "";
} else if (this.strategy === "recurring-interval") {
// For intervals, the pattern is calculated in the run function as the interval-option is set
this.cron = "* * * * *";
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
this.duration = this.calcDuration();
log.debug("maintenance", "Cron: " + this.cron);
log.debug("maintenance", "Duration: " + this.duration);

View File

@@ -2,9 +2,9 @@ const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT, evaluateJsonQuery
SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, mssqlQuery, postgresQuery, mysqlQuery, httpNtlm, radius, grpcQuery,
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
@@ -17,6 +17,7 @@ const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { DockerHost } = require("../docker");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const { UptimeCalculator } = require("../uptime-calculator");
@@ -24,7 +25,6 @@ const { CookieJar } = require("tough-cookie");
const { HttpsCookieAgent } = require("http-cookie-agent/http");
const https = require("https");
const http = require("http");
const { Settings } = require("../settings");
const rootCertificates = rootCertificatesFingerprints();
@@ -72,12 +72,23 @@ class Monitor extends BeanModel {
/**
* Return an object that ready to parse to JSON
* @param {object} preloadData to prevent n+1 problems, we query the data in a batch outside of this function
* @param {boolean} includeSensitiveData Include sensitive data in
* JSON
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
toJSON(preloadData = {}, includeSensitiveData = true) {
async toJSON(includeSensitiveData = true) {
let notificationIDList = {};
let list = await R.find("monitor_notification", " monitor_id = ? ", [
this.id,
]);
for (let bean of list) {
notificationIDList[bean.notification_id] = true;
}
const tags = await this.getTags();
let screenshot = null;
@@ -85,7 +96,7 @@ class Monitor extends BeanModel {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
const path = preloadData.paths.get(this.id) || [];
const path = await this.getPath();
const pathName = path.join(" / ");
let data = {
@@ -95,15 +106,15 @@ class Monitor extends BeanModel {
path,
pathName,
parent: this.parent,
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
url: this.url,
method: this.method,
hostname: this.hostname,
port: this.port,
maxretries: this.maxretries,
weight: this.weight,
active: preloadData.activeStatus.get(this.id),
forceInactive: preloadData.forceInactive.get(this.id),
active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
type: this.type,
timeout: this.timeout,
interval: this.interval,
@@ -123,9 +134,9 @@ class Monitor extends BeanModel {
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList: preloadData.notifications.get(this.id) || {},
tags: preloadData.tags.get(this.id) || [],
maintenance: preloadData.maintenanceStatus.get(this.id),
notificationIDList,
tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
mqttCheckType: this.mqttCheckType,
@@ -149,12 +160,7 @@ class Monitor extends BeanModel {
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
kafkaProducerMessage: this.kafkaProducerMessage,
screenshot,
cacheBust: this.getCacheBust(),
remote_browser: this.remote_browser,
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
conditions: JSON.parse(this.conditions),
};
if (includeSensitiveData) {
@@ -191,6 +197,16 @@ class Monitor extends BeanModel {
return data;
}
/**
* Checks if the monitor is active based on itself and its parents
* @returns {Promise<boolean>} Is the monitor active?
*/
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);
return (this.active === 1) && parentActive;
}
/**
* Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>} List of tags on the
@@ -277,14 +293,6 @@ class Monitor extends BeanModel {
return Boolean(this.grpcEnableTls);
}
/**
* Parse to boolean
* @returns {boolean} if cachebusting is enabled
*/
getCacheBust() {
return Boolean(this.cacheBust);
}
/**
* Get accepted status codes
* @returns {object} Accepted status codes
@@ -490,14 +498,6 @@ class Monitor extends BeanModel {
options.data = bodyValue;
}
if (this.cacheBust) {
const randomFloatString = Math.random().toString(36);
const cacheBust = randomFloatString.substring(2);
options.params = {
uptime_kuma_cachebuster: cacheBust,
};
}
if (this.proxy_id) {
const proxy = await R.load("proxy", this.proxy_id);
@@ -598,15 +598,25 @@ class Monitor extends BeanModel {
} else if (this.type === "json-query") {
let data = res.data;
const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue);
if (status) {
bean.status = UP;
bean.msg = `JSON query passes (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`;
} else {
throw new Error(`JSON query does not pass (comparing ${response} ${this.jsonPathOperator} ${this.expectedValue})`);
// convert data to object
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);
let result = await expression.evaluate(data);
if (result.toString() === this.expectedValue) {
bean.msg += ", expected value is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
}
}
} else if (this.type === "port") {
@@ -652,7 +662,7 @@ class Monitor extends BeanModel {
} else if (this.type === "steam") {
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
const steamAPIKey = await Settings.get("steamAPIKey");
const steamAPIKey = await setting("steamAPIKey");
const filter = `addr\\${this.hostname}:${this.port}`;
if (!steamAPIKey) {
@@ -887,7 +897,6 @@ class Monitor extends BeanModel {
retries = 0;
} catch (error) {
if (error?.name === "CanceledError") {
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
} else {
@@ -960,6 +969,7 @@ class Monitor extends BeanModel {
} else if (bean.status === MAINTENANCE) {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
} else {
beatInterval = this.retryInterval;
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
}
@@ -1176,18 +1186,6 @@ class Monitor extends BeanModel {
return checkCertificateResult;
}
/**
* Checks if the monitor is active based on itself and its parents
* @param {number} monitorID ID of monitor to send
* @param {boolean} active is active
* @returns {Promise<boolean>} Is the monitor active?
*/
static async isActive(monitorID, active) {
const parentActive = await Monitor.isParentActive(monitorID);
return (active === 1) && parentActive;
}
/**
* Send statistics to clients
* @param {Server} io Socket server instance
@@ -1324,10 +1322,7 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
const heartbeatJSON = bean.toJSON();
const monitorData = [{ id: monitor.id,
active: monitor.active
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
@@ -1338,7 +1333,7 @@ class Monitor extends BeanModel {
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON);
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
@@ -1374,12 +1369,11 @@ class Monitor extends BeanModel {
return;
}
let notifyDays = await Settings.get("tlsExpiryNotifyDays");
let notifyDays = await setting("tlsExpiryNotifyDays");
if (notifyDays == null || !Array.isArray(notifyDays)) {
// Reset Default
await Settings.set("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
notifyDays = [ 7, 14, 21 ];
await Settings.set("tlsExpiryNotifyDays", notifyDays, "general");
}
if (Array.isArray(notifyDays)) {
@@ -1501,111 +1495,6 @@ class Monitor extends BeanModel {
}
}
/**
* Gets monitor notification of multiple monitor
* @param {Array} monitorIDs IDs of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async getMonitorNotification(monitorIDs) {
return await R.getAll(`
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
FROM monitor_notification
WHERE monitor_notification.monitor_id IN (?)
`, [
monitorIDs,
]);
}
/**
* Gets monitor tags of multiple monitor
* @param {Array} monitorIDs IDs of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (?)
`, [
monitorIDs,
]);
}
/**
* prepare preloaded data for efficient access
* @param {Array} monitorData IDs & active field of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async preparePreloadData(monitorData) {
const notificationsMap = new Map();
const tagsMap = new Map();
const maintenanceStatusMap = new Map();
const childrenIDsMap = new Map();
const activeStatusMap = new Map();
const forceInactiveMap = new Map();
const pathsMap = new Map();
if (monitorData.length > 0) {
const monitorIDs = monitorData.map(monitor => monitor.id);
const notifications = await Monitor.getMonitorNotification(monitorIDs);
const tags = await Monitor.getMonitorTag(monitorIDs);
const maintenanceStatuses = await Promise.all(monitorData.map(monitor => Monitor.isUnderMaintenance(monitor.id)));
const childrenIDs = await Promise.all(monitorData.map(monitor => Monitor.getAllChildrenIDs(monitor.id)));
const activeStatuses = await Promise.all(monitorData.map(monitor => Monitor.isActive(monitor.id, monitor.active)));
const forceInactiveStatuses = await Promise.all(monitorData.map(monitor => Monitor.isParentActive(monitor.id)));
const paths = await Promise.all(monitorData.map(monitor => Monitor.getAllPath(monitor.id, monitor.name)));
notifications.forEach(row => {
if (!notificationsMap.has(row.monitor_id)) {
notificationsMap.set(row.monitor_id, {});
}
notificationsMap.get(row.monitor_id)[row.notification_id] = true;
});
tags.forEach(row => {
if (!tagsMap.has(row.monitor_id)) {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
name: row.name,
color: row.color
});
});
monitorData.forEach((monitor, index) => {
maintenanceStatusMap.set(monitor.id, maintenanceStatuses[index]);
});
monitorData.forEach((monitor, index) => {
childrenIDsMap.set(monitor.id, childrenIDs[index]);
});
monitorData.forEach((monitor, index) => {
activeStatusMap.set(monitor.id, activeStatuses[index]);
});
monitorData.forEach((monitor, index) => {
forceInactiveMap.set(monitor.id, !forceInactiveStatuses[index]);
});
monitorData.forEach((monitor, index) => {
pathsMap.set(monitor.id, paths[index]);
});
}
return {
notifications: notificationsMap,
tags: tagsMap,
maintenanceStatus: maintenanceStatusMap,
childrenIDs: childrenIDsMap,
activeStatus: activeStatusMap,
forceInactive: forceInactiveMap,
paths: pathsMap,
};
}
/**
* Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get
@@ -1638,18 +1527,16 @@ class Monitor extends BeanModel {
/**
* Gets the full path
* @param {number} monitorID ID of the monitor to get
* @param {string} name of the monitor to get
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
*/
static async getAllPath(monitorID, name) {
const path = [ name ];
async getPath() {
const path = [ this.name ];
if (this.parent === null) {
return path;
}
let parent = await Monitor.getParent(monitorID);
let parent = await Monitor.getParent(this.id);
while (parent !== null) {
path.unshift(parent.name);
parent = await Monitor.getParent(parent.id);

View File

@@ -4,11 +4,6 @@ const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const googleAnalytics = require("../google-analytics");
const { marked } = require("marked");
const { Feed } = require("feed");
const config = require("../config");
const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util");
class StatusPage extends BeanModel {
@@ -18,24 +13,6 @@ class StatusPage extends BeanModel {
*/
static domainMappingList = { };
/**
* Handle responses to RSS pages
* @param {Response} response Response object
* @param {string} slug Status page slug
* @returns {Promise<void>}
*/
static async handleStatusPageRSSResponse(response, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [
slug
]);
if (statusPage) {
response.send(await StatusPage.renderRSS(statusPage, slug));
} else {
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
}
}
/**
* Handle responses to status page
* @param {Response} response Response object
@@ -61,38 +38,6 @@ class StatusPage extends BeanModel {
}
}
/**
* SSR for RSS feed
* @param {statusPage} statusPage object
* @param {slug} slug from router
* @returns {Promise<string>} the rendered html
*/
static async renderRSS(statusPage, slug) {
const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage);
let proto = config.isSSL ? "https" : "http";
let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`;
const feed = new Feed({
title: "uptime kuma rss feed",
description: `current status: ${statusDescription}`,
link: host,
language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
updated: new Date(), // optional, default = today
});
heartbeats.forEach(heartbeat => {
feed.addItem({
title: `${heartbeat.name} is down`,
description: `${heartbeat.name} has been down since ${heartbeat.time}`,
id: heartbeat.monitorID,
date: new Date(heartbeat.time),
});
});
return feed.rss2();
}
/**
* SSR for status pages
* @param {string} indexHTML HTML page to render
@@ -101,11 +46,7 @@ class StatusPage extends BeanModel {
*/
static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML);
const description155 = marked(statusPage.description ?? "")
.replace(/<[^>]+>/gm, "")
.trim()
.substring(0, 155);
const description155 = statusPage.description?.substring(0, 155) ?? "";
$("title").text(statusPage.title);
$("meta[name=description]").attr("content", description155);
@@ -125,6 +66,10 @@ class StatusPage extends BeanModel {
head.append($(escapedGoogleAnalyticsScript));
}
if (process.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1") {
head.append(statusPage.customHtml);
}
// OG Meta Tags
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
head.append(ogTitle);
@@ -152,109 +97,6 @@ class StatusPage extends BeanModel {
return $.root().html();
}
/**
* @param {heartbeats} heartbeats from getRSSPageData
* @returns {number} status_page constant from util.ts
*/
static overallStatus(heartbeats) {
if (heartbeats.length === 0) {
return -1;
}
let status = STATUS_PAGE_ALL_UP;
let hasUp = false;
for (let beat of heartbeats) {
if (beat.status === MAINTENANCE) {
return STATUS_PAGE_MAINTENANCE;
} else if (beat.status === UP) {
hasUp = true;
} else {
status = STATUS_PAGE_PARTIAL_DOWN;
}
}
if (! hasUp) {
status = STATUS_PAGE_ALL_DOWN;
}
return status;
}
/**
* @param {number} status from overallStatus
* @returns {string} description
*/
static getStatusDescription(status) {
if (status === -1) {
return "No Services";
}
if (status === STATUS_PAGE_ALL_UP) {
return "All Systems Operational";
}
if (status === STATUS_PAGE_PARTIAL_DOWN) {
return "Partially Degraded Service";
}
if (status === STATUS_PAGE_ALL_DOWN) {
return "Degraded Service";
}
// TODO: show the real maintenance information: title, description, time
if (status === MAINTENANCE) {
return "Under maintenance";
}
return "?";
}
/**
* Get all data required for RSS
* @param {StatusPage} statusPage Status page to get data for
* @returns {object} Status page data
*/
static async getRSSPageData(statusPage) {
// get all heartbeats that correspond to this statusPage
const config = await statusPage.toPublicJSON();
// Public Group List
const showTags = !!statusPage.show_tags;
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
statusPage.id
]);
let heartbeats = [];
for (let groupBean of list) {
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
for (const monitor of monitorGroup.monitorList) {
const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
if (heartbeat) {
heartbeats.push({
...monitor,
status: heartbeat.status,
time: heartbeat.time
});
}
}
}
// calculate RSS feed description
let status = StatusPage.overallStatus(heartbeats);
let statusDescription = StatusPage.getStatusDescription(status);
// keep only DOWN heartbeats in the RSS feed
heartbeats = heartbeats.filter(heartbeat => heartbeat.status === DOWN);
return {
heartbeats,
statusDescription
};
}
/**
* Get all status page data in one call
* @param {StatusPage} statusPage Status page to get data for
@@ -409,6 +251,7 @@ class StatusPage extends BeanModel {
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
customHtml: this.custom_html
};
}
@@ -432,6 +275,7 @@ class StatusPage extends BeanModel {
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
customHtml: this.custom_html
};
}

View File

@@ -1,6 +1,5 @@
let url = require("url");
let MemoryCache = require("./memory-cache");
const { log } = require("../../../src/util");
let t = {
ms: 1,
@@ -91,6 +90,24 @@ function ApiCache() {
instances.push(this);
this.id = instances.length;
/**
* Logs a message to the console if the `DEBUG` environment variable is set.
* @param {string} a The first argument to log.
* @param {string} b The second argument to log.
* @param {string} c The third argument to log.
* @param {string} d The fourth argument to log, and so on... (optional)
*
* Generated by Trelent
*/
function debug(a, b, c, d) {
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
return arg !== undefined;
});
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1;
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
}
/**
* Returns true if the given request and response should be logged.
* @param {Object} request The HTTP request object.
@@ -129,7 +146,7 @@ function ApiCache() {
let groupName = req.apicacheGroup;
if (groupName) {
log.debug("apicache", `group detected "${groupName}"`);
debug("group detected \"" + groupName + "\"");
let group = (index.groups[groupName] = index.groups[groupName] || []);
group.unshift(key);
}
@@ -195,7 +212,7 @@ function ApiCache() {
redis.hset(key, "duration", duration);
redis.expire(key, duration / 1000, expireCallback || function () {});
} catch (err) {
log.debug("apicache", `error in redis.hset(): ${err}`);
debug("[apicache] error in redis.hset()");
}
} else {
memCache.add(key, value, duration, expireCallback);
@@ -303,10 +320,10 @@ function ApiCache() {
// display log entry
let elapsed = new Date() - req.apicacheTimer;
log.debug("apicache", `adding cache entry for "${key}" @ ${strDuration} ${logDuration(elapsed)}`);
log.debug("apicache", `_apicache.headers: ${JSON.stringify(res._apicache.headers)}`);
log.debug("apicache", `res.getHeaders(): ${JSON.stringify(getSafeHeaders(res))}`);
log.debug("apicache", `cacheObject: ${JSON.stringify(cacheObject)}`);
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed));
debug("_apicache.headers: ", res._apicache.headers);
debug("res.getHeaders(): ", getSafeHeaders(res));
debug("cacheObject: ", cacheObject);
}
}
@@ -385,10 +402,10 @@ function ApiCache() {
let redis = globalOptions.redisClient;
if (group) {
log.debug("apicache", `clearing group "${target}"`);
debug("clearing group \"" + target + "\"");
group.forEach(function (key) {
log.debug("apicache", `clearing cached entry for "${key}"`);
debug("clearing cached entry for \"" + key + "\"");
clearTimeout(timers[key]);
delete timers[key];
if (!globalOptions.redisClient) {
@@ -397,7 +414,7 @@ function ApiCache() {
try {
redis.del(key);
} catch (err) {
log.info("apicache", "error in redis.del(\"" + key + "\")");
console.log("[apicache] error in redis.del(\"" + key + "\")");
}
}
index.all = index.all.filter(doesntMatch(key));
@@ -405,7 +422,7 @@ function ApiCache() {
delete index.groups[target];
} else if (target) {
log.debug("apicache", `clearing ${isAutomatic ? "expired" : "cached"} entry for "${target}"`);
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\"");
clearTimeout(timers[target]);
delete timers[target];
// clear actual cached entry
@@ -415,7 +432,7 @@ function ApiCache() {
try {
redis.del(target);
} catch (err) {
log.error("apicache", "error in redis.del(\"" + target + "\")");
console.log("[apicache] error in redis.del(\"" + target + "\")");
}
}
@@ -432,7 +449,7 @@ function ApiCache() {
}
});
} else {
log.debug("apicache", "clearing entire index");
debug("clearing entire index");
if (!redis) {
memCache.clear();
@@ -444,7 +461,7 @@ function ApiCache() {
try {
redis.del(key);
} catch (err) {
log.error("apicache", `error in redis.del("${key}"): ${err}`);
console.log("[apicache] error in redis.del(\"" + key + "\")");
}
});
}
@@ -511,7 +528,7 @@ function ApiCache() {
/**
* Get index of a group
* @param {string} group
* @param {string} group
* @returns {number}
*/
this.getIndex = function (group) {
@@ -526,9 +543,9 @@ function ApiCache() {
* Express middleware
* @param {(string|number)} strDuration Duration to cache responses
* for.
* @param {function(Object, Object):boolean} middlewareToggle
* @param {function(Object, Object):boolean} middlewareToggle
* @param {Object} localOptions Options for APICache
* @returns
* @returns
*/
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
let duration = instance.getDuration(strDuration);
@@ -735,7 +752,7 @@ function ApiCache() {
*/
let cache = function (req, res, next) {
function bypass() {
log.debug("apicache", "bypass detected, skipping cache.");
debug("bypass detected, skipping cache.");
return next();
}
@@ -788,7 +805,7 @@ function ApiCache() {
// send if cache hit from memory-cache
if (cached) {
let elapsed = new Date() - req.apicacheTimer;
log.debug("apicache", `sending cached (memory-cache) version of ${key} ${logDuration(elapsed)}`);
debug("sending cached (memory-cache) version of", key, logDuration(elapsed));
perf.hit(key);
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
@@ -800,7 +817,7 @@ function ApiCache() {
redis.hgetall(key, function (err, obj) {
if (!err && obj && obj.response) {
let elapsed = new Date() - req.apicacheTimer;
log.debug("apicache", "sending cached (redis) version of "+ key+" "+ logDuration(elapsed));
debug("sending cached (redis) version of", key, logDuration(elapsed));
perf.hit(key);
return sendCachedResponse(
@@ -842,7 +859,7 @@ function ApiCache() {
/**
* Process options
* @param {Object} options
* @param {Object} options
* @returns {Object}
*/
this.options = function (options) {
@@ -856,7 +873,7 @@ function ApiCache() {
}
if (globalOptions.trackPerformance) {
log.debug("apicache", "WARNING: using trackPerformance flag can cause high memory usage!");
debug("WARNING: using trackPerformance flag can cause high memory usage!");
}
return this;

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 CatButtes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,77 +0,0 @@
'use strict';
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/flags.js
module.exports.NTLMFLAG_NEGOTIATE_UNICODE = 1 << 0;
/* Indicates that Unicode strings are supported for use in security buffer
data. */
module.exports.NTLMFLAG_NEGOTIATE_OEM = 1 << 1;
/* Indicates that OEM strings are supported for use in security buffer data. */
module.exports.NTLMFLAG_REQUEST_TARGET = 1 << 2;
/* Requests that the server's authentication realm be included in the Type 2
message. */
/* unknown (1<<3) */
module.exports.NTLMFLAG_NEGOTIATE_SIGN = 1 << 4;
/* Specifies that authenticated communication between the client and server
should carry a digital signature (message integrity). */
module.exports.NTLMFLAG_NEGOTIATE_SEAL = 1 << 5;
/* Specifies that authenticated communication between the client and server
should be encrypted (message confidentiality). */
module.exports.NTLMFLAG_NEGOTIATE_DATAGRAM_STYLE = 1 << 6;
/* Indicates that datagram authentication is being used. */
module.exports.NTLMFLAG_NEGOTIATE_LM_KEY = 1 << 7;
/* Indicates that the LAN Manager session key should be used for signing and
sealing authenticated communications. */
module.exports.NTLMFLAG_NEGOTIATE_NETWARE = 1 << 8;
/* unknown purpose */
module.exports.NTLMFLAG_NEGOTIATE_NTLM_KEY = 1 << 9;
/* Indicates that NTLM authentication is being used. */
/* unknown (1<<10) */
module.exports.NTLMFLAG_NEGOTIATE_ANONYMOUS = 1 << 11;
/* Sent by the client in the Type 3 message to indicate that an anonymous
context has been established. This also affects the response fields. */
module.exports.NTLMFLAG_NEGOTIATE_DOMAIN_SUPPLIED = 1 << 12;
/* Sent by the client in the Type 1 message to indicate that a desired
authentication realm is included in the message. */
module.exports.NTLMFLAG_NEGOTIATE_WORKSTATION_SUPPLIED = 1 << 13;
/* Sent by the client in the Type 1 message to indicate that the client
workstation's name is included in the message. */
module.exports.NTLMFLAG_NEGOTIATE_LOCAL_CALL = 1 << 14;
/* Sent by the server to indicate that the server and client are on the same
machine. Implies that the client may use a pre-established local security
context rather than responding to the challenge. */
module.exports.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN = 1 << 15;
/* Indicates that authenticated communication between the client and server
should be signed with a "dummy" signature. */
module.exports.NTLMFLAG_TARGET_TYPE_DOMAIN = 1 << 16;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a domain. */
module.exports.NTLMFLAG_TARGET_TYPE_SERVER = 1 << 17;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a server. */
module.exports.NTLMFLAG_TARGET_TYPE_SHARE = 1 << 18;
/* Sent by the server in the Type 2 message to indicate that the target
authentication realm is a share. Presumably, this is for share-level
authentication. Usage is unclear. */
module.exports.NTLMFLAG_NEGOTIATE_NTLM2_KEY = 1 << 19;
/* Indicates that the NTLM2 signing and sealing scheme should be used for
protecting authenticated communications. */
module.exports.NTLMFLAG_REQUEST_INIT_RESPONSE = 1 << 20;
/* unknown purpose */
module.exports.NTLMFLAG_REQUEST_ACCEPT_RESPONSE = 1 << 21;
/* unknown purpose */
module.exports.NTLMFLAG_REQUEST_NONNT_SESSION_KEY = 1 << 22;
/* unknown purpose */
module.exports.NTLMFLAG_NEGOTIATE_TARGET_INFO = 1 << 23;
/* Sent by the server in the Type 2 message to indicate that it is including a
Target Information block in the message. */
/* unknown (1<24) */
/* unknown (1<25) */
/* unknown (1<26) */
/* unknown (1<27) */
/* unknown (1<28) */
module.exports.NTLMFLAG_NEGOTIATE_128 = 1 << 29;
/* Indicates that 128-bit encryption is supported. */
module.exports.NTLMFLAG_NEGOTIATE_KEY_EXCHANGE = 1 << 30;
/* Indicates that the client will provide an encrypted master key in
the "Session Key" field of the Type 3 message. */
module.exports.NTLMFLAG_NEGOTIATE_56 = 1 << 31;
//# sourceMappingURL=flags.js.map

View File

@@ -1,122 +0,0 @@
'use strict';
// Original source at https://github.com/elasticio/node-ntlm-client/blob/master/lib/hash.js
var crypto = require('crypto');
function createLMResponse(challenge, lmhash) {
var buf = new Buffer.alloc(24), pwBuffer = new Buffer.alloc(21).fill(0);
lmhash.copy(pwBuffer);
calculateDES(pwBuffer.slice(0, 7), challenge).copy(buf);
calculateDES(pwBuffer.slice(7, 14), challenge).copy(buf, 8);
calculateDES(pwBuffer.slice(14), challenge).copy(buf, 16);
return buf;
}
function createLMHash(password) {
var buf = new Buffer.alloc(16), pwBuffer = new Buffer.alloc(14), magicKey = new Buffer.from('KGS!@#$%', 'ascii');
if (password.length > 14) {
buf.fill(0);
return buf;
}
pwBuffer.fill(0);
pwBuffer.write(password.toUpperCase(), 0, 'ascii');
return Buffer.concat([
calculateDES(pwBuffer.slice(0, 7), magicKey),
calculateDES(pwBuffer.slice(7), magicKey)
]);
}
function calculateDES(key, message) {
var desKey = new Buffer.alloc(8);
desKey[0] = key[0] & 0xFE;
desKey[1] = ((key[0] << 7) & 0xFF) | (key[1] >> 1);
desKey[2] = ((key[1] << 6) & 0xFF) | (key[2] >> 2);
desKey[3] = ((key[2] << 5) & 0xFF) | (key[3] >> 3);
desKey[4] = ((key[3] << 4) & 0xFF) | (key[4] >> 4);
desKey[5] = ((key[4] << 3) & 0xFF) | (key[5] >> 5);
desKey[6] = ((key[5] << 2) & 0xFF) | (key[6] >> 6);
desKey[7] = (key[6] << 1) & 0xFF;
for (var i = 0; i < 8; i++) {
var parity = 0;
for (var j = 1; j < 8; j++) {
parity += (desKey[i] >> j) % 2;
}
desKey[i] |= (parity % 2) === 0 ? 1 : 0;
}
var des = crypto.createCipheriv('DES-ECB', desKey, '');
return des.update(message);
}
function createNTLMResponse(challenge, ntlmhash) {
var buf = new Buffer.alloc(24), ntlmBuffer = new Buffer.alloc(21).fill(0);
ntlmhash.copy(ntlmBuffer);
calculateDES(ntlmBuffer.slice(0, 7), challenge).copy(buf);
calculateDES(ntlmBuffer.slice(7, 14), challenge).copy(buf, 8);
calculateDES(ntlmBuffer.slice(14), challenge).copy(buf, 16);
return buf;
}
function createNTLMHash(password) {
var md4sum = crypto.createHash('md4');
md4sum.update(new Buffer.from(password, 'ucs2'));
return md4sum.digest();
}
function createNTLMv2Hash(ntlmhash, username, authTargetName) {
var hmac = crypto.createHmac('md5', ntlmhash);
hmac.update(new Buffer.from(username.toUpperCase() + authTargetName, 'ucs2'));
return hmac.digest();
}
function createLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
var buf = new Buffer.alloc(24), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
//server challenge
type2message.challenge.copy(buf, 8);
//client nonce
buf.write(nonce || createPseudoRandomValue(16), 16, 'hex');
//create hash
hmac.update(buf.slice(8));
var hashedBuffer = hmac.digest();
hashedBuffer.copy(buf);
return buf;
}
function createNTLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
var buf = new Buffer.alloc(48 + type2message.targetInfo.buffer.length), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
//the first 8 bytes are spare to store the hashed value before the blob
//server challenge
type2message.challenge.copy(buf, 8);
//blob signature
buf.writeUInt32BE(0x01010000, 16);
//reserved
buf.writeUInt32LE(0, 20);
//timestamp
//TODO: we are loosing precision here since js is not able to handle those large integers
// maybe think about a different solution here
// 11644473600000 = diff between 1970 and 1601
var timestamp = ((Date.now() + 11644473600000) * 10000).toString(16);
var timestampLow = Number('0x' + timestamp.substring(Math.max(0, timestamp.length - 8)));
var timestampHigh = Number('0x' + timestamp.substring(0, Math.max(0, timestamp.length - 8)));
buf.writeUInt32LE(timestampLow, 24, false);
buf.writeUInt32LE(timestampHigh, 28, false);
//random client nonce
buf.write(nonce || createPseudoRandomValue(16), 32, 'hex');
//zero
buf.writeUInt32LE(0, 40);
//complete target information block from type 2 message
type2message.targetInfo.buffer.copy(buf, 44);
//zero
buf.writeUInt32LE(0, 44 + type2message.targetInfo.buffer.length);
hmac.update(buf.slice(8));
var hashedBuffer = hmac.digest();
hashedBuffer.copy(buf);
return buf;
}
function createPseudoRandomValue(length) {
var str = '';
while (str.length < length) {
str += Math.floor(Math.random() * 16).toString(16);
}
return str;
}
module.exports = {
createLMHash: createLMHash,
createNTLMHash: createNTLMHash,
createLMResponse: createLMResponse,
createNTLMResponse: createNTLMResponse,
createLMv2Response: createLMv2Response,
createNTLMv2Response: createNTLMv2Response,
createPseudoRandomValue: createPseudoRandomValue
};
//# sourceMappingURL=hash.js.map

View File

@@ -1,220 +0,0 @@
'use strict';
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/ntlm.js
var os = require('os'), flags = require('./flags'), hash = require('./hash');
var NTLMSIGNATURE = "NTLMSSP\0";
function createType1Message(workstation, target) {
var dataPos = 32, pos = 0, buf = new Buffer.alloc(1024);
workstation = workstation === undefined ? os.hostname() : workstation;
target = target === undefined ? '' : target;
//signature
buf.write(NTLMSIGNATURE, pos, NTLMSIGNATURE.length, 'ascii');
pos += NTLMSIGNATURE.length;
//message type
buf.writeUInt32LE(1, pos);
pos += 4;
//flags
buf.writeUInt32LE(flags.NTLMFLAG_NEGOTIATE_OEM |
flags.NTLMFLAG_REQUEST_TARGET |
flags.NTLMFLAG_NEGOTIATE_NTLM_KEY |
flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY |
flags.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN, pos);
pos += 4;
//domain security buffer
buf.writeUInt16LE(target.length, pos);
pos += 2;
buf.writeUInt16LE(target.length, pos);
pos += 2;
buf.writeUInt32LE(target.length === 0 ? 0 : dataPos, pos);
pos += 4;
if (target.length > 0) {
dataPos += buf.write(target, dataPos, 'ascii');
}
//workstation security buffer
buf.writeUInt16LE(workstation.length, pos);
pos += 2;
buf.writeUInt16LE(workstation.length, pos);
pos += 2;
buf.writeUInt32LE(workstation.length === 0 ? 0 : dataPos, pos);
pos += 4;
if (workstation.length > 0) {
dataPos += buf.write(workstation, dataPos, 'ascii');
}
return 'NTLM ' + buf.toString('base64', 0, dataPos);
}
function decodeType2Message(str) {
if (str === undefined) {
throw new Error('Invalid argument');
}
//convenience
if (Object.prototype.toString.call(str) !== '[object String]') {
if (str.hasOwnProperty('headers') && str.headers.hasOwnProperty('www-authenticate')) {
str = str.headers['www-authenticate'];
}
else {
throw new Error('Invalid argument');
}
}
var ntlmMatch = /^NTLM ([^,\s]+)/.exec(str);
if (ntlmMatch) {
str = ntlmMatch[1];
}
var buf = new Buffer.from(str, 'base64'), obj = {};
//check signature
if (buf.toString('ascii', 0, NTLMSIGNATURE.length) !== NTLMSIGNATURE) {
throw new Error('Invalid message signature: ' + str);
}
//check message type
if (buf.readUInt32LE(NTLMSIGNATURE.length) !== 2) {
throw new Error('Invalid message type (no type 2)');
}
//read flags
obj.flags = buf.readUInt32LE(20);
obj.encoding = (obj.flags & flags.NTLMFLAG_NEGOTIATE_OEM) ? 'ascii' : 'ucs2';
obj.version = (obj.flags & flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY) ? 2 : 1;
obj.challenge = buf.slice(24, 32);
//read target name
obj.targetName = (function () {
var length = buf.readUInt16LE(12);
//skipping allocated space
var offset = buf.readUInt32LE(16);
if (length === 0) {
return '';
}
if ((offset + length) > buf.length || offset < 32) {
throw new Error('Bad type 2 message');
}
return buf.toString(obj.encoding, offset, offset + length);
})();
//read target info
if (obj.flags & flags.NTLMFLAG_NEGOTIATE_TARGET_INFO) {
obj.targetInfo = (function () {
var info = {};
var length = buf.readUInt16LE(40);
//skipping allocated space
var offset = buf.readUInt32LE(44);
var targetInfoBuffer = new Buffer.alloc(length);
buf.copy(targetInfoBuffer, 0, offset, offset + length);
if (length === 0) {
return info;
}
if ((offset + length) > buf.length || offset < 32) {
throw new Error('Bad type 2 message');
}
var pos = offset;
while (pos < (offset + length)) {
var blockType = buf.readUInt16LE(pos);
pos += 2;
var blockLength = buf.readUInt16LE(pos);
pos += 2;
if (blockType === 0) {
//reached the terminator subblock
break;
}
var blockTypeStr = void 0;
switch (blockType) {
case 1:
blockTypeStr = 'SERVER';
break;
case 2:
blockTypeStr = 'DOMAIN';
break;
case 3:
blockTypeStr = 'FQDN';
break;
case 4:
blockTypeStr = 'DNS';
break;
case 5:
blockTypeStr = 'PARENT_DNS';
break;
default:
blockTypeStr = '';
break;
}
if (blockTypeStr) {
info[blockTypeStr] = buf.toString('ucs2', pos, pos + blockLength);
}
pos += blockLength;
}
return {
parsed: info,
buffer: targetInfoBuffer
};
})();
}
return obj;
}
function createType3Message(type2Message, username, password, workstation, target) {
var dataPos = 52, buf = new Buffer.alloc(1024);
if (workstation === undefined) {
workstation = os.hostname();
}
if (target === undefined) {
target = type2Message.targetName;
}
//signature
buf.write(NTLMSIGNATURE, 0, NTLMSIGNATURE.length, 'ascii');
//message type
buf.writeUInt32LE(3, 8);
if (type2Message.version === 2) {
dataPos = 64;
var ntlmHash = hash.createNTLMHash(password), nonce = hash.createPseudoRandomValue(16), lmv2 = hash.createLMv2Response(type2Message, username, ntlmHash, nonce, target), ntlmv2 = hash.createNTLMv2Response(type2Message, username, ntlmHash, nonce, target);
//lmv2 security buffer
buf.writeUInt16LE(lmv2.length, 12);
buf.writeUInt16LE(lmv2.length, 14);
buf.writeUInt32LE(dataPos, 16);
lmv2.copy(buf, dataPos);
dataPos += lmv2.length;
//ntlmv2 security buffer
buf.writeUInt16LE(ntlmv2.length, 20);
buf.writeUInt16LE(ntlmv2.length, 22);
buf.writeUInt32LE(dataPos, 24);
ntlmv2.copy(buf, dataPos);
dataPos += ntlmv2.length;
}
else {
var lmHash = hash.createLMHash(password), ntlmHash = hash.createNTLMHash(password), lm = hash.createLMResponse(type2Message.challenge, lmHash), ntlm = hash.createNTLMResponse(type2Message.challenge, ntlmHash);
//lm security buffer
buf.writeUInt16LE(lm.length, 12);
buf.writeUInt16LE(lm.length, 14);
buf.writeUInt32LE(dataPos, 16);
lm.copy(buf, dataPos);
dataPos += lm.length;
//ntlm security buffer
buf.writeUInt16LE(ntlm.length, 20);
buf.writeUInt16LE(ntlm.length, 22);
buf.writeUInt32LE(dataPos, 24);
ntlm.copy(buf, dataPos);
dataPos += ntlm.length;
}
//target name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 28);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 30);
buf.writeUInt32LE(dataPos, 32);
dataPos += buf.write(target, dataPos, type2Message.encoding);
//user name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 36);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 38);
buf.writeUInt32LE(dataPos, 40);
dataPos += buf.write(username, dataPos, type2Message.encoding);
//workstation name security buffer
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 44);
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 46);
buf.writeUInt32LE(dataPos, 48);
dataPos += buf.write(workstation, dataPos, type2Message.encoding);
if (type2Message.version === 2) {
//session key security buffer
buf.writeUInt16LE(0, 52);
buf.writeUInt16LE(0, 54);
buf.writeUInt32LE(0, 56);
//flags
buf.writeUInt32LE(type2Message.flags, 60);
}
return 'NTLM ' + buf.toString('base64', 0, dataPos);
}
module.exports = {
createType1Message: createType1Message,
decodeType2Message: decodeType2Message,
createType3Message: createType3Message
};
//# sourceMappingURL=ntlm.js.map

View File

@@ -1,127 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NtlmClient = void 0;
var axios_1 = __importDefault(require("axios"));
var ntlm = __importStar(require("./ntlm"));
var https = __importStar(require("https"));
var http = __importStar(require("http"));
var dev_null_1 = __importDefault(require("dev-null"));
/**
* @param credentials An NtlmCredentials object containing the username and password
* @param AxiosConfig The Axios config for the instance you wish to create
*
* @returns This function returns an axios instance configured to use the provided credentials
*/
function NtlmClient(credentials, AxiosConfig) {
var _this = this;
var config = AxiosConfig !== null && AxiosConfig !== void 0 ? AxiosConfig : {};
if (!config.httpAgent) {
config.httpAgent = new http.Agent({ keepAlive: true });
}
if (!config.httpsAgent) {
config.httpsAgent = new https.Agent({ keepAlive: true });
}
var client = axios_1.default.create(config);
client.interceptors.response.use(function (response) {
return response;
}, function (err) { return __awaiter(_this, void 0, void 0, function () {
var error, t1Msg, t2Msg, t3Msg, stream_1;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
error = err.response;
if (!(error && error.status === 401
&& error.headers['www-authenticate']
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
// This length check is a hack because SharePoint is awkward and will
// include the Negotiate option when responding with the T2 message
// There is nore we could do to ensure we are processing correctly,
// but this is the easiest option for now
if (error.headers['www-authenticate'].length < 50) {
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
error.config.headers["Authorization"] = t1Msg;
}
else {
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
error.config.headers["X-retry"] = "false";
error.config.headers["Authorization"] = t3Msg;
}
if (!(error.config.responseType === "stream")) return [3 /*break*/, 2];
stream_1 = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data;
if (!(stream_1 && !stream_1.readableEnded)) return [3 /*break*/, 2];
return [4 /*yield*/, new Promise(function (resolve) {
stream_1.pipe((0, dev_null_1.default)());
stream_1.once('close', resolve);
})];
case 1:
_b.sent();
_b.label = 2;
case 2: return [2 /*return*/, client(error.config)];
case 3: throw err;
}
});
}); });
return client;
}
exports.NtlmClient = NtlmClient;
//# sourceMappingURL=ntlmClient.js.map

View File

@@ -1,71 +0,0 @@
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("./expression");
const { operatorMap } = require("./operators");
/**
* @param {ConditionExpression} expression Expression to evaluate
* @param {object} context Context to evaluate against; These are values for variables in the expression
* @returns {boolean} Whether the expression evaluates true or false
* @throws {Error}
*/
function evaluateExpression(expression, context) {
/**
* @type {import("./operators").ConditionOperator|null}
*/
const operator = operatorMap.get(expression.operator) || null;
if (operator === null) {
throw new Error("Unexpected expression operator ID '" + expression.operator + "'. Expected one of [" + operatorMap.keys().join(",") + "]");
}
if (!Object.prototype.hasOwnProperty.call(context, expression.variable)) {
throw new Error("Variable missing in context: " + expression.variable);
}
return operator.test(context[expression.variable], expression.value);
}
/**
* @param {ConditionExpressionGroup} group Group of expressions to evaluate
* @param {object} context Context to evaluate against; These are values for variables in the expression
* @returns {boolean} Whether the group evaluates true or false
* @throws {Error}
*/
function evaluateExpressionGroup(group, context) {
if (!group.children.length) {
throw new Error("ConditionExpressionGroup must contain at least one child.");
}
let result = null;
for (const child of group.children) {
let childResult;
if (child instanceof ConditionExpression) {
childResult = evaluateExpression(child, context);
} else if (child instanceof ConditionExpressionGroup) {
childResult = evaluateExpressionGroup(child, context);
} else {
throw new Error("Invalid child type in ConditionExpressionGroup. Expected ConditionExpression or ConditionExpressionGroup");
}
if (result === null) {
result = childResult; // Initialize result with the first child's result
} else if (child.andOr === LOGICAL.OR) {
result = result || childResult;
} else if (child.andOr === LOGICAL.AND) {
result = result && childResult;
} else {
throw new Error("Invalid logical operator in child of ConditionExpressionGroup. Expected 'and' or 'or'. Got '" + group.andOr + "'");
}
}
if (result === null) {
throw new Error("ConditionExpressionGroup did not result in a boolean.");
}
return result;
}
module.exports = {
evaluateExpression,
evaluateExpressionGroup,
};

View File

@@ -1,111 +0,0 @@
/**
* @readonly
* @enum {string}
*/
const LOGICAL = {
AND: "and",
OR: "or",
};
/**
* Recursively processes an array of raw condition objects and populates the given parent group with
* corresponding ConditionExpression or ConditionExpressionGroup instances.
* @param {Array} conditions Array of raw condition objects, where each object represents either a group or an expression.
* @param {ConditionExpressionGroup} parentGroup The parent group to which the instantiated ConditionExpression or ConditionExpressionGroup objects will be added.
* @returns {void}
*/
function processMonitorConditions(conditions, parentGroup) {
conditions.forEach(condition => {
const andOr = condition.andOr === LOGICAL.OR ? LOGICAL.OR : LOGICAL.AND;
if (condition.type === "group") {
const group = new ConditionExpressionGroup([], andOr);
// Recursively process the group's children
processMonitorConditions(condition.children, group);
parentGroup.children.push(group);
} else if (condition.type === "expression") {
const expression = new ConditionExpression(condition.variable, condition.operator, condition.value, andOr);
parentGroup.children.push(expression);
}
});
}
class ConditionExpressionGroup {
/**
* @type {ConditionExpressionGroup[]|ConditionExpression[]} Groups and/or expressions to test
*/
children = [];
/**
* @type {LOGICAL} Connects group result with previous group/expression results
*/
andOr;
/**
* @param {ConditionExpressionGroup[]|ConditionExpression[]} children Groups and/or expressions to test
* @param {LOGICAL} andOr Connects group result with previous group/expression results
*/
constructor(children = [], andOr = LOGICAL.AND) {
this.children = children;
this.andOr = andOr;
}
/**
* @param {Monitor} monitor Monitor instance
* @returns {ConditionExpressionGroup|null} A ConditionExpressionGroup with the Monitor's conditions
*/
static fromMonitor(monitor) {
const conditions = JSON.parse(monitor.conditions);
if (conditions.length === 0) {
return null;
}
const root = new ConditionExpressionGroup();
processMonitorConditions(conditions, root);
return root;
}
}
class ConditionExpression {
/**
* @type {string} ID of variable
*/
variable;
/**
* @type {string} ID of operator
*/
operator;
/**
* @type {string} Value to test with the operator
*/
value;
/**
* @type {LOGICAL} Connects expression result with previous group/expression results
*/
andOr;
/**
* @param {string} variable ID of variable to test against
* @param {string} operator ID of operator to test the variable with
* @param {string} value Value to test with the operator
* @param {LOGICAL} andOr Connects expression result with previous group/expression results
*/
constructor(variable, operator, value, andOr = LOGICAL.AND) {
this.variable = variable;
this.operator = operator;
this.value = value;
this.andOr = andOr;
}
}
module.exports = {
LOGICAL,
ConditionExpressionGroup,
ConditionExpression,
};

View File

@@ -1,318 +0,0 @@
class ConditionOperator {
id = undefined;
caption = undefined;
/**
* @type {mixed} variable
* @type {mixed} value
*/
test(variable, value) {
throw new Error("You need to override test()");
}
}
const OP_STR_EQUALS = "equals";
const OP_STR_NOT_EQUALS = "not_equals";
const OP_CONTAINS = "contains";
const OP_NOT_CONTAINS = "not_contains";
const OP_STARTS_WITH = "starts_with";
const OP_NOT_STARTS_WITH = "not_starts_with";
const OP_ENDS_WITH = "ends_with";
const OP_NOT_ENDS_WITH = "not_ends_with";
const OP_NUM_EQUALS = "num_equals";
const OP_NUM_NOT_EQUALS = "num_not_equals";
const OP_LT = "lt";
const OP_GT = "gt";
const OP_LTE = "lte";
const OP_GTE = "gte";
/**
* Asserts a variable is equal to a value.
*/
class StringEqualsOperator extends ConditionOperator {
id = OP_STR_EQUALS;
caption = "equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable === value;
}
}
/**
* Asserts a variable is not equal to a value.
*/
class StringNotEqualsOperator extends ConditionOperator {
id = OP_STR_NOT_EQUALS;
caption = "not equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable !== value;
}
}
/**
* Asserts a variable contains a value.
* Handles both Array and String variable types.
*/
class ContainsOperator extends ConditionOperator {
id = OP_CONTAINS;
caption = "contains";
/**
* @inheritdoc
*/
test(variable, value) {
if (Array.isArray(variable)) {
return variable.includes(value);
}
return variable.indexOf(value) !== -1;
}
}
/**
* Asserts a variable does not contain a value.
* Handles both Array and String variable types.
*/
class NotContainsOperator extends ConditionOperator {
id = OP_NOT_CONTAINS;
caption = "not contains";
/**
* @inheritdoc
*/
test(variable, value) {
if (Array.isArray(variable)) {
return !variable.includes(value);
}
return variable.indexOf(value) === -1;
}
}
/**
* Asserts a variable starts with a value.
*/
class StartsWithOperator extends ConditionOperator {
id = OP_STARTS_WITH;
caption = "starts with";
/**
* @inheritdoc
*/
test(variable, value) {
return variable.startsWith(value);
}
}
/**
* Asserts a variable does not start with a value.
*/
class NotStartsWithOperator extends ConditionOperator {
id = OP_NOT_STARTS_WITH;
caption = "not starts with";
/**
* @inheritdoc
*/
test(variable, value) {
return !variable.startsWith(value);
}
}
/**
* Asserts a variable ends with a value.
*/
class EndsWithOperator extends ConditionOperator {
id = OP_ENDS_WITH;
caption = "ends with";
/**
* @inheritdoc
*/
test(variable, value) {
return variable.endsWith(value);
}
}
/**
* Asserts a variable does not end with a value.
*/
class NotEndsWithOperator extends ConditionOperator {
id = OP_NOT_ENDS_WITH;
caption = "not ends with";
/**
* @inheritdoc
*/
test(variable, value) {
return !variable.endsWith(value);
}
}
/**
* Asserts a numeric variable is equal to a value.
*/
class NumberEqualsOperator extends ConditionOperator {
id = OP_NUM_EQUALS;
caption = "equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable === Number(value);
}
}
/**
* Asserts a numeric variable is not equal to a value.
*/
class NumberNotEqualsOperator extends ConditionOperator {
id = OP_NUM_NOT_EQUALS;
caption = "not equals";
/**
* @inheritdoc
*/
test(variable, value) {
return variable !== Number(value);
}
}
/**
* Asserts a variable is less than a value.
*/
class LessThanOperator extends ConditionOperator {
id = OP_LT;
caption = "less than";
/**
* @inheritdoc
*/
test(variable, value) {
return variable < Number(value);
}
}
/**
* Asserts a variable is greater than a value.
*/
class GreaterThanOperator extends ConditionOperator {
id = OP_GT;
caption = "greater than";
/**
* @inheritdoc
*/
test(variable, value) {
return variable > Number(value);
}
}
/**
* Asserts a variable is less than or equal to a value.
*/
class LessThanOrEqualToOperator extends ConditionOperator {
id = OP_LTE;
caption = "less than or equal to";
/**
* @inheritdoc
*/
test(variable, value) {
return variable <= Number(value);
}
}
/**
* Asserts a variable is greater than or equal to a value.
*/
class GreaterThanOrEqualToOperator extends ConditionOperator {
id = OP_GTE;
caption = "greater than or equal to";
/**
* @inheritdoc
*/
test(variable, value) {
return variable >= Number(value);
}
}
const operatorMap = new Map([
[ OP_STR_EQUALS, new StringEqualsOperator ],
[ OP_STR_NOT_EQUALS, new StringNotEqualsOperator ],
[ OP_CONTAINS, new ContainsOperator ],
[ OP_NOT_CONTAINS, new NotContainsOperator ],
[ OP_STARTS_WITH, new StartsWithOperator ],
[ OP_NOT_STARTS_WITH, new NotStartsWithOperator ],
[ OP_ENDS_WITH, new EndsWithOperator ],
[ OP_NOT_ENDS_WITH, new NotEndsWithOperator ],
[ OP_NUM_EQUALS, new NumberEqualsOperator ],
[ OP_NUM_NOT_EQUALS, new NumberNotEqualsOperator ],
[ OP_LT, new LessThanOperator ],
[ OP_GT, new GreaterThanOperator ],
[ OP_LTE, new LessThanOrEqualToOperator ],
[ OP_GTE, new GreaterThanOrEqualToOperator ],
]);
const defaultStringOperators = [
operatorMap.get(OP_STR_EQUALS),
operatorMap.get(OP_STR_NOT_EQUALS),
operatorMap.get(OP_CONTAINS),
operatorMap.get(OP_NOT_CONTAINS),
operatorMap.get(OP_STARTS_WITH),
operatorMap.get(OP_NOT_STARTS_WITH),
operatorMap.get(OP_ENDS_WITH),
operatorMap.get(OP_NOT_ENDS_WITH)
];
const defaultNumberOperators = [
operatorMap.get(OP_NUM_EQUALS),
operatorMap.get(OP_NUM_NOT_EQUALS),
operatorMap.get(OP_LT),
operatorMap.get(OP_GT),
operatorMap.get(OP_LTE),
operatorMap.get(OP_GTE)
];
module.exports = {
OP_STR_EQUALS,
OP_STR_NOT_EQUALS,
OP_CONTAINS,
OP_NOT_CONTAINS,
OP_STARTS_WITH,
OP_NOT_STARTS_WITH,
OP_ENDS_WITH,
OP_NOT_ENDS_WITH,
OP_NUM_EQUALS,
OP_NUM_NOT_EQUALS,
OP_LT,
OP_GT,
OP_LTE,
OP_GTE,
operatorMap,
defaultStringOperators,
defaultNumberOperators,
ConditionOperator,
};

View File

@@ -1,31 +0,0 @@
/**
* Represents a variable used in a condition and the set of operators that can be applied to this variable.
*
* A `ConditionVariable` holds the ID of the variable and a list of operators that define how this variable can be evaluated
* in conditions. For example, if the variable is a request body or a specific field in a request, the operators can include
* operations such as equality checks, comparisons, or other custom evaluations.
*/
class ConditionVariable {
/**
* @type {string}
*/
id;
/**
* @type {import("./operators").ConditionOperator[]}
*/
operators = {};
/**
* @param {string} id ID of variable
* @param {import("./operators").ConditionOperator[]} operators Operators the condition supports
*/
constructor(id, operators = []) {
this.id = id;
this.operators = operators;
}
}
module.exports = {
ConditionVariable,
};

View File

@@ -1,22 +1,13 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN } = require("../../src/util");
const { UP } = require("../../src/util");
const dayjs = require("dayjs");
const { dnsResolve } = require("../util-server");
const { R } = require("redbean-node");
const { ConditionVariable } = require("../monitor-conditions/variables");
const { defaultStringOperators } = require("../monitor-conditions/operators");
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
class DnsMonitorType extends MonitorType {
name = "dns";
supportsConditions = true;
conditionVariables = [
new ConditionVariable("record", defaultStringOperators ),
];
/**
* @inheritdoc
*/
@@ -27,48 +18,28 @@ class DnsMonitorType extends MonitorType {
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type);
heartbeat.ping = dayjs().valueOf() - startTime;
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
let conditionsResult = true;
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
switch (monitor.dns_resolve_type) {
case "A":
case "AAAA":
case "TXT":
case "PTR":
dnsMessage = `Records: ${dnsRes.join(" | ")}`;
conditionsResult = dnsRes.some(record => handleConditions({ record }));
break;
case "CNAME":
dnsMessage = dnsRes[0];
conditionsResult = handleConditions({ record: dnsRes[0] });
break;
case "CAA":
dnsMessage = dnsRes[0].issue;
conditionsResult = handleConditions({ record: dnsRes[0].issue });
break;
case "MX":
dnsMessage = dnsRes.map(record => `Hostname: ${record.exchange} - Priority: ${record.priority}`).join(" | ");
conditionsResult = dnsRes.some(record => handleConditions({ record: record.exchange }));
break;
case "NS":
dnsMessage = `Servers: ${dnsRes.join(" | ")}`;
conditionsResult = dnsRes.some(record => handleConditions({ record }));
break;
case "SOA":
dnsMessage = `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
conditionsResult = handleConditions({ record: dnsRes.nsname });
break;
case "SRV":
dnsMessage = dnsRes.map(record => `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight}`).join(" | ");
conditionsResult = dnsRes.some(record => handleConditions({ record: record.name }));
break;
if (monitor.dns_resolve_type === "A" || monitor.dns_resolve_type === "AAAA" || monitor.dns_resolve_type === "TXT" || monitor.dns_resolve_type === "PTR") {
dnsMessage += "Records: ";
dnsMessage += dnsRes.join(" | ");
} else if (monitor.dns_resolve_type === "CNAME" || monitor.dns_resolve_type === "PTR") {
dnsMessage += dnsRes[0];
} else if (monitor.dns_resolve_type === "CAA") {
dnsMessage += dnsRes[0].issue;
} else if (monitor.dns_resolve_type === "MX") {
dnsRes.forEach(record => {
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
});
dnsMessage = dnsMessage.slice(0, -2);
} else if (monitor.dns_resolve_type === "NS") {
dnsMessage += "Servers: ";
dnsMessage += dnsRes.join(" | ");
} else if (monitor.dns_resolve_type === "SOA") {
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
} else if (monitor.dns_resolve_type === "SRV") {
dnsRes.forEach(record => {
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
});
dnsMessage = dnsMessage.slice(0, -2);
}
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
@@ -76,7 +47,7 @@ class DnsMonitorType extends MonitorType {
}
heartbeat.msg = dnsMessage;
heartbeat.status = conditionsResult ? UP : DOWN;
heartbeat.status = UP;
}
}

View File

@@ -4,6 +4,7 @@ const { MongoClient } = require("mongodb");
const jsonata = require("jsonata");
class MongodbMonitorType extends MonitorType {
name = "mongodb";
/**
@@ -48,7 +49,8 @@ class MongodbMonitorType extends MonitorType {
* Connect to and run MongoDB command on a MongoDB database
* @param {string} connectionString The database connection string
* @param {object} command MongoDB command to run on the database
* @returns {Promise<(string[] | object[] | object)>} Response from server
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
*/
async runMongodbCommand(connectionString, command) {
let client = await MongoClient.connect(connectionString);

View File

@@ -1,19 +1,6 @@
class MonitorType {
name = undefined;
/**
* Whether or not this type supports monitor conditions. Controls UI visibility in monitor form.
* @type {boolean}
*/
supportsConditions = false;
/**
* Variables supported by this type. e.g. an HTTP type could have a "response_code" variable to test against.
* This property controls the choices displayed in the monitor edit form.
* @type {import("../monitor-conditions/variables").ConditionVariable[]}
*/
conditionVariables = [];
/**
* Run the monitoring check on the given monitor
* @param {Monitor} monitor Monitor to check
@@ -24,6 +11,7 @@ class MonitorType {
async check(monitor, heartbeat, server) {
throw new Error("You need to override check()");
}
}
module.exports = {

View File

@@ -4,10 +4,15 @@ const mqtt = require("mqtt");
const jsonata = require("jsonata");
class MqttMonitorType extends MonitorType {
name = "mqtt";
/**
* @inheritdoc
* Run the monitoring check on the MQTT monitor
* @param {Monitor} monitor Monitor to check
* @param {Heartbeat} heartbeat Monitor heartbeat to update
* @param {UptimeKumaServer} server Uptime Kuma server
* @returns {Promise<void>}
*/
async check(monitor, heartbeat, server) {
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {

View File

@@ -63,7 +63,7 @@ if (process.platform === "win32") {
* @returns {Promise<boolean>} The executable is allowed?
*/
async function isAllowedChromeExecutable(executablePath) {
log.info("Chromium", config.args);
console.log(config.args);
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
return true;
}
@@ -102,8 +102,7 @@ async function getBrowser() {
*/
async function getRemoteBrowser(remoteBrowserID, userId) {
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
log.debug("Chromium", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
browser = chromium.connect(remoteBrowser.url);
log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
browser = await chromium.connect(remoteBrowser.url);
return browser;
}

View File

@@ -1,63 +0,0 @@
const { MonitorType } = require("./monitor-type");
const { UP, log, evaluateJsonQuery } = require("../../src/util");
const snmp = require("net-snmp");
class SNMPMonitorType extends MonitorType {
name = "snmp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let session;
try {
const sessionOptions = {
port: monitor.port || "161",
retries: monitor.maxretries,
timeout: monitor.timeout * 1000,
version: snmp.Version[monitor.snmpVersion],
};
session = snmp.createSession(monitor.hostname, monitor.radiusPassword, sessionOptions);
// Handle errors during session creation
session.on("error", (error) => {
throw new Error(`Error creating SNMP session: ${error.message}`);
});
const varbinds = await new Promise((resolve, reject) => {
session.get([ monitor.snmpOid ], (error, varbinds) => {
error ? reject(error) : resolve(varbinds);
});
});
log.debug("monitor", `SNMP: Received varbinds (Type: ${snmp.ObjectType[varbinds[0].type]} Value: ${varbinds[0].value})`);
if (varbinds.length === 0) {
throw new Error(`No varbinds returned from SNMP session (OID: ${monitor.snmpOid})`);
}
if (varbinds[0].type === snmp.ObjectType.NoSuchInstance) {
throw new Error(`The SNMP query returned that no instance exists for OID ${monitor.snmpOid}`);
}
// We restrict querying to one OID per monitor, therefore `varbinds[0]` will always contain the value we're interested in.
const value = varbinds[0].value;
const { status, response } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue);
if (status) {
heartbeat.status = UP;
heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`;
} else {
throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`);
}
} finally {
if (session) {
session.close();
}
}
}
}
module.exports = {
SNMPMonitorType,
};

View File

@@ -2,13 +2,23 @@ const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const childProcessAsync = require("promisify-child-process");
/**
* A TailscalePing class extends the MonitorType.
* It runs Tailscale ping to monitor the status of a specific node.
*/
class TailscalePing extends MonitorType {
name = "tailscale-ping";
/**
* @inheritdoc
* Checks the ping status of the URL associated with the monitor.
* It then parses the Tailscale ping command output to update the heatrbeat.
* @param {object} monitor The monitor object associated with the check.
* @param {object} heartbeat The heartbeat object to update.
* @returns {Promise<void>}
* @throws Error if checking Tailscale ping encounters any error
*/
async check(monitor, heartbeat, _server) {
async check(monitor, heartbeat) {
try {
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
this.parseTailscaleOutput(tailscaleOutput, heartbeat);

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
const { Settings } = require("../settings");
class AlertNow extends NotificationProvider {
name = "AlertNow";
@@ -29,7 +29,7 @@ class AlertNow extends NotificationProvider {
textMsg += ` - ${msg}`;
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
textMsg += ` >> ${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
}

View File

@@ -33,6 +33,26 @@ class Discord extends NotificationProvider {
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:
address = monitorJSON["url"];
break;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
if (heartbeatJSON["status"] === DOWN) {
let discorddowndata = {
@@ -48,7 +68,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAdress(monitorJSON),
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
@@ -85,7 +105,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAdress(monitorJSON),
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: `Time (${heartbeatJSON["timezone"]})`,

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { Settings } = require("../settings");
const { setting } = require("../util-server");
const successMessage = "Sent Successfully.";
class FlashDuty extends NotificationProvider {
@@ -84,7 +84,7 @@ class FlashDuty extends NotificationProvider {
}
};
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { Settings } = require("../settings");
class GoogleChat extends NotificationProvider {
name = "GoogleChat";
@@ -45,7 +45,7 @@ class GoogleChat extends NotificationProvider {
}
// add button for monitor link if available
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL) {
const urlPath = monitorJSON ? getMonitorRelativeURL(monitorJSON.id) : "/";
sectionWidgets.push({

View File

@@ -1,3 +1,4 @@
const { log } = require("../../src/util");
const NotificationProvider = require("./notification-provider");
const {
relayInit,
@@ -11,7 +12,16 @@ const {
// polyfills for node versions
const semver = require("semver");
const nodeVersion = process.version;
if (semver.lt(nodeVersion, "20.0.0")) {
if (semver.lt(nodeVersion, "16.0.0")) {
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
} else if (semver.lt(nodeVersion, "18.0.0")) {
// polyfills for node 16
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
crypto.subtle = crypto.webcrypto.subtle;
}
} else if (semver.lt(nodeVersion, "20.0.0")) {
// polyfills for node 18
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");

View File

@@ -19,36 +19,6 @@ class NotificationProvider {
throw new Error("Have to override Notification.send(...)");
}
/**
* Extracts the address from a monitor JSON object based on its type.
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} The extracted address based on the monitor type.
*/
extractAdress(monitorJSON) {
if (!monitorJSON) {
return "";
}
switch (monitorJSON["type"]) {
case "push":
return "Heartbeat";
case "ping":
return monitorJSON["hostname"];
case "port":
case "dns":
case "gamedig":
case "steam":
if (monitorJSON["port"]) {
return monitorJSON["hostname"] + ":" + monitorJSON["port"];
}
return monitorJSON["hostname"];
default:
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
return monitorJSON["url"];
}
return "";
}
}
/**
* Throws an error
* @param {any} error The error to throw

View File

@@ -1,47 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Onesender extends NotificationProvider {
name = "Onesender";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
to: notification.onesenderReceiver,
type: "text",
recipient_type: "individual",
text: {
body: msg
}
};
if (notification.onesenderTypeReceiver === "private") {
data.to = notification.onesenderReceiver + "@s.whatsapp.net";
} else {
data.recipient_type = "group";
data.to = notification.onesenderReceiver + "@g.us";
}
let config = {
headers: {
"Authorization": "Bearer " + notification.onesenderToken,
}
};
await axios.post(notification.onesenderURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Onesender;

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { Settings } = require("../settings");
const { setting } = require("../util-server");
let successMessage = "Sent Successfully.";
class PagerDuty extends NotificationProvider {
@@ -95,7 +95,7 @@ class PagerDuty extends NotificationProvider {
}
};
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { Settings } = require("../settings");
const { setting } = require("../util-server");
let successMessage = "Sent Successfully.";
class PagerTree extends NotificationProvider {
@@ -74,7 +74,7 @@ class PagerTree extends NotificationProvider {
}
};
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorJSON.id);

View File

@@ -1,6 +1,3 @@
const { getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
@@ -26,12 +23,6 @@ class Pushover extends NotificationProvider {
"html": 1,
};
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
data["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
data["url_title"] = "Link to Monitor";
}
if (notification.pushoverdevice) {
data.device = notification.pushoverdevice;
}

View File

@@ -1,8 +1,8 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const Slack = require("./slack");
const { setting } = require("../util-server");
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
const { Settings } = require("../settings");
class RocketChat extends NotificationProvider {
name = "rocket.chat";
@@ -49,7 +49,7 @@ class RocketChat extends NotificationProvider {
await Slack.deprecateURL(notification.rocketbutton);
}
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL) {
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);

View File

@@ -11,13 +11,8 @@ class ServerChan extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
// serverchan3 requires sending via ft07.com
const url = String(notification.serverChanSendKey).startsWith("sctp")
? `https://${notification.serverChanSendKey}.push.ft07.com/send`
: `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`;
try {
await axios.post(url, {
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
"title": this.checkStatus(heartbeatJSON, monitorJSON),
"desp": msg,
});

View File

@@ -32,7 +32,28 @@ class SevenIO extends NotificationProvider {
return okMsg;
}
let address = this.extractAdress(monitorJSON);
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}) `;
}

View File

@@ -1,52 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN } = require("../../src/util");
class SIGNL4 extends NotificationProvider {
name = "SIGNL4";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
msg,
// Source system
"X-S4-SourceSystem": "UptimeKuma",
monitorUrl: this.extractAdress(monitorJSON),
};
const config = {
headers: {
"Content-Type": "application/json"
}
};
if (heartbeatJSON == null) {
// Test alert
data.title = "Uptime Kuma Alert";
data.message = msg;
} else if (heartbeatJSON.status === UP) {
data.title = "Uptime Kuma Monitor ✅ Up";
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
data["X-S4-Status"] = "resolved";
} else if (heartbeatJSON.status === DOWN) {
data.title = "Uptime Kuma Monitor 🔴 Down";
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
data["X-S4-Status"] = "new";
}
await axios.post(notification.webhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SIGNL4;

View File

@@ -1,8 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { Settings } = require("../settings");
const { log } = require("../../src/util");
class Slack extends NotificationProvider {
name = "slack";
@@ -15,13 +14,15 @@ class Slack extends NotificationProvider {
* @returns {Promise<void>}
*/
static async deprecateURL(url) {
let currentPrimaryBaseURL = await Settings.get("primaryBaseURL");
let currentPrimaryBaseURL = await setting("primaryBaseURL");
if (!currentPrimaryBaseURL) {
log.error("notification", "Move the url to be the primary base URL");
await Settings.set("primaryBaseURL", url, "general");
console.log("Move the url to be the primary base URL");
await setSettings("general", {
primaryBaseURL: url,
});
} else {
log.debug("notification", "Already there, no need to move the primary base URL");
console.log("Already there, no need to move the primary base URL");
}
}
@@ -47,8 +48,7 @@ class Slack extends NotificationProvider {
}
const address = this.extractAdress(monitorJSON);
if (address) {
if (monitorJSON.url) {
actions.push({
"type": "button",
"text": {
@@ -56,7 +56,7 @@ class Slack extends NotificationProvider {
"text": "Visit site",
},
"value": "Site",
"url": address,
"url": monitorJSON.url,
});
}
@@ -135,26 +135,21 @@ class Slack extends NotificationProvider {
return okMsg;
}
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
const title = "Uptime Kuma Alert";
let data = {
"text": `${title}\n${msg}`,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"attachments": [],
};
if (notification.slackrichmessage) {
data.attachments.push(
"attachments": [
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
}
);
} else {
data.text = `${title}\n${msg}`;
}
]
};
if (notification.slackbutton) {
await Slack.deprecateURL(notification.slackbutton);

View File

@@ -93,7 +93,12 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
monitorHostnameOrURL = this.extractAdress(monitorJSON);
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];
}
}
let serviceStatus = "⚠️ Test";

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
const { Settings } = require("../settings");
const { setting } = require("../util-server");
let successMessage = "Sent Successfully.";
class Splunk extends NotificationProvider {
@@ -95,7 +95,7 @@ class Splunk extends NotificationProvider {
}
};
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorInfo) {
options.client = "Uptime Kuma";
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);

View File

@@ -34,7 +34,25 @@ class Squadcast extends NotificationProvider {
data.status = "resolve";
}
data.tags["AlertAddress"] = this.extractAdress(monitorJSON);
let address;
switch (monitorJSON["type"]) {
case "ping":
address = monitorJSON["hostname"];
break;
case "port":
case "dns":
case "steam":
address = monitorJSON["hostname"];
if (monitorJSON["port"]) {
address += ":" + monitorJSON["port"];
}
break;
default:
address = monitorJSON["url"];
break;
}
data.tags["AlertAddress"] = address;
monitorJSON["tags"].forEach(tag => {
data.tags[tag["name"]] = {

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setting } = require("../util-server");
const { getMonitorRelativeURL } = require("../../src/util");
const { Settings } = require("../settings");
class Stackfield extends NotificationProvider {
name = "stackfield";
@@ -23,7 +23,7 @@ class Stackfield extends NotificationProvider {
textMsg += `\n${msg}`;
const baseURL = await Settings.get("primaryBaseURL");
const baseURL = await setting("primaryBaseURL");
if (baseURL) {
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
}

View File

@@ -216,6 +216,21 @@ class Teams extends NotificationProvider {
return okMsg;
}
let monitorUrl;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
monitorUrl = monitorJSON["url"];
break;
case "docker":
monitorUrl = monitorJSON["docker_host"];
break;
default:
monitorUrl = monitorJSON["hostname"];
break;
}
const baseURL = await setting("primaryBaseURL");
let dashboardUrl;
if (baseURL) {
@@ -225,7 +240,7 @@ class Teams extends NotificationProvider {
const payload = this._notificationPayloadFactory({
heartbeatJSON: heartbeatJSON,
monitorName: monitorJSON.name,
monitorUrl: this.extractAdress(monitorJSON),
monitorUrl: monitorUrl,
dashboardUrl: dashboardUrl,
});

View File

@@ -1,77 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Threema extends NotificationProvider {
name = "threema";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const url = "https://msgapi.threema.ch/send_simple";
const config = {
headers: {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
}
};
const data = {
from: notification.threemaSenderIdentity,
secret: notification.threemaSecret,
text: msg
};
switch (notification.threemaRecipientType) {
case "identity":
data.to = notification.threemaRecipient;
break;
case "phone":
data.phone = notification.threemaRecipient;
break;
case "email":
data.email = notification.threemaRecipient;
break;
default:
throw new Error(`Unsupported recipient type: ${notification.threemaRecipientType}`);
}
try {
await axios.post(url, new URLSearchParams(data), config);
return "Threema notification sent successfully.";
} catch (error) {
const errorMessage = this.handleApiError(error);
this.throwGeneralAxiosError(errorMessage);
}
}
/**
* Handle Threema API errors
* @param {any} error The error to handle
* @returns {string} Additional error context
*/
handleApiError(error) {
if (!error.response) {
return error.message;
}
switch (error.response.status) {
case 400:
return "Invalid recipient identity or account not set up for basic mode (400).";
case 401:
return "Incorrect API identity or secret (401).";
case 402:
return "No credits remaining (402).";
case 404:
return "Recipient not found (404).";
case 413:
return "Message is too long (413).";
case 500:
return "Temporary internal server error (500).";
default:
return error.message;
}
}
}
module.exports = Threema;

View File

@@ -32,17 +32,20 @@ class WeCom extends NotificationProvider {
* @returns {object} Message
*/
composeMessage(heartbeatJSON, msg) {
let title = "UptimeKuma Message";
let title;
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor Up";
}
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
title = "UptimeKuma Monitor Down";
}
if (msg != null) {
title = "UptimeKuma Message";
}
return {
msgtype: "text",
text: {
content: title + "\n" + msg
content: title + msg
}
};
}

View File

@@ -1,51 +0,0 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class WPush extends NotificationProvider {
name = "WPush";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
const context = {
"title": this.checkStatus(heartbeatJSON, monitorJSON),
"content": msg,
"apikey": notification.wpushAPIkey,
"channel": notification.wpushChannel
};
const result = await axios.post("https://api.wpush.cn/api/v1/send", context);
if (result.data.code !== 0) {
throw result.data.message;
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
/**
* Get the formatted title for message
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @param {?object} monitorJSON Monitor details (For Up/Down only)
* @returns {string} Formatted title
*/
checkStatus(heartbeatJSON, monitorJSON) {
let title = "UptimeKuma Message";
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
}
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
}
return title;
}
}
module.exports = WPush;

View File

@@ -13,9 +13,9 @@ class ZohoCliq extends NotificationProvider {
*/
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 [${monitorName}] went down\n`;
return `🔴 Application [${monitorName}] went down\n`;
} else if (status === UP) {
return `### ✅ [${monitorName}] is back online\n`;
return ` Application [${monitorName}] is back online\n`;
}
return "Notification\n";
};
@@ -46,11 +46,16 @@ class ZohoCliq extends NotificationProvider {
monitorUrl,
}) => {
const payload = [];
payload.push("### Uptime Kuma\n");
payload.push(this._statusMessageFactory(status, monitorName));
payload.push(`*Description:* ${monitorMessage}`);
if (monitorName) {
payload.push(`*Monitor:* ${monitorName}`);
}
if (monitorUrl && monitorUrl !== "https://") {
payload.push(`*URL:* ${monitorUrl}`);
payload.push(`*URL:* [${monitorUrl}](${monitorUrl})`);
}
return payload;
@@ -82,10 +87,24 @@ class ZohoCliq extends NotificationProvider {
return okMsg;
}
let url;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
url = monitorJSON["url"];
break;
case "docker":
url = monitorJSON["docker_host"];
break;
default:
url = monitorJSON["hostname"];
break;
}
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: this.extractAdress(monitorJSON),
monitorUrl: url,
status: heartbeatJSON.status
});

View File

@@ -42,7 +42,6 @@ const Pushy = require("./notification-providers/pushy");
const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal");
const SIGNL4 = require("./notification-providers/signl4");
const Slack = require("./notification-providers/slack");
const SMSPartner = require("./notification-providers/smspartner");
const SMSEagle = require("./notification-providers/smseagle");
@@ -52,7 +51,6 @@ const Stackfield = require("./notification-providers/stackfield");
const Teams = require("./notification-providers/teams");
const TechulusPush = require("./notification-providers/techulus-push");
const Telegram = require("./notification-providers/telegram");
const Threema = require("./notification-providers/threema");
const Twilio = require("./notification-providers/twilio");
const Splunk = require("./notification-providers/splunk");
const Webhook = require("./notification-providers/webhook");
@@ -65,8 +63,6 @@ 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");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
class Notification {
@@ -114,7 +110,6 @@ class Notification {
new Ntfy(),
new Octopush(),
new OneBot(),
new Onesender(),
new Opsgenie(),
new PagerDuty(),
new FlashDuty(),
@@ -128,7 +123,6 @@ class Notification {
new ServerChan(),
new SerwerSMS(),
new Signal(),
new SIGNL4(),
new SMSManager(),
new SMSPartner(),
new Slack(),
@@ -139,7 +133,6 @@ class Notification {
new Teams(),
new TechulusPush(),
new Telegram(),
new Threema(),
new Twilio(),
new Splunk(),
new Webhook(),
@@ -150,7 +143,6 @@ class Notification {
new Whapi(),
new GtxMessaging(),
new Cellsynt(),
new Wpush(),
];
for (let item of list) {
if (! item.name) {

View File

@@ -2,7 +2,7 @@ const { R } = require("redbean-node");
const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const SocksProxyAgent = require("socks-proxy-agent");
const { log } = require("../src/util");
const { debug } = require("../src/util");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const { CookieJar } = require("tough-cookie");
const { createCookieAgent } = require("http-cookie-agent/http");
@@ -110,9 +110,9 @@ class Proxy {
proxyOptions.auth = `${proxy.username}:${proxy.password}`;
}
log.debug("update-proxy", `Proxy Options: ${JSON.stringify(proxyOptions)}`);
log.debug("update-proxy", `HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
log.debug("update-proxy", `HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
debug(`Proxy Options: ${JSON.stringify(proxyOptions)}`);
debug(`HTTP Agent Options: ${JSON.stringify(httpAgentOptions)}`);
debug(`HTTPS Agent Options: ${JSON.stringify(httpsAgentOptions)}`);
switch (proxy.protocol) {
case "http":

View File

@@ -1,5 +1,6 @@
let express = require("express");
const {
setting,
allowDevAllOrigin,
allowAllOrigin,
percentageToColor,
@@ -17,7 +18,6 @@ const { makeBadge } = require("badge-maker");
const { Prometheus } = require("../prometheus");
const Database = require("../database");
const { UptimeCalculator } = require("../uptime-calculator");
const { Settings } = require("../settings");
let router = express.Router();
@@ -30,7 +30,7 @@ router.get("/api/entry-page", async (request, response) => {
let result = { };
let hostname = request.hostname;
if ((await Settings.get("trustProxy")) && request.headers["x-forwarded-host"]) {
if ((await setting("trustProxy")) && request.headers["x-forwarded-host"]) {
hostname = request.headers["x-forwarded-host"];
}
@@ -232,8 +232,8 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
const overrideValue = value && parseFloat(value);
if (/^[0-9]+$/.test(requestedDuration)) {
requestedDuration = `${requestedDuration}h`;
if (requestedDuration === "24") {
requestedDuration = "24h";
}
let publicMonitor = await R.getRow(`
@@ -265,7 +265,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
// build a label string. If a custom label is given, override the default one (requestedDuration)
badgeValues.label = filterAndJoin([
labelPrefix,
label ?? `Uptime (${requestedDuration.slice(0, -1)}${labelSuffix})`,
label ?? `Uptime (${requestedDuration}${labelSuffix})`,
]);
badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]);
}
@@ -302,8 +302,8 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
const overrideValue = value && parseFloat(value);
if (/^[0-9]+$/.test(requestedDuration)) {
requestedDuration = `${requestedDuration}h`;
if (requestedDuration === "24") {
requestedDuration = "24h";
}
// Check if monitor is public
@@ -325,7 +325,7 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
// use a given, custom labelColor or use the default badge label color (defined by badge-maker)
badgeValues.labelColor = labelColor ?? "";
// build a lable string. If a custom label is given, override the default one (requestedDuration)
badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration.slice(0, -1)}${labelSuffix})` ]);
badgeValues.label = filterAndJoin([ labelPrefix, label ?? `Avg. Ping (${requestedDuration}${labelSuffix})` ]);
badgeValues.message = filterAndJoin([ prefix, avgPing, suffix ]);
}

View File

@@ -18,11 +18,6 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => {
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
});
router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => {
let slug = request.params.slug;
await StatusPage.handleStatusPageRSSResponse(response, slug);
});
router.get("/status", cache("5 minutes"), async (request, response) => {
let slug = "default";
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);

View File

@@ -19,7 +19,7 @@ const nodeVersion = process.versions.node;
// Get the required Node.js version from package.json
const requiredNodeVersions = require("../package.json").engines.node;
const bannedNodeVersions = " < 18 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
console.log(`Your Node.js version: ${nodeVersion}`);
const semver = require("semver");
@@ -90,7 +90,8 @@ const Monitor = require("./model/monitor");
const User = require("./model/user");
log.debug("server", "Importing Settings");
const { initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin } = require("./util-server");
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
} = require("./util-server");
log.debug("server", "Importing Notification");
const { Notification } = require("./notification");
@@ -131,9 +132,9 @@ const twoFAVerifyOptions = {
const testMode = !!args["test"] || false;
// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList, sendMonitorTypeList } = require("./client");
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const { databaseSocketHandler } = require("./socket-handlers/database-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
@@ -200,7 +201,7 @@ let needSetup = false;
// Entry Page
app.get("/", async (request, response) => {
let hostname = request.hostname;
if (await Settings.get("trustProxy")) {
if (await setting("trustProxy")) {
const proxy = request.headers["x-forwarded-host"];
if (proxy) {
hostname = proxy;
@@ -245,42 +246,12 @@ let needSetup = false;
log.debug("test", request.body);
response.send("OK");
});
const fs = require("fs");
app.get("/_e2e/take-sqlite-snapshot", async (request, response) => {
await Database.close();
try {
fs.cpSync(Database.sqlitePath, `${Database.sqlitePath}.e2e-snapshot`);
} catch (err) {
throw new Error("Unable to copy SQLite DB.");
}
await Database.connect();
response.send("Snapshot taken.");
});
app.get("/_e2e/restore-sqlite-snapshot", async (request, response) => {
if (!fs.existsSync(`${Database.sqlitePath}.e2e-snapshot`)) {
throw new Error("Snapshot doesn't exist.");
}
await Database.close();
try {
fs.cpSync(`${Database.sqlitePath}.e2e-snapshot`, Database.sqlitePath);
} catch (err) {
throw new Error("Unable to copy snapshot file.");
}
await Database.connect();
response.send("Snapshot restored.");
});
}
// Robots.txt
app.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:";
if (!await Settings.get("searchEngineIndex")) {
if (!await setting("searchEngineIndex")) {
txt += " /";
}
response.setHeader("Content-Type", "text/plain");
@@ -715,8 +686,6 @@ let needSetup = false;
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
monitor.conditions = JSON.stringify(monitor.conditions);
bean.import(monitor);
bean.user_id = socket.userID;
@@ -726,13 +695,13 @@ let needSetup = false;
await updateMonitorNotification(bean.id, notificationIDList);
await server.sendUpdateMonitorIntoList(socket, bean.id);
await server.sendMonitorList(socket);
if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
}
log.info("monitor", `Added Monitor: ${bean.id} User ID: ${socket.userID}`);
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
callback({
ok: true,
@@ -857,17 +826,11 @@ let needSetup = false;
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
bean.cacheBust = monitor.cacheBust;
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
bean.kafkaProducerAllowAutoTopicCreation =
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser;
bean.snmpVersion = monitor.snmpVersion;
bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator;
bean.timeout = monitor.timeout;
bean.conditions = JSON.stringify(monitor.conditions);
bean.validate();
@@ -879,11 +842,11 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (await Monitor.isActive(bean.id, bean.active)) {
if (await bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}
await server.sendUpdateMonitorIntoList(socket, bean.id);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -923,17 +886,14 @@ let needSetup = false;
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
monitorID,
socket.userID,
]);
const monitorData = [{ id: monitor.id,
active: monitor.active
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
callback({
ok: true,
monitor: monitor.toJSON(preloadData),
monitor: await bean.toJSON(),
});
} catch (e) {
@@ -984,7 +944,7 @@ let needSetup = false;
try {
checkLogin(socket);
await startMonitor(socket.userID, monitorID);
await server.sendUpdateMonitorIntoList(socket, monitorID);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -1004,7 +964,7 @@ let needSetup = false;
try {
checkLogin(socket);
await pauseMonitor(socket.userID, monitorID);
await server.sendUpdateMonitorIntoList(socket, monitorID);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -1050,7 +1010,8 @@ let needSetup = false;
msg: "successDeleted",
msgi18n: true,
});
await server.sendDeleteMonitorFromList(socket, monitorID);
await server.sendMonitorList(socket);
} catch (e) {
callback({
@@ -1326,7 +1287,7 @@ let needSetup = false;
socket.on("getSettings", async (callback) => {
try {
checkLogin(socket);
const data = await Settings.getSettings("general");
const data = await getSettings("general");
if (!data.serverTimezone) {
data.serverTimezone = await server.getTimezone();
@@ -1354,7 +1315,7 @@ let needSetup = false;
// Disabled Auth + Want to Enable Auth => No Check
// Enabled Auth + Want to Disable Auth => Check!!
// Enabled Auth + Want to Enable Auth => No Check
const currentDisabledAuth = await Settings.get("disableAuth");
const currentDisabledAuth = await setting("disableAuth");
if (!currentDisabledAuth && data.disableAuth) {
await doubleCheckPassword(socket, currentPassword);
}
@@ -1368,7 +1329,7 @@ let needSetup = false;
const previousChromeExecutable = await Settings.get("chromeExecutable");
const previousNSCDStatus = await Settings.get("nscd");
await Settings.setSettings("general", data);
await setSettings("general", data);
server.entryPage = data.entryPage;
// Also need to apply timezone globally
@@ -1464,7 +1425,7 @@ let needSetup = false;
});
} catch (e) {
log.error("server", e);
console.error(e);
callback({
ok: false,
@@ -1577,7 +1538,7 @@ let needSetup = false;
// ***************************
log.debug("auth", "check auto login");
if (await Settings.get("disableAuth")) {
if (await setting("disableAuth")) {
log.info("auth", "Disabled Auth: auto login to admin");
await afterLogin(socket, await R.findOne("user"));
socket.emit("autoLogin");
@@ -1675,18 +1636,17 @@ async function afterLogin(socket, user) {
sendDockerHostList(socket),
sendAPIKeyList(socket),
sendRemoteBrowserList(socket),
sendMonitorTypeList(socket),
]);
await StatusPage.sendStatusPageList(io, socket);
const monitorPromises = [];
for (let monitorID in monitorList) {
monitorPromises.push(sendHeartbeatList(socket, monitorID));
monitorPromises.push(Monitor.sendStats(io, monitorID, user.id));
await sendHeartbeatList(socket, monitorID);
}
await Promise.all(monitorPromises);
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
// Set server timezone from client browser if not set
// It should be run once only

View File

@@ -60,7 +60,7 @@ module.exports.apiKeySocketHandler = (socket) => {
ok: true,
});
} catch (e) {
log.error("apikeys", e);
console.error(e);
callback({
ok: false,
msg: e.message,

View File

@@ -1,8 +1,7 @@
const { checkLogin, doubleCheckPassword } = require("../util-server");
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { log } = require("../../src/util");
const { Settings } = require("../settings");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
@@ -41,7 +40,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.join("cloudflared");
io.to(socket.userID).emit(prefix + "installed", cloudflared.checkInstalled());
io.to(socket.userID).emit(prefix + "running", cloudflared.running);
io.to(socket.userID).emit(prefix + "token", await Settings.get("cloudflaredTunnelToken"));
io.to(socket.userID).emit(prefix + "token", await setting("cloudflaredTunnelToken"));
} catch (error) { }
});
@@ -56,7 +55,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
try {
checkLogin(socket);
if (token && typeof token === "string") {
await Settings.set("cloudflaredTunnelToken", token);
await setSetting("cloudflaredTunnelToken", token);
cloudflared.token = token;
} else {
cloudflared.token = null;
@@ -68,7 +67,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "stop", async (currentPassword, callback) => {
try {
checkLogin(socket);
const disabledAuth = await Settings.get("disableAuth");
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
await doubleCheckPassword(socket, currentPassword);
}
@@ -84,7 +83,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
socket.on(prefix + "removeToken", async () => {
try {
checkLogin(socket);
await Settings.set("cloudflaredTunnelToken", "");
await setSetting("cloudflaredTunnelToken", "");
} catch (error) { }
});
@@ -97,15 +96,15 @@ module.exports.cloudflaredSocketHandler = (socket) => {
*/
module.exports.autoStart = async (token) => {
if (!token) {
token = await Settings.get("cloudflaredTunnelToken");
token = await setting("cloudflaredTunnelToken");
} else {
// Override the current token via args or env var
await Settings.set("cloudflaredTunnelToken", token);
log.info("cloudflare", "Use cloudflared token from args or env var");
await setSetting("cloudflaredTunnelToken", token);
console.log("Use cloudflared token from args or env var");
}
if (token) {
log.info("cloudflare", "Start cloudflared");
console.log("Start cloudflared");
cloudflared.token = token;
cloudflared.start();
}

View File

@@ -6,7 +6,7 @@ const Database = require("../database");
* @param {Socket} socket Socket.io instance
* @returns {void}
*/
module.exports.databaseSocketHandler = (socket) => {
module.exports = (socket) => {
// Post or edit incident
socket.on("getDatabaseSize", async (callback) => {

View File

@@ -29,13 +29,8 @@ function getGameList() {
return gameList;
}
/**
* Handler for general events
* @param {Socket} socket Socket.io instance
* @param {UptimeKumaServer} server Uptime Kuma server
* @returns {void}
*/
module.exports.generalSocketHandler = (socket, server) => {
socket.on("initServerTimezone", async (timezone) => {
try {
checkLogin(socket);

View File

@@ -67,7 +67,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
});
} catch (e) {
log.error("maintenance", e);
console.error(e);
callback({
ok: false,
msg: e.message,
@@ -177,7 +177,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
ok: true,
});
} catch (e) {
log.error("maintenance", e);
console.error(e);
callback({
ok: false,
msg: e.message,
@@ -201,7 +201,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
});
} catch (e) {
log.error("maintenance", e);
console.error(e);
callback({
ok: false,
msg: e.message,
@@ -225,7 +225,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
});
} catch (e) {
log.error("maintenance", e);
console.error(e);
callback({
ok: false,
msg: e.message,

View File

@@ -1,5 +1,5 @@
const { R } = require("redbean-node");
const { checkLogin } = require("../util-server");
const { checkLogin, setSetting } = require("../util-server");
const dayjs = require("dayjs");
const { log } = require("../../src/util");
const ImageDataURI = require("../image-data-uri");
@@ -7,7 +7,6 @@ const Database = require("../database");
const apicache = require("../modules/apicache");
const StatusPage = require("../model/status_page");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { Settings } = require("../settings");
/**
* Socket handlers for status page
@@ -102,10 +101,11 @@ module.exports.statusPageSocketHandler = (socket) => {
if (!statusPage) {
throw new Error("No slug?");
}
const config = await statusPage.toJSON();
config.allowEditingCustomHtml = import.meta.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1";
callback({
ok: true,
config: await statusPage.toJSON(),
config,
});
} catch (error) {
callback({
@@ -168,6 +168,9 @@ module.exports.statusPageSocketHandler = (socket) => {
statusPage.show_certificate_expiry = config.showCertificateExpiry;
statusPage.modified_date = R.isoDateTime();
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
if (process.env.UPTIME_KUMA_ALLOW_CUSTOM_HTML === "1") {
statusPage.custom_html = config.customHtml;
}
await R.store(statusPage);
@@ -234,7 +237,7 @@ module.exports.statusPageSocketHandler = (socket) => {
// Also change entry page to new slug if it is the default one, and slug is changed.
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
server.entryPage = "statusPage-" + statusPage.slug;
await Settings.set("entryPage", server.entryPage, "general");
await setSetting("entryPage", server.entryPage, "general");
}
apicache.clear();
@@ -292,7 +295,7 @@ module.exports.statusPageSocketHandler = (socket) => {
});
} catch (error) {
log.error("socket", error);
console.error(error);
callback({
ok: false,
msg: error.message,
@@ -314,7 +317,7 @@ module.exports.statusPageSocketHandler = (socket) => {
// Reset entry page if it is the default one.
if (server.entryPage === "statusPage-" + slug) {
server.entryPage = "dashboard";
await Settings.set("entryPage", server.entryPage, "general");
await setSetting("entryPage", server.entryPage, "general");
}
// No need to delete records from `status_page_cname`, because it has cascade foreign key.

View File

@@ -543,9 +543,7 @@ class UptimeCalculator {
if (type === "minute" && num > 24 * 60) {
throw new Error("The maximum number of minutes is 1440");
}
if (type === "day" && num > 365) {
throw new Error("The maximum number of days is 365");
}
// Get the current time period key based on the type
let key = this.getKey(this.getCurrentDate(), type);
@@ -743,36 +741,20 @@ class UptimeCalculator {
}
/**
* Get the uptime data for given duration.
* @param {string} duration A string with a number and a unit (m,h,d,w,M,y), such as 24h, 30d, 1y.
* Get the uptime data by duration
* @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
* @returns {UptimeDataResult} UptimeDataResult
* @throws {Error} Invalid duration / Unsupported unit
* @throws {Error} Invalid duration
*/
getDataByDuration(duration) {
const durationNumStr = duration.slice(0, -1);
if (!/^[0-9]+$/.test(durationNumStr)) {
throw new Error(`Invalid duration: ${duration}`);
}
const num = Number(durationNumStr);
const unit = duration.slice(-1);
switch (unit) {
case "m":
return this.getData(num, "minute");
case "h":
return this.getData(num, "hour");
case "d":
return this.getData(num, "day");
case "w":
return this.getData(7 * num, "day");
case "M":
return this.getData(30 * num, "day");
case "y":
return this.getData(365 * num, "day");
default:
throw new Error(`Unsupported unit (${unit}) for badge duration ${duration}`
);
if (duration === "24h") {
return this.get24Hour();
} else if (duration === "30d") {
return this.get30Day();
} else if (duration === "1y") {
return this.get1Year();
} else {
throw new Error("Invalid duration");
}
}

View File

@@ -113,7 +113,6 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
// Allow all CORS origins (polling) in development
@@ -205,56 +204,24 @@ class UptimeKumaServer {
return list;
}
/**
* Update Monitor into list
* @param {Socket} socket Socket to send list on
* @param {number} monitorID update or deleted monitor id
* @returns {Promise<void>}
*/
async sendUpdateMonitorIntoList(socket, monitorID) {
let list = await this.getMonitorJSONList(socket.userID, monitorID);
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
}
/**
* Delete Monitor from list
* @param {Socket} socket Socket to send list on
* @param {number} monitorID update or deleted monitor id
* @returns {Promise<void>}
*/
async sendDeleteMonitorFromList(socket, monitorID) {
this.io.to(socket.userID).emit("deleteMonitorFromList", monitorID);
}
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @param {number} monitorID - The ID of monitor for.
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async getMonitorJSONList(userID, monitorID = null) {
async getMonitorJSONList(userID) {
let result = {};
let query = " user_id = ? ";
let queryParams = [ userID ];
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
if (monitorID) {
query += "AND id = ? ";
queryParams.push(monitorID);
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
}
let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams);
const monitorData = monitorList.map(monitor => ({
id: monitor.id,
active: monitor.active,
name: monitor.name,
}));
const preloadData = await Monitor.preparePreloadData(monitorData);
const result = {};
monitorList.forEach(monitor => result[monitor.id] = monitor.toJSON(preloadData));
return result;
}
@@ -550,6 +517,4 @@ 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 { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const Monitor = require("./model/monitor");

View File

@@ -11,7 +11,8 @@ const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
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");
@@ -520,6 +521,46 @@ exports.redisPingAsync = function (dsn, rejectUnauthorized) {
});
};
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
* @returns {Promise<any>} Value
* @deprecated Use await Settings.get(key)
*/
exports.setting = async function (key) {
return await Settings.get(key);
};
/**
* Sets the specified setting to specified value
* @param {string} key Key of setting to set
* @param {any} value Value to set to
* @param {?string} type Type of setting
* @returns {Promise<void>}
*/
exports.setSetting = async function (key, value, type = null) {
await Settings.set(key, value, type);
};
/**
* Get settings based on type
* @param {string} type The type of setting
* @returns {Promise<Bean>} Settings of requested type
*/
exports.getSettings = async function (type) {
return await Settings.getSettings(type);
};
/**
* Set settings based on type
* @param {string} type Type of settings to set
* @param {object} data Values of settings
* @returns {Promise<void>}
*/
exports.setSettings = async function (type, data) {
await Settings.setSettings(type, data);
};
// ssl-checker by @dyaa
//https://github.com/dyaa/ssl-checker/blob/master/src/index.ts

View File

@@ -576,12 +576,6 @@ optgroup {
outline: none !important;
}
.prism-editor__container {
.important {
font-weight: var(--bs-body-font-weight) !important;
}
}
h5.settings-subheading::after {
content: "";
display: block;

View File

@@ -1,152 +0,0 @@
<template>
<div class="monitor-condition mb-3" data-testid="condition">
<button
v-if="!isInGroup || !isFirst || !isLast"
class="btn btn-outline-danger remove-button"
type="button"
:aria-label="$t('conditionDelete')"
data-testid="remove-condition"
@click="remove"
>
<font-awesome-icon icon="trash" />
</button>
<select v-if="!isFirst" v-model="model.andOr" class="form-select and-or-select" data-testid="condition-and-or">
<option value="and">{{ $t("and") }}</option>
<option value="or">{{ $t("or") }}</option>
</select>
<select v-model="model.variable" class="form-select" data-testid="condition-variable">
<option
v-for="variable in conditionVariables"
:key="variable.id"
:value="variable.id"
>
{{ $t(variable.id) }}
</option>
</select>
<select v-model="model.operator" class="form-select" data-testid="condition-operator">
<option
v-for="operator in getVariableOperators(model.variable)"
:key="operator.id"
:value="operator.id"
>
{{ $t(operator.caption) }}
</option>
</select>
<input
v-model="model.value"
type="text"
class="form-control"
:aria-label="$t('conditionValuePlaceholder')"
data-testid="condition-value"
required
/>
</div>
</template>
<script>
export default {
name: "EditMonitorCondition",
props: {
/**
* The monitor condition
*/
modelValue: {
type: Object,
required: true,
},
/**
* Whether this is the first condition
*/
isFirst: {
type: Boolean,
required: true,
},
/**
* Whether this is the last condition
*/
isLast: {
type: Boolean,
required: true,
},
/**
* Whether this condition is in a group
*/
isInGroup: {
type: Boolean,
required: false,
default: false,
},
/**
* Variable choices
*/
conditionVariables: {
type: Array,
required: true,
},
},
emits: [ "update:modelValue", "remove" ],
computed: {
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
methods: {
remove() {
this.$emit("remove", this.model);
},
getVariableOperators(variableId) {
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.monitor-condition {
display: flex;
flex-wrap: wrap;
}
.remove-button {
justify-self: flex-end;
margin-bottom: 12px;
margin-left: auto;
}
@container (min-width: 500px) {
.monitor-condition {
display: flex;
flex-wrap: nowrap;
}
.remove-button {
margin-bottom: 0;
margin-left: 10px;
order: 100;
}
.and-or-select {
width: auto;
}
}
</style>

View File

@@ -1,189 +0,0 @@
<template>
<div class="condition-group mb-3" data-testid="condition-group">
<div class="d-flex">
<select v-if="!isFirst" v-model="model.andOr" class="form-select" style="width: auto;" data-testid="condition-group-and-or">
<option value="and">{{ $t("and") }}</option>
<option value="or">{{ $t("or") }}</option>
</select>
</div>
<div class="condition-group-inner mt-2 pa-2">
<div class="condition-group-conditions">
<template v-for="(child, childIndex) in model.children" :key="childIndex">
<EditMonitorConditionGroup
v-if="child.type === 'group'"
v-model="model.children[childIndex]"
:is-first="childIndex === 0"
:get-new-group="getNewGroup"
:get-new-condition="getNewCondition"
:condition-variables="conditionVariables"
@remove="removeChild"
/>
<EditMonitorCondition
v-else
v-model="model.children[childIndex]"
:is-first="childIndex === 0"
:is-last="childIndex === model.children.length - 1"
:is-in-group="true"
:condition-variables="conditionVariables"
@remove="removeChild"
/>
</template>
</div>
<div class="condition-group-actions mt-3">
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
{{ $t("conditionAdd") }}
</button>
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
{{ $t("conditionAddGroup") }}
</button>
<button
class="btn btn-outline-danger"
type="button"
:aria-label="$t('conditionDeleteGroup')"
data-testid="remove-condition-group"
@click="remove"
>
<font-awesome-icon icon="trash" />
</button>
</div>
</div>
</div>
</template>
<script>
import EditMonitorCondition from "./EditMonitorCondition.vue";
export default {
name: "EditMonitorConditionGroup",
components: {
EditMonitorCondition,
},
props: {
/**
* The condition group
*/
modelValue: {
type: Object,
required: true,
},
/**
* Whether this is the first condition
*/
isFirst: {
type: Boolean,
required: true,
},
/**
* Function to generate a new group model
*/
getNewGroup: {
type: Function,
required: true,
},
/**
* Function to generate a new condition model
*/
getNewCondition: {
type: Function,
required: true,
},
/**
* Variable choices
*/
conditionVariables: {
type: Array,
required: true,
},
},
emits: [ "update:modelValue", "remove" ],
computed: {
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
methods: {
addGroup() {
const conditions = [ ...this.model.children ];
conditions.push(this.getNewGroup());
this.model.children = conditions;
},
addCondition() {
const conditions = [ ...this.model.children ];
conditions.push(this.getNewCondition());
this.model.children = conditions;
},
remove() {
this.$emit("remove", this.model);
},
removeChild(child) {
const idx = this.model.children.indexOf(child);
if (idx !== -1) {
this.model.children.splice(idx, 1);
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.condition-group-inner {
background: rgba(0, 0, 0, 0.05);
padding: 20px;
}
.dark .condition-group-inner {
background: rgba(255, 255, 255, 0.05);
}
.condition-group-conditions {
container-type: inline-size;
}
.condition-group-actions {
display: grid;
gap: 10px;
}
// Delete button
.condition-group-actions > :last-child {
margin-left: auto;
margin-top: 14px;
}
@container (min-width: 400px) {
.condition-group-actions {
display: flex;
}
// Delete button
.condition-group-actions > :last-child {
margin-left: auto;
margin-top: 0;
}
.btn-delete-group {
margin-left: auto;
}
}
</style>

View File

@@ -1,149 +0,0 @@
<template>
<div class="monitor-conditions">
<label class="form-label">{{ $t("Conditions") }}</label>
<div class="monitor-conditions-conditions">
<template v-for="(condition, conditionIndex) in model" :key="conditionIndex">
<EditMonitorConditionGroup
v-if="condition.type === 'group'"
v-model="model[conditionIndex]"
:is-first="conditionIndex === 0"
:get-new-group="getNewGroup"
:get-new-condition="getNewCondition"
:condition-variables="conditionVariables"
@remove="removeCondition"
/>
<EditMonitorCondition
v-else
v-model="model[conditionIndex]"
:is-first="conditionIndex === 0"
:is-last="conditionIndex === model.length - 1"
:condition-variables="conditionVariables"
@remove="removeCondition"
/>
</template>
</div>
<div class="monitor-conditions-buttons">
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-condition-button" @click="addCondition">
{{ $t("conditionAdd") }}
</button>
<button class="btn btn-outline-secondary me-2" type="button" data-testid="add-group-button" @click="addGroup">
{{ $t("conditionAddGroup") }}
</button>
</div>
</div>
</template>
<script>
import EditMonitorConditionGroup from "./EditMonitorConditionGroup.vue";
import EditMonitorCondition from "./EditMonitorCondition.vue";
export default {
name: "EditMonitorConditions",
components: {
EditMonitorConditionGroup,
EditMonitorCondition,
},
props: {
/**
* The monitor conditions
*/
modelValue: {
type: Array,
required: true,
},
conditionVariables: {
type: Array,
required: true,
},
},
emits: [ "update:modelValue" ],
computed: {
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
}
},
created() {
if (this.model.length === 0) {
this.addCondition();
}
},
methods: {
getNewGroup() {
return {
type: "group",
children: [ this.getNewCondition() ],
andOr: "and",
};
},
getNewCondition() {
const firstVariable = this.conditionVariables[0]?.id || null;
const firstOperator = this.getVariableOperators(firstVariable)[0] || null;
return {
type: "expression",
variable: firstVariable,
operator: firstOperator?.id || null,
value: "",
andOr: "and",
};
},
addGroup() {
const conditions = [ ...this.model ];
conditions.push(this.getNewGroup());
this.$emit("update:modelValue", conditions);
},
addCondition() {
const conditions = [ ...this.model ];
conditions.push(this.getNewCondition());
this.$emit("update:modelValue", conditions);
},
removeCondition(condition) {
const conditions = [ ...this.model ];
const idx = conditions.indexOf(condition);
if (idx !== -1) {
conditions.splice(idx, 1);
this.$emit("update:modelValue", conditions);
}
},
getVariableOperators(variableId) {
return this.conditionVariables.find(v => v.id === variableId)?.operators ?? [];
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.monitor-conditions,
.monitor-conditions-conditions {
container-type: inline-size;
}
.monitor-conditions-buttons {
display: grid;
gap: 10px;
}
@container (min-width: 400px) {
.monitor-conditions-buttons {
display: flex;
}
}
</style>

View File

@@ -14,7 +14,7 @@
v-if="!$root.isMobile && size !== 'small' && beatList.length > 4 && $root.styleElapsedTime !== 'none'"
class="d-flex justify-content-between align-items-center word" :style="timeStyle"
>
<div>{{ timeSinceFirstBeat }}</div>
<div>{{ timeSinceFirstBeat }} ago</div>
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
<div>{{ timeSinceLastBeat }}</div>
</div>
@@ -184,11 +184,11 @@ export default {
}
if (seconds < tolerance) {
return this.$t("now");
return "now";
} else if (seconds < 60 * 60) {
return this.$t("time ago", [ (seconds / 60).toFixed(0) + "m" ]);
return (seconds / 60).toFixed(0) + "m ago";
} else {
return this.$t("time ago", [ (seconds / 60 / 60).toFixed(0) + "h" ]);
return (seconds / 60 / 60).toFixed(0) + "h ago";
}
}
},

View File

@@ -45,7 +45,7 @@
</span>
</div>
</div>
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle" data-testid="monitor-list">
<div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>

View File

@@ -43,15 +43,12 @@
<div v-if="!isCollapsed" class="childs">
<MonitorListItem
v-for="(item, index) in sortedChildMonitorList"
:key="index"
:monitor="item"
:key="index" :monitor="item"
:isSelectMode="isSelectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
:depth="depth + 1"
:filter-func="filterFunc"
:sort-func="sortFunc"
/>
</div>
</transition>

View File

@@ -135,7 +135,6 @@ export default {
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",
"Onesender": "Onesender",
"Opsgenie": "Opsgenie",
"PagerDuty": "PagerDuty",
"PagerTree": "PagerTree",
@@ -145,7 +144,6 @@ export default {
"pushy": "Pushy",
"rocket.chat": "Rocket.Chat",
"signal": "Signal",
"SIGNL4": "SIGNL4",
"slack": "Slack",
"squadcast": "SquadCast",
"SMSEagle": "SMSEagle",
@@ -154,7 +152,6 @@ export default {
"stackfield": "Stackfield",
"teams": "Microsoft Teams",
"telegram": "Telegram",
"threema": "Threema",
"twilio": "Twilio",
"Splunk": "Splunk",
"webhook": "Webhook",
@@ -180,7 +177,6 @@ export default {
"WeCom": "WeCom (企业微信群机器人)",
"ServerChan": "ServerChan (Server酱)",
"smsc": "SMSC",
"WPush": "WPush(wpush.cn)",
};
// Sort by notification name

View File

@@ -7,12 +7,12 @@
:animation="100"
>
<template #item="group">
<div class="mb-5" data-testid="group">
<div class="mb-5 ">
<!-- Group Title -->
<h2 class="group-title">
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" data-testid="group-name" />
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" />
</h2>
<div class="shadow-box monitor-list mt-4 position-relative">
@@ -31,9 +31,9 @@
item-key="id"
>
<template #item="monitor">
<div class="item" data-testid="monitor">
<div class="item">
<div class="row">
<div class="col-6 col-md-4 small-padding">
<div class="col-9 col-md-8 small-padding">
<div class="info">
<font-awesome-icon v-if="editMode" icon="arrows-alt-v" class="action drag me-3" />
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeMonitor(group.index, monitor.index)" />
@@ -45,11 +45,10 @@
class="item-name"
target="_blank"
rel="noopener noreferrer"
data-testid="monitor-name"
>
{{ monitor.element.name }}
</a>
<p v-else class="item-name" data-testid="monitor-name"> {{ monitor.element.name }} </p>
<p v-else class="item-name"> {{ monitor.element.name }} </p>
<span
title="Setting"
@@ -67,11 +66,11 @@
<Tag :item="{name: $t('Cert Exp.'), value: formattedCertExpiryMessage(monitor), color: certExpiryColor(monitor)}" :size="'sm'" />
</div>
<div v-if="showTags">
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" data-testid="monitor-tag" />
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
</div>
<div :key="$root.userHeartbeatBar" class="col-6 col-md-8">
<div :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="mid" :monitor-id="monitor.element.id" />
</div>
</div>

View File

@@ -14,7 +14,6 @@
type="button"
class="btn btn-outline-secondary btn-add"
:disabled="processing"
data-testid="add-tag-button"
@click.stop="showAddDialog"
>
<font-awesome-icon class="me-1" icon="plus" /> {{ $t("Add") }}
@@ -60,7 +59,6 @@
v-model="newDraftTag.name" class="form-control"
:class="{'is-invalid': validateDraftTag.nameInvalid}"
:placeholder="$t('Name')"
data-testid="tag-name-input"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
@@ -78,7 +76,6 @@
label="name"
select-label=""
deselect-label=""
data-testid="tag-color-select"
>
<template #option="{ option }">
<div
@@ -106,7 +103,6 @@
v-model="newDraftTag.value" class="form-control"
:class="{'is-invalid': validateDraftTag.valueInvalid}"
:placeholder="$t('value (optional)')"
data-testid="tag-value-input"
@keydown.enter.prevent="onEnter"
/>
<div class="invalid-feedback">
@@ -118,7 +114,6 @@
type="button"
class="btn btn-secondary float-end"
:disabled="processing || validateDraftTag.invalid"
data-testid="tag-submit-button"
@click.stop="addDraftTag"
>
{{ $t("Add") }}

View File

@@ -1,81 +0,0 @@
<template>
<div class="mb-3">
<label for="host-onesender" class="form-label">{{ $t("Host Onesender") }}</label>
<input
id="host-onesender"
v-model="$parent.notification.onesenderURL"
type="url"
placeholder="https://xxxxxxxxxxx.com/api/v1/messages"
pattern="https?://.+"
class="form-control"
required
/>
</div>
<div class="mb-3">
<label for="receiver-onesender" class="form-label">{{ $t("Token Onesender") }}</label>
<HiddenInput id="receiver-onesender" v-model="$parent.notification.onesenderToken" :required="true" autocomplete="false"></HiddenInput>
<i18n-t tag="div" keypath="wayToGetOnesenderUrlandToken" class="form-text">
<a href="https://onesender.net/" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</div>
<div class="mb-3">
<label for="webhook-request-body" class="form-label">{{ $t("Recipient Type") }}</label>
<select
id="webhook-request-body"
v-model="$parent.notification.onesenderTypeReceiver"
class="form-select"
required
>
<option value="private">{{ $t("Private Number") }}</option>
<option value="group">{{ $t("Group ID") }}</option>
</select>
</div>
<div v-if="$parent.notification.onesenderTypeReceiver == 'private'" class="form-text">{{ $t("privateOnesenderDesc", ['"application/json"']) }}</div>
<div v-else class="form-text">{{ $t("groupOnesenderDesc") }}</div>
<div class="mb-3">
<input
id="type-receiver-onesender"
v-model="$parent.notification.onesenderReceiver"
type="text"
placeholder="628123456789 or 628123456789-34534"
class="form-control"
required
/>
</div>
<div class="mb-3">
<input
id="type-receiver-onesender"
v-model="computedReceiverResult"
type="text"
class="form-control"
disabled
/>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
data() {
return {};
},
computed: {
computedReceiverResult() {
let receiver = this.$parent.notification.onesenderReceiver;
return this.$parent.notification.onesenderTypeReceiver === "private" ? receiver + "@s.whatsapp.net" : receiver + "@g.us";
},
},
};
</script>
<style lang="scss" scoped>
textarea {
min-height: 200px;
}
</style>

View File

@@ -1,16 +0,0 @@
<template>
<div class="mb-3">
<label for="signl4-webhook-url" class="form-label">{{ $t("SIGNL4 Webhook URL") }}</label>
<input
id="signl4-webhook-url"
v-model="$parent.notification.webhookURL"
type="url"
pattern="https?://.+"
class="form-control"
required
/>
<i18n-t tag="div" keypath="signl4Docs" class="form-text">
<a href="https://docs.signl4.com/integrations/uptime-kuma/uptime-kuma.html" target="_blank">SIGNL4 Docs</a>
</i18n-t>
</div>
</template>

View File

@@ -3,7 +3,7 @@
<label for="smspartner-key" class="form-label">{{ $t("API Key") }}</label>
<HiddenInput id="smspartner-key" v-model="$parent.notification.smspartnerApikey" :required="true" autocomplete="new-password"></HiddenInput>
<div class="form-text">
<i18n-t keypath="smspartnerApiurl" tag="div" class="form-text">
<i18n-t keypath="smspartnerApiurl" as="div" class="form-text">
<a href="https://my.smspartner.fr/dashboard/api" target="_blank">my.smspartner.fr/dashboard/api</a>
</i18n-t>
</div>
@@ -12,7 +12,7 @@
<label for="smspartner-phone-number" class="form-label">{{ $t("smspartnerPhoneNumber") }}</label>
<input id="smspartner-phone-number" v-model="$parent.notification.smspartnerPhoneNumber" type="text" minlength="3" maxlength="20" pattern="^[\d+,]+$" class="form-control" required>
<div class="form-text">
<i18n-t keypath="smspartnerPhoneNumberHelptext" tag="div" class="form-text">
<i18n-t keypath="smspartnerPhoneNumberHelptext" as="div" class="form-text">
<code>+336xxxxxxxx</code>
<code>+496xxxxxxxx</code>
<code>,</code>

View File

@@ -9,12 +9,6 @@
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
<label class="form-label">{{ $t("Message format") }}</label>
<div class="form-check form-switch">
<input id="slack-text-message" v-model="$parent.notification.slackrichmessage" type="checkbox" class="form-check-input">
<label for="slack-text-message" class="form-label">{{ $t("Send rich messages") }}</label>
</div>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">

View File

@@ -1,87 +0,0 @@
<template>
<div class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipientType") }}</label>
<select
id="threema-recipient" v-model="$parent.notification.threemaRecipientType" required
class="form-select"
>
<option value="identity">{{ $t("threemaRecipientTypeIdentity") }}</option>
<option value="phone">{{ $t("threemaRecipientTypePhone") }}</option>
<option value="email">{{ $t("threemaRecipientTypeEmail") }}</option>
</select>
</div>
<div v-if="$parent.notification.threemaRecipientType === 'identity'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeIdentity") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
minlength="8"
maxlength="8"
pattern="[A-Z0-9]{8}"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaRecipientTypeIdentityFormat") }}</p>
</div>
</div>
<div v-else-if="$parent.notification.threemaRecipientType === 'phone'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypePhone") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
maxlength="15"
pattern="\d{1,15}"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaRecipientTypePhoneFormat") }}</p>
</div>
</div>
<div v-else-if="$parent.notification.threemaRecipientType === 'email'" class="mb-3">
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeEmail") }}</label>
<input
id="threema-recipient"
v-model="$parent.notification.threemaRecipient"
class="form-control"
maxlength="254"
required
type="email"
>
</div>
<div class="mb-3">
<label class="form-label" for="threema-sender">{{ $t("threemaSenderIdentity") }}</label>
<input
id="threema-sender"
v-model="$parent.notification.threemaSenderIdentity"
class="form-control"
minlength="8"
maxlength="8"
pattern="^\*[A-Z0-9]{7}$"
required
type="text"
>
<div class="form-text">
<p>{{ $t("threemaSenderIdentityFormat") }}</p>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="threema-secret">{{ $t("threemaApiAuthenticationSecret") }}</label>
<HiddenInput
id="threema-secret" v-model="$parent.notification.threemaSecret" required
autocomplete="false"
></HiddenInput>
</div>
<i18n-t class="form-text" keypath="wayToGetThreemaGateway" tag="div">
<a href="https://threema.ch/en/gateway" target="_blank">{{ $t("here") }}</a>
</i18n-t>
<i18n-t class="form-text" keypath="threemaBasicModeInfo" tag="div">
<a href="https://gateway.threema.ch/en/developer/api" target="_blank">{{ $t("here") }}</a>
</i18n-t>
</template>
<script lang="ts" setup>
import HiddenInput from "../HiddenInput.vue";
</script>

View File

@@ -1,31 +0,0 @@
<template>
<div class="mb-3">
<label for="wpush-apikey" class="form-label">WPush {{ $t("API Key") }}</label>
<HiddenInput id="wpush-apikey" v-model="$parent.notification.wpushAPIkey" :required="true" autocomplete="new-password" placeholder="WPushxxxxx"></HiddenInput>
</div>
<div class="mb-3">
<label for="wpush-channel" class="form-label">发送通道</label>
<select id="wpush-channel" v-model="$parent.notification.wpushChannel" class="form-select" required>
<option value="wechat">微信</option>
<option value="sms">短信</option>
<option value="mail">邮件</option>
<option value="feishu">飞书</option>
<option value="dingtalk">钉钉</option>
<option value="wechat_work">企业微信</option>
</select>
</div>
<i18n-t tag="p" keypath="More info on:">
<a href="https://wpush.cn/" rel="noopener noreferrer" target="_blank">https://wpush.cn/</a>
</i18n-t>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

Some files were not shown because too many files have changed in this diff Show More