Merge branch '2.0.X' into npm-publish

This commit is contained in:
Louis Lam 2023-08-11 20:27:34 +08:00 committed by GitHub
commit 80d5f6840b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
238 changed files with 5473 additions and 2371 deletions

View file

@ -1,6 +1,6 @@
/.idea /.idea
/node_modules /node_modules
/data /data*
/cypress /cypress
/out /out
/test /test
@ -18,6 +18,7 @@ README.md
.vscode .vscode
.eslint* .eslint*
.stylelint* .stylelint*
/.devcontainer
/.github /.github
yarn.lock yarn.lock
app.json app.json
@ -35,6 +36,7 @@ tsconfig.json
/extra/healthcheck /extra/healthcheck
extra/exe-builder extra/exe-builder
### .gitignore content (commented rules are duplicated) ### .gitignore content (commented rules are duplicated)
#node_modules #node_modules

View file

@ -14,6 +14,7 @@ module.exports = {
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
"plugin:vue/vue3-recommended", "plugin:vue/vue3-recommended",
"plugin:jsdoc/recommended-error",
], ],
parser: "vue-eslint-parser", parser: "vue-eslint-parser",
parserOptions: { parserOptions: {
@ -21,6 +22,9 @@ module.exports = {
sourceType: "module", sourceType: "module",
requireConfigFile: false, requireConfigFile: false,
}, },
plugins: [
"jsdoc"
],
rules: { rules: {
"yoda": "error", "yoda": "error",
eqeqeq: [ "warn", "smart" ], eqeqeq: [ "warn", "smart" ],
@ -97,7 +101,42 @@ module.exports = {
}], }],
"no-control-regex": "off", "no-control-regex": "off",
"one-var": [ "error", "never" ], "one-var": [ "error", "never" ],
"max-statements-per-line": [ "error", { "max": 1 }] "max-statements-per-line": [ "error", { "max": 1 }],
"jsdoc/check-tag-names": [
"error",
{
"definedTags": [ "link" ]
}
],
"jsdoc/no-undefined-types": "off",
"jsdoc/no-defaults": [
"error",
{ "noOptionalParamNames": true }
],
"jsdoc/require-throws": "error",
"jsdoc/require-jsdoc": [
"error",
{
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
}
}
],
"jsdoc/no-blank-block-descriptions": "error",
"jsdoc/require-returns-check": [
"error",
{ "reportMissingReturnForUndefinedTypes": false }
],
"jsdoc/require-returns": [
"error",
{
"forceRequireReturn": true,
"forceReturnsWithAsync": true
}
],
"jsdoc/require-param-type": "error",
"jsdoc/require-param-description": "error"
}, },
"overrides": [ "overrides": [
{ {

View file

@ -71,10 +71,10 @@ jobs:
- run: git config --global core.autocrlf false # Mainly for Windows - run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Use Node.js 14 - name: Use Node.js 20
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 14 node-version: 20
- run: npm install - run: npm install
- run: npm run lint - run: npm run lint

1
.gitignore vendored
View file

@ -7,6 +7,7 @@ dist-ssr
/data /data
!/data/.gitkeep !/data/.gitkeep
/data*
.vscode .vscode
/private /private

View file

@ -4,8 +4,4 @@ if (process.env.TEST_FRONTEND) {
config.presets = [ "@babel/preset-env" ]; config.presets = [ "@babel/preset-env" ];
} }
if (process.env.TEST_BACKEND) {
config.plugins = [ "babel-plugin-rewire" ];
}
module.exports = config; module.exports = config;

559
db/knex_init_db.js Normal file
View file

@ -0,0 +1,559 @@
const { R } = require("redbean-node");
const { log } = require("../src/util");
/**
* DO NOT ADD ANYTHING HERE!
* IF YOU NEED TO ADD FIELDS, ADD IT TO ./db/knex_migrations
* See ./db/knex_migrations/README.md for more information
* @returns {Promise<void>}
*/
async function createTables() {
log.info("mariadb", "Creating basic tables for MariaDB");
const knex = R.knex;
// TODO: Should check later if it is really the final patch sql file.
// docker_host
await knex.schema.createTable("docker_host", (table) => {
table.increments("id");
table.integer("user_id").unsigned().notNullable();
table.string("docker_daemon", 255);
table.string("docker_type", 255);
table.string("name", 255);
});
// group
await knex.schema.createTable("group", (table) => {
table.increments("id");
table.string("name", 255).notNullable();
table.datetime("created_date").notNullable().defaultTo(knex.fn.now());
table.boolean("public").notNullable().defaultTo(false);
table.boolean("active").notNullable().defaultTo(true);
table.integer("weight").notNullable().defaultTo(1000);
table.integer("status_page_id").unsigned();
});
// proxy
await knex.schema.createTable("proxy", (table) => {
table.increments("id");
table.integer("user_id").unsigned().notNullable();
table.string("protocol", 10).notNullable();
table.string("host", 255).notNullable();
table.smallint("port").notNullable(); // TODO: Maybe a issue with MariaDB, need migration to int
table.boolean("auth").notNullable();
table.string("username", 255).nullable();
table.string("password", 255).nullable();
table.boolean("active").notNullable().defaultTo(true);
table.boolean("default").notNullable().defaultTo(false);
table.datetime("created_date").notNullable().defaultTo(knex.fn.now());
table.index("user_id", "proxy_user_id");
});
// user
await knex.schema.createTable("user", (table) => {
table.increments("id");
table.string("username", 255).notNullable().unique().collate("utf8_general_ci");
table.string("password", 255);
table.boolean("active").notNullable().defaultTo(true);
table.string("timezone", 150);
table.string("twofa_secret", 64);
table.boolean("twofa_status").notNullable().defaultTo(false);
table.string("twofa_last_token", 6);
});
// monitor
await knex.schema.createTable("monitor", (table) => {
table.increments("id");
table.string("name", 150);
table.boolean("active").notNullable().defaultTo(true);
table.integer("user_id").unsigned()
.references("id").inTable("user")
.onDelete("SET NULL")
.onUpdate("CASCADE");
table.integer("interval").notNullable().defaultTo(20);
table.text("url");
table.string("type", 20);
table.integer("weight").defaultTo(2000);
table.string("hostname", 255);
table.integer("port");
table.datetime("created_date").notNullable().defaultTo(knex.fn.now());
table.string("keyword", 255);
table.integer("maxretries").notNullable().defaultTo(0);
table.boolean("ignore_tls").notNullable().defaultTo(false);
table.boolean("upside_down").notNullable().defaultTo(false);
table.integer("maxredirects").notNullable().defaultTo(10);
table.text("accepted_statuscodes_json").notNullable().defaultTo("[\"200-299\"]");
table.string("dns_resolve_type", 5);
table.string("dns_resolve_server", 255);
table.string("dns_last_result", 255);
table.integer("retry_interval").notNullable().defaultTo(0);
table.string("push_token", 20).defaultTo(null);
table.text("method").notNullable().defaultTo("GET");
table.text("body").defaultTo(null);
table.text("headers").defaultTo(null);
table.text("basic_auth_user").defaultTo(null);
table.text("basic_auth_pass").defaultTo(null);
table.integer("docker_host").unsigned()
.references("id").inTable("docker_host");
table.string("docker_container", 255);
table.integer("proxy_id").unsigned()
.references("id").inTable("proxy");
table.boolean("expiry_notification").defaultTo(true);
table.text("mqtt_topic");
table.string("mqtt_success_message", 255);
table.string("mqtt_username", 255);
table.string("mqtt_password", 255);
table.string("database_connection_string", 2000);
table.text("database_query");
table.string("auth_method", 250);
table.text("auth_domain");
table.text("auth_workstation");
table.string("grpc_url", 255).defaultTo(null);
table.text("grpc_protobuf").defaultTo(null);
table.text("grpc_body").defaultTo(null);
table.text("grpc_metadata").defaultTo(null);
table.text("grpc_method").defaultTo(null);
table.text("grpc_service_name").defaultTo(null);
table.boolean("grpc_enable_tls").notNullable().defaultTo(false);
table.string("radius_username", 255);
table.string("radius_password", 255);
table.string("radius_calling_station_id", 50);
table.string("radius_called_station_id", 50);
table.string("radius_secret", 255);
table.integer("resend_interval").notNullable().defaultTo(0);
table.integer("packet_size").notNullable().defaultTo(56);
table.string("game", 255);
});
// heartbeat
await knex.schema.createTable("heartbeat", (table) => {
table.increments("id");
table.boolean("important").notNullable().defaultTo(false);
table.integer("monitor_id").unsigned().notNullable()
.references("id").inTable("monitor")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.smallint("status").notNullable();
table.text("msg");
table.datetime("time").notNullable();
table.integer("ping");
table.integer("duration").notNullable().defaultTo(0);
table.integer("down_count").notNullable().defaultTo(0);
table.index("important");
table.index([ "monitor_id", "time" ], "monitor_time_index");
table.index("monitor_id");
table.index([ "monitor_id", "important", "time" ], "monitor_important_time_index");
});
// incident
await knex.schema.createTable("incident", (table) => {
table.increments("id");
table.string("title", 255).notNullable();
table.text("content", 255).notNullable();
table.string("style", 30).notNullable().defaultTo("warning");
table.datetime("created_date").notNullable().defaultTo(knex.fn.now());
table.datetime("last_updated_date");
table.boolean("pin").notNullable().defaultTo(true);
table.boolean("active").notNullable().defaultTo(true);
table.integer("status_page_id").unsigned();
});
// maintenance
await knex.schema.createTable("maintenance", (table) => {
table.increments("id");
table.string("title", 150).notNullable();
table.text("description").notNullable();
table.integer("user_id").unsigned()
.references("id").inTable("user")
.onDelete("SET NULL")
.onUpdate("CASCADE");
table.boolean("active").notNullable().defaultTo(true);
table.string("strategy", 50).notNullable().defaultTo("single");
table.datetime("start_date");
table.datetime("end_date");
table.time("start_time");
table.time("end_time");
table.string("weekdays", 250).defaultTo("[]");
table.text("days_of_month").defaultTo("[]");
table.integer("interval_day");
table.index("active");
table.index([ "strategy", "active" ], "manual_active");
table.index("user_id", "maintenance_user_id");
});
// status_page
await knex.schema.createTable("status_page", (table) => {
table.increments("id");
table.string("slug", 255).notNullable().unique().collate("utf8_general_ci");
table.string("title", 255).notNullable();
table.text("description");
table.string("icon", 255).notNullable();
table.string("theme", 30).notNullable();
table.boolean("published").notNullable().defaultTo(true);
table.boolean("search_engine_index").notNullable().defaultTo(true);
table.boolean("show_tags").notNullable().defaultTo(false);
table.string("password");
table.datetime("created_date").notNullable().defaultTo(knex.fn.now());
table.datetime("modified_date").notNullable().defaultTo(knex.fn.now());
table.text("footer_text");
table.text("custom_css");
table.boolean("show_powered_by").notNullable().defaultTo(true);
table.string("google_analytics_tag_id");
});
// maintenance_status_page
await knex.schema.createTable("maintenance_status_page", (table) => {
table.increments("id");
table.integer("status_page_id").unsigned().notNullable()
.references("id").inTable("status_page")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.integer("maintenance_id").unsigned().notNullable()
.references("id").inTable("maintenance")
.onDelete("CASCADE")
.onUpdate("CASCADE");
});
// maintenance_timeslot
await knex.schema.createTable("maintenance_timeslot", (table) => {
table.increments("id");
table.integer("maintenance_id").unsigned().notNullable()
.references("id").inTable("maintenance")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.datetime("start_date").notNullable();
table.datetime("end_date");
table.boolean("generated_next").defaultTo(false);
table.index("maintenance_id");
table.index([ "maintenance_id", "start_date", "end_date" ], "active_timeslot_index");
table.index("generated_next", "generated_next_index");
});
// monitor_group
await knex.schema.createTable("monitor_group", (table) => {
table.increments("id");
table.integer("monitor_id").unsigned().notNullable()
.references("id").inTable("monitor")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.integer("group_id").unsigned().notNullable()
.references("id").inTable("group")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.integer("weight").notNullable().defaultTo(1000);
table.boolean("send_url").notNullable().defaultTo(false);
table.index([ "monitor_id", "group_id" ], "fk");
});
// monitor_maintenance
await knex.schema.createTable("monitor_maintenance", (table) => {
table.increments("id");
table.integer("monitor_id").unsigned().notNullable()
.references("id").inTable("monitor")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.integer("maintenance_id").unsigned().notNullable()
.references("id").inTable("maintenance")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.index("maintenance_id", "maintenance_id_index2");
table.index("monitor_id", "monitor_id_index");
});
// notification
await knex.schema.createTable("notification", (table) => {
table.increments("id");
table.string("name", 255);
table.string("config", 255); // TODO: should use TEXT!
table.boolean("active").notNullable().defaultTo(true);
table.integer("user_id").unsigned();
table.boolean("is_default").notNullable().defaultTo(false);
});
// monitor_notification
await knex.schema.createTable("monitor_notification", (table) => {
table.increments("id").unsigned(); // TODO: no auto increment????
table.integer("monitor_id").unsigned().notNullable()
.references("id").inTable("monitor")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.integer("notification_id").unsigned().notNullable()
.references("id").inTable("notification")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.index([ "monitor_id", "notification_id" ], "monitor_notification_index");
});
// tag
await knex.schema.createTable("tag", (table) => {
table.increments("id");
table.string("name", 255).notNullable();
table.string("color", 255).notNullable();
table.datetime("created_date").notNullable().defaultTo(knex.fn.now());
});
// monitor_tag
await knex.schema.createTable("monitor_tag", (table) => {
table.increments("id");
table.integer("monitor_id").unsigned().notNullable()
.references("id").inTable("monitor")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.integer("tag_id").unsigned().notNullable()
.references("id").inTable("tag")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.text("value");
});
// monitor_tls_info
await knex.schema.createTable("monitor_tls_info", (table) => {
table.increments("id");
table.integer("monitor_id").unsigned().notNullable(); //TODO: no fk ?
table.text("info_json");
});
// notification_sent_history
await knex.schema.createTable("notification_sent_history", (table) => {
table.increments("id");
table.string("type", 50).notNullable();
table.integer("monitor_id").unsigned().notNullable();
table.integer("days").notNullable();
table.unique([ "type", "monitor_id", "days" ]);
table.index([ "type", "monitor_id", "days" ], "good_index");
});
// setting
await knex.schema.createTable("setting", (table) => {
table.increments("id");
table.string("key", 200).notNullable().unique().collate("utf8_general_ci");
table.text("value");
table.string("type", 20);
});
// status_page_cname
await knex.schema.createTable("status_page_cname", (table) => {
table.increments("id");
table.integer("status_page_id").unsigned()
.references("id").inTable("status_page")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.string("domain").notNullable().unique().collate("utf8_general_ci");
});
/*********************
* Converted Patch here
*********************/
// 2023-06-30-1348-http-body-encoding.js
// ALTER TABLE monitor ADD http_body_encoding VARCHAR(25);
// UPDATE monitor SET http_body_encoding = 'json' WHERE (type = 'http' or type = 'keyword') AND http_body_encoding IS NULL;
await knex.schema.table("monitor", function (table) {
table.string("http_body_encoding", 25);
});
await knex("monitor")
.where(function () {
this.where("type", "http").orWhere("type", "keyword");
})
.whereNull("http_body_encoding")
.update({
http_body_encoding: "json",
});
// 2023-06-30-1354-add-description-monitor.js
// ALTER TABLE monitor ADD description TEXT default null;
await knex.schema.table("monitor", function (table) {
table.text("description").defaultTo(null);
});
// 2023-06-30-1357-api-key-table.js
/*
CREATE TABLE [api_key] (
[id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
[key] VARCHAR(255) NOT NULL,
[name] VARCHAR(255) NOT NULL,
[user_id] INTEGER NOT NULL,
[created_date] DATETIME DEFAULT (DATETIME('now')) NOT NULL,
[active] BOOLEAN DEFAULT 1 NOT NULL,
[expires] DATETIME DEFAULT NULL,
CONSTRAINT FK_user FOREIGN KEY ([user_id]) REFERENCES [user]([id]) ON DELETE CASCADE ON UPDATE CASCADE
);
*/
await knex.schema.createTable("api_key", function (table) {
table.increments("id").primary();
table.string("key", 255).notNullable();
table.string("name", 255).notNullable();
table.integer("user_id").unsigned().notNullable()
.references("id").inTable("user")
.onDelete("CASCADE")
.onUpdate("CASCADE");
table.dateTime("created_date").defaultTo(knex.fn.now()).notNullable();
table.boolean("active").defaultTo(1).notNullable();
table.dateTime("expires").defaultTo(null);
});
// 2023-06-30-1400-monitor-tls.js
/*
ALTER TABLE monitor
ADD tls_ca TEXT default null;
ALTER TABLE monitor
ADD tls_cert TEXT default null;
ALTER TABLE monitor
ADD tls_key TEXT default null;
*/
await knex.schema.table("monitor", function (table) {
table.text("tls_ca").defaultTo(null);
table.text("tls_cert").defaultTo(null);
table.text("tls_key").defaultTo(null);
});
// 2023-06-30-1401-maintenance-cron.js
/*
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
DROP TABLE maintenance_timeslot;
ALTER TABLE maintenance ADD cron TEXT;
ALTER TABLE maintenance ADD timezone VARCHAR(255);
ALTER TABLE maintenance ADD duration INTEGER;
*/
await knex.schema
.dropTableIfExists("maintenance_timeslot")
.table("maintenance", function (table) {
table.text("cron");
table.string("timezone", 255);
table.integer("duration");
});
// 2023-06-30-1413-add-parent-monitor.js.
/*
ALTER TABLE monitor
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;
*/
await knex.schema.table("monitor", function (table) {
table.integer("parent").unsigned()
.references("id").inTable("monitor")
.onDelete("SET NULL")
.onUpdate("CASCADE");
});
/*
patch-add-invert-keyword.sql
ALTER TABLE monitor
ADD invert_keyword BOOLEAN default 0 not null;
*/
await knex.schema.table("monitor", function (table) {
table.boolean("invert_keyword").defaultTo(0).notNullable();
});
/*
patch-added-json-query.sql
ALTER TABLE monitor
ADD json_path TEXT;
ALTER TABLE monitor
ADD expected_value VARCHAR(255);
*/
await knex.schema.table("monitor", function (table) {
table.text("json_path");
table.string("expected_value", 255);
});
/*
patch-added-kafka-producer.sql
ALTER TABLE monitor
ADD kafka_producer_topic VARCHAR(255);
ALTER TABLE monitor
ADD kafka_producer_brokers TEXT;
ALTER TABLE monitor
ADD kafka_producer_ssl INTEGER;
ALTER TABLE monitor
ADD kafka_producer_allow_auto_topic_creation VARCHAR(255);
ALTER TABLE monitor
ADD kafka_producer_sasl_options TEXT;
ALTER TABLE monitor
ADD kafka_producer_message TEXT;
*/
await knex.schema.table("monitor", function (table) {
table.string("kafka_producer_topic", 255);
table.text("kafka_producer_brokers");
table.integer("kafka_producer_ssl");
table.string("kafka_producer_allow_auto_topic_creation", 255);
table.text("kafka_producer_sasl_options");
table.text("kafka_producer_message");
});
/*
patch-add-certificate-expiry-status-page.sql
ALTER TABLE status_page
ADD show_certificate_expiry BOOLEAN default 0 NOT NULL;
*/
await knex.schema.table("status_page", function (table) {
table.boolean("show_certificate_expiry").defaultTo(0).notNullable();
});
/*
patch-monitor-oauth-cc.sql
ALTER TABLE monitor
ADD oauth_client_id TEXT default null;
ALTER TABLE monitor
ADD oauth_client_secret TEXT default null;
ALTER TABLE monitor
ADD oauth_token_url TEXT default null;
ALTER TABLE monitor
ADD oauth_scopes TEXT default null;
ALTER TABLE monitor
ADD oauth_auth_method TEXT default null;
*/
await knex.schema.table("monitor", function (table) {
table.text("oauth_client_id").defaultTo(null);
table.text("oauth_client_secret").defaultTo(null);
table.text("oauth_token_url").defaultTo(null);
table.text("oauth_scopes").defaultTo(null);
table.text("oauth_auth_method").defaultTo(null);
});
/*
patch-add-timeout-monitor.sql
ALTER TABLE monitor
ADD timeout DOUBLE default 0 not null;
*/
await knex.schema.table("monitor", function (table) {
table.double("timeout").defaultTo(0).notNullable();
});
/*
patch-add-gamedig-given-port.sql
ALTER TABLE monitor
ADD gamedig_given_port_only BOOLEAN default 1 not null;
*/
await knex.schema.table("monitor", function (table) {
table.boolean("gamedig_given_port_only").defaultTo(1).notNullable();
});
log.info("mariadb", "Created basic tables for MariaDB");
}
module.exports = {
createTables,
};

View file

@ -0,0 +1,55 @@
## Info
https://knexjs.org/guide/migrations.html#knexfile-in-other-languages
## Template
Filename: YYYYMMDDHHMMSS_name.js
```js
exports.up = function(knex) {
};
exports.down = function(knex) {
};
// exports.config = { transaction: false };
```
## Example
YYYY-MM-DD-HHMM-create-users-products.js
2023-06-30-1348-create-users-products.js
```js
exports.up = function(knex) {
return knex.schema
.createTable('users', function (table) {
table.increments('id');
table.string('first_name', 255).notNullable();
table.string('last_name', 255).notNullable();
})
.createTable('products', function (table) {
table.increments('id');
table.decimal('price').notNullable();
table.string('name', 1000).notNullable();
}).then(() => {
knex("products").insert([
{ price: 10, name: "Apple" },
{ price: 20, name: "Orange" },
]);
});
};
exports.down = function(knex) {
return knex.schema
.dropTable("products")
.dropTable("users");
};
```
https://knexjs.org/guide/migrations.html#transactions-in-migrations

View file

@ -0,0 +1,3 @@
# Don't create a new migration file here
Please go to ./db/knex_migrations/README.md

View file

@ -0,0 +1,7 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD gamedig_given_port_only BOOLEAN default 1 not null;
COMMIT;

View file

@ -0,0 +1,6 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD timeout DOUBLE default 0 not null;
COMMIT;

View file

@ -1,8 +0,0 @@
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
FROM node:16-alpine3.12
WORKDIR /app
# Install apprise, iputils for non-root ping, setpriv
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \
pip3 --no-cache-dir install apprise==1.4.0 && \
rm -rf /root/.cache

View file

@ -1,12 +1,11 @@
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
# If the image changed, the second stage image should be changed too # If the image changed, the second stage image should be changed too
FROM node:16-buster-slim FROM node:20-bookworm-slim AS base2-slim
ARG TARGETPLATFORM ARG TARGETPLATFORM
WORKDIR /app WORKDIR /app
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller! # Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
# python3* = apprise's dependencies # apprise = for notifications (From testing repo)
# sqlite3 = for debugging # sqlite3 = for debugging
# iputils-ping = for ping # iputils-ping = for ping
# util-linux = for setpriv (Should be dropped in 2.0.0?) # util-linux = for setpriv (Should be dropped in 2.0.0?)
@ -15,29 +14,25 @@ WORKDIR /app
# ca-certificates = keep the cert up-to-date # ca-certificates = keep the cert up-to-date
# sudo = for start service nscd with non-root user # sudo = for start service nscd with non-root user
# nscd = for better DNS caching # nscd = for better DNS caching
# (pip) apprise = for notifications RUN echo "deb http://deb.debian.org/debian testing main" >> /etc/apt/sources.list && \
RUN apt-get update && \ apt update && \
apt-get --yes --no-install-recommends install \ apt --yes --no-install-recommends -t testing install apprise sqlite3 ca-certificates && \
python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ apt --yes --no-install-recommends -t stable install \
sqlite3 \
iputils-ping \ iputils-ping \
util-linux \ util-linux \
dumb-init \ dumb-init \
curl \ curl \
ca-certificates \
sudo \ sudo \
nscd && \ nscd && \
pip3 --no-cache-dir install apprise==1.4.5 && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove apt --yes autoremove
# Install cloudflared # Install cloudflared
RUN set -eux && \ RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
mkdir -p --mode=0755 /usr/share/keyrings && \ echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | tee /etc/apt/sources.list.d/cloudflared.list && \
curl --fail --show-error --silent --location --insecure https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \ apt update && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared buster main' | tee /etc/apt/sources.list.d/cloudflared.list && \ apt install --yes --no-install-recommends -t stable cloudflared && \
apt-get update && \
apt-get install --yes --no-install-recommends cloudflared && \
cloudflared version && \ cloudflared version && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove apt --yes autoremove
@ -46,3 +41,16 @@ RUN set -eux && \
COPY ./docker/etc/nscd.conf /etc/nscd.conf COPY ./docker/etc/nscd.conf /etc/nscd.conf
COPY ./docker/etc/sudoers /etc/sudoers COPY ./docker/etc/sudoers /etc/sudoers
# Full Base Image
# MariaDB, Chromium and fonts
# Not working for armv7, so use the older version (10.5) of MariaDB from the debian repo
# curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version="mariadb-11.1" && \
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 && \
apt --yes remove curl && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove && \
chown -R node:node /var/lib/mysql

View file

@ -0,0 +1,14 @@
version: '3.8'
services:
uptime-kuma:
container_name: uptime-kuma-dev
image: louislam/uptime-kuma:nightly2
volumes:
#- ./data:/app/data
- ../server:/app/server
- ../db:/app/db
ports:
- "3001:3001" # <Host Port>:<Container Port>
- "3307:3306"

View file

@ -1,14 +1,15 @@
# Simple docker-compose.yml version: '3.8'
# You can change your port or volume location
version: '3.3'
services: services:
uptime-kuma: uptime-kuma:
image: louislam/uptime-kuma:1 image: louislam/uptime-kuma:2
container_name: uptime-kuma container_name: uptime-kuma
volumes: volumes:
- ./uptime-kuma-data:/app/data - uptime-kuma:/app/data
ports: ports:
- 3001:3001 # <Host Port>:<Container Port> - "3001:3001" # <Host Port>:<Container Port>
restart: always restart: always
volumes:
uptime-kuma:

View file

@ -1,6 +1,8 @@
ARG BASE_IMAGE=louislam/uptime-kuma:base2
############################################ ############################################
# Build in Golang # Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck # Run npm run build-healthcheck-armv7 in the host first, otherwise it will be super slow where it is building the armv7 healthcheck
# Check file: builder-go.dockerfile # Check file: builder-go.dockerfile
############################################ ############################################
FROM louislam/uptime-kuma:builder-go AS build_healthcheck FROM louislam/uptime-kuma:builder-go AS build_healthcheck
@ -8,49 +10,47 @@ FROM louislam/uptime-kuma:builder-go AS build_healthcheck
############################################ ############################################
# Build in Node.js # Build in Node.js
############################################ ############################################
FROM louislam/uptime-kuma:base-debian AS build FROM louislam/uptime-kuma:base2 AS build
USER node
WORKDIR /app WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
COPY .npmrc .npmrc COPY --chown=node:node .npmrc .npmrc
COPY package.json package.json COPY --chown=node:node package.json package.json
COPY package-lock.json package-lock.json COPY --chown=node:node package-lock.json package-lock.json
RUN npm ci --omit=dev RUN npm ci --omit=dev
COPY . . COPY . .
COPY --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck COPY --chown=node:node --from=build_healthcheck /app/extra/healthcheck /app/extra/healthcheck
RUN chmod +x /app/extra/entrypoint.sh
############################################ ############################################
# ⭐ Main Image # ⭐ Main Image
############################################ ############################################
FROM louislam/uptime-kuma:base-debian AS release FROM $BASE_IMAGE AS release
USER node
WORKDIR /app WORKDIR /app
ENV UPTIME_KUMA_IS_CONTAINER=1 ENV UPTIME_KUMA_IS_CONTAINER=1
# Copy app files from build layer # Copy app files from build layer
COPY --from=build /app /app COPY --chown=node:node --from=build /app /app
EXPOSE 3001 EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"] ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["node", "server/server.js"] CMD ["node", "server/server.js"]
############################################ ############################################
# Mark as Nightly # Mark as Nightly
############################################ ############################################
FROM release AS nightly FROM release AS nightly
USER node
RUN npm run mark-as-nightly RUN npm run mark-as-nightly
############################################ ############################################
# Build an image for testing pr # Build an image for testing pr
############################################ ############################################
FROM louislam/uptime-kuma:base-debian AS pr-test FROM louislam/uptime-kuma:base2 AS pr-test2
WORKDIR /app WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
## Install Git ## Install Git
@ -78,7 +78,7 @@ CMD ["npm", "run", "start-pr-test"]
############################################ ############################################
# Upload the artifact to Github # Upload the artifact to Github
############################################ ############################################
FROM louislam/uptime-kuma:base-debian AS upload-artifact FROM louislam/uptime-kuma:base2 AS upload-artifact
WORKDIR / WORKDIR /
RUN apt update && \ RUN apt update && \
apt --yes install curl file apt --yes install curl file

View file

@ -1,27 +0,0 @@
FROM louislam/uptime-kuma:base-alpine AS build
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
COPY .npmrc .npmrc
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm ci --omit=dev
COPY . .
RUN chmod +x /app/extra/entrypoint.sh
FROM louislam/uptime-kuma:base-alpine AS release
WORKDIR /app
# Copy app files from build layer
COPY --from=build /app /app
EXPOSE 3001
VOLUME ["/app/data"]
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"]
CMD ["node", "server/server.js"]
FROM release AS nightly
RUN npm run mark-as-nightly

View file

@ -36,6 +36,8 @@ if (! exists) {
/** /**
* Commit updated files * Commit updated files
* @param {string} version Version to update to * @param {string} version Version to update to
* @returns {void}
* @throws Error committing files
*/ */
function commit(version) { function commit(version) {
let msg = "Update to " + version; let msg = "Update to " + version;
@ -55,6 +57,7 @@ function commit(version) {
/** /**
* Create a tag with the specified version * Create a tag with the specified version
* @param {string} version Tag to create * @param {string} version Tag to create
* @returns {void}
*/ */
function tag(version) { function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]); let res = childProcess.spawnSync("git", [ "tag", version ]);
@ -68,6 +71,7 @@ function tag(version) {
* Check if a tag exists for the specified version * Check if a tag exists for the specified version
* @param {string} version Version to check * @param {string} version Version to check
* @returns {boolean} Does the tag already exist * @returns {boolean} Does the tag already exist
* @throws Version is not valid
*/ */
function tagExists(version) { function tagExists(version) {
if (! version) { if (! version) {

View file

@ -15,6 +15,7 @@ download(url);
/** /**
* Downloads the latest version of the dist from a GitHub release. * Downloads the latest version of the dist from a GitHub release.
* @param {string} url The URL to download from. * @param {string} url The URL to download from.
* @returns {void}
* *
* Generated by Trelent * Generated by Trelent
*/ */

View file

@ -1,21 +0,0 @@
#!/usr/bin/env sh
# set -e Exit the script if an error happens
set -e
PUID=${PUID=0}
PGID=${PGID=0}
files_ownership () {
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link.
# -R Recursively descends the specified directories
# -c Like verbose but report only when a change is made
chown -hRc "$PUID":"$PGID" /app/data
}
echo "==> Performing startup jobs and maintenance tasks"
files_ownership
echo "==> Starting application with user $PUID group $PGID"
# --clear-groups Clear supplementary groups.
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@"

View file

@ -4,12 +4,12 @@ const fs = require("fs");
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16, * 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. * or the `recursive` property removing completely in the future Node.js version.
* See the link below. * 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`. * @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 infomation of `fs.rmdirSync` * @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation infomation of `fs.rmdirSync`
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync` * @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.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`. * @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) => { const rmSync = (path, options) => {
if (typeof fs.rmSync === "function") { if (typeof fs.rmSync === "function") {

View file

@ -12,7 +12,7 @@ const rl = readline.createInterface({
}); });
const main = async () => { const main = async () => {
Database.init(args); Database.initDataDir(args);
await Database.connect(); await Database.connect();
try { try {

View file

@ -13,7 +13,7 @@ const rl = readline.createInterface({
const main = async () => { const main = async () => {
console.log("Connecting the database"); console.log("Connecting the database");
Database.init(args); Database.initDataDir(args);
await Database.connect(false, false, true); await Database.connect(false, false, true);
try { try {

View file

@ -138,7 +138,7 @@ server.listen({
/** /**
* Get human readable request type from request code * Get human readable request type from request code
* @param {number} code Request code to translate * @param {number} code Request code to translate
* @returns {string} Human readable request type * @returns {string|void} Human readable request type
*/ */
function type(code) { function type(code) {
for (let name in Packet.TYPE) { for (let name in Packet.TYPE) {

View file

@ -7,11 +7,17 @@ class SimpleMqttServer {
aedes = require("aedes")(); aedes = require("aedes")();
server = require("net").createServer(this.aedes.handle); server = require("net").createServer(this.aedes.handle);
/**
* @param {number} port Port to listen on
*/
constructor(port) { constructor(port) {
this.port = port; this.port = port;
} }
/** Start the MQTT server */ /**
* Start the MQTT server
* @returns {void}
*/
start() { start() {
this.server.listen(this.port, () => { this.server.listen(this.port, () => {
console.log("server started and listening on port ", this.port); console.log("server started and listening on port ", this.port);

View file

@ -12,6 +12,7 @@ import rmSync from "../fs-rmSync.js";
* created with this code if one does not already exist * created with this code if one does not already exist
* @param {string} baseLang The second base language file to copy. This * @param {string} baseLang The second base language file to copy. This
* will be ignored if set to "en" as en.js is copied by default * will be ignored if set to "en" as en.js is copied by default
* @returns {void}
*/ */
function copyFiles(langCode, baseLang) { function copyFiles(langCode, baseLang) {
if (fs.existsSync("./languages")) { if (fs.existsSync("./languages")) {
@ -33,7 +34,8 @@ function copyFiles(langCode, baseLang) {
/** /**
* Update the specified language file * Update the specified language file
* @param {string} langCode Language code to update * @param {string} langCode Language code to update
* @param {string} baseLang Second language to copy keys from * @param {string} baseLangCode Second language to copy keys from
* @returns {void}
*/ */
async function updateLanguage(langCode, baseLangCode) { async function updateLanguage(langCode, baseLangCode) {
const en = (await import("./languages/en.js")).default; const en = (await import("./languages/en.js")).default;

View file

@ -39,6 +39,8 @@ if (! exists) {
/** /**
* Commit updated files * Commit updated files
* @param {string} version Version to update to * @param {string} version Version to update to
* @returns {void}
* @throws Error when committing files
*/ */
function commit(version) { function commit(version) {
let msg = "Update to " + version; let msg = "Update to " + version;
@ -55,6 +57,7 @@ function commit(version) {
/** /**
* Create a tag with the specified version * Create a tag with the specified version
* @param {string} version Tag to create * @param {string} version Tag to create
* @returns {void}
*/ */
function tag(version) { function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]); let res = childProcess.spawnSync("git", [ "tag", version ]);
@ -65,6 +68,7 @@ function tag(version) {
* Check if a tag exists for the specified version * Check if a tag exists for the specified version
* @param {string} version Version to check * @param {string} version Version to check
* @returns {boolean} Does the tag already exist * @returns {boolean} Does the tag already exist
* @throws Version is not valid
*/ */
function tagExists(version) { function tagExists(version) {
if (! version) { if (! version) {

View file

@ -13,6 +13,7 @@ updateWiki(newVersion);
/** /**
* Update the wiki with new version number * Update the wiki with new version number
* @param {string} newVersion Version to update to * @param {string} newVersion Version to update to
* @returns {void}
*/ */
function updateWiki(newVersion) { function updateWiki(newVersion) {
const wikiDir = "./tmp/wiki"; const wikiDir = "./tmp/wiki";
@ -46,6 +47,7 @@ function updateWiki(newVersion) {
/** /**
* Check if a directory exists and then delete it * Check if a directory exists and then delete it
* @param {string} dir Directory to delete * @param {string} dir Directory to delete
* @returns {void}
*/ */
function safeDelete(dir) { function safeDelete(dir) {
if (fs.existsSync(dir)) { if (fs.existsSync(dir)) {

2958
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "uptime-kuma", "name": "uptime-kuma",
"version": "1.22.1", "version": "1.23.0-beta.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@ -32,15 +32,14 @@
"jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js", "jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
"tsc": "tsc", "tsc": "tsc",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js", "vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker": "npm run build && npm run build-docker-debian && npm run build-docker-alpine", "build-docker": "npm run build && npm run build-docker-full && npm run build-docker-slim",
"build-docker-alpine-base": "docker buildx build -f docker/alpine-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-alpine . --push", "build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
"build-docker-debian-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base-debian . --push", "build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push", "build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push", "build-docker-slim": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim -t louislam/uptime-kuma:$VERSION-slim --target release --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push", "build-docker-full": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2 -t louislam/uptime-kuma:$VERSION --target release . --push",
"build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push", "build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly2 --target nightly . --push",
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push", "build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push", "build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --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", "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.22.1 && npm ci --production && npm run download-dist", "setup": "git checkout 1.22.1 && npm ci --production && npm run download-dist",
@ -51,7 +50,6 @@
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1", "compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
"test-install-script-rockylinux": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/rockylinux.dockerfile .", "test-install-script-rockylinux": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/rockylinux.dockerfile .",
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .", "test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .", "test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .",
"test-install-script-debian-buster": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian-buster.dockerfile .", "test-install-script-debian-buster": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian-buster.dockerfile .",
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .", "test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
@ -60,7 +58,6 @@
"simple-dns-server": "node extra/simple-dns-server.js", "simple-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-server.js", "simple-mqtt-server": "node extra/simple-mqtt-server.js",
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix", "update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
"ncu-patch": "npm-check-updates -u -t patch",
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js", "release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts", "release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
"git-remove-tag": "git tag -d", "git-remove-tag": "git tag -d",
@ -72,7 +69,9 @@
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"", "cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go", "build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"deploy-demo-server": "node extra/deploy-demo-server.js", "deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js" "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"
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "~1.7.3", "@grpc/grpc-js": "~1.7.3",
@ -109,6 +108,7 @@
"jsonwebtoken": "~9.0.0", "jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2", "jwt-decode": "~3.1.2",
"kafkajs": "^2.2.4", "kafkajs": "^2.2.4",
"knex": "^2.4.2",
"limiter": "~2.1.0", "limiter": "~2.1.0",
"liquidjs": "^10.7.0", "liquidjs": "^10.7.0",
"mongodb": "~4.14.0", "mongodb": "~4.14.0",
@ -156,7 +156,6 @@
"@vue/compiler-sfc": "~3.3.4", "@vue/compiler-sfc": "~3.3.4",
"@vuepic/vue-datepicker": "~3.4.8", "@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"chart.js": "~4.2.1", "chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4", "chartjs-adapter-dayjs-4": "~1.0.4",
@ -169,6 +168,7 @@
"dns2": "~2.0.1", "dns2": "~2.0.1",
"dompurify": "~2.4.3", "dompurify": "~2.4.3",
"eslint": "~8.14.0", "eslint": "~8.14.0",
"eslint-plugin-jsdoc": "^46.4.6",
"eslint-plugin-vue": "~8.7.1", "eslint-plugin-vue": "~8.7.1",
"favico.js": "~0.3.10", "favico.js": "~0.3.10",
"jest": "~29.6.1", "jest": "~29.6.1",

View file

@ -9,9 +9,9 @@ const dayjs = require("dayjs");
/** /**
* Login to web app * Login to web app
* @param {string} username * @param {string} username Username to login with
* @param {string} password * @param {string} password Password to login with
* @returns {Promise<(Bean|null)>} * @returns {Promise<(Bean|null)>} User or null if login failed
*/ */
exports.login = async function (username, password) { exports.login = async function (username, password) {
if (typeof username !== "string" || typeof password !== "string") { if (typeof username !== "string" || typeof password !== "string") {
@ -39,6 +39,7 @@ exports.login = async function (username, password) {
/** /**
* Validate a provided API key * Validate a provided API key
* @param {string} key API key to verify * @param {string} key API key to verify
* @returns {boolean} API is ok?
*/ */
async function verifyAPIKey(key) { async function verifyAPIKey(key) {
if (typeof key !== "string") { if (typeof key !== "string") {
@ -73,9 +74,10 @@ async function verifyAPIKey(key) {
/** /**
* Custom authorizer for express-basic-auth * Custom authorizer for express-basic-auth
* @param {string} username * @param {string} username Username to login with
* @param {string} password * @param {string} password Password to login with
* @param {authCallback} callback * @param {authCallback} callback Callback to handle login result
* @returns {void}
*/ */
function apiAuthorizer(username, password, callback) { function apiAuthorizer(username, password, callback) {
// API Rate Limit // API Rate Limit
@ -99,9 +101,10 @@ function apiAuthorizer(username, password, callback) {
/** /**
* Custom authorizer for express-basic-auth * Custom authorizer for express-basic-auth
* @param {string} username * @param {string} username Username to login with
* @param {string} password * @param {string} password Password to login with
* @param {authCallback} callback * @param {authCallback} callback Callback to handle login result
* @returns {void}
*/ */
function userAuthorizer(username, password, callback) { function userAuthorizer(username, password, callback) {
// Login Rate Limit // Login Rate Limit
@ -126,7 +129,8 @@ function userAuthorizer(username, password, callback) {
* Use basic auth if auth is not disabled * Use basic auth if auth is not disabled
* @param {express.Request} req Express request object * @param {express.Request} req Express request object
* @param {express.Response} res Express response object * @param {express.Response} res Express response object
* @param {express.NextFunction} next * @param {express.NextFunction} next Next handler in chain
* @returns {void}
*/ */
exports.basicAuth = async function (req, res, next) { exports.basicAuth = async function (req, res, next) {
const middleware = basicAuth({ const middleware = basicAuth({
@ -148,7 +152,8 @@ exports.basicAuth = async function (req, res, next) {
* Use use API Key if API keys enabled, else use basic auth * Use use API Key if API keys enabled, else use basic auth
* @param {express.Request} req Express request object * @param {express.Request} req Express request object
* @param {express.Response} res Express response object * @param {express.Response} res Express response object
* @param {express.NextFunction} next * @param {express.NextFunction} next Next handler in chain
* @returns {void}
*/ */
exports.apiAuth = async function (req, res, next) { exports.apiAuth = async function (req, res, next) {
if (!await Settings.get("disableAuth")) { if (!await Settings.get("disableAuth")) {

View file

@ -15,6 +15,7 @@ class CacheableDnsHttpAgent {
/** /**
* Register/Disable cacheable to global agents * Register/Disable cacheable to global agents
* @returns {void}
*/ */
static async update() { static async update() {
log.debug("CacheableDnsHttpAgent", "update"); log.debug("CacheableDnsHttpAgent", "update");
@ -40,14 +41,15 @@ class CacheableDnsHttpAgent {
/** /**
* Attach cacheable to HTTP agent * Attach cacheable to HTTP agent
* @param {http.Agent} agent Agent to install * @param {http.Agent} agent Agent to install
* @returns {void}
*/ */
static install(agent) { static install(agent) {
this.cacheable.install(agent); this.cacheable.install(agent);
} }
/** /**
* @var {https.AgentOptions} agentOptions * @param {https.AgentOptions} agentOptions Options to pass to HTTPS agent
* @return {https.Agent} * @returns {https.Agent} The new HTTPS agent
*/ */
static getHttpsAgent(agentOptions) { static getHttpsAgent(agentOptions) {
if (!this.enable) { if (!this.enable) {
@ -63,8 +65,8 @@ class CacheableDnsHttpAgent {
} }
/** /**
* @var {http.AgentOptions} agentOptions * @param {http.AgentOptions} agentOptions Options to pass to the HTTP agent
* @return {https.Agents} * @returns {https.Agents} The new HTTP agent
*/ */
static getHttpAgent(agentOptions) { static getHttpAgent(agentOptions) {
if (!this.enable) { if (!this.enable) {

View file

@ -12,7 +12,7 @@ const checkVersion = require("./check-version");
/** /**
* Send list of notification providers to client * Send list of notification providers to client
* @param {Socket} socket Socket.io socket instance * @param {Socket} socket Socket.io socket instance
* @returns {Promise<Bean[]>} * @returns {Promise<Bean[]>} List of notifications
*/ */
async function sendNotificationList(socket) { async function sendNotificationList(socket) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
@ -40,8 +40,8 @@ async function sendNotificationList(socket) {
* Send Heartbeat History list to socket * Send Heartbeat History list to socket
* @param {Socket} socket Socket.io instance * @param {Socket} socket Socket.io instance
* @param {number} monitorID ID of monitor to send heartbeat history * @param {number} monitorID ID of monitor to send heartbeat history
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only * @param {boolean} toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list * @param {boolean} overwrite Overwrite client-side's heartbeat list
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) { async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
@ -71,8 +71,8 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
* Important Heart beat list (aka event list) * Important Heart beat list (aka event list)
* @param {Socket} socket Socket.io instance * @param {Socket} socket Socket.io instance
* @param {number} monitorID ID of monitor to send heartbeat history * @param {number} monitorID ID of monitor to send heartbeat history
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only * @param {boolean} toUser True = send to all browsers with the same user id, False = send to the current browser only
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list * @param {boolean} overwrite Overwrite client-side's heartbeat list
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) { async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
@ -100,7 +100,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
/** /**
* Emit proxy list to client * Emit proxy list to client
* @param {Socket} socket Socket.io socket instance * @param {Socket} socket Socket.io socket instance
* @return {Promise<Bean[]>} * @returns {Promise<Bean[]>} List of proxies
*/ */
async function sendProxyList(socket) { async function sendProxyList(socket) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
@ -141,7 +141,7 @@ async function sendAPIKeyList(socket) {
/** /**
* Emits the version information to the client. * Emits the version information to the client.
* @param {Socket} socket Socket.io socket instance * @param {Socket} socket Socket.io socket instance
* @param {boolean} hideVersion * @param {boolean} hideVersion Should we hide the version information in the response?
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function sendInfo(socket, hideVersion = false) { async function sendInfo(socket, hideVersion = false) {
@ -165,7 +165,7 @@ async function sendInfo(socket, hideVersion = false) {
/** /**
* Send list of docker hosts to client * Send list of docker hosts to client
* @param {Socket} socket Socket.io socket instance * @param {Socket} socket Socket.io socket instance
* @returns {Promise<Bean[]>} * @returns {Promise<Bean[]>} List of docker hosts
*/ */
async function sendDockerHostList(socket) { async function sendDockerHostList(socket) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();

View file

@ -3,6 +3,9 @@ const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server"); const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util"); const { log, sleep } = require("../src/util");
const knex = require("knex"); const knex = require("knex");
const path = require("path");
const { EmbeddedMariaDB } = require("./embedded-mariadb");
const mysql = require("mysql2/promise");
/** /**
* Database & App Data Folder * Database & App Data Folder
@ -23,7 +26,9 @@ class Database {
static screenshotDir; static screenshotDir;
static path; static sqlitePath;
static dockerTLSDir;
/** /**
* @type {boolean} * @type {boolean}
@ -31,11 +36,13 @@ class Database {
static patched = false; static patched = false;
/** /**
* SQLite only
* Add patch filename in key * Add patch filename in key
* Values: * Values:
* true: Add it regardless of order * true: Add it regardless of order
* false: Do nothing * false: Do nothing
* { parents: []}: Need parents before add it * { parents: []}: Need parents before add it
* @deprecated
*/ */
static patchList = { static patchList = {
"patch-setting-value-type.sql": true, "patch-setting-value-type.sql": true,
@ -76,6 +83,8 @@ class Database {
"patch-added-kafka-producer.sql": true, "patch-added-kafka-producer.sql": true,
"patch-add-certificate-expiry-status-page.sql": true, "patch-add-certificate-expiry-status-page.sql": true,
"patch-monitor-oauth-cc.sql": true, "patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
"patch-add-gamedig-given-port.sql": true, // The last file so far converted to a knex migration file
}; };
/** /**
@ -86,53 +95,104 @@ class Database {
static noReject = true; static noReject = true;
static dbConfig = {};
static knexMigrationsPath = "./db/knex_migrations";
/** /**
* Initialize the database * Initialize the data directory
* @param {Object} args Arguments to initialize DB with * @param {object} args Arguments to initialize DB with
* @returns {void}
*/ */
static init(args) { static initDataDir(args) {
// Data Directory (must be end with "/") // Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/"; Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
Database.path = Database.dataDir + "kuma.db"; Database.sqlitePath = path.join(Database.dataDir, "kuma.db");
if (! fs.existsSync(Database.dataDir)) { if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true }); fs.mkdirSync(Database.dataDir, { recursive: true });
} }
Database.uploadDir = Database.dataDir + "upload/"; Database.uploadDir = path.join(Database.dataDir, "upload/");
if (! fs.existsSync(Database.uploadDir)) { if (! fs.existsSync(Database.uploadDir)) {
fs.mkdirSync(Database.uploadDir, { recursive: true }); fs.mkdirSync(Database.uploadDir, { recursive: true });
} }
// Create screenshot dir // Create screenshot dir
Database.screenshotDir = Database.dataDir + "screenshots/"; Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
if (! fs.existsSync(Database.screenshotDir)) { if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true }); fs.mkdirSync(Database.screenshotDir, { recursive: true });
} }
Database.dockerTLSDir = path.join(Database.dataDir, "docker-tls/");
if (! fs.existsSync(Database.dockerTLSDir)) {
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
}
log.info("db", `Data Dir: ${Database.dataDir}`); log.info("db", `Data Dir: ${Database.dataDir}`);
} }
static readDBConfig() {
let dbConfig;
let dbConfigString = fs.readFileSync(path.join(Database.dataDir, "db-config.json")).toString("utf-8");
dbConfig = JSON.parse(dbConfigString);
if (typeof dbConfig !== "object") {
throw new Error("Invalid db-config.json, it must be an object");
}
if (typeof dbConfig.type !== "string") {
throw new Error("Invalid db-config.json, type must be a string");
}
return dbConfig;
}
static writeDBConfig(dbConfig) {
fs.writeFileSync(path.join(Database.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
}
/** /**
* Connect to the database * Connect to the database
* @param {boolean} [testMode=false] Should the connection be * @param {boolean} testMode Should the connection be
* started in test mode? * started in test mode?
* @param {boolean} [autoloadModels=true] Should models be * @param {boolean} autoloadModels Should models be
* automatically loaded? * automatically loaded?
* @param {boolean} [noLog=false] Should logs not be output? * @param {boolean} noLog Should logs not be output?
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async connect(testMode = false, autoloadModels = true, noLog = false) { static async connect(testMode = false, autoloadModels = true, noLog = false) {
const acquireConnectionTimeout = 120 * 1000; const acquireConnectionTimeout = 120 * 1000;
let dbConfig;
try {
dbConfig = this.readDBConfig();
Database.dbConfig = dbConfig;
} catch (err) {
log.warn("db", err.message);
dbConfig = {
type: "sqlite",
};
}
let config = {};
log.info("db", `Database Type: ${dbConfig.type}`);
if (dbConfig.type === "sqlite") {
if (! fs.existsSync(Database.sqlitePath)) {
log.info("server", "Copying Database");
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js"); const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3"); Dialect.prototype._driver = () => require("@louislam/sqlite3");
const knexInstance = knex({ config = {
client: Dialect, client: Dialect,
connection: { connection: {
filename: Database.path, filename: Database.sqlitePath,
acquireConnectionTimeout: acquireConnectionTimeout, acquireConnectionTimeout: acquireConnectionTimeout,
}, },
useNullAsDefault: true, useNullAsDefault: true,
@ -143,8 +203,59 @@ class Database {
propagateCreateError: false, propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout, acquireTimeoutMillis: acquireConnectionTimeout,
} }
};
} else if (dbConfig.type === "mariadb") {
if (!/^\w+$/.test(dbConfig.dbName)) {
throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores");
}
const connection = await mysql.createConnection({
host: dbConfig.hostname,
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
}); });
await connection.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName + " CHARACTER SET utf8mb4");
connection.end();
config = {
client: "mysql2",
connection: {
host: dbConfig.hostname,
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.dbName,
}
};
} else if (dbConfig.type === "embedded-mariadb") {
let embeddedMariaDB = EmbeddedMariaDB.getInstance();
await embeddedMariaDB.start();
log.info("mariadb", "Embedded MariaDB started");
config = {
client: "mysql2",
connection: {
socketPath: embeddedMariaDB.socketPath,
user: "node",
database: "kuma",
}
};
} else {
throw new Error("Unknown Database type: " + dbConfig.type);
}
// Set to utf8mb4 for MariaDB
if (dbConfig.type.endsWith("mariadb")) {
config.pool = {
afterCreate(conn, done) {
conn.query("SET CHARACTER SET utf8mb4;", (err) => done(err, conn));
},
};
}
const knexInstance = knex(config);
R.setup(knexInstance); R.setup(knexInstance);
if (process.env.SQL_LOG === "1") { if (process.env.SQL_LOG === "1") {
@ -158,6 +269,14 @@ class Database {
await R.autoloadModels("./server/model"); await R.autoloadModels("./server/model");
} }
if (dbConfig.type === "sqlite") {
await this.initSQLite(testMode, noLog);
} else if (dbConfig.type.endsWith("mariadb")) {
await this.initMariaDB();
}
}
static async initSQLite(testMode, noLog) {
await R.exec("PRAGMA foreign_keys = ON"); await R.exec("PRAGMA foreign_keys = ON");
if (testMode) { if (testMode) {
// Change to MEMORY // Change to MEMORY
@ -182,8 +301,54 @@ class Database {
} }
} }
/** Patch the database */ static async initMariaDB() {
log.debug("db", "Checking if MariaDB database exists...");
let hasTable = await R.hasTable("docker_host");
if (!hasTable) {
const { createTables } = require("../db/knex_init_db");
await createTables();
} else {
log.debug("db", "MariaDB database already exists");
}
}
/**
* Patch the database
* @returns {void}
*/
static async patch() { static async patch() {
// Still need to keep this for old versions of Uptime Kuma
if (Database.dbConfig.type === "sqlite") {
await this.patchSqlite();
}
// Using knex migrations
// https://knexjs.org/guide/migrations.html
// https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
try {
await R.knex.migrate.latest({
directory: Database.knexMigrationsPath,
});
} catch (e) {
log.error("db", "Database migration failed");
throw e;
}
}
/**
*
* @returns {Promise<void>}
*/
static async rollbackLatestPatch() {
}
/**
* Patch the database for SQLite
* @deprecated
*/
static async patchSqlite() {
let version = parseInt(await setting("database_version")); let version = parseInt(await setting("database_version"));
if (! version) { if (! version) {
@ -203,7 +368,7 @@ class Database {
// Try catch anything here // Try catch anything here
try { try {
for (let i = version + 1; i <= this.latestVersion; i++) { for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/patch${i}.sql`; const sqlFile = `./db/old_migrations/patch${i}.sql`;
log.info("db", `Patching ${sqlFile}`); log.info("db", `Patching ${sqlFile}`);
await Database.importSQLFile(sqlFile); await Database.importSQLFile(sqlFile);
log.info("db", `Patched ${sqlFile}`); log.info("db", `Patched ${sqlFile}`);
@ -220,17 +385,18 @@ class Database {
} }
} }
await this.patch2(); await this.patchSqlite2();
await this.migrateNewStatusPage(); await this.migrateNewStatusPage();
} }
/** /**
* Patch DB using new process * Patch DB using new process
* Call it from patch() only * Call it from patch() only
* @deprecated
* @private * @private
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async patch2() { static async patchSqlite2() {
log.info("db", "Database Patch 2.0 Process"); log.info("db", "Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles"); let databasePatchedFiles = await setting("databasePatchedFiles");
@ -264,6 +430,7 @@ class Database {
} }
/** /**
* SQlite only
* Migrate status page value in setting to "status_page" table * Migrate status page value in setting to "status_page" table
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
@ -335,8 +502,8 @@ class Database {
* Patch database using new patching process * Patch database using new patching process
* Used it patch2() only * Used it patch2() only
* @private * @private
* @param sqlFilename * @param {string} sqlFilename Name of SQL file to load
* @param databasePatchedFiles * @param {object} databasePatchedFiles Patch status of database files
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async patch2Recursion(sqlFilename, databasePatchedFiles) { static async patch2Recursion(sqlFilename, databasePatchedFiles) {
@ -360,7 +527,7 @@ class Database {
log.info("db", sqlFilename + " is patching"); log.info("db", sqlFilename + " is patching");
this.patched = true; this.patched = true;
await this.importSQLFile("./db/" + sqlFilename); await this.importSQLFile("./db/old_migrations/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true; databasePatchedFiles[sqlFilename] = true;
log.info("db", sqlFilename + " was patched successfully"); log.info("db", sqlFilename + " was patched successfully");
@ -371,7 +538,7 @@ class Database {
/** /**
* Load an SQL file and execute it * Load an SQL file and execute it
* @param filename Filename of SQL file to import * @param {string} filename Filename of SQL file to import
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async importSQLFile(filename) { static async importSQLFile(filename) {
@ -405,7 +572,7 @@ class Database {
/** /**
* Aquire a direct connection to database * Aquire a direct connection to database
* @returns {any} * @returns {any} Database connection
*/ */
static getBetterSQLite3Database() { static getBetterSQLite3Database() {
return R.knex.client.acquireConnection(); return R.knex.client.acquireConnection();
@ -442,10 +609,13 @@ class Database {
process.removeListener("unhandledRejection", listener); process.removeListener("unhandledRejection", listener);
} }
/** Get the size of the database */ /**
* Get the size of the database
* @returns {number} Size of database
*/
static getSize() { static getSize() {
log.debug("db", "Database.getSize()"); log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.path); let stats = fs.statSync(Database.sqlitePath);
log.debug("db", stats); log.debug("db", stats);
return stats.size; return stats.size;
} }
@ -457,6 +627,15 @@ class Database {
static async shrink() { static async shrink() {
await R.exec("VACUUM"); await R.exec("VACUUM");
} }
static sqlHourOffset() {
if (this.dbConfig.client === "sqlite3") {
return "DATETIME('now', ? || ' hours')";
} else {
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
}
}
} }
module.exports = Database; module.exports = Database;

View file

@ -2,14 +2,22 @@ const axios = require("axios");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const version = require("../package.json").version; const version = require("../package.json").version;
const https = require("https"); const https = require("https");
const fs = require("fs");
const path = require("path");
const Database = require("./database");
class DockerHost { class DockerHost {
static CertificateFileNameCA = "ca.pem";
static CertificateFileNameCert = "cert.pem";
static CertificateFileNameKey = "key.pem";
/** /**
* Save a docker host * Save a docker host
* @param {Object} dockerHost Docker host to save * @param {object} dockerHost Docker host to save
* @param {?number} dockerHostID ID of the docker host to update * @param {?number} dockerHostID ID of the docker host to update
* @param {number} userID ID of the user who adds the docker host * @param {number} userID ID of the user who adds the docker host
* @returns {Promise<Bean>} * @returns {Promise<Bean>} Updated docker host
*/ */
static async save(dockerHost, dockerHostID, userID) { static async save(dockerHost, dockerHostID, userID) {
let bean; let bean;
@ -56,7 +64,7 @@ class DockerHost {
/** /**
* Fetches the amount of containers on the Docker host * Fetches the amount of containers on the Docker host
* @param {Object} dockerHost Docker host to check for * @param {object} dockerHost Docker host to check for
* @returns {number} Total amount of containers on the host * @returns {number} Total amount of containers on the host
*/ */
static async testDockerHost(dockerHost) { static async testDockerHost(dockerHost) {
@ -66,10 +74,6 @@ class DockerHost {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version "User-Agent": "Uptime-Kuma/" + version
}, },
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: false,
}),
}; };
if (dockerHost.dockerType === "socket") { if (dockerHost.dockerType === "socket") {
@ -77,6 +81,7 @@ class DockerHost {
} else if (dockerHost.dockerType === "tcp") { } else if (dockerHost.dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon); options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
} }
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
let res = await axios.request(options); let res = await axios.request(options);
@ -103,6 +108,8 @@ class DockerHost {
/** /**
* Since axios 0.27.X, it does not accept `tcp://` protocol. * Since axios 0.27.X, it does not accept `tcp://` protocol.
* Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165) * Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
* @param {any} url URL to fix
* @returns {any} URL with tcp:// replaced by http://
*/ */
static patchDockerURL(url) { static patchDockerURL(url) {
if (typeof url === "string") { if (typeof url === "string") {
@ -111,6 +118,52 @@ class DockerHost {
} }
return url; return url;
} }
/**
* Returns HTTPS agent options with client side TLS parameters if certificate files
* for the given host are available under a predefined directory path.
*
* The base path where certificates are looked for can be set with the
* 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'.
*
* If a directory in this path exists with a name matching the FQDN of the docker host
* (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory
* 'data/docker-tls/example.com/' would be searched for certificate files),
* then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
* @param {string} dockerType i.e. "tcp" or "socket"
* @param {string} url The docker host URL rewritten to https://
* @returns {object} HTTP agent options
*/
static getHttpsAgentOptions(dockerType, url) {
let baseOptions = {
maxCachedSessions: 0,
rejectUnauthorized: true
};
let certOptions = {};
let dirName = (new URL(url)).hostname;
let caPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCA);
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
let ca = fs.readFileSync(caPath);
let key = fs.readFileSync(keyPath);
let cert = fs.readFileSync(certPath);
certOptions = {
ca,
key,
cert
};
}
return {
...baseOptions,
...certOptions
};
}
} }
module.exports = { module.exports = {

157
server/embedded-mariadb.js Normal file
View file

@ -0,0 +1,157 @@
const { log } = require("../src/util");
const childProcess = require("child_process");
const fs = require("fs");
const mysql = require("mysql2");
/**
* It is only used inside the docker container
*/
class EmbeddedMariaDB {
static instance = null;
exec = "mariadbd";
mariadbDataDir = "/app/data/mariadb";
runDir = "/app/data/run/mariadb";
socketPath = this.runDir + "/mysqld.sock";
childProcess = null;
running = false;
started = false;
/**
*
* @returns {EmbeddedMariaDB}
*/
static getInstance() {
if (!EmbeddedMariaDB.instance) {
EmbeddedMariaDB.instance = new EmbeddedMariaDB();
}
return EmbeddedMariaDB.instance;
}
static hasInstance() {
return !!EmbeddedMariaDB.instance;
}
/**
*
*/
start() {
if (this.childProcess) {
log.info("mariadb", "Already started");
return;
}
this.initDB();
this.running = true;
log.info("mariadb", "Starting Embedded MariaDB");
this.childProcess = childProcess.spawn(this.exec, [
"--user=node",
"--datadir=" + this.mariadbDataDir,
`--socket=${this.socketPath}`,
`--pid-file=${this.runDir}/mysqld.pid`,
]);
this.childProcess.on("close", (code) => {
this.running = false;
this.childProcess = null;
this.started = false;
log.info("mariadb", "Stopped Embedded MariaDB: " + code);
if (code !== 0) {
log.info("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
this.start();
}
});
this.childProcess.on("error", (err) => {
if (err.code === "ENOENT") {
log.error("mariadb", `Embedded MariaDB: ${this.exec} is not found`);
} else {
log.error("mariadb", err);
}
});
let handler = (data) => {
log.debug("mariadb", data.toString("utf-8"));
if (data.toString("utf-8").includes("ready for connections")) {
this.initDBAfterStarted();
}
};
this.childProcess.stdout.on("data", handler);
this.childProcess.stderr.on("data", handler);
return new Promise((resolve) => {
let interval = setInterval(() => {
if (this.started) {
clearInterval(interval);
resolve();
} else {
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
}
}, 1000);
});
}
stop() {
if (this.childProcess) {
this.childProcess.kill("SIGINT");
this.childProcess = null;
}
}
initDB() {
if (!fs.existsSync(this.mariadbDataDir)) {
log.info("mariadb", `Embedded MariaDB: ${this.mariadbDataDir} is not found, create one now.`);
fs.mkdirSync(this.mariadbDataDir, {
recursive: true,
});
let result = childProcess.spawnSync("mysql_install_db", [
"--user=node",
"--ldata=" + this.mariadbDataDir,
]);
if (result.status !== 0) {
let error = result.stderr.toString("utf-8");
log.error("mariadb", error);
return;
} else {
log.info("mariadb", "Embedded MariaDB: mysql_install_db done:" + result.stdout.toString("utf-8"));
}
}
if (!fs.existsSync(this.runDir)) {
log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`);
fs.mkdirSync(this.runDir, {
recursive: true,
});
}
}
async initDBAfterStarted() {
const connection = mysql.createConnection({
socketPath: this.socketPath,
user: "node",
});
let result = await connection.execute("CREATE DATABASE IF NOT EXISTS `kuma`");
log.debug("mariadb", "CREATE DATABASE: " + JSON.stringify(result));
log.info("mariadb", "Embedded MariaDB is ready for connections");
this.started = true;
}
}
module.exports = {
EmbeddedMariaDB,
};

View file

@ -3,8 +3,8 @@ const jsesc = require("jsesc");
/** /**
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts * Returns a string that represents the javascript that is required to insert the Google Analytics scripts
* into a webpage. * into a webpage.
* @param tagId Google UA/G/AW/DC Property ID to use with the Google Analytics script. * @param {string} tagId Google UA/G/AW/DC Property ID to use with the Google Analytics script.
* @returns {string} * @returns {string} HTML script tags to inject into page
*/ */
function getGoogleAnalyticsScript(tagId) { function getGoogleAnalyticsScript(tagId) {
let escapedTagId = jsesc(tagId, { isScriptContext: true }); let escapedTagId = jsesc(tagId, { isScriptContext: true });

View file

@ -10,7 +10,7 @@ let ImageDataURI = (() => {
/** /**
* Decode the data:image/ URI * Decode the data:image/ URI
* @param {string} dataURI data:image/ URI to decode * @param {string} dataURI data:image/ URI to decode
* @returns {?Object} An object with properties "imageType" and "dataBase64". * @returns {?object} An object with properties "imageType" and "dataBase64".
* The former is the image type, e.g., "png", and the latter is a base64 * The former is the image type, e.g., "png", and the latter is a base64
* encoded string of the image's binary data. If it fails to parse, returns * encoded string of the image's binary data. If it fails to parse, returns
* null instead of an object. * null instead of an object.
@ -52,8 +52,8 @@ let ImageDataURI = (() => {
/** /**
* Write data URI to file * Write data URI to file
* @param {string} dataURI data:image/ URI * @param {string} dataURI data:image/ URI
* @param {string} [filePath] Path to write file to * @param {string} filePath Path to write file to
* @returns {Promise<string>} * @returns {Promise<string|void>} Write file error
*/ */
function outputFile(dataURI, filePath) { function outputFile(dataURI, filePath) {
filePath = filePath || "./"; filePath = filePath || "./";

View file

@ -39,7 +39,10 @@ const initBackgroundJobs = async function () {
}; };
/** Stop all background jobs if running */ /**
* Stop all background jobs if running
* @returns {void}
*/
const stopBackgroundJobs = function () { const stopBackgroundJobs = function () {
for (const job of jobs) { for (const job of jobs) {
if (job.croner) { if (job.croner) {

View file

@ -1,12 +1,13 @@
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { log } = require("../../src/util"); const { log } = require("../../src/util");
const { setSetting, setting } = require("../util-server"); const { setSetting, setting } = require("../util-server");
const Database = require("../database");
const DEFAULT_KEEP_PERIOD = 180; const DEFAULT_KEEP_PERIOD = 180;
/** /**
* Clears old data from the heartbeat table of the database. * Clears old data from the heartbeat table of the database.
* @return {Promise<void>} A promise that resolves when the data has been cleared. * @returns {Promise<void>} A promise that resolves when the data has been cleared.
*/ */
const clearOldData = async () => { const clearOldData = async () => {
@ -34,10 +35,12 @@ const clearOldData = async () => {
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`); log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
const sqlHourOffset = Database.sqlHourOffset();
try { try {
await R.exec( await R.exec(
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ", "DELETE FROM heartbeat WHERE time < " + sqlHourOffset,
[ parsedPeriod ] [ parsedPeriod * -24 ]
); );
await R.exec("PRAGMA optimize;"); await R.exec("PRAGMA optimize;");

View file

@ -1,13 +1,19 @@
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { log } = require("../../src/util"); const { log } = require("../../src/util");
const Database = require("../database");
/** /**
* Run incremental_vacuum and checkpoint the WAL. * Run incremental_vacuum and checkpoint the WAL.
* @return {Promise<void>} A promise that resolves when the process is finished. * @returns {Promise<void>} A promise that resolves when the process is finished.
*/ */
const incrementalVacuum = async () => { const incrementalVacuum = async () => {
try { try {
if (Database.dbConfig.type !== "sqlite") {
log.debug("incrementalVacuum", "Skipping incremental_vacuum, not using SQLite.");
return;
}
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)..."); log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
await R.exec("PRAGMA incremental_vacuum(200)"); await R.exec("PRAGMA incremental_vacuum(200)");
await R.exec("PRAGMA wal_checkpoint(PASSIVE)"); await R.exec("PRAGMA wal_checkpoint(PASSIVE)");

View file

@ -19,7 +19,7 @@ class APIKey extends BeanModel {
/** /**
* Returns an object that ready to parse to JSON * Returns an object that ready to parse to JSON
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toJSON() { toJSON() {
return { return {
@ -37,7 +37,7 @@ class APIKey extends BeanModel {
/** /**
* Returns an object that ready to parse to JSON with sensitive fields * Returns an object that ready to parse to JSON with sensitive fields
* removed * removed
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toPublicJSON() { toPublicJSON() {
return { return {
@ -53,9 +53,9 @@ class APIKey extends BeanModel {
/** /**
* Create a new API Key and store it in the database * Create a new API Key and store it in the database
* @param {Object} key Object sent by client * @param {object} key Object sent by client
* @param {int} userID ID of socket user * @param {int} userID ID of socket user
* @returns {Promise<bean>} * @returns {Promise<bean>} API key
*/ */
static async save(key, userID) { static async save(key, userID) {
let bean; let bean;

View file

@ -3,7 +3,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
class DockerHost extends BeanModel { class DockerHost extends BeanModel {
/** /**
* Returns an object that ready to parse to JSON * Returns an object that ready to parse to JSON
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toJSON() { toJSON() {
return { return {

View file

@ -4,10 +4,12 @@ const { R } = require("redbean-node");
class Group extends BeanModel { class Group extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public Only show
* Only show necessary data to public * necessary data to public
* @param {boolean} [showTags=false] Should the JSON include monitor tags * @param {boolean} showTags Should the JSON include monitor tags
* @returns {Object} * @param {boolean} certExpiry Should JSON include info about
* certificate expiry?
* @returns {object} Object ready to parse
*/ */
async toPublicJSON(showTags = false, certExpiry = false) { async toPublicJSON(showTags = false, certExpiry = false) {
let monitorBeanList = await this.getMonitorList(); let monitorBeanList = await this.getMonitorList();
@ -27,7 +29,7 @@ class Group extends BeanModel {
/** /**
* Get all monitors * Get all monitors
* @returns {Bean[]} * @returns {Bean[]} List of monitors
*/ */
async getMonitorList() { async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(` return R.convertToBeans("monitor", await R.getAll(`

View file

@ -12,7 +12,7 @@ class Heartbeat extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public * Only show necessary data to public
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toPublicJSON() { toPublicJSON() {
return { return {
@ -25,7 +25,7 @@ class Heartbeat extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toJSON() { toJSON() {
return { return {

View file

@ -5,7 +5,7 @@ class Incident extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public * Only show necessary data to public
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toPublicJSON() { toPublicJSON() {
return { return {

View file

@ -11,7 +11,7 @@ class Maintenance extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public * Only show necessary data to public
* @returns {Object} * @returns {object} Object ready to parse
*/ */
async toPublicJSON() { async toPublicJSON() {
@ -98,7 +98,7 @@ class Maintenance extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC * @param {string} timezone If not specified, the timeRange will be in UTC
* @returns {Object} * @returns {object} Object ready to parse
*/ */
async toJSON(timezone = null) { async toJSON(timezone = null) {
return this.toPublicJSON(timezone); return this.toPublicJSON(timezone);
@ -142,7 +142,7 @@ class Maintenance extends BeanModel {
/** /**
* Convert data from socket to bean * Convert data from socket to bean
* @param {Bean} bean Bean to fill in * @param {Bean} bean Bean to fill in
* @param {Object} obj Data to fill bean with * @param {object} obj Data to fill bean with
* @returns {Bean} Filled bean * @returns {Bean} Filled bean
*/ */
static async jsonToBean(bean, obj) { static async jsonToBean(bean, obj) {
@ -188,7 +188,7 @@ class Maintenance extends BeanModel {
/** /**
* Throw error if cron is invalid * Throw error if cron is invalid
* @param cron * @param {string|Date} cron Pattern or date
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async validateCron(cron) { static async validateCron(cron) {
@ -198,6 +198,8 @@ class Maintenance extends BeanModel {
/** /**
* Run the cron * Run the cron
* @param {boolean} throwError Should an error be thrown on failure
* @returns {Promise<void>}
*/ */
async run(throwError = false) { async run(throwError = false) {
if (this.beanMeta.job) { if (this.beanMeta.job) {
@ -290,6 +292,10 @@ class Maintenance extends BeanModel {
} }
} }
/**
* Get timeslots where maintenance is running
* @returns {object|null} Maintenance time slot
*/
getRunningTimeslot() { getRunningTimeslot() {
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate())); let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
let end = start.add(this.duration, "second"); let end = start.add(this.duration, "second");
@ -305,6 +311,10 @@ class Maintenance extends BeanModel {
} }
} }
/**
* Stop the maintenance
* @returns {void}
*/
stop() { stop() {
if (this.beanMeta.job) { if (this.beanMeta.job) {
this.beanMeta.job.stop(); this.beanMeta.job.stop();
@ -312,10 +322,18 @@ class Maintenance extends BeanModel {
} }
} }
/**
* Is this maintenance currently active
* @returns {boolean} The maintenance is active?
*/
async isUnderMaintenance() { async isUnderMaintenance() {
return (await this.getStatus()) === "under-maintenance"; return (await this.getStatus()) === "under-maintenance";
} }
/**
* Get the timezone of the maintenance
* @returns {string} timezone
*/
async getTimezone() { async getTimezone() {
if (!this.timezone || this.timezone === "SAME_AS_SERVER") { if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
return await UptimeKumaServer.getInstance().getTimezone(); return await UptimeKumaServer.getInstance().getTimezone();
@ -323,10 +341,18 @@ class Maintenance extends BeanModel {
return this.timezone; return this.timezone;
} }
/**
* Get offset for timezone
* @returns {string} offset
*/
async getTimezoneOffset() { async getTimezoneOffset() {
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z"); return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
} }
/**
* Get the current status of the maintenance
* @returns {string} Current status
*/
async getStatus() { async getStatus() {
if (!this.active) { if (!this.active) {
return "inactive"; return "inactive";

View file

@ -22,6 +22,7 @@ const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig"); const Gamedig = require("gamedig");
const jsonata = require("jsonata"); const jsonata = require("jsonata");
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
const Database = require("../database");
/** /**
* status: * status:
@ -33,9 +34,12 @@ const jwt = require("jsonwebtoken");
class Monitor extends BeanModel { class Monitor extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public Only show
* Only show necessary data to public * necessary data to public
* @returns {Object} * @param {boolean} showTags Include tags in JSON
* @param {boolean} certExpiry Include certificate expiry info in
* JSON
* @returns {object} Object ready to parse
*/ */
async toPublicJSON(showTags = false, certExpiry = false) { async toPublicJSON(showTags = false, certExpiry = false) {
let obj = { let obj = {
@ -64,7 +68,9 @@ class Monitor extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @returns {Object} * @param {boolean} includeSensitiveData Include sensitive data in
* JSON
* @returns {object} Object ready to parse
*/ */
async toJSON(includeSensitiveData = true) { async toJSON(includeSensitiveData = true) {
@ -102,6 +108,7 @@ class Monitor extends BeanModel {
active: await this.isActive(), active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id), forceInactive: !await Monitor.isParentActive(this.id),
type: this.type, type: this.type,
timeout: this.timeout,
interval: this.interval, interval: this.interval,
retryInterval: this.retryInterval, retryInterval: this.retryInterval,
resendInterval: this.resendInterval, resendInterval: this.resendInterval,
@ -134,6 +141,7 @@ class Monitor extends BeanModel {
radiusCalledStationId: this.radiusCalledStationId, radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId, radiusCallingStationId: this.radiusCallingStationId,
game: this.game, game: this.game,
gamedigGivenPortOnly: this.getGameDigGivenPortOnly(),
httpBodyEncoding: this.httpBodyEncoding, httpBodyEncoding: this.httpBodyEncoding,
jsonPath: this.jsonPath, jsonPath: this.jsonPath,
expectedValue: this.expectedValue, expectedValue: this.expectedValue,
@ -181,7 +189,7 @@ class Monitor extends BeanModel {
/** /**
* Checks if the monitor is active based on itself and its parents * Checks if the monitor is active based on itself and its parents
* @returns {Promise<Boolean>} * @returns {Promise<boolean>} Is the monitor active?
*/ */
async isActive() { async isActive() {
const parentActive = await Monitor.isParentActive(this.id); const parentActive = await Monitor.isParentActive(this.id);
@ -191,7 +199,8 @@ class Monitor extends BeanModel {
/** /**
* Get all tags applied to this monitor * Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>} * @returns {Promise<LooseObject<any>[]>} List of tags on the
* monitor
*/ */
async getTags() { async getTags() {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]); return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
@ -200,7 +209,8 @@ class Monitor extends BeanModel {
/** /**
* Gets certificate expiry for this monitor * Gets certificate expiry for this monitor
* @param {number} monitorID ID of monitor to send * @param {number} monitorID ID of monitor to send
* @returns {Promise<LooseObject<any>>} * @returns {Promise<LooseObject<any>>} Certificate expiry info for
* monitor
*/ */
async getCertExpiry(monitorID) { async getCertExpiry(monitorID) {
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@ -225,7 +235,9 @@ class Monitor extends BeanModel {
/** /**
* Encode user and password to Base64 encoding * Encode user and password to Base64 encoding
* for HTTP "basic" auth, as per RFC-7617 * for HTTP "basic" auth, as per RFC-7617
* @returns {string} * @param {string} user Username to encode
* @param {string} pass Password to encode
* @returns {string} Encoded username:password
*/ */
encodeBase64(user, pass) { encodeBase64(user, pass) {
return Buffer.from(user + ":" + pass).toString("base64"); return Buffer.from(user + ":" + pass).toString("base64");
@ -233,7 +245,7 @@ class Monitor extends BeanModel {
/** /**
* Is the TLS expiry notification enabled? * Is the TLS expiry notification enabled?
* @returns {boolean} * @returns {boolean} Enabled?
*/ */
isEnabledExpiryNotification() { isEnabledExpiryNotification() {
return Boolean(this.expiryNotification); return Boolean(this.expiryNotification);
@ -241,7 +253,7 @@ class Monitor extends BeanModel {
/** /**
* Parse to boolean * Parse to boolean
* @returns {boolean} * @returns {boolean} Should TLS errors be ignored?
*/ */
getIgnoreTls() { getIgnoreTls() {
return Boolean(this.ignoreTls); return Boolean(this.ignoreTls);
@ -249,7 +261,7 @@ class Monitor extends BeanModel {
/** /**
* Parse to boolean * Parse to boolean
* @returns {boolean} * @returns {boolean} Is the monitor in upside down mode?
*/ */
isUpsideDown() { isUpsideDown() {
return Boolean(this.upsideDown); return Boolean(this.upsideDown);
@ -257,7 +269,7 @@ class Monitor extends BeanModel {
/** /**
* Parse to boolean * Parse to boolean
* @returns {boolean} * @returns {boolean} Invert keyword match?
*/ */
isInvertKeyword() { isInvertKeyword() {
return Boolean(this.invertKeyword); return Boolean(this.invertKeyword);
@ -265,7 +277,7 @@ class Monitor extends BeanModel {
/** /**
* Parse to boolean * Parse to boolean
* @returns {boolean} * @returns {boolean} Enable TLS for gRPC?
*/ */
getGrpcEnableTls() { getGrpcEnableTls() {
return Boolean(this.grpcEnableTls); return Boolean(this.grpcEnableTls);
@ -273,15 +285,20 @@ class Monitor extends BeanModel {
/** /**
* Get accepted status codes * Get accepted status codes
* @returns {Object} * @returns {object} Accepted status codes
*/ */
getAcceptedStatuscodes() { getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json); return JSON.parse(this.accepted_statuscodes_json);
} }
getGameDigGivenPortOnly() {
return Boolean(this.gamedigGivenPortOnly);
}
/** /**
* Start monitor * Start monitor
* @param {Server} io Socket server instance * @param {Server} io Socket server instance
* @returns {void}
*/ */
start(io) { start(io) {
let previousBeat = null; let previousBeat = null;
@ -351,7 +368,10 @@ class Monitor extends BeanModel {
const lastBeat = await Monitor.getPreviousHeartbeat(child.id); const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
// Only change state if the monitor is in worse conditions then the ones before // Only change state if the monitor is in worse conditions then the ones before
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) { // lastBeat.status could be null
if (!lastBeat) {
bean.status = PENDING;
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
bean.status = lastBeat.status; bean.status = lastBeat.status;
} else if (bean.status === PENDING && lastBeat.status === DOWN) { } else if (bean.status === PENDING && lastBeat.status === DOWN) {
bean.status = lastBeat.status; bean.status = lastBeat.status;
@ -425,7 +445,7 @@ class Monitor extends BeanModel {
const options = { const options = {
url: this.url, url: this.url,
method: (this.method || "get").toLowerCase(), method: (this.method || "get").toLowerCase(),
timeout: this.interval * 1000 * 0.8, timeout: this.timeout * 1000,
headers: { headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
@ -645,7 +665,7 @@ class Monitor extends BeanModel {
} }
let res = await axios.get(steamApiUrl, { let res = await axios.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8, timeout: this.timeout * 1000,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
@ -683,7 +703,7 @@ class Monitor extends BeanModel {
type: this.game, type: this.game,
host: this.hostname, host: this.hostname,
port: this.port, port: this.port,
givenPortOnly: true, givenPortOnly: this.getGameDigGivenPortOnly(),
}); });
bean.msg = state.name; bean.msg = state.name;
@ -717,6 +737,9 @@ class Monitor extends BeanModel {
options.socketPath = dockerHost._dockerDaemon; options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") { } else if (dockerHost._dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon); options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
);
} }
log.debug("monitor", `[${this.name}] Axios Request`); log.debug("monitor", `[${this.name}] Axios Request`);
@ -967,7 +990,10 @@ class Monitor extends BeanModel {
}; };
/** Get a heartbeat and handle errors */ /**
* Get a heartbeat and handle errors7
* @returns {void}
*/
const safeBeat = async () => { const safeBeat = async () => {
try { try {
await beat(); await beat();
@ -995,10 +1021,10 @@ class Monitor extends BeanModel {
/** /**
* Make a request using axios * Make a request using axios
* @param {Object} options Options for Axios * @param {object} options Options for Axios
* @param {boolean} finalCall Should this be the final call i.e * @param {boolean} finalCall Should this be the final call i.e
* don't retry on faliure * don't retry on failure
* @returns {Object} Axios response * @returns {object} Axios response
*/ */
async makeAxiosRequest(options, finalCall = false) { async makeAxiosRequest(options, finalCall = false) {
try { try {
@ -1033,7 +1059,10 @@ class Monitor extends BeanModel {
} }
} }
/** Stop monitor */ /**
* Stop monitor
* @returns {void}
*/
stop() { stop() {
clearTimeout(this.heartbeatInterval); clearTimeout(this.heartbeatInterval);
this.isStop = true; this.isStop = true;
@ -1043,7 +1072,7 @@ class Monitor extends BeanModel {
/** /**
* Get prometheus instance * Get prometheus instance
* @returns {Prometheus|undefined} * @returns {Prometheus|undefined} Current prometheus instance
*/ */
getPrometheus() { getPrometheus() {
return this.prometheus; return this.prometheus;
@ -1053,7 +1082,7 @@ class Monitor extends BeanModel {
* Helper Method: * Helper Method:
* returns URL object for further usage * returns URL object for further usage
* returns null if url is invalid * returns null if url is invalid
* @returns {(null|URL)} * @returns {(null|URL)} Monitor URL
*/ */
getUrl() { getUrl() {
try { try {
@ -1065,8 +1094,8 @@ class Monitor extends BeanModel {
/** /**
* Store TLS info to database * Store TLS info to database
* @param checkCertificateResult * @param {object} checkCertificateResult Certificate to update
* @returns {Promise<Object>} * @returns {Promise<object>} Updated certificate
*/ */
async updateTlsInfo(checkCertificateResult) { async updateTlsInfo(checkCertificateResult) {
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@ -1113,6 +1142,7 @@ class Monitor extends BeanModel {
* @param {Server} io Socket server instance * @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send * @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to * @param {number} userID ID of user to send to
* @returns {void}
*/ */
static async sendStats(io, monitorID, userID) { static async sendStats(io, monitorID, userID) {
const hasClients = getTotalClientInRoom(io, userID) > 0; const hasClients = getTotalClientInRoom(io, userID) > 0;
@ -1130,14 +1160,19 @@ class Monitor extends BeanModel {
/** /**
* Send the average ping to user * Send the average ping to user
* @param {number} duration Hours * @param {number} duration Hours
* @param {Server} io Socket instance to send data to
* @param {number} monitorID ID of monitor to read
* @param {number} userID ID of user to send data to
* @returns {void}
*/ */
static async sendAvgPing(duration, io, monitorID, userID) { static async sendAvgPing(duration, io, monitorID, userID) {
const timeLogger = new TimeLogger(); const timeLogger = new TimeLogger();
const sqlHourOffset = Database.sqlHourOffset();
let avgPing = parseInt(await R.getCell(` let avgPing = parseInt(await R.getCell(`
SELECT AVG(ping) SELECT AVG(ping)
FROM heartbeat FROM heartbeat
WHERE time > DATETIME('now', ? || ' hours') WHERE time > ${sqlHourOffset}
AND ping IS NOT NULL AND ping IS NOT NULL
AND monitor_id = ? `, [ AND monitor_id = ? `, [
-duration, -duration,
@ -1154,6 +1189,7 @@ class Monitor extends BeanModel {
* @param {Server} io Socket server instance * @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send * @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to * @param {number} userID ID of user to send to
* @returns {void}
*/ */
static async sendCertInfo(io, monitorID, userID) { static async sendCertInfo(io, monitorID, userID) {
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [ let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
@ -1170,6 +1206,8 @@ class Monitor extends BeanModel {
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime * https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
* @param {number} duration Hours * @param {number} duration Hours
* @param {number} monitorID ID of monitor to calculate * @param {number} monitorID ID of monitor to calculate
* @param {boolean} forceNoCache Should the uptime be recalculated?
* @returns {number} Uptime of monitor
*/ */
static async calcUptime(duration, monitorID, forceNoCache = false) { static async calcUptime(duration, monitorID, forceNoCache = false) {
@ -1250,6 +1288,7 @@ class Monitor extends BeanModel {
* @param {Server} io Socket server instance * @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send * @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to * @param {number} userID ID of user to send to
* @returns {void}
*/ */
static async sendUptime(duration, io, monitorID, userID) { static async sendUptime(duration, io, monitorID, userID) {
const uptime = await this.calcUptime(duration, monitorID); const uptime = await this.calcUptime(duration, monitorID);
@ -1324,6 +1363,7 @@ class Monitor extends BeanModel {
* @param {boolean} isFirstBeat Is this beat the first of this monitor? * @param {boolean} isFirstBeat Is this beat the first of this monitor?
* @param {Monitor} monitor The monitor to send a notificaton about * @param {Monitor} monitor The monitor to send a notificaton about
* @param {Bean} bean Status information about monitor * @param {Bean} bean Status information about monitor
* @returns {void}
*/ */
static async sendNotification(isFirstBeat, monitor, bean) { static async sendNotification(isFirstBeat, monitor, bean) {
if (!isFirstBeat || bean.status === DOWN) { if (!isFirstBeat || bean.status === DOWN) {
@ -1364,7 +1404,7 @@ class Monitor extends BeanModel {
/** /**
* Get list of notification providers for a given monitor * Get list of notification providers for a given monitor
* @param {Monitor} monitor Monitor to get notification providers for * @param {Monitor} monitor Monitor to get notification providers for
* @returns {Promise<LooseObject<any>[]>} * @returns {Promise<LooseObject<any>[]>} List of notifications
*/ */
static async getNotificationList(monitor) { static async getNotificationList(monitor) {
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
@ -1375,7 +1415,8 @@ class Monitor extends BeanModel {
/** /**
* checks certificate chain for expiring certificates * checks certificate chain for expiring certificates
* @param {Object} tlsInfoObject Information about certificate * @param {object} tlsInfoObject Information about certificate
* @returns {void}
*/ */
async checkCertExpiryNotifications(tlsInfoObject) { async checkCertExpiryNotifications(tlsInfoObject) {
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
@ -1462,7 +1503,7 @@ class Monitor extends BeanModel {
/** /**
* Get the status of the previous heartbeat * Get the status of the previous heartbeat
* @param {number} monitorID ID of monitor to check * @param {number} monitorID ID of monitor to check
* @returns {Promise<LooseObject<any>>} * @returns {Promise<LooseObject<any>>} Previous heartbeat
*/ */
static async getPreviousHeartbeat(monitorID) { static async getPreviousHeartbeat(monitorID) {
return await R.getRow(` return await R.getRow(`
@ -1476,7 +1517,7 @@ class Monitor extends BeanModel {
/** /**
* Check if monitor is under maintenance * Check if monitor is under maintenance
* @param {number} monitorID ID of monitor to check * @param {number} monitorID ID of monitor to check
* @returns {Promise<boolean>} * @returns {Promise<boolean>} Is the monitor under maintenance
*/ */
static async isUnderMaintenance(monitorID) { static async isUnderMaintenance(monitorID) {
const maintenanceIDList = await R.getCol(` const maintenanceIDList = await R.getCol(`
@ -1499,7 +1540,11 @@ class Monitor extends BeanModel {
return false; return false;
} }
/** Make sure monitor interval is between bounds */ /**
* Make sure monitor interval is between bounds
* @returns {void}
* @throws Interval is outside of range
*/
validate() { validate() {
if (this.interval > MAX_INTERVAL_SECOND) { if (this.interval > MAX_INTERVAL_SECOND) {
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`); throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
@ -1512,7 +1557,7 @@ class Monitor extends BeanModel {
/** /**
* Gets Parent of the monitor * Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get * @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>} * @returns {Promise<LooseObject<any>>} Parent
*/ */
static async getParent(monitorID) { static async getParent(monitorID) {
return await R.getRow(` return await R.getRow(`
@ -1528,7 +1573,7 @@ class Monitor extends BeanModel {
/** /**
* Gets all Children of the monitor * Gets all Children of the monitor
* @param {number} monitorID ID of monitor to get * @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>} * @returns {Promise<LooseObject<any>>} Children
*/ */
static async getChildren(monitorID) { static async getChildren(monitorID) {
return await R.getAll(` return await R.getAll(`
@ -1541,7 +1586,7 @@ class Monitor extends BeanModel {
/** /**
* Gets Full Path-Name (Groups and Name) * Gets Full Path-Name (Groups and Name)
* @returns {Promise<String>} * @returns {Promise<string>} Full path name of this monitor
*/ */
async getPathName() { async getPathName() {
let path = this.name; let path = this.name;
@ -1562,7 +1607,7 @@ class Monitor extends BeanModel {
/** /**
* Gets recursive all child ids * Gets recursive all child ids
* @param {number} monitorID ID of the monitor to get * @param {number} monitorID ID of the monitor to get
* @returns {Promise<Array>} * @returns {Promise<Array>} IDs of all children
*/ */
static async getAllChildrenIDs(monitorID) { static async getAllChildrenIDs(monitorID) {
const childs = await Monitor.getChildren(monitorID); const childs = await Monitor.getChildren(monitorID);
@ -1595,7 +1640,7 @@ class Monitor extends BeanModel {
/** /**
* Checks recursive if parent (ancestors) are active * Checks recursive if parent (ancestors) are active
* @param {number} monitorID ID of the monitor to get * @param {number} monitorID ID of the monitor to get
* @returns {Promise<Boolean>} * @returns {Promise<boolean>} Is the parent monitor active?
*/ */
static async isParentActive(monitorID) { static async isParentActive(monitorID) {
const parent = await Monitor.getParent(monitorID); const parent = await Monitor.getParent(monitorID);

View file

@ -3,7 +3,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
class Proxy extends BeanModel { class Proxy extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toJSON() { toJSON() {
return { return {

View file

@ -14,10 +14,11 @@ class StatusPage extends BeanModel {
static domainMappingList = { }; static domainMappingList = { };
/** /**
* * Handle responses to status page
* @param {Response} response * @param {Response} response Response object
* @param {string} indexHTML * @param {string} indexHTML HTML to render
* @param {string} slug * @param {string} slug Status page slug
* @returns {void}
*/ */
static async handleStatusPageResponse(response, indexHTML, slug) { static async handleStatusPageResponse(response, indexHTML, slug) {
let statusPage = await R.findOne("status_page", " slug = ? ", [ let statusPage = await R.findOne("status_page", " slug = ? ", [
@ -33,8 +34,9 @@ class StatusPage extends BeanModel {
/** /**
* SSR for status pages * SSR for status pages
* @param {string} indexHTML * @param {string} indexHTML HTML page to render
* @param {StatusPage} statusPage * @param {StatusPage} statusPage Status page populate HTML with
* @returns {void}
*/ */
static async renderHTML(indexHTML, statusPage) { static async renderHTML(indexHTML, statusPage) {
const $ = cheerio.load(indexHTML); const $ = cheerio.load(indexHTML);
@ -87,7 +89,8 @@ class StatusPage extends BeanModel {
/** /**
* Get all status page data in one call * Get all status page data in one call
* @param {StatusPage} statusPage * @param {StatusPage} statusPage Status page to get data for
* @returns {object} Status page data
*/ */
static async getStatusPageData(statusPage) { static async getStatusPageData(statusPage) {
const config = await statusPage.toPublicJSON(); const config = await statusPage.toPublicJSON();
@ -142,7 +145,7 @@ class StatusPage extends BeanModel {
* Send status page list to client * Send status page list to client
* @param {Server} io io Socket server instance * @param {Server} io io Socket server instance
* @param {Socket} socket Socket.io instance * @param {Socket} socket Socket.io instance
* @returns {Promise<Bean[]>} * @returns {Promise<Bean[]>} Status page list
*/ */
static async sendStatusPageList(io, socket) { static async sendStatusPageList(io, socket) {
let result = {}; let result = {};
@ -159,7 +162,7 @@ class StatusPage extends BeanModel {
/** /**
* Update list of domain names * Update list of domain names
* @param {string[]} domainNameList * @param {string[]} domainNameList List of status page domains
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async updateDomainNameList(domainNameList) { async updateDomainNameList(domainNameList) {
@ -203,7 +206,7 @@ class StatusPage extends BeanModel {
/** /**
* Get list of domain names * Get list of domain names
* @returns {Object[]} * @returns {object[]} List of status page domains
*/ */
getDomainNameList() { getDomainNameList() {
let domainList = []; let domainList = [];
@ -219,7 +222,7 @@ class StatusPage extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @returns {Object} * @returns {object} Object ready to parse
*/ */
async toJSON() { async toJSON() {
return { return {
@ -243,7 +246,7 @@ class StatusPage extends BeanModel {
/** /**
* Return an object that ready to parse to JSON for public * Return an object that ready to parse to JSON for public
* Only show necessary data to public * Only show necessary data to public
* @returns {Object} * @returns {object} Object ready to parse
*/ */
async toPublicJSON() { async toPublicJSON() {
return { return {
@ -264,7 +267,8 @@ class StatusPage extends BeanModel {
/** /**
* Convert slug to status page ID * Convert slug to status page ID
* @param {string} slug * @param {string} slug Status page slug
* @returns {Promise<number>} ID of status page
*/ */
static async slugToID(slug) { static async slugToID(slug) {
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [ return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
@ -274,7 +278,7 @@ class StatusPage extends BeanModel {
/** /**
* Get path to the icon for the page * Get path to the icon for the page
* @returns {string} * @returns {string} Path
*/ */
getIcon() { getIcon() {
if (!this.icon) { if (!this.icon) {
@ -287,7 +291,7 @@ class StatusPage extends BeanModel {
/** /**
* Get list of maintenances * Get list of maintenances
* @param {number} statusPageId ID of status page to get maintenance for * @param {number} statusPageId ID of status page to get maintenance for
* @returns {Object} Object representing maintenances sanitized for public * @returns {object} Object representing maintenances sanitized for public
*/ */
static async getMaintenanceList(statusPageId) { static async getMaintenanceList(statusPageId) {
try { try {

View file

@ -4,7 +4,7 @@ class Tag extends BeanModel {
/** /**
* Return an object that ready to parse to JSON * Return an object that ready to parse to JSON
* @returns {Object} * @returns {object} Object ready to parse
*/ */
toJSON() { toJSON() {
return { return {

View file

@ -7,7 +7,7 @@ class User extends BeanModel {
* Reset user password * Reset user password
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead. * Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
* @param {number} userID ID of user to update * @param {number} userID ID of user to update
* @param {string} newPassword * @param {string} newPassword Users new password
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async resetPassword(userID, newPassword) { static async resetPassword(userID, newPassword) {
@ -19,7 +19,7 @@ class User extends BeanModel {
/** /**
* Reset this users password * Reset this users password
* @param {string} newPassword * @param {string} newPassword Users new password
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async resetPassword(newPassword) { async resetPassword(newPassword) {

View file

@ -3,10 +3,10 @@ class MonitorType {
name = undefined; name = undefined;
/** /**
* * Run the monitoring check on the given monitor
* @param {Monitor} monitor * @param {Monitor} monitor Monitor to check
* @param {Heartbeat} heartbeat * @param {Heartbeat} heartbeat Monitor heartbeat to update
* @param {UptimeKumaServer} server * @param {UptimeKumaServer} server Uptime Kuma server
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async check(monitor, heartbeat, server) { async check(monitor, heartbeat, server) {

View file

@ -51,6 +51,11 @@ if (process.platform === "win32") {
log.debug("chrome", allowedList); log.debug("chrome", allowedList);
/**
* Is the executable path allowed?
* @param {string} executablePath Path to executable
* @returns {Promise<boolean>} The executable is allowed?
*/
async function isAllowedChromeExecutable(executablePath) { async function isAllowedChromeExecutable(executablePath) {
console.log(config.args); console.log(config.args);
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") { if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
@ -61,6 +66,11 @@ async function isAllowedChromeExecutable(executablePath) {
return allowedList.includes(executablePath); return allowedList.includes(executablePath);
} }
/**
* Get the current instance of the browser. If there isn't one, create
* it.
* @returns {Promise<Browser>} The browser
*/
async function getBrowser() { async function getBrowser() {
if (!browser) { if (!browser) {
let executablePath = await Settings.get("chromeExecutable"); let executablePath = await Settings.get("chromeExecutable");
@ -75,6 +85,11 @@ async function getBrowser() {
return browser; return browser;
} }
/**
* Prepare the chrome executable path
* @param {string} executablePath Path to chrome executable
* @returns {Promise<string>} Executable path
*/
async function prepareChromeExecutable(executablePath) { async function prepareChromeExecutable(executablePath) {
// Special code for using the playwright_chromium // Special code for using the playwright_chromium
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") { if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
@ -121,6 +136,12 @@ async function prepareChromeExecutable(executablePath) {
return executablePath; return executablePath;
} }
/**
* Find the chrome executable
* @param {any[]} executables Executables to search through
* @returns {any} Executable
* @throws Could not find executable
*/
function findChrome(executables) { function findChrome(executables) {
// Use the last working executable, so we don't have to search for it again // Use the last working executable, so we don't have to search for it again
if (lastAutoDetectChromeExecutable) { if (lastAutoDetectChromeExecutable) {
@ -138,6 +159,10 @@ function findChrome(executables) {
throw new Error("Chromium not found, please specify Chromium executable path in the settings page."); throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
} }
/**
* Reset chrome
* @returns {Promise<void>}
*/
async function resetChrome() { async function resetChrome() {
if (browser) { if (browser) {
await browser.close(); await browser.close();
@ -147,8 +172,8 @@ async function resetChrome() {
/** /**
* Test if the chrome executable is valid and return the version * Test if the chrome executable is valid and return the version
* @param executablePath * @param {string} executablePath Path to executable
* @returns {Promise<string>} * @returns {Promise<string>} Chrome version
*/ */
async function testChrome(executablePath) { async function testChrome(executablePath) {
try { try {
@ -175,6 +200,9 @@ class RealBrowserMonitorType extends MonitorType {
name = "real-browser"; name = "real-browser";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, server) { async check(monitor, heartbeat, server) {
const browser = await getBrowser(); const browser = await getBrowser();
const context = await browser.newContext(); const context = await browser.newContext();

View file

@ -13,9 +13,9 @@ class TailscalePing extends MonitorType {
/** /**
* Checks the ping status of the URL associated with the monitor. * Checks the ping status of the URL associated with the monitor.
* It then parses the Tailscale ping command output to update the heatrbeat. * It then parses the Tailscale ping command output to update the heatrbeat.
* * @param {object} monitor The monitor object associated with the check.
* @param {Object} monitor - The monitor object associated with the check. * @param {object} heartbeat The heartbeat object to update.
* @param {Object} heartbeat - The heartbeat object to update. * @returns {Promise<void>}
* @throws Will throw an error if checking Tailscale ping encounters any error * @throws Will throw an error if checking Tailscale ping encounters any error
*/ */
async check(monitor, heartbeat) { async check(monitor, heartbeat) {
@ -31,9 +31,9 @@ class TailscalePing extends MonitorType {
/** /**
* Runs the Tailscale ping command to the given URL. * Runs the Tailscale ping command to the given URL.
* * @param {string} hostname The hostname to ping.
* @param {string} hostname - The hostname to ping. * @param {number} interval Interval to send ping
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command * @returns {Promise<string>} A Promise that resolves to the output of the Tailscale ping command
* @throws Will throw an error if the command execution encounters any error. * @throws Will throw an error if the command execution encounters any error.
*/ */
async runTailscalePing(hostname, interval) { async runTailscalePing(hostname, interval) {
@ -61,9 +61,9 @@ class TailscalePing extends MonitorType {
/** /**
* Parses the output of the Tailscale ping command to update the heartbeat. * Parses the output of the Tailscale ping command to update the heartbeat.
* * @param {string} tailscaleOutput The output of the Tailscale ping command.
* @param {string} tailscaleOutput - The output of the Tailscale ping command. * @param {object} heartbeat The heartbeat object to update.
* @param {Object} heartbeat - The heartbeat object to update. * @returns {void}
* @throws Will throw an eror if the output contains any unexpected string. * @throws Will throw an eror if the output contains any unexpected string.
*/ */
parseTailscaleOutput(tailscaleOutput, heartbeat) { parseTailscaleOutput(tailscaleOutput, heartbeat) {

View file

@ -6,6 +6,9 @@ class Alerta extends NotificationProvider {
name = "alerta"; name = "alerta";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";

View file

@ -7,6 +7,9 @@ class AlertNow extends NotificationProvider {
name = "AlertNow"; name = "AlertNow";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
try { try {

View file

@ -7,6 +7,9 @@ const qs = require("qs");
class AliyunSMS extends NotificationProvider { class AliyunSMS extends NotificationProvider {
name = "AliyunSMS"; name = "AliyunSMS";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully."; let okMsg = "Sent Successfully.";
@ -78,9 +81,9 @@ class AliyunSMS extends NotificationProvider {
/** /**
* Aliyun request sign * Aliyun request sign
* @param {Object} param Parameters object to sign * @param {object} param Parameters object to sign
* @param {string} AccessKeySecret Secret key to sign parameters with * @param {string} AccessKeySecret Secret key to sign parameters with
* @returns {string} * @returns {string} Base64 encoded request
*/ */
sign(param, AccessKeySecret) { sign(param, AccessKeySecret) {
let param2 = {}; let param2 = {};
@ -122,7 +125,7 @@ class AliyunSMS extends NotificationProvider {
/** /**
* Convert status constant to string * Convert status constant to string
* @param {const} status The status constant * @param {const} status The status constant
* @returns {string} * @returns {string} Status
*/ */
statusToString(status) { statusToString(status) {
switch (status) { switch (status) {

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