mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-03-04 08:25:57 +00:00
Merge branch 'master' into postgres-support
This commit is contained in:
commit
c22d97bde6
73 changed files with 2888 additions and 4160 deletions
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -15,7 +15,7 @@ Please delete any options that are not relevant.
|
||||||
- Bug fix (non-breaking change which fixes an issue)
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
- User interface (UI)
|
- User interface (UI)
|
||||||
- New feature (non-breaking change which adds functionality)
|
- New feature (non-breaking change which adds functionality)
|
||||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- Breaking change (a fix or feature that would cause existing functionality to not work as expected)
|
||||||
- Other
|
- Other
|
||||||
- This change requires a documentation update
|
- This change requires a documentation update
|
||||||
|
|
||||||
|
@ -24,9 +24,8 @@ Please delete any options that are not relevant.
|
||||||
- [ ] My code follows the style guidelines of this project
|
- [ ] My code follows the style guidelines of this project
|
||||||
- [ ] I ran ESLint and other linters for modified files
|
- [ ] I ran ESLint and other linters for modified files
|
||||||
- [ ] I have performed a self-review of my own code and tested it
|
- [ ] I have performed a self-review of my own code and tested it
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented my code, particularly in hard-to-understand areas (including JSDoc for methods)
|
||||||
(including JSDoc for methods)
|
- [ ] My changes generates no new warnings
|
||||||
- [ ] My changes generate no new warnings
|
|
||||||
- [ ] My code needed automated testing. I have added them (this is optional task)
|
- [ ] My code needed automated testing. I have added them (this is optional task)
|
||||||
|
|
||||||
## Screenshots (if any)
|
## Screenshots (if any)
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# Project Info
|
# Project Info
|
||||||
|
|
||||||
First of all, I want to thank everyone who made pull requests for Uptime Kuma. I never thought the GitHub Community would be so nice! Because of this, I also never thought that other people would actually read and edit my code. It is not very well structured or commented, sorry about that.
|
First of all, I want to thank everyone who have made pull requests for Uptime Kuma. I never thought the GitHub community would be so nice! Because of this, I also never thought that other people would actually read and edit my code. It is not very well structured or commented, sorry about that.
|
||||||
|
|
||||||
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for the server part. Both frontend and backend share the same package.json.
|
The project was created with vite.js (vue3). Then I created a subdirectory called "server" for the server part. Both frontend and backend share the same `package.json`.
|
||||||
|
|
||||||
The frontend code builds into "dist" directory. The server (express.js) exposes the "dist" directory as the root of the endpoint. This is how production is working.
|
The frontend code builds into "dist" directory. The server (express.js) exposes the "dist" directory as the root of the endpoint. This is how production is working.
|
||||||
|
|
||||||
## Key Technical Skills
|
## Key Technical Skills
|
||||||
|
|
||||||
- Node.js (You should know about promise, async/await and arrow function etc.)
|
- Node.js (You should know about promises, async/await, arrow functions, etc.)
|
||||||
- Socket.io
|
- Socket.io
|
||||||
- SCSS
|
- SCSS
|
||||||
- Vue.js
|
- Vue.js
|
||||||
|
@ -62,7 +62,7 @@ Here are some references:
|
||||||
|
|
||||||
The above cases may not cover all possible situations.
|
The above cases may not cover all possible situations.
|
||||||
|
|
||||||
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
I ([@louislam](https://github.com/louislam)) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spent on it. Therefore, it is essential to have a discussion beforehand.
|
||||||
|
|
||||||
I will assign your pull request to a [milestone](https://github.com/louislam/uptime-kuma/milestones), if I plan to review and merge it.
|
I will assign your pull request to a [milestone](https://github.com/louislam/uptime-kuma/milestones), if I plan to review and merge it.
|
||||||
|
|
||||||
|
@ -73,15 +73,14 @@ Also, please don't rush or ask for an ETA, because I have to understand the pull
|
||||||
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
Before deep into coding, discussion first is preferred. Creating an empty pull request for discussion would be recommended.
|
||||||
|
|
||||||
1. Fork the project
|
1. Fork the project
|
||||||
1. Clone your fork repo to local
|
2. Clone your fork repo to local
|
||||||
1. Create a new branch
|
3. Create a new branch
|
||||||
1. Create an empty commit
|
4. Create an empty commit: `git commit -m "[empty commit] pull request for <YOUR TASK NAME>" --allow-empty`
|
||||||
`git commit -m "[empty commit] pull request for <YOUR TASK NAME>" --allow-empty`
|
5. Push to your fork repo
|
||||||
1. Push to your fork repo
|
6. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
||||||
1. Create a pull request: https://github.com/louislam/uptime-kuma/compare
|
7. Write a proper description
|
||||||
1. Write a proper description
|
8. Click "Change to draft"
|
||||||
1. Click "Change to draft"
|
9. Discussion
|
||||||
1. Discussion
|
|
||||||
|
|
||||||
## Project Styles
|
## Project Styles
|
||||||
|
|
||||||
|
@ -114,9 +113,9 @@ I personally do not like something that requires so many configurations before y
|
||||||
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
|
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
|
||||||
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
|
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
|
||||||
|
|
||||||
### GitHub Codespace
|
### GitHub Codespaces
|
||||||
|
|
||||||
If you don't want to setup an local environment, you can now develop on GitHub Codespace, read more:
|
If you don't want to setup an local environment, you can now develop on GitHub Codespaces, read more:
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma/tree/master/.devcontainer
|
https://github.com/louislam/uptime-kuma/tree/master/.devcontainer
|
||||||
|
|
||||||
|
@ -231,9 +230,9 @@ If for security / bug / other reasons, a library must be updated, breaking chang
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are omitted, they can not be translated).
|
Please add **all** the strings which are translatable to `src/lang/en.json` (if translation keys are omitted, they can not be translated.)
|
||||||
|
|
||||||
**Don't include any other languages in your initial Pull-Request** (even if this is your mother tongue), to avoid merge-conflicts between weblate and `master`.
|
**Don't include any other languages in your initial pull request** (even if this is your mother tongue), to avoid merge-conflicts between weblate and `master`.
|
||||||
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language skills.
|
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language skills.
|
||||||
|
|
||||||
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
||||||
|
@ -245,7 +244,7 @@ My mother language is not English and my grammar is not that great.
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
|
|
||||||
Since there is no way to make a pull request to wiki's repo, I have set up another repo to do that.
|
Since there is no way to make a pull request to the wiki, I have set up another repo to do that.
|
||||||
|
|
||||||
https://github.com/louislam/uptime-kuma-wiki
|
https://github.com/louislam/uptime-kuma-wiki
|
||||||
|
|
||||||
|
|
29
README.md
29
README.md
|
@ -136,28 +136,33 @@ Telegram Notification Sample:
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
- I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the close ones is statping. Unfortunately, it is not stable and no longer maintained.
|
- I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. One of the closest ones is statping. Unfortunately, it is not stable and no longer maintained.
|
||||||
- Want to build a fancy UI.
|
- Wanted to build a fancy UI.
|
||||||
- Learn Vue 3 and vite.js.
|
- Learn Vue 3 and vite.js.
|
||||||
- Show the power of Bootstrap 5.
|
- Show the power of Bootstrap 5.
|
||||||
- Try to use WebSocket with SPA instead of REST API.
|
- Try to use WebSocket with SPA instead of a REST API.
|
||||||
- Deploy my first Docker image to Docker Hub.
|
- Deploy my first Docker image to Docker Hub.
|
||||||
|
|
||||||
If you love this project, please consider giving me a ⭐.
|
If you love this project, please consider giving it a ⭐.
|
||||||
|
|
||||||
## 🗣️ Discussion / Ask for Help
|
## 🗣️ Discussion / Ask for Help
|
||||||
|
|
||||||
⚠️ For any general or technical questions, please don't send me an email, as I am unable to provide support in that manner. I will not respond if you asked such questions.
|
⚠️ For any general or technical questions, please don't send me an email, as I am unable to provide support in that manner. I will not respond if you ask questions there.
|
||||||
|
|
||||||
I recommend using Google, GitHub Issues, or Uptime Kuma's Subreddit for finding answers to your question. If you cannot find the information you need, feel free to ask:
|
I recommend using Google, GitHub Issues, or Uptime Kuma's subreddit for finding answers to your question. If you cannot find the information you need, feel free to ask:
|
||||||
|
|
||||||
- [GitHub Issues](https://github.com/louislam/uptime-kuma/issues)
|
- [GitHub Issues](https://github.com/louislam/uptime-kuma/issues)
|
||||||
- [Subreddit r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
- [Subreddit (r/UptimeKuma)](https://www.reddit.com/r/UptimeKuma/)
|
||||||
|
|
||||||
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam)
|
||||||
You can mention me if you ask a question on Reddit.
|
You can mention me if you ask a question on the subreddit.
|
||||||
|
|
||||||
## Contribute
|
## Contributions
|
||||||
|
|
||||||
|
### Create Pull Requests
|
||||||
|
|
||||||
|
We DO NOT accept all types of pull requests and do not want to waste your time. Please be sure that you have read and follow pull request rules:
|
||||||
|
[CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)
|
||||||
|
|
||||||
### Test Pull Requests
|
### Test Pull Requests
|
||||||
|
|
||||||
|
@ -181,8 +186,6 @@ If you want to translate Uptime Kuma into your language, please visit [Weblate R
|
||||||
### Spelling & Grammar
|
### Spelling & Grammar
|
||||||
|
|
||||||
Feel free to correct the grammar in the documentation or code.
|
Feel free to correct the grammar in the documentation or code.
|
||||||
My mother language is not english and my grammar is not that great.
|
My mother language is not English and my grammar is not that great.
|
||||||
|
|
||||||
### Create Pull Requests
|
|
||||||
|
|
||||||
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
1. Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
1. Please report security issues to https://github.com/louislam/uptime-kuma/security/advisories/new.
|
||||||
1. Please also create an empty security issue to alert me, as GitHub Advisories do not send a notification, I probably will miss it without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
|
2. Please also create an empty security issue to alert me, as GitHub Advisories do not send a notification, I probably will miss it without this. https://github.com/louislam/uptime-kuma/issues/new?assignees=&labels=help&template=security.md
|
||||||
|
|
||||||
Do not use the public issue tracker or discuss it in public as it will cause more damage.
|
Do not use the public issue tracker or discuss it in public as it will cause more damage.
|
||||||
|
|
||||||
|
@ -19,12 +19,12 @@ You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X` vers
|
||||||
|
|
||||||
### Upgradable Docker Tags
|
### Upgradable Docker Tags
|
||||||
|
|
||||||
| Tag | Supported |
|
| Tag | Supported |
|
||||||
| ------- | ------------------ |
|
|-|-|
|
||||||
| 1 | :white_check_mark: |
|
| 1 | :white_check_mark: |
|
||||||
| 1-debian | :white_check_mark: |
|
| 1-debian | :white_check_mark: |
|
||||||
| latest | :white_check_mark: |
|
| latest | :white_check_mark: |
|
||||||
| debian | :white_check_mark: |
|
| debian | :white_check_mark: |
|
||||||
| 1-alpine | ⚠️ Deprecated |
|
| 1-alpine | ⚠️ Deprecated |
|
||||||
| alpine | ⚠️ Deprecated |
|
| alpine | ⚠️ Deprecated |
|
||||||
| All other tags | ❌ |
|
| All other tags | ❌ |
|
||||||
|
|
|
@ -494,8 +494,11 @@ ALTER TABLE monitor
|
||||||
await knex.schema.table("monitor", function (table) {
|
await knex.schema.table("monitor", function (table) {
|
||||||
table.string("kafka_producer_topic", 255);
|
table.string("kafka_producer_topic", 255);
|
||||||
table.text("kafka_producer_brokers");
|
table.text("kafka_producer_brokers");
|
||||||
table.integer("kafka_producer_ssl");
|
|
||||||
table.string("kafka_producer_allow_auto_topic_creation", 255);
|
// patch-fix-kafka-producer-booleans.sql
|
||||||
|
table.boolean("kafka_producer_ssl").defaultTo(0).notNullable();
|
||||||
|
table.boolean("kafka_producer_allow_auto_topic_creation").defaultTo(0).notNullable();
|
||||||
|
|
||||||
table.text("kafka_producer_sasl_options");
|
table.text("kafka_producer_sasl_options");
|
||||||
table.text("kafka_producer_message");
|
table.text("kafka_producer_message");
|
||||||
});
|
});
|
||||||
|
|
15
db/knex_migrations/2023-09-29-0000-heartbeat-retires.js
Normal file
15
db/knex_migrations/2023-09-29-0000-heartbeat-retires.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
// Add new column heartbeat.retries
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("heartbeat", function (table) {
|
||||||
|
table.integer("retries").notNullable().defaultTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("heartbeat", function (table) {
|
||||||
|
table.dropColumn("retries");
|
||||||
|
});
|
||||||
|
};
|
16
db/knex_migrations/2023-10-08-0000-mqtt-query.js
Normal file
16
db/knex_migrations/2023-10-08-0000-mqtt-query.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
// Add new column monitor.mqtt_check_type
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.string("mqtt_check_type", 255).notNullable().defaultTo("keyword");
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
// Drop column monitor.mqtt_check_type
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("mqtt_check_type");
|
||||||
|
});
|
||||||
|
};
|
21
db/knex_migrations/2023-10-16-0000-create-remote-browsers.js
Normal file
21
db/knex_migrations/2023-10-16-0000-create-remote-browsers.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.createTable("remote_browser", function (table) {
|
||||||
|
table.increments("id");
|
||||||
|
table.string("name", 255).notNullable();
|
||||||
|
table.string("url", 255).notNullable();
|
||||||
|
table.integer("user_id").unsigned();
|
||||||
|
}).alterTable("monitor", function (table) {
|
||||||
|
// Add new column monitor.remote_browser
|
||||||
|
table.integer("remote_browser").nullable().defaultTo(null).unsigned()
|
||||||
|
.index()
|
||||||
|
.references("id")
|
||||||
|
.inTable("remote_browser");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.dropTable("remote_browser").alterTable("monitor", function (table) {
|
||||||
|
table.dropColumn("remote_browser");
|
||||||
|
});
|
||||||
|
};
|
34
db/old_migrations/patch-fix-kafka-producer-booleans.sql
Normal file
34
db/old_migrations/patch-fix-kafka-producer-booleans.sql
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Rename COLUMNs to another one (suffixed by `_old`)
|
||||||
|
ALTER TABLE monitor
|
||||||
|
RENAME COLUMN kafka_producer_ssl TO kafka_producer_ssl_old;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
RENAME COLUMN kafka_producer_allow_auto_topic_creation TO kafka_producer_allow_auto_topic_creation_old;
|
||||||
|
|
||||||
|
-- Add correct COLUMNs
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD COLUMN kafka_producer_ssl BOOLEAN default 0 NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD COLUMN kafka_producer_allow_auto_topic_creation BOOLEAN default 0 NOT NULL;
|
||||||
|
|
||||||
|
-- These SQL is still not fully safe. See https://github.com/louislam/uptime-kuma/issues/4039.
|
||||||
|
|
||||||
|
-- Set bring old values from `_old` COLUMNs to correct ones
|
||||||
|
-- UPDATE monitor SET kafka_producer_allow_auto_topic_creation = monitor.kafka_producer_allow_auto_topic_creation_old
|
||||||
|
-- WHERE monitor.kafka_producer_allow_auto_topic_creation_old IS NOT NULL;
|
||||||
|
|
||||||
|
-- UPDATE monitor SET kafka_producer_ssl = monitor.kafka_producer_ssl_old
|
||||||
|
-- WHERE monitor.kafka_producer_ssl_old IS NOT NULL;
|
||||||
|
|
||||||
|
-- Remove old COLUMNs
|
||||||
|
ALTER TABLE monitor
|
||||||
|
DROP COLUMN kafka_producer_allow_auto_topic_creation_old;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
DROP COLUMN kafka_producer_ssl_old;
|
||||||
|
|
||||||
|
COMMIT;
|
7
db/old_migrations/patch-timeout.sql
Normal file
7
db/old_migrations/patch-timeout.sql
Normal 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;
|
||||||
|
|
||||||
|
UPDATE monitor SET timeout = (interval * 0.8)
|
||||||
|
WHERE timeout IS NULL OR timeout <= 0;
|
||||||
|
|
||||||
|
COMMIT;
|
|
@ -42,13 +42,20 @@ HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD ext
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["node", "server/server.js"]
|
CMD ["node", "server/server.js"]
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Rootless Image
|
||||||
|
############################################
|
||||||
|
FROM release AS rootless
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
# 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
|
||||||
|
|
||||||
|
FROM nightly AS nightly-rootless
|
||||||
|
USER node
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
# Build an image for testing pr
|
# Build an image for testing pr
|
||||||
############################################
|
############################################
|
||||||
|
|
|
@ -5,7 +5,7 @@ const fs = require("fs");
|
||||||
* 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 information 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`.
|
||||||
|
|
|
@ -1,276 +0,0 @@
|
||||||
// install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
|
||||||
// "npm run compile-install-script" to compile install.sh
|
|
||||||
// The command is working on Windows PowerShell and Docker for Windows only.
|
|
||||||
|
|
||||||
|
|
||||||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
|
||||||
println("=====================");
|
|
||||||
println("Uptime Kuma Install Script");
|
|
||||||
println("=====================");
|
|
||||||
println("Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8");
|
|
||||||
println("---------------------------------------");
|
|
||||||
println("This script is designed for Linux and basic usage.");
|
|
||||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
|
||||||
println("---------------------------------------");
|
|
||||||
println("");
|
|
||||||
println("Local - Install Uptime Kuma on your current machine with git, Node.js and pm2");
|
|
||||||
println("Docker - Install Uptime Kuma Docker container");
|
|
||||||
println("");
|
|
||||||
|
|
||||||
if ("$1" != "") {
|
|
||||||
type = "$1";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type");
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultPort = "3001";
|
|
||||||
|
|
||||||
function checkNode() {
|
|
||||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
|
||||||
println("Node Version: " ++ nodeVersion);
|
|
||||||
|
|
||||||
if (nodeVersion <= "12") {
|
|
||||||
println("Error: Required Node.js 14");
|
|
||||||
call("exit", "1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deb() {
|
|
||||||
bash("nodeCheck=$(node -v)");
|
|
||||||
bash("apt --yes update");
|
|
||||||
|
|
||||||
if (nodeCheck != "") {
|
|
||||||
checkNode();
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Old nodejs binary name is "nodejs"
|
|
||||||
bash("check=$(nodejs --version)");
|
|
||||||
if (check != "") {
|
|
||||||
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old.");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("curlCheck=$(curl --version)");
|
|
||||||
if (curlCheck == "") {
|
|
||||||
println("Installing Curl");
|
|
||||||
bash("apt --yes install curl");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Installing Node.js 16");
|
|
||||||
bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt");
|
|
||||||
bash("apt --yes install nodejs");
|
|
||||||
bash("node -v");
|
|
||||||
|
|
||||||
bash("nodeCheckAgain=$(node -v)");
|
|
||||||
|
|
||||||
if (nodeCheckAgain == "") {
|
|
||||||
println("Error during Node.js installation");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(git --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Installing Git");
|
|
||||||
bash("apt --yes install git");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == "local") {
|
|
||||||
defaultInstallPath = "/opt/uptime-kuma";
|
|
||||||
|
|
||||||
if (exists("/etc/redhat-release")) {
|
|
||||||
os = call("cat", "/etc/redhat-release");
|
|
||||||
distribution = "rhel";
|
|
||||||
|
|
||||||
} else if (exists("/etc/issue")) {
|
|
||||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
|
||||||
if (os == "Ubuntu") {
|
|
||||||
distribution = "ubuntu";
|
|
||||||
|
|
||||||
// Get ubuntu version
|
|
||||||
bash(". /etc/lsb-release");
|
|
||||||
version = DISTRIB_RELEASE;
|
|
||||||
}
|
|
||||||
if (os == "Debian") {
|
|
||||||
distribution = "debian";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("arch=$(uname -i)");
|
|
||||||
|
|
||||||
println("Your OS: " ++ os);
|
|
||||||
println("Distribution: " ++ distribution);
|
|
||||||
println("Version: " ++ version);
|
|
||||||
println("Arch: " ++ arch);
|
|
||||||
|
|
||||||
if ("$3" != "") {
|
|
||||||
port = "$3";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Listening Port [$defaultPort]: ", "port");
|
|
||||||
|
|
||||||
if (port == "") {
|
|
||||||
port = defaultPort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("$2" != "") {
|
|
||||||
installPath = "$2";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath");
|
|
||||||
|
|
||||||
if (installPath == "") {
|
|
||||||
installPath = defaultInstallPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CentOS
|
|
||||||
if (distribution == "rhel") {
|
|
||||||
bash("nodeCheck=$(node -v)");
|
|
||||||
|
|
||||||
if (nodeCheck != "") {
|
|
||||||
checkNode();
|
|
||||||
} else {
|
|
||||||
|
|
||||||
bash("dnfCheck=$(dnf --version)");
|
|
||||||
|
|
||||||
// Use yum
|
|
||||||
if (dnfCheck == "") {
|
|
||||||
bash("curlCheck=$(curl --version)");
|
|
||||||
if (curlCheck == "") {
|
|
||||||
println("Installing Curl");
|
|
||||||
bash("yum -y -q install curl");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Installing Node.js 16");
|
|
||||||
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
|
||||||
bash("yum install -y -q nodejs");
|
|
||||||
} else {
|
|
||||||
bash("curlCheck=$(curl --version)");
|
|
||||||
if (curlCheck == "") {
|
|
||||||
println("Installing Curl");
|
|
||||||
bash("dnf -y install curl");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Installing Node.js 16");
|
|
||||||
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
|
||||||
bash("dnf install -y nodejs");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bash("node -v");
|
|
||||||
|
|
||||||
bash("nodeCheckAgain=$(node -v)");
|
|
||||||
|
|
||||||
if (nodeCheckAgain == "") {
|
|
||||||
println("Error during Node.js installation");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(git --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Installing Git");
|
|
||||||
bash("yum -y -q install git");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ubuntu
|
|
||||||
} else if (distribution == "ubuntu") {
|
|
||||||
deb();
|
|
||||||
|
|
||||||
// Debian
|
|
||||||
} else if (distribution == "debian") {
|
|
||||||
deb();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Unknown distribution
|
|
||||||
error = 0;
|
|
||||||
|
|
||||||
bash("check=$(git --version)");
|
|
||||||
if (check == "") {
|
|
||||||
error = 1;
|
|
||||||
println("Error: git is not found!");
|
|
||||||
println("help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(node -v)");
|
|
||||||
if (check == "") {
|
|
||||||
error = 1;
|
|
||||||
println("Error: node is not found");
|
|
||||||
println("help: an installation guide is available at https://nodejs.org/en/download");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error > 0) {
|
|
||||||
println("Please install above missing software");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(pm2 --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Installing PM2");
|
|
||||||
bash("npm install pm2 -g && pm2 install pm2-logrotate");
|
|
||||||
bash("pm2 startup");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Check again
|
|
||||||
bash("check=$(pm2 --version)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Error: pm2 is not found!");
|
|
||||||
println("help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("mkdir -p $installPath");
|
|
||||||
bash("cd $installPath");
|
|
||||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
|
||||||
bash("npm run setup");
|
|
||||||
|
|
||||||
bash("pm2 start server/server.js --name uptime-kuma -- --port=$port");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
defaultVolume = "uptime-kuma";
|
|
||||||
|
|
||||||
bash("check=$(docker -v)");
|
|
||||||
if (check == "") {
|
|
||||||
println("Error: docker is not found!");
|
|
||||||
println("help: an installation guide is available at https://docs.docker.com/desktop/");
|
|
||||||
bash("exit 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
bash("check=$(docker info)");
|
|
||||||
|
|
||||||
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then
|
|
||||||
\"echo\" \"Error: docker is not running\"
|
|
||||||
\"echo\" \"help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/\"
|
|
||||||
\"exit\" \"1\"
|
|
||||||
fi");
|
|
||||||
|
|
||||||
if ("$3" != "") {
|
|
||||||
port = "$3";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Expose Port [$defaultPort]: ", "port");
|
|
||||||
|
|
||||||
if (port == "") {
|
|
||||||
port = defaultPort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("$2" != "") {
|
|
||||||
volume = "$2";
|
|
||||||
} else {
|
|
||||||
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume");
|
|
||||||
|
|
||||||
if (volume == "") {
|
|
||||||
volume = defaultVolume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Port: $port");
|
|
||||||
println("Volume: $volume");
|
|
||||||
bash("docker volume create $volume");
|
|
||||||
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1");
|
|
||||||
}
|
|
||||||
|
|
||||||
println("http://localhost:$port");
|
|
44
extra/reformat-changelog.js
Normal file
44
extra/reformat-changelog.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Generate on GitHub
|
||||||
|
const input = `
|
||||||
|
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
|
||||||
|
`;
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
### 🆕 New Features
|
||||||
|
|
||||||
|
### 💇♀️ Improvements
|
||||||
|
|
||||||
|
### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
### ⬆️ Security Fixes
|
||||||
|
|
||||||
|
### 🦎 Translation Contributions
|
||||||
|
|
||||||
|
### Others
|
||||||
|
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||||
|
`;
|
||||||
|
|
||||||
|
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Split the last " by "
|
||||||
|
const usernamePullRequesURL = line.split(" by ").pop();
|
||||||
|
|
||||||
|
if (!usernamePullRequesURL) {
|
||||||
|
console.log("Unable to parse", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
|
||||||
|
const pullRequestID = "#" + pullRequestURL.split("/").pop();
|
||||||
|
let message = line.split(" by ").shift();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
console.log("Unable to parse", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = message.split("* ").pop();
|
||||||
|
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
||||||
|
}
|
||||||
|
console.log(template);
|
228
install.sh
228
install.sh
|
@ -1,228 +0,0 @@
|
||||||
# install.sh is generated by ./extra/install.batsh, do not modify it directly.
|
|
||||||
# "npm run compile-install-script" to compile install.sh
|
|
||||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
|
||||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
|
||||||
"echo" "-e" "====================="
|
|
||||||
"echo" "-e" "Uptime Kuma Install Script"
|
|
||||||
"echo" "-e" "====================="
|
|
||||||
"echo" "-e" "Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8"
|
|
||||||
"echo" "-e" "---------------------------------------"
|
|
||||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
|
||||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
|
||||||
"echo" "-e" "---------------------------------------"
|
|
||||||
"echo" "-e" ""
|
|
||||||
"echo" "-e" "Local - Install Uptime Kuma on your current machine with git, Node.js and pm2"
|
|
||||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
|
||||||
"echo" "-e" ""
|
|
||||||
if [ "$1" != "" ]; then
|
|
||||||
type="$1"
|
|
||||||
else
|
|
||||||
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type"
|
|
||||||
fi
|
|
||||||
defaultPort="3001"
|
|
||||||
function checkNode {
|
|
||||||
local _0
|
|
||||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
|
||||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
|
||||||
_0="12"
|
|
||||||
if [ $(($nodeVersion <= $_0)) == 1 ]; then
|
|
||||||
"echo" "-e" "Error: Required Node.js 14"
|
|
||||||
"exit" "1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
function deb {
|
|
||||||
nodeCheck=$(node -v)
|
|
||||||
apt --yes update
|
|
||||||
if [ "$nodeCheck" != "" ]; then
|
|
||||||
"checkNode"
|
|
||||||
else
|
|
||||||
# Old nodejs binary name is "nodejs"
|
|
||||||
check=$(nodejs --version)
|
|
||||||
if [ "$check" != "" ]; then
|
|
||||||
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
curlCheck=$(curl --version)
|
|
||||||
if [ "$curlCheck" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Curl"
|
|
||||||
apt --yes install curl
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Installing Node.js 16"
|
|
||||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt
|
|
||||||
apt --yes install nodejs
|
|
||||||
node -v
|
|
||||||
nodeCheckAgain=$(node -v)
|
|
||||||
if [ "$nodeCheckAgain" == "" ]; then
|
|
||||||
"echo" "-e" "Error during Node.js installation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
check=$(git --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Git"
|
|
||||||
apt --yes install git
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
if [ "$type" == "local" ]; then
|
|
||||||
defaultInstallPath="/opt/uptime-kuma"
|
|
||||||
if [ -e "/etc/redhat-release" ]; then
|
|
||||||
os=$("cat" "/etc/redhat-release")
|
|
||||||
distribution="rhel"
|
|
||||||
else
|
|
||||||
if [ -e "/etc/issue" ]; then
|
|
||||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
|
||||||
if [ "$os" == "Ubuntu" ]; then
|
|
||||||
distribution="ubuntu"
|
|
||||||
# Get ubuntu version
|
|
||||||
. /etc/lsb-release
|
|
||||||
version="$DISTRIB_RELEASE"
|
|
||||||
fi
|
|
||||||
if [ "$os" == "Debian" ]; then
|
|
||||||
distribution="debian"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
arch=$(uname -i)
|
|
||||||
"echo" "-e" "Your OS: ""$os"
|
|
||||||
"echo" "-e" "Distribution: ""$distribution"
|
|
||||||
"echo" "-e" "Version: ""$version"
|
|
||||||
"echo" "-e" "Arch: ""$arch"
|
|
||||||
if [ "$3" != "" ]; then
|
|
||||||
port="$3"
|
|
||||||
else
|
|
||||||
"read" "-p" "Listening Port [$defaultPort]: " "port"
|
|
||||||
if [ "$port" == "" ]; then
|
|
||||||
port="$defaultPort"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "$2" != "" ]; then
|
|
||||||
installPath="$2"
|
|
||||||
else
|
|
||||||
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath"
|
|
||||||
if [ "$installPath" == "" ]; then
|
|
||||||
installPath="$defaultInstallPath"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# CentOS
|
|
||||||
if [ "$distribution" == "rhel" ]; then
|
|
||||||
nodeCheck=$(node -v)
|
|
||||||
if [ "$nodeCheck" != "" ]; then
|
|
||||||
"checkNode"
|
|
||||||
else
|
|
||||||
dnfCheck=$(dnf --version)
|
|
||||||
# Use yum
|
|
||||||
if [ "$dnfCheck" == "" ]; then
|
|
||||||
curlCheck=$(curl --version)
|
|
||||||
if [ "$curlCheck" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Curl"
|
|
||||||
yum -y -q install curl
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Installing Node.js 16"
|
|
||||||
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
|
||||||
yum install -y -q nodejs
|
|
||||||
else
|
|
||||||
curlCheck=$(curl --version)
|
|
||||||
if [ "$curlCheck" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Curl"
|
|
||||||
dnf -y install curl
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Installing Node.js 16"
|
|
||||||
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
|
||||||
dnf install -y nodejs
|
|
||||||
fi
|
|
||||||
node -v
|
|
||||||
nodeCheckAgain=$(node -v)
|
|
||||||
if [ "$nodeCheckAgain" == "" ]; then
|
|
||||||
"echo" "-e" "Error during Node.js installation"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
check=$(git --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Installing Git"
|
|
||||||
yum -y -q install git
|
|
||||||
fi
|
|
||||||
# Ubuntu
|
|
||||||
else
|
|
||||||
if [ "$distribution" == "ubuntu" ]; then
|
|
||||||
"deb"
|
|
||||||
# Debian
|
|
||||||
else
|
|
||||||
if [ "$distribution" == "debian" ]; then
|
|
||||||
"deb"
|
|
||||||
else
|
|
||||||
# Unknown distribution
|
|
||||||
error=$((0))
|
|
||||||
check=$(git --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
error=$((1))
|
|
||||||
"echo" "-e" "Error: git is not found!"
|
|
||||||
"echo" "-e" "help: an installation guide is available at https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"
|
|
||||||
fi
|
|
||||||
check=$(node -v)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
error=$((1))
|
|
||||||
"echo" "-e" "Error: node is not found"
|
|
||||||
"echo" "-e" "help: an installation guide is available at https://nodejs.org/en/download"
|
|
||||||
fi
|
|
||||||
if [ $(($error > 0)) == 1 ]; then
|
|
||||||
"echo" "-e" "Please install above missing software"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
check=$(pm2 --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Installing PM2"
|
|
||||||
npm install pm2 -g && pm2 install pm2-logrotate
|
|
||||||
pm2 startup
|
|
||||||
fi
|
|
||||||
# Check again
|
|
||||||
check=$(pm2 --version)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Error: pm2 is not found!"
|
|
||||||
"echo" "-e" "help: an installation guide is available at https://pm2.keymetrics.io/docs/usage/quick-start/"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
mkdir -p $installPath
|
|
||||||
cd $installPath
|
|
||||||
git clone https://github.com/louislam/uptime-kuma.git .
|
|
||||||
npm run setup
|
|
||||||
pm2 start server/server.js --name uptime-kuma -- --port=$port
|
|
||||||
else
|
|
||||||
defaultVolume="uptime-kuma"
|
|
||||||
check=$(docker -v)
|
|
||||||
if [ "$check" == "" ]; then
|
|
||||||
"echo" "-e" "Error: docker is not found!"
|
|
||||||
"echo" "-e" "help: an installation guide is available at https://docs.docker.com/desktop/"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
check=$(docker info)
|
|
||||||
if [[ "$check" == *"Is the docker daemon running"* ]]; then
|
|
||||||
"echo" "Error: docker is not running"
|
|
||||||
"echo" "help: a troubleshooting guide is available at https://docs.docker.com/config/daemon/troubleshoot/"
|
|
||||||
"exit" "1"
|
|
||||||
fi
|
|
||||||
if [ "$3" != "" ]; then
|
|
||||||
port="$3"
|
|
||||||
else
|
|
||||||
"read" "-p" "Expose Port [$defaultPort]: " "port"
|
|
||||||
if [ "$port" == "" ]; then
|
|
||||||
port="$defaultPort"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "$2" != "" ]; then
|
|
||||||
volume="$2"
|
|
||||||
else
|
|
||||||
"read" "-p" "Volume Name [$defaultVolume]: " "volume"
|
|
||||||
if [ "$volume" == "" ]; then
|
|
||||||
volume="$defaultVolume"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
"echo" "-e" "Port: $port"
|
|
||||||
"echo" "-e" "Volume: $volume"
|
|
||||||
docker volume create $volume
|
|
||||||
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1
|
|
||||||
fi
|
|
||||||
"echo" "-e" "http://localhost:$port"
|
|
4656
package-lock.json
generated
4656
package-lock.json
generated
File diff suppressed because it is too large
Load diff
34
package.json
34
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.23.3",
|
"version": "2.0.0-dev",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -38,25 +38,22 @@
|
||||||
"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-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-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-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:nightly2 --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-slim-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-slim-rootless -t louislam/uptime-kuma:$VERSION-slim-rootless --target rootless --build-arg BASE_IMAGE=louislam/uptime-kuma:base2-slim . --push",
|
||||||
|
"build-docker-full-rootless": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:2-rootless -t louislam/uptime-kuma:$VERSION-rootless --target rootless . --push",
|
||||||
|
"build-docker-nightly-rootless": "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-rootless --target nightly-rootless . --push",
|
||||||
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
|
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
|
||||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.23.3 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.23.8 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
"remove-2fa": "node extra/remove-2fa.js",
|
"remove-2fa": "node extra/remove-2fa.js",
|
||||||
"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-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.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-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
|
||||||
"test-install-script-ubuntu1804": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1804.dockerfile .",
|
|
||||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
|
||||||
"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",
|
||||||
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
|
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
|
||||||
|
"simple-postgres": "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres",
|
||||||
|
"simple-mariadb": "docker run --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mariadb# mariadb",
|
||||||
"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",
|
||||||
"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",
|
||||||
|
@ -72,7 +69,8 @@
|
||||||
"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",
|
"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",
|
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
|
||||||
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
|
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
|
||||||
|
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "~1.7.3",
|
"@grpc/grpc-js": "~1.7.3",
|
||||||
|
@ -98,7 +96,9 @@
|
||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "~4.1.0",
|
"gamedig": "^4.2.0",
|
||||||
|
"http-cookie-agent": "~5.0.4",
|
||||||
|
"html-escaper": "^3.0.3",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "~5.0.0",
|
"http-proxy-agent": "~5.0.0",
|
||||||
"https-proxy-agent": "~5.0.1",
|
"https-proxy-agent": "~5.0.1",
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
"mongodb": "~4.17.1",
|
"mongodb": "~4.17.1",
|
||||||
"mqtt": "~4.3.7",
|
"mqtt": "~4.3.7",
|
||||||
"mssql": "~8.1.4",
|
"mssql": "~8.1.4",
|
||||||
"mysql2": "~2.3.3",
|
"mysql2": "~3.6.2",
|
||||||
"nanoid": "~3.3.4",
|
"nanoid": "~3.3.4",
|
||||||
"node-cloudflared-tunnel": "~1.0.9",
|
"node-cloudflared-tunnel": "~1.0.9",
|
||||||
"node-radius-client": "~1.0.0",
|
"node-radius-client": "~1.0.0",
|
||||||
|
@ -125,11 +125,12 @@
|
||||||
"notp": "~2.0.3",
|
"notp": "~2.0.3",
|
||||||
"openid-client": "^5.4.2",
|
"openid-client": "^5.4.2",
|
||||||
"password-hash": "~1.2.2",
|
"password-hash": "~1.2.2",
|
||||||
"pg": "~8.8.0",
|
"pg": "~8.11.3",
|
||||||
"pg-connection-string": "~2.5.0",
|
"pg-connection-string": "~2.6.2",
|
||||||
"playwright-core": "~1.35.1",
|
"playwright-core": "~1.35.1",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"promisify-child-process": "~4.1.2",
|
||||||
"protobufjs": "~7.2.4",
|
"protobufjs": "~7.2.4",
|
||||||
"qs": "~6.10.4",
|
"qs": "~6.10.4",
|
||||||
"redbean-node": "~0.3.0",
|
"redbean-node": "~0.3.0",
|
||||||
|
@ -141,6 +142,7 @@
|
||||||
"tar": "~6.1.11",
|
"tar": "~6.1.11",
|
||||||
"tcp-ping": "~0.1.1",
|
"tcp-ping": "~0.1.1",
|
||||||
"thirty-two": "~1.0.2",
|
"thirty-two": "~1.0.2",
|
||||||
|
"tough-cookie": "~4.1.3",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -205,7 +207,7 @@
|
||||||
"vue-router": "~4.0.14",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0",
|
"vuedraggable": "~4.1.0",
|
||||||
"wait-on": "^6.0.1",
|
"wait-on": "^7.2.0",
|
||||||
"whatwg-url": "~12.0.1"
|
"whatwg-url": "~12.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,6 +185,30 @@ async function sendDockerHostList(socket) {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send list of docker hosts to client
|
||||||
|
* @param {Socket} socket Socket.io socket instance
|
||||||
|
* @returns {Promise<Bean[]>} List of docker hosts
|
||||||
|
*/
|
||||||
|
async function sendRemoteBrowserList(socket) {
|
||||||
|
const timeLogger = new TimeLogger();
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
let list = await R.find("remote_browser", " user_id = ? ", [
|
||||||
|
socket.userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let bean of list) {
|
||||||
|
result.push(bean.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(socket.userID).emit("remoteBrowserList", result);
|
||||||
|
|
||||||
|
timeLogger.print("Send Remote Browser List");
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotificationList,
|
sendNotificationList,
|
||||||
sendImportantHeartbeatList,
|
sendImportantHeartbeatList,
|
||||||
|
@ -192,5 +216,6 @@ module.exports = {
|
||||||
sendProxyList,
|
sendProxyList,
|
||||||
sendAPIKeyList,
|
sendAPIKeyList,
|
||||||
sendInfo,
|
sendInfo,
|
||||||
sendDockerHostList
|
sendDockerHostList,
|
||||||
|
sendRemoteBrowserList,
|
||||||
};
|
};
|
||||||
|
|
|
@ -104,7 +104,9 @@ class Database {
|
||||||
"patch-monitor-oauth-cc.sql": true,
|
"patch-monitor-oauth-cc.sql": true,
|
||||||
"patch-add-timeout-monitor.sql": true,
|
"patch-add-timeout-monitor.sql": true,
|
||||||
"patch-add-gamedig-given-port.sql": true,
|
"patch-add-gamedig-given-port.sql": true,
|
||||||
"patch-notification-config.sql": true, // The last file so far converted to a knex migration file
|
"patch-notification-config.sql": true,
|
||||||
|
"patch-fix-kafka-producer-booleans.sql": true,
|
||||||
|
"patch-timeout.sql": true, // The last file so far converted to a knex migration file
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -271,7 +273,14 @@ class Database {
|
||||||
user: dbConfig.username,
|
user: dbConfig.username,
|
||||||
password: dbConfig.password,
|
password: dbConfig.password,
|
||||||
database: dbConfig.dbName,
|
database: dbConfig.dbName,
|
||||||
timezone: "+00:00",
|
timezone: "Z",
|
||||||
|
typeCast: function (field, next) {
|
||||||
|
if (field.type === "DATETIME") {
|
||||||
|
// Do not perform timezone conversion
|
||||||
|
return field.string();
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
pool: mariadbPoolConfig,
|
pool: mariadbPoolConfig,
|
||||||
};
|
};
|
||||||
|
@ -285,6 +294,14 @@ class Database {
|
||||||
socketPath: embeddedMariaDB.socketPath,
|
socketPath: embeddedMariaDB.socketPath,
|
||||||
user: "node",
|
user: "node",
|
||||||
database: "kuma",
|
database: "kuma",
|
||||||
|
timezone: "Z",
|
||||||
|
typeCast: function (field, next) {
|
||||||
|
if (field.type === "DATETIME") {
|
||||||
|
// Do not perform timezone conversion
|
||||||
|
return field.string();
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
pool: mariadbPoolConfig,
|
pool: mariadbPoolConfig,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const version = require("../package.json").version;
|
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
|
const { axiosAbortSignal } = require("./util-server");
|
||||||
|
|
||||||
class DockerHost {
|
class DockerHost {
|
||||||
|
|
||||||
|
@ -70,9 +70,11 @@ class DockerHost {
|
||||||
static async testDockerHost(dockerHost) {
|
static async testDockerHost(dockerHost) {
|
||||||
const options = {
|
const options = {
|
||||||
url: "/containers/json?all=true",
|
url: "/containers/json?all=true",
|
||||||
|
timeout: 5000,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
},
|
},
|
||||||
|
signal: axiosAbortSignal(6000),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dockerHost.dockerType === "socket") {
|
if (dockerHost.dockerType === "socket") {
|
||||||
|
@ -82,26 +84,33 @@ class DockerHost {
|
||||||
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await axios.request(options);
|
try {
|
||||||
|
let res = await axios.request(options);
|
||||||
|
|
||||||
if (Array.isArray(res.data)) {
|
if (Array.isArray(res.data)) {
|
||||||
|
|
||||||
if (res.data.length > 1) {
|
if (res.data.length > 1) {
|
||||||
|
|
||||||
|
if ("ImageID" in res.data[0]) {
|
||||||
|
return res.data.length;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
|
||||||
if ("ImageID" in res.data[0]) {
|
|
||||||
return res.data.length;
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
return res.data.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return res.data.length;
|
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === "ECONNABORTED" || e.name === "CanceledError") {
|
||||||
|
throw new Error("Connection to Docker daemon timed out.");
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
|
const { escape } = require("html-escaper");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
@ -7,15 +8,18 @@ const jsesc = require("jsesc");
|
||||||
* @returns {string} HTML script tags to inject into page
|
* @returns {string} HTML script tags to inject into page
|
||||||
*/
|
*/
|
||||||
function getGoogleAnalyticsScript(tagId) {
|
function getGoogleAnalyticsScript(tagId) {
|
||||||
let escapedTagId = jsesc(tagId, { isScriptContext: true });
|
let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
|
||||||
|
|
||||||
if (escapedTagId) {
|
if (escapedTagIdJS) {
|
||||||
escapedTagId = escapedTagId.trim();
|
escapedTagIdJS = escapedTagIdJS.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape the tag ID for use in an HTML attribute.
|
||||||
|
let escapedTagIdHTMLAttribute = escape(tagId);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
|
||||||
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
|
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,14 @@ class Heartbeat extends BeanModel {
|
||||||
*/
|
*/
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
monitorID: this.monitor_id,
|
monitorID: this._monitorId,
|
||||||
status: this.status,
|
status: this._status,
|
||||||
time: this.time,
|
time: this._time,
|
||||||
msg: this.msg,
|
msg: this._msg,
|
||||||
ping: this.ping,
|
ping: this._ping,
|
||||||
important: this.important,
|
important: this._important,
|
||||||
duration: this.duration,
|
duration: this._duration,
|
||||||
|
retries: this._retries,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
const https = require("https");
|
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||||
SQL_DATETIME_FORMAT
|
SQL_DATETIME_FORMAT
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
|
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||||
} = require("../util-server");
|
} = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
@ -21,7 +20,12 @@ const { DockerHost } = require("../docker");
|
||||||
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 crypto = require("crypto");
|
||||||
const { UptimeCalculator } = require("../uptime-calculator");
|
const { UptimeCalculator } = require("../uptime-calculator");
|
||||||
|
const { CookieJar } = require("tough-cookie");
|
||||||
|
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
||||||
|
|
||||||
|
const rootCertificates = rootCertificatesFingerprints();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
|
@ -130,6 +134,7 @@ class Monitor extends BeanModel {
|
||||||
maintenance: await Monitor.isUnderMaintenance(this.id),
|
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||||
mqttTopic: this.mqttTopic,
|
mqttTopic: this.mqttTopic,
|
||||||
mqttSuccessMessage: this.mqttSuccessMessage,
|
mqttSuccessMessage: this.mqttSuccessMessage,
|
||||||
|
mqttCheckType: this.mqttCheckType,
|
||||||
databaseQuery: this.databaseQuery,
|
databaseQuery: this.databaseQuery,
|
||||||
authMethod: this.authMethod,
|
authMethod: this.authMethod,
|
||||||
grpcUrl: this.grpcUrl,
|
grpcUrl: this.grpcUrl,
|
||||||
|
@ -146,10 +151,11 @@ class Monitor extends BeanModel {
|
||||||
expectedValue: this.expectedValue,
|
expectedValue: this.expectedValue,
|
||||||
kafkaProducerTopic: this.kafkaProducerTopic,
|
kafkaProducerTopic: this.kafkaProducerTopic,
|
||||||
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
||||||
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
|
kafkaProducerSsl: this.getKafkaProducerSsl(),
|
||||||
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
|
kafkaProducerAllowAutoTopicCreation: this.getKafkaProducerAllowAutoTopicCreation(),
|
||||||
kafkaProducerMessage: this.kafkaProducerMessage,
|
kafkaProducerMessage: this.kafkaProducerMessage,
|
||||||
screenshot,
|
screenshot,
|
||||||
|
remote_browser: this.remote_browser,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeSensitiveData) {
|
if (includeSensitiveData) {
|
||||||
|
@ -298,6 +304,22 @@ class Monitor extends BeanModel {
|
||||||
return Boolean(this.gamedigGivenPortOnly);
|
return Boolean(this.gamedigGivenPortOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Kafka Producer Ssl enabled?
|
||||||
|
*/
|
||||||
|
getKafkaProducerSsl() {
|
||||||
|
return Boolean(this.kafkaProducerSsl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse to boolean
|
||||||
|
* @returns {boolean} Kafka Producer Allow Auto Topic Creation Enabled?
|
||||||
|
*/
|
||||||
|
getKafkaProducerAllowAutoTopicCreation() {
|
||||||
|
return Boolean(this.kafkaProducerAllowAutoTopicCreation);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitor
|
* Start monitor
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
|
@ -332,6 +354,9 @@ class Monitor extends BeanModel {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
|
if (previousBeat) {
|
||||||
|
retries = previousBeat.retries;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirstBeat = !previousBeat;
|
const isFirstBeat = !previousBeat;
|
||||||
|
@ -346,6 +371,12 @@ class Monitor extends BeanModel {
|
||||||
bean.status = flipStatus(bean.status);
|
bean.status = flipStatus(bean.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runtime patch timeout if it is 0
|
||||||
|
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
|
||||||
|
if (!this.timeout || this.timeout <= 0) {
|
||||||
|
this.timeout = this.interval * 1000 * 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||||
bean.msg = "Monitor under maintenance";
|
bean.msg = "Monitor under maintenance";
|
||||||
|
@ -416,6 +447,7 @@ class Monitor extends BeanModel {
|
||||||
const httpsAgentOptions = {
|
const httpsAgentOptions = {
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||||
};
|
};
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||||
|
@ -456,6 +488,7 @@ class Monitor extends BeanModel {
|
||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
|
signal: axiosAbortSignal((this.timeout + 10) * 1000),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bodyValue) {
|
if (bodyValue) {
|
||||||
|
@ -477,7 +510,12 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.httpsAgent) {
|
if (!options.httpsAgent) {
|
||||||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
let jar = new CookieJar();
|
||||||
|
let httpsCookieAgentOptions = {
|
||||||
|
...httpsAgentOptions,
|
||||||
|
cookies: { jar }
|
||||||
|
};
|
||||||
|
options.httpsAgent = new HttpsCookieAgent(httpsCookieAgentOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.auth_method === "mtls") {
|
if (this.auth_method === "mtls") {
|
||||||
|
@ -595,6 +633,7 @@ class Monitor extends BeanModel {
|
||||||
// If the previous beat was down or pending we use the regular
|
// If the previous beat was down or pending we use the regular
|
||||||
// beatInterval/retryInterval in the setTimeout further below
|
// beatInterval/retryInterval in the setTimeout further below
|
||||||
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
||||||
|
bean.duration = Math.round(msSinceLastBeat / 1000);
|
||||||
throw new Error("No heartbeat in the time window");
|
throw new Error("No heartbeat in the time window");
|
||||||
} else {
|
} else {
|
||||||
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
||||||
|
@ -610,6 +649,7 @@ class Monitor extends BeanModel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
bean.duration = beatInterval;
|
||||||
throw new Error("No heartbeat in the time window");
|
throw new Error("No heartbeat in the time window");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,6 +670,7 @@ class Monitor extends BeanModel {
|
||||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||||
}),
|
}),
|
||||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||||
maxCachedSessions: 0,
|
maxCachedSessions: 0,
|
||||||
|
@ -672,8 +713,6 @@ class Monitor extends BeanModel {
|
||||||
} else if (this.type === "docker") {
|
} else if (this.type === "docker") {
|
||||||
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
|
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
|
||||||
|
|
||||||
const dockerHost = await R.load("docker_host", this.docker_host);
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
url: `/containers/${this.docker_container}/json`,
|
url: `/containers/${this.docker_container}/json`,
|
||||||
timeout: this.interval * 1000 * 0.8,
|
timeout: this.interval * 1000 * 0.8,
|
||||||
|
@ -683,12 +722,19 @@ class Monitor extends BeanModel {
|
||||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||||
rejectUnauthorized: !this.getIgnoreTls(),
|
rejectUnauthorized: !this.getIgnoreTls(),
|
||||||
|
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
||||||
}),
|
}),
|
||||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||||
maxCachedSessions: 0,
|
maxCachedSessions: 0,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||||
|
|
||||||
|
if (!dockerHost) {
|
||||||
|
throw new Error("Failed to load docker host config");
|
||||||
|
}
|
||||||
|
|
||||||
if (dockerHost._dockerType === "socket") {
|
if (dockerHost._dockerType === "socket") {
|
||||||
options.socketPath = dockerHost._dockerDaemon;
|
options.socketPath = dockerHost._dockerDaemon;
|
||||||
} else if (dockerHost._dockerType === "tcp") {
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
|
@ -712,18 +758,10 @@ class Monitor extends BeanModel {
|
||||||
} else {
|
} else {
|
||||||
throw Error("Container State is " + res.data.State.Status);
|
throw Error("Container State is " + res.data.State.Status);
|
||||||
}
|
}
|
||||||
} else if (this.type === "mqtt") {
|
|
||||||
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
|
||||||
port: this.port,
|
|
||||||
username: this.mqttUsername,
|
|
||||||
password: this.mqttPassword,
|
|
||||||
interval: this.interval,
|
|
||||||
});
|
|
||||||
bean.status = UP;
|
|
||||||
} else if (this.type === "sqlserver") {
|
} else if (this.type === "sqlserver") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
await mssqlQuery(this.databaseConnectionString, this.databaseQuery);
|
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
|
||||||
|
|
||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
@ -762,7 +800,7 @@ class Monitor extends BeanModel {
|
||||||
} else if (this.type === "postgres") {
|
} else if (this.type === "postgres") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
|
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
|
||||||
|
|
||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
|
@ -770,7 +808,11 @@ class Monitor extends BeanModel {
|
||||||
} else if (this.type === "mysql") {
|
} else if (this.type === "mysql") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
// Use `radius_password` as `password` field, since there are too many unnecessary fields
|
||||||
|
// TODO: rename `radius_password` to `password` later for general use
|
||||||
|
let mysqlPassword = this.radiusPassword;
|
||||||
|
|
||||||
|
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
} else if (this.type === "mongodb") {
|
} else if (this.type === "mongodb") {
|
||||||
|
@ -858,7 +900,11 @@ class Monitor extends BeanModel {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
bean.msg = error.message;
|
if (error?.name === "CanceledError") {
|
||||||
|
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
||||||
|
} else {
|
||||||
|
bean.msg = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
// If UP come in here, it must be upside down mode
|
// If UP come in here, it must be upside down mode
|
||||||
// Just reset the retries
|
// Just reset the retries
|
||||||
|
@ -868,9 +914,14 @@ class Monitor extends BeanModel {
|
||||||
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
||||||
retries++;
|
retries++;
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
|
} else {
|
||||||
|
// Continue counting retries during DOWN
|
||||||
|
retries++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bean.retries = retries;
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Check isImportant`);
|
log.debug("monitor", `[${this.name}] Check isImportant`);
|
||||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||||
|
|
||||||
|
@ -1315,7 +1366,10 @@ class Monitor extends BeanModel {
|
||||||
let certInfo = tlsInfoObject.certInfo;
|
let certInfo = tlsInfoObject.certInfo;
|
||||||
while (certInfo) {
|
while (certInfo) {
|
||||||
let subjectCN = certInfo.subject["CN"];
|
let subjectCN = certInfo.subject["CN"];
|
||||||
if (certInfo.daysRemaining > targetDays) {
|
if (rootCertificates.has(certInfo.fingerprint256)) {
|
||||||
|
log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||||
|
break;
|
||||||
|
} else if (certInfo.daysRemaining > targetDays) {
|
||||||
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||||
} else {
|
} else {
|
||||||
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
||||||
|
@ -1381,10 +1435,7 @@ class Monitor extends BeanModel {
|
||||||
* @returns {Promise<LooseObject<any>>} Previous heartbeat
|
* @returns {Promise<LooseObject<any>>} Previous heartbeat
|
||||||
*/
|
*/
|
||||||
static async getPreviousHeartbeat(monitorID) {
|
static async getPreviousHeartbeat(monitorID) {
|
||||||
return await R.getRow(`
|
return await R.findOne("heartbeat", " id = (select MAX(id) from heartbeat where monitor_id = ?)", [
|
||||||
SELECT ping, status, time FROM heartbeat
|
|
||||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
|
||||||
`, [
|
|
||||||
monitorID
|
monitorID
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
17
server/model/remote_browser.js
Normal file
17
server/model/remote_browser.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
|
||||||
|
class RemoteBrowser extends BeanModel {
|
||||||
|
/**
|
||||||
|
* Returns an object that ready to parse to JSON
|
||||||
|
* @returns {object} Object ready to parse
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
url: this.url,
|
||||||
|
name: this.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RemoteBrowser;
|
121
server/monitor-types/mqtt.js
Normal file
121
server/monitor-types/mqtt.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
const { MonitorType } = require("./monitor-type");
|
||||||
|
const { log, UP } = require("../../src/util");
|
||||||
|
const mqtt = require("mqtt");
|
||||||
|
const jsonata = require("jsonata");
|
||||||
|
|
||||||
|
class MqttMonitorType extends MonitorType {
|
||||||
|
|
||||||
|
name = "mqtt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the monitoring check on the MQTT monitor
|
||||||
|
* @param {Monitor} monitor Monitor to check
|
||||||
|
* @param {Heartbeat} heartbeat Monitor heartbeat to update
|
||||||
|
* @param {UptimeKumaServer} server Uptime Kuma server
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async check(monitor, heartbeat, server) {
|
||||||
|
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
|
||||||
|
port: monitor.port,
|
||||||
|
username: monitor.mqttUsername,
|
||||||
|
password: monitor.mqttPassword,
|
||||||
|
interval: monitor.interval,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
|
||||||
|
// use old default
|
||||||
|
monitor.mqttCheckType = "keyword";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.mqttCheckType === "keyword") {
|
||||||
|
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
|
||||||
|
heartbeat.msg = `Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`;
|
||||||
|
heartbeat.status = UP;
|
||||||
|
} else {
|
||||||
|
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
|
||||||
|
}
|
||||||
|
} else if (monitor.mqttCheckType === "json-query") {
|
||||||
|
const parsedMessage = JSON.parse(receivedMessage);
|
||||||
|
|
||||||
|
let expression = jsonata(monitor.jsonPath);
|
||||||
|
|
||||||
|
let result = await expression.evaluate(parsedMessage);
|
||||||
|
|
||||||
|
if (result?.toString() === monitor.expectedValue) {
|
||||||
|
heartbeat.msg = "Message received, expected value is found";
|
||||||
|
heartbeat.status = UP;
|
||||||
|
} else {
|
||||||
|
throw new Error("Message received but value is not equal to expected value, value was: [" + result + "]");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Error("Unknown MQTT Check Type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to MQTT Broker, subscribe to topic and receive message as String
|
||||||
|
* @param {string} hostname Hostname / address of machine to test
|
||||||
|
* @param {string} topic MQTT topic
|
||||||
|
* @param {object} options MQTT options. Contains port, username,
|
||||||
|
* password and interval (interval defaults to 20)
|
||||||
|
* @returns {Promise<string>} Received MQTT message
|
||||||
|
*/
|
||||||
|
mqttAsync(hostname, topic, options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { port, username, password, interval = 20 } = options;
|
||||||
|
|
||||||
|
// Adds MQTT protocol to the hostname if not already present
|
||||||
|
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
|
||||||
|
hostname = "mqtt://" + hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutID = setTimeout(() => {
|
||||||
|
log.debug("mqtt", "MQTT timeout triggered");
|
||||||
|
client.end();
|
||||||
|
reject(new Error("Timeout, Message not received"));
|
||||||
|
}, interval * 1000 * 0.8);
|
||||||
|
|
||||||
|
const mqttUrl = `${hostname}:${port}`;
|
||||||
|
|
||||||
|
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
|
||||||
|
|
||||||
|
let client = mqtt.connect(mqttUrl, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("connect", () => {
|
||||||
|
log.debug("mqtt", "MQTT connected");
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.subscribe(topic, () => {
|
||||||
|
log.debug("mqtt", "MQTT subscribed to topic");
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
client.end();
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
reject(new Error("Cannot subscribe topic"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (error) => {
|
||||||
|
client.end();
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("message", (messageTopic, message) => {
|
||||||
|
if (messageTopic === topic) {
|
||||||
|
client.end();
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
resolve(message.toString("utf8"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
MqttMonitorType,
|
||||||
|
};
|
|
@ -8,6 +8,7 @@ const path = require("path");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const jwt = require("jsonwebtoken");
|
const jwt = require("jsonwebtoken");
|
||||||
const config = require("../config");
|
const config = require("../config");
|
||||||
|
const { RemoteBrowser } = require("../remote-browser");
|
||||||
|
|
||||||
let browser = null;
|
let browser = null;
|
||||||
|
|
||||||
|
@ -24,6 +25,9 @@ if (process.platform === "win32") {
|
||||||
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
||||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
||||||
|
|
||||||
|
// Allow MS Edge
|
||||||
|
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Microsoft\\Edge\\Application\\msedge.exe");
|
||||||
|
|
||||||
// For Loop A to Z
|
// For Loop A to Z
|
||||||
for (let i = 65; i <= 90; i++) {
|
for (let i = 65; i <= 90; i++) {
|
||||||
let drive = String.fromCharCode(i);
|
let drive = String.fromCharCode(i);
|
||||||
|
@ -40,9 +44,9 @@ if (process.platform === "win32") {
|
||||||
"/usr/bin/chromium",
|
"/usr/bin/chromium",
|
||||||
"/usr/bin/chromium-browser",
|
"/usr/bin/chromium-browser",
|
||||||
"/usr/bin/google-chrome",
|
"/usr/bin/google-chrome",
|
||||||
|
"/snap/bin/chromium", // Ubuntu
|
||||||
];
|
];
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
|
||||||
allowedList = [
|
allowedList = [
|
||||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||||
|
@ -83,6 +87,19 @@ async function getBrowser() {
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current instance of the browser. If there isn't one, create it
|
||||||
|
* @param {integer} remoteBrowserID Path to executable
|
||||||
|
* @param {integer} userId User ID
|
||||||
|
* @returns {Promise<Browser>} The browser
|
||||||
|
*/
|
||||||
|
async function getRemoteBrowser(remoteBrowserID, userId) {
|
||||||
|
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
|
||||||
|
log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
|
||||||
|
browser = chromium.connect(remoteBrowser.url);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the chrome executable path
|
* Prepare the chrome executable path
|
||||||
* @param {string} executablePath Path to chrome executable
|
* @param {string} executablePath Path to chrome executable
|
||||||
|
@ -189,11 +206,21 @@ async function testChrome(executablePath) {
|
||||||
throw new Error(e.message);
|
throw new Error(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// test remote browser
|
||||||
/**
|
/**
|
||||||
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
|
* @param {string} remoteBrowserURL Remote Browser URL
|
||||||
*
|
* @returns {Promise<boolean>} Returns if connection worked
|
||||||
*/
|
*/
|
||||||
|
async function testRemoteBrowser(remoteBrowserURL) {
|
||||||
|
try {
|
||||||
|
const browser = await chromium.connect(remoteBrowserURL);
|
||||||
|
browser.version();
|
||||||
|
await browser.close();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
class RealBrowserMonitorType extends MonitorType {
|
class RealBrowserMonitorType extends MonitorType {
|
||||||
|
|
||||||
name = "real-browser";
|
name = "real-browser";
|
||||||
|
@ -202,7 +229,7 @@ class RealBrowserMonitorType extends MonitorType {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async check(monitor, heartbeat, server) {
|
async check(monitor, heartbeat, server) {
|
||||||
const browser = await getBrowser();
|
const browser = monitor.remote_browser ? await getRemoteBrowser(monitor.remote_browser, monitor.user_id) : await getBrowser();
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
@ -235,4 +262,5 @@ module.exports = {
|
||||||
RealBrowserMonitorType,
|
RealBrowserMonitorType,
|
||||||
testChrome,
|
testChrome,
|
||||||
resetChrome,
|
resetChrome,
|
||||||
|
testRemoteBrowser,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const { MonitorType } = require("./monitor-type");
|
const { MonitorType } = require("./monitor-type");
|
||||||
const { UP, log } = require("../../src/util");
|
const { UP } = require("../../src/util");
|
||||||
const exec = require("child_process").exec;
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TailscalePing class extends the MonitorType.
|
* A TailscalePing class extends the MonitorType.
|
||||||
|
@ -23,7 +23,6 @@ class TailscalePing extends MonitorType {
|
||||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.debug("Tailscale", err);
|
|
||||||
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
||||||
throw new Error(`Error checking Tailscale ping: ${err}`);
|
throw new Error(`Error checking Tailscale ping: ${err}`);
|
||||||
}
|
}
|
||||||
|
@ -37,26 +36,18 @@ class TailscalePing extends MonitorType {
|
||||||
* @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) {
|
||||||
let cmd = `tailscale ping ${hostname}`;
|
let timeout = interval * 1000 * 0.8;
|
||||||
|
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
||||||
log.debug("Tailscale", cmd);
|
timeout: timeout
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let timeout = interval * 1000 * 0.8;
|
|
||||||
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
|
|
||||||
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
|
|
||||||
if (error) {
|
|
||||||
reject(`Execution error: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
reject(`Error in output: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(stdout);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
if (res.stderr && res.stderr.toString()) {
|
||||||
|
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||||
|
}
|
||||||
|
if (res.stdout && res.stdout.toString()) {
|
||||||
|
return res.stdout.toString();
|
||||||
|
} else {
|
||||||
|
throw new Error("No output from Tailscale ping");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,7 +65,7 @@ class TailscalePing extends MonitorType {
|
||||||
heartbeat.status = UP;
|
heartbeat.status = UP;
|
||||||
let time = line.split(" in ")[1].split(" ")[0];
|
let time = line.split(" in ")[1].split(" ")[0];
|
||||||
heartbeat.ping = parseInt(time);
|
heartbeat.ping = parseInt(time);
|
||||||
heartbeat.msg = line;
|
heartbeat.msg = "OK";
|
||||||
break;
|
break;
|
||||||
} else if (line.includes("timed out")) {
|
} else if (line.includes("timed out")) {
|
||||||
throw new Error(`Ping timed out: "${line}"`);
|
throw new Error(`Ping timed out: "${line}"`);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
class Apprise extends NotificationProvider {
|
class Apprise extends NotificationProvider {
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class Apprise extends NotificationProvider {
|
||||||
args.push("-t");
|
args.push("-t");
|
||||||
args.push(notification.title);
|
args.push(notification.title);
|
||||||
}
|
}
|
||||||
const s = childProcess.spawnSync("apprise", args);
|
const s = await childProcessAsync.spawn("apprise", args);
|
||||||
|
|
||||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
|
|
||||||
|
|
|
@ -78,12 +78,12 @@ class Mattermost extends NotificationProvider {
|
||||||
{
|
{
|
||||||
fallback:
|
fallback:
|
||||||
"Your " +
|
"Your " +
|
||||||
monitorJSON.name +
|
monitorJSON.pathName +
|
||||||
" service went " +
|
" service went " +
|
||||||
statusText,
|
statusText,
|
||||||
color: color,
|
color: color,
|
||||||
title:
|
title:
|
||||||
monitorJSON.name +
|
monitorJSON.pathName +
|
||||||
" service went " +
|
" service went " +
|
||||||
statusText,
|
statusText,
|
||||||
title_link: monitorJSON.url,
|
title_link: monitorJSON.url,
|
||||||
|
|
|
@ -4,6 +4,8 @@ const HttpsProxyAgent = require("https-proxy-agent");
|
||||||
const SocksProxyAgent = require("socks-proxy-agent");
|
const SocksProxyAgent = require("socks-proxy-agent");
|
||||||
const { debug } = require("../src/util");
|
const { debug } = require("../src/util");
|
||||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||||
|
const { CookieJar } = require("tough-cookie");
|
||||||
|
const { createCookieAgent } = require("http-cookie-agent/http");
|
||||||
|
|
||||||
class Proxy {
|
class Proxy {
|
||||||
|
|
||||||
|
@ -95,10 +97,13 @@ class Proxy {
|
||||||
let httpAgent;
|
let httpAgent;
|
||||||
let httpsAgent;
|
let httpsAgent;
|
||||||
|
|
||||||
|
let jar = new CookieJar();
|
||||||
|
|
||||||
const proxyOptions = {
|
const proxyOptions = {
|
||||||
protocol: proxy.protocol,
|
protocol: proxy.protocol,
|
||||||
host: proxy.host,
|
host: proxy.host,
|
||||||
port: proxy.port,
|
port: proxy.port,
|
||||||
|
cookies: { jar },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (proxy.auth) {
|
if (proxy.auth) {
|
||||||
|
@ -112,12 +117,17 @@ class Proxy {
|
||||||
switch (proxy.protocol) {
|
switch (proxy.protocol) {
|
||||||
case "http":
|
case "http":
|
||||||
case "https":
|
case "https":
|
||||||
httpAgent = new HttpProxyAgent({
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const HttpCookieProxyAgent = createCookieAgent(HttpProxyAgent);
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const HttpsCookieProxyAgent = createCookieAgent(HttpsProxyAgent);
|
||||||
|
|
||||||
|
httpAgent = new HttpCookieProxyAgent({
|
||||||
...httpAgentOptions || {},
|
...httpAgentOptions || {},
|
||||||
...proxyOptions
|
...proxyOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
httpsAgent = new HttpsProxyAgent({
|
httpsAgent = new HttpsCookieProxyAgent({
|
||||||
...httpsAgentOptions || {},
|
...httpsAgentOptions || {},
|
||||||
...proxyOptions,
|
...proxyOptions,
|
||||||
});
|
});
|
||||||
|
@ -126,7 +136,9 @@ class Proxy {
|
||||||
case "socks5":
|
case "socks5":
|
||||||
case "socks5h":
|
case "socks5h":
|
||||||
case "socks4":
|
case "socks4":
|
||||||
agent = new SocksProxyAgent({
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const SocksCookieProxyAgent = createCookieAgent(SocksProxyAgent);
|
||||||
|
agent = new SocksCookieProxyAgent({
|
||||||
...httpAgentOptions,
|
...httpAgentOptions,
|
||||||
...httpsAgentOptions,
|
...httpsAgentOptions,
|
||||||
...proxyOptions,
|
...proxyOptions,
|
||||||
|
|
84
server/remote-browser.js
Normal file
84
server/remote-browser.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
const { testRemoteBrowser } = require("./monitor-types/real-browser-monitor-type.js");
|
||||||
|
class RemoteBrowser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets remote browser from ID
|
||||||
|
* @param {number} remoteBrowserID ID of the remote browser
|
||||||
|
* @param {number} userID ID of the user who created the remote browser
|
||||||
|
* @returns {Promise<Bean>} Remote Browser
|
||||||
|
*/
|
||||||
|
static async get(remoteBrowserID, userID) {
|
||||||
|
let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("Remote browser not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a Remote Browser
|
||||||
|
* @param {object} remoteBrowser Remote Browser to save
|
||||||
|
* @param {?number} remoteBrowserID ID of the Remote Browser to update
|
||||||
|
* @param {number} userID ID of the user who adds the Remote Browser
|
||||||
|
* @returns {Promise<Bean>} Updated Remote Browser
|
||||||
|
*/
|
||||||
|
static async save(remoteBrowser, remoteBrowserID, userID) {
|
||||||
|
let bean;
|
||||||
|
|
||||||
|
if (remoteBrowserID) {
|
||||||
|
bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("Remote browser not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bean = R.dispense("remote_browser");
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.user_id = userID;
|
||||||
|
bean.name = remoteBrowser.name;
|
||||||
|
bean.url = remoteBrowser.url;
|
||||||
|
|
||||||
|
await R.store(bean);
|
||||||
|
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Remote Browser
|
||||||
|
* @param {number} remoteBrowserID ID of the Remote Browser to delete
|
||||||
|
* @param {number} userID ID of the user who created the Remote Browser
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async delete(remoteBrowserID, userID) {
|
||||||
|
let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [ remoteBrowserID, userID ]);
|
||||||
|
|
||||||
|
if (!bean) {
|
||||||
|
throw new Error("Remote Browser not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removed remote browser from monitors if exists
|
||||||
|
await R.exec("UPDATE monitor SET remote_browser = null WHERE remote_browser = ?", [ remoteBrowserID ]);
|
||||||
|
|
||||||
|
await R.trash(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the connection to Remote Browser
|
||||||
|
* @param {object} remoteBrowser Docker host to check for
|
||||||
|
* @returns {boolean} Returns if connection worked
|
||||||
|
*/
|
||||||
|
static async test(remoteBrowser) {
|
||||||
|
const testResult = await testRemoteBrowser(remoteBrowser.id, remoteBrowser.user_id);
|
||||||
|
return testResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
RemoteBrowser,
|
||||||
|
};
|
|
@ -50,7 +50,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
|
|
||||||
let pushToken = request.params.pushToken;
|
let pushToken = request.params.pushToken;
|
||||||
let msg = request.query.msg || "OK";
|
let msg = request.query.msg || "OK";
|
||||||
let ping = parseInt(request.query.ping) || null;
|
let ping = parseFloat(request.query.ping) || null;
|
||||||
let statusString = request.query.status || "up";
|
let statusString = request.query.status || "up";
|
||||||
let status = (statusString === "up") ? UP : DOWN;
|
let status = (statusString === "up") ? UP : DOWN;
|
||||||
|
|
||||||
|
@ -64,38 +64,57 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
|
|
||||||
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||||
|
|
||||||
if (monitor.isUpsideDown()) {
|
|
||||||
status = flipStatus(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
let isFirstBeat = true;
|
let isFirstBeat = true;
|
||||||
let previousStatus = status;
|
|
||||||
let duration = 0;
|
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
|
bean.monitor_id = monitor.id;
|
||||||
|
bean.ping = ping;
|
||||||
|
bean.msg = msg;
|
||||||
|
bean.downCount = previousHeartbeat?.downCount || 0;
|
||||||
|
|
||||||
if (previousHeartbeat) {
|
if (previousHeartbeat) {
|
||||||
isFirstBeat = false;
|
isFirstBeat = false;
|
||||||
previousStatus = previousHeartbeat.status;
|
bean.duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await Monitor.isUnderMaintenance(monitor.id)) {
|
if (await Monitor.isUnderMaintenance(monitor.id)) {
|
||||||
msg = "Monitor under maintenance";
|
msg = "Monitor under maintenance";
|
||||||
status = MAINTENANCE;
|
bean.status = MAINTENANCE;
|
||||||
|
} else {
|
||||||
|
determineStatus(status, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
// Calculate uptime
|
||||||
log.debug("router", "PreviousStatus: " + previousStatus);
|
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitor.id);
|
||||||
log.debug("router", "Current Status: " + status);
|
let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping));
|
||||||
|
bean.end_time = R.isoDateTimeMillis(endTimeDayjs);
|
||||||
|
|
||||||
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
|
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
bean.monitor_id = monitor.id;
|
log.debug("router", "PreviousStatus: " + previousHeartbeat?.status);
|
||||||
bean.status = status;
|
log.debug("router", "Current Status: " + bean.status);
|
||||||
bean.msg = msg;
|
|
||||||
bean.ping = ping;
|
bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, status);
|
||||||
bean.duration = duration;
|
|
||||||
|
if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, status)) {
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
|
} else {
|
||||||
|
if (bean.status === DOWN && this.resendInterval > 0) {
|
||||||
|
++bean.downCount;
|
||||||
|
if (bean.downCount >= this.resendInterval) {
|
||||||
|
// Send notification again, because we are still DOWN
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
@ -107,11 +126,6 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
response.json({
|
response.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
|
|
||||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response.status(404).json({
|
response.status(404).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -562,4 +576,58 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the status of the next beat in the push route handling.
|
||||||
|
* @param {string} status - The reported new status.
|
||||||
|
* @param {object} previousHeartbeat - The previous heartbeat object.
|
||||||
|
* @param {number} maxretries - The maximum number of retries allowed.
|
||||||
|
* @param {boolean} isUpsideDown - Indicates if the monitor is upside down.
|
||||||
|
* @param {object} bean - The new heartbeat object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean) {
|
||||||
|
if (isUpsideDown) {
|
||||||
|
status = flipStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousHeartbeat) {
|
||||||
|
if (previousHeartbeat.status === UP && status === DOWN) {
|
||||||
|
// Going Down
|
||||||
|
if ((maxretries > 0) && (previousHeartbeat.retries < maxretries)) {
|
||||||
|
// Retries available
|
||||||
|
bean.retries = previousHeartbeat.retries + 1;
|
||||||
|
bean.status = PENDING;
|
||||||
|
} else {
|
||||||
|
// No more retries
|
||||||
|
bean.retries = 0;
|
||||||
|
bean.status = DOWN;
|
||||||
|
}
|
||||||
|
} else if (previousHeartbeat.status === PENDING && status === DOWN && previousHeartbeat.retries < maxretries) {
|
||||||
|
// Retries available
|
||||||
|
bean.retries = previousHeartbeat.retries + 1;
|
||||||
|
bean.status = PENDING;
|
||||||
|
} else {
|
||||||
|
// No more retries or not pending
|
||||||
|
if (status === DOWN) {
|
||||||
|
bean.retries = previousHeartbeat.retries + 1;
|
||||||
|
bean.status = status;
|
||||||
|
} else {
|
||||||
|
bean.retries = 0;
|
||||||
|
bean.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First beat?
|
||||||
|
if (status === DOWN && maxretries > 0) {
|
||||||
|
// Retries available
|
||||||
|
bean.retries = 1;
|
||||||
|
bean.status = PENDING;
|
||||||
|
} else {
|
||||||
|
// Retires not enabled
|
||||||
|
bean.retries = 0;
|
||||||
|
bean.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -131,9 +131,10 @@ const testMode = !!args["test"] || false;
|
||||||
const e2eTestMode = !!args["e2e"] || false;
|
const e2eTestMode = !!args["e2e"] || false;
|
||||||
|
|
||||||
// Must be after io instantiation
|
// Must be after io instantiation
|
||||||
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
|
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
|
const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
const StatusPage = require("./model/status_page");
|
const StatusPage = require("./model/status_page");
|
||||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
||||||
|
@ -318,12 +319,12 @@ let needSetup = false;
|
||||||
decoded.username,
|
decoded.username,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if the password changed
|
|
||||||
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
|
|
||||||
throw new Error("The token is invalid due to password change or old token");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
// Check if the password changed
|
||||||
|
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
|
||||||
|
throw new Error("The token is invalid due to password change or old token");
|
||||||
|
}
|
||||||
|
|
||||||
log.debug("auth", "afterLogin");
|
log.debug("auth", "afterLogin");
|
||||||
afterLogin(socket, user);
|
afterLogin(socket, user);
|
||||||
log.debug("auth", "afterLogin ok");
|
log.debug("auth", "afterLogin ok");
|
||||||
|
@ -798,6 +799,7 @@ let needSetup = false;
|
||||||
bean.mqttPassword = monitor.mqttPassword;
|
bean.mqttPassword = monitor.mqttPassword;
|
||||||
bean.mqttTopic = monitor.mqttTopic;
|
bean.mqttTopic = monitor.mqttTopic;
|
||||||
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
|
||||||
|
bean.mqttCheckType = monitor.mqttCheckType;
|
||||||
bean.databaseConnectionString = monitor.databaseConnectionString;
|
bean.databaseConnectionString = monitor.databaseConnectionString;
|
||||||
bean.databaseQuery = monitor.databaseQuery;
|
bean.databaseQuery = monitor.databaseQuery;
|
||||||
bean.authMethod = monitor.authMethod;
|
bean.authMethod = monitor.authMethod;
|
||||||
|
@ -823,7 +825,11 @@ let needSetup = false;
|
||||||
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
||||||
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||||
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
||||||
|
bean.kafkaProducerSsl = monitor.kafkaProducerSsl;
|
||||||
|
bean.kafkaProducerAllowAutoTopicCreation =
|
||||||
|
monitor.kafkaProducerAllowAutoTopicCreation;
|
||||||
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
||||||
|
bean.remote_browser = monitor.remote_browser;
|
||||||
|
|
||||||
bean.validate();
|
bean.validate();
|
||||||
|
|
||||||
|
@ -1332,9 +1338,9 @@ let needSetup = false;
|
||||||
// Update nscd status
|
// Update nscd status
|
||||||
if (previousNSCDStatus !== data.nscd) {
|
if (previousNSCDStatus !== data.nscd) {
|
||||||
if (data.nscd) {
|
if (data.nscd) {
|
||||||
server.startNSCDServices();
|
await server.startNSCDServices();
|
||||||
} else {
|
} else {
|
||||||
server.stopNSCDServices();
|
await server.stopNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1505,6 +1511,7 @@ let needSetup = false;
|
||||||
dockerSocketHandler(socket);
|
dockerSocketHandler(socket);
|
||||||
maintenanceSocketHandler(socket);
|
maintenanceSocketHandler(socket);
|
||||||
apiKeySocketHandler(socket);
|
apiKeySocketHandler(socket);
|
||||||
|
remoteBrowserSocketHandler(socket);
|
||||||
generalSocketHandler(socket, server);
|
generalSocketHandler(socket, server);
|
||||||
|
|
||||||
log.debug("server", "added all socket handlers");
|
log.debug("server", "added all socket handlers");
|
||||||
|
@ -1613,6 +1620,7 @@ async function afterLogin(socket, user) {
|
||||||
sendProxyList(socket);
|
sendProxyList(socket);
|
||||||
sendDockerHostList(socket);
|
sendDockerHostList(socket);
|
||||||
sendAPIKeyList(socket);
|
sendAPIKeyList(socket);
|
||||||
|
sendRemoteBrowserList(socket);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
|
@ -1725,6 +1733,7 @@ async function pauseMonitor(userID, monitorID) {
|
||||||
|
|
||||||
if (monitorID in server.monitorList) {
|
if (monitorID in server.monitorList) {
|
||||||
server.monitorList[monitorID].stop();
|
server.monitorList[monitorID].stop();
|
||||||
|
server.monitorList[monitorID].active = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1793,8 +1802,10 @@ gracefulShutdown(server.httpServer, {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Catch unexpected errors here
|
// Catch unexpected errors here
|
||||||
process.addListener("unhandledRejection", (error, promise) => {
|
let unexpectedErrorHandler = (error, promise) => {
|
||||||
console.trace(error);
|
console.trace(error);
|
||||||
UptimeKumaServer.errorLog(error, false);
|
UptimeKumaServer.errorLog(error, false);
|
||||||
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
|
||||||
});
|
};
|
||||||
|
process.addListener("unhandledRejection", unexpectedErrorHandler);
|
||||||
|
process.addListener("uncaughtException", unexpectedErrorHandler);
|
||||||
|
|
|
@ -44,29 +44,45 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getGameList", async (callback) => {
|
socket.on("getGameList", async (callback) => {
|
||||||
callback({
|
try {
|
||||||
ok: true,
|
checkLogin(socket);
|
||||||
gameList: getGameList(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("testChrome", (executable, callback) => {
|
|
||||||
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
|
||||||
testChrome(executable).then((version) => {
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: {
|
gameList: getGameList(),
|
||||||
key: "foundChromiumVersion",
|
|
||||||
values: [ version ],
|
|
||||||
},
|
|
||||||
msgi18n: true,
|
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("testChrome", (executable, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
||||||
|
testChrome(executable).then((version) => {
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: {
|
||||||
|
key: "foundChromiumVersion",
|
||||||
|
values: [ version ],
|
||||||
|
},
|
||||||
|
msgi18n: true,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getPushExample", (language, callback) => {
|
socket.on("getPushExample", (language, callback) => {
|
||||||
|
|
82
server/socket-handlers/remote-browser-socket-handler.js
Normal file
82
server/socket-handlers/remote-browser-socket-handler.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
const { sendRemoteBrowserList } = require("../client");
|
||||||
|
const { checkLogin } = require("../util-server");
|
||||||
|
const { RemoteBrowser } = require("../remote-browser");
|
||||||
|
|
||||||
|
const { log } = require("../../src/util");
|
||||||
|
const { testRemoteBrowser } = require("../monitor-types/real-browser-monitor-type");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handlers for docker hosts
|
||||||
|
* @param {Socket} socket Socket.io instance
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
module.exports.remoteBrowserSocketHandler = (socket) => {
|
||||||
|
socket.on("addRemoteBrowser", async (remoteBrowser, remoteBrowserID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let remoteBrowserBean = await RemoteBrowser.save(remoteBrowser, remoteBrowserID, socket.userID);
|
||||||
|
await sendRemoteBrowserList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Saved.",
|
||||||
|
msgi18n: true,
|
||||||
|
id: remoteBrowserBean.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("deleteRemoteBrowser", async (dockerHostID, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
await RemoteBrowser.delete(dockerHostID, socket.userID);
|
||||||
|
await sendRemoteBrowserList(socket);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "successDeleted",
|
||||||
|
msgi18n: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("testRemoteBrowser", async (remoteBrowser, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
let check = await testRemoteBrowser(remoteBrowser.url);
|
||||||
|
log.info("remoteBrowser", "Tested remote browser: " + check);
|
||||||
|
let msg;
|
||||||
|
|
||||||
|
if (check) {
|
||||||
|
msg = "Connected Successfully.";
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log.error("remoteBrowser", e);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -10,7 +10,7 @@ const util = require("util");
|
||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||||
|
@ -87,7 +87,10 @@ class UptimeKumaServer {
|
||||||
// Set axios default user-agent to Uptime-Kuma/version
|
// Set axios default user-agent to Uptime-Kuma/version
|
||||||
axios.defaults.headers.common["User-Agent"] = this.getUserAgent();
|
axios.defaults.headers.common["User-Agent"] = this.getUserAgent();
|
||||||
|
|
||||||
log.debug("server", "Creating express and socket.io instance");
|
// Set default axios timeout to 5 minutes instead of infinity
|
||||||
|
axios.defaults.timeout = 300 * 1000;
|
||||||
|
|
||||||
|
log.info("server", "Creating express and socket.io instance");
|
||||||
this.app = express();
|
this.app = express();
|
||||||
if (sslKey && sslCert) {
|
if (sslKey && sslCert) {
|
||||||
log.info("server", "Server Type: HTTPS");
|
log.info("server", "Server Type: HTTPS");
|
||||||
|
@ -115,6 +118,7 @@ class UptimeKumaServer {
|
||||||
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||||
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
||||||
|
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
||||||
|
|
||||||
this.io = new Server(this.httpServer);
|
this.io = new Server(this.httpServer);
|
||||||
}
|
}
|
||||||
|
@ -369,7 +373,7 @@ class UptimeKumaServer {
|
||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.startNSCDServices();
|
await this.startNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,7 +385,7 @@ class UptimeKumaServer {
|
||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.stopNSCDServices();
|
await this.stopNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -390,11 +394,11 @@ class UptimeKumaServer {
|
||||||
* For now, only used in Docker
|
* For now, only used in Docker
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
startNSCDServices() {
|
async startNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Starting nscd");
|
log.info("services", "Starting nscd");
|
||||||
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
|
await childProcessAsync.exec("sudo service nscd start");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to start nscd");
|
log.info("services", "Failed to start nscd");
|
||||||
}
|
}
|
||||||
|
@ -405,11 +409,11 @@ class UptimeKumaServer {
|
||||||
* Stop all system services
|
* Stop all system services
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
stopNSCDServices() {
|
async stopNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Stopping nscd");
|
log.info("services", "Stopping nscd");
|
||||||
childProcess.execSync("sudo service nscd stop");
|
await childProcessAsync.exec("sudo service nscd stop");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to stop nscd");
|
log.info("services", "Failed to stop nscd");
|
||||||
}
|
}
|
||||||
|
@ -433,3 +437,4 @@ module.exports = {
|
||||||
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
||||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||||
const { DnsMonitorType } = require("./monitor-types/dns");
|
const { DnsMonitorType } = require("./monitor-types/dns");
|
||||||
|
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
||||||
|
|
|
@ -7,7 +7,6 @@ const { Resolver } = require("dns");
|
||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
const iconv = require("iconv-lite");
|
const iconv = require("iconv-lite");
|
||||||
const chardet = require("chardet");
|
const chardet = require("chardet");
|
||||||
const mqtt = require("mqtt");
|
|
||||||
const chroma = require("chroma-js");
|
const chroma = require("chroma-js");
|
||||||
const { badgeConstants } = require("./config");
|
const { badgeConstants } = require("./config");
|
||||||
const mssql = require("mssql");
|
const mssql = require("mssql");
|
||||||
|
@ -22,6 +21,7 @@ const protojs = require("protobufjs");
|
||||||
const radiusClient = require("node-radius-client");
|
const radiusClient = require("node-radius-client");
|
||||||
const redis = require("redis");
|
const redis = require("redis");
|
||||||
const oidc = require("openid-client");
|
const oidc = require("openid-client");
|
||||||
|
const tls = require("tls");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dictionaries: {
|
dictionaries: {
|
||||||
|
@ -172,73 +172,6 @@ exports.pingAsync = function (hostname, ipv6 = false, size = 56) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* MQTT Monitor
|
|
||||||
* @param {string} hostname Hostname / address of machine to test
|
|
||||||
* @param {string} topic MQTT topic
|
|
||||||
* @param {string} okMessage Expected result
|
|
||||||
* @param {object} options MQTT options. Contains port, username,
|
|
||||||
* password and interval (interval defaults to 20)
|
|
||||||
* @returns {Promise<string>} Received MQTT message
|
|
||||||
*/
|
|
||||||
exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const { port, username, password, interval = 20 } = options;
|
|
||||||
|
|
||||||
// Adds MQTT protocol to the hostname if not already present
|
|
||||||
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
|
|
||||||
hostname = "mqtt://" + hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeoutID = setTimeout(() => {
|
|
||||||
log.debug("mqtt", "MQTT timeout triggered");
|
|
||||||
client.end();
|
|
||||||
reject(new Error("Timeout"));
|
|
||||||
}, interval * 1000 * 0.8);
|
|
||||||
|
|
||||||
const mqttUrl = `${hostname}:${port}`;
|
|
||||||
|
|
||||||
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
|
|
||||||
|
|
||||||
let client = mqtt.connect(mqttUrl, {
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("connect", () => {
|
|
||||||
log.debug("mqtt", "MQTT connected");
|
|
||||||
|
|
||||||
try {
|
|
||||||
log.debug("mqtt", "MQTT subscribe topic");
|
|
||||||
client.subscribe(topic);
|
|
||||||
} catch (e) {
|
|
||||||
client.end();
|
|
||||||
clearTimeout(timeoutID);
|
|
||||||
reject(new Error("Cannot subscribe topic"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("error", (error) => {
|
|
||||||
client.end();
|
|
||||||
clearTimeout(timeoutID);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("message", (messageTopic, message) => {
|
|
||||||
if (messageTopic === topic) {
|
|
||||||
client.end();
|
|
||||||
clearTimeout(timeoutID);
|
|
||||||
if (okMessage != null && okMessage !== "" && message.toString() !== okMessage) {
|
|
||||||
reject(new Error(`Message Mismatch - Topic: ${messageTopic}; Message: ${message.toString()}`));
|
|
||||||
} else {
|
|
||||||
resolve(`Topic: ${messageTopic}; Message: ${message.toString()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitor Kafka using Producer
|
* Monitor Kafka using Producer
|
||||||
* @param {string[]} brokers List of kafka brokers to connect, host and
|
* @param {string[]} brokers List of kafka brokers to connect, host and
|
||||||
|
@ -397,6 +330,9 @@ exports.mssqlQuery = async function (connectionString, query) {
|
||||||
try {
|
try {
|
||||||
pool = new mssql.ConnectionPool(connectionString);
|
pool = new mssql.ConnectionPool(connectionString);
|
||||||
await pool.connect();
|
await pool.connect();
|
||||||
|
if (!query) {
|
||||||
|
query = "SELECT 1";
|
||||||
|
}
|
||||||
await pool.request().query(query);
|
await pool.request().query(query);
|
||||||
pool.close();
|
pool.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -418,12 +354,22 @@ exports.postgresQuery = function (connectionString, query) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const config = postgresConParse(connectionString);
|
const config = postgresConParse(connectionString);
|
||||||
|
|
||||||
if (config.password === "") {
|
// Fix #3868, which true/false is not parsed to boolean
|
||||||
// See https://github.com/brianc/node-postgres/issues/1927
|
if (typeof config.ssl === "string") {
|
||||||
return reject(new Error("Password is undefined."));
|
config.ssl = config.ssl === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new Client({ connectionString });
|
if (config.password === "") {
|
||||||
|
// See https://github.com/brianc/node-postgres/issues/1927
|
||||||
|
reject(new Error("Password is undefined."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = new Client(config);
|
||||||
|
|
||||||
|
client.on("error", (error) => {
|
||||||
|
log.debug("postgres", "Error caught in the error event handler.");
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
client.connect((err) => {
|
client.connect((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -447,6 +393,7 @@ exports.postgresQuery = function (connectionString, query) {
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
client.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -458,11 +405,15 @@ exports.postgresQuery = function (connectionString, query) {
|
||||||
* Run a query on MySQL/MariaDB
|
* Run a query on MySQL/MariaDB
|
||||||
* @param {string} connectionString The database connection string
|
* @param {string} connectionString The database connection string
|
||||||
* @param {string} query The query to validate the database with
|
* @param {string} query The query to validate the database with
|
||||||
|
* @param {?string} password The password to use
|
||||||
* @returns {Promise<(string)>} Response from server
|
* @returns {Promise<(string)>} Response from server
|
||||||
*/
|
*/
|
||||||
exports.mysqlQuery = function (connectionString, query) {
|
exports.mysqlQuery = function (connectionString, query, password = undefined) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const connection = mysql.createConnection(connectionString);
|
const connection = mysql.createConnection({
|
||||||
|
uri: connectionString,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
connection.on("error", (err) => {
|
connection.on("error", (err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
|
@ -1060,6 +1011,30 @@ module.exports.grpcQuery = async (options) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of SHA256 fingerprints for all known root certificates.
|
||||||
|
* @returns {Set} A set of SHA256 fingerprints.
|
||||||
|
*/
|
||||||
|
module.exports.rootCertificatesFingerprints = () => {
|
||||||
|
let fingerprints = tls.rootCertificates.map(cert => {
|
||||||
|
let certLines = cert.split("\n");
|
||||||
|
certLines.shift();
|
||||||
|
certLines.pop();
|
||||||
|
let certBody = certLines.join("");
|
||||||
|
let buf = Buffer.from(certBody, "base64");
|
||||||
|
|
||||||
|
const shasum = crypto.createHash("sha256");
|
||||||
|
shasum.update(buf);
|
||||||
|
|
||||||
|
return shasum.digest("hex").toUpperCase().replace(/(.{2})(?!$)/g, "$1:");
|
||||||
|
});
|
||||||
|
|
||||||
|
fingerprints.push("6D:99:FB:26:5E:B1:C5:B3:74:47:65:FC:BC:64:8F:3C:D8:E1:BF:FA:FD:C4:C2:F9:9B:9D:47:CF:7F:F1:C2:4F"); // ISRG X1 cross-signed with DST X3
|
||||||
|
fingerprints.push("8B:05:B6:8C:C6:59:E5:ED:0F:CB:38:F2:C9:42:FB:FD:20:0E:6F:2F:F9:F8:5D:63:C6:99:4E:F5:E0:B0:27:01"); // ISRG X2 cross-signed with ISRG X1
|
||||||
|
|
||||||
|
return new Set(fingerprints);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.SHAKE256_LENGTH = 16;
|
module.exports.SHAKE256_LENGTH = 16;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1095,3 +1070,29 @@ if (process.env.TEST_BACKEND) {
|
||||||
return module.exports.__test[functionName];
|
return module.exports.__test[functionName];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an abort signal with the specified timeout.
|
||||||
|
* @param {number} timeoutMs - The timeout in milliseconds.
|
||||||
|
* @returns {AbortSignal | null} - The generated abort signal, or null if not supported.
|
||||||
|
*/
|
||||||
|
module.exports.axiosAbortSignal = (timeoutMs) => {
|
||||||
|
try {
|
||||||
|
// Just in case, as 0 timeout here will cause the request to be aborted immediately
|
||||||
|
if (!timeoutMs || timeoutMs <= 0) {
|
||||||
|
timeoutMs = 5000;
|
||||||
|
}
|
||||||
|
return AbortSignal.timeout(timeoutMs);
|
||||||
|
} catch (_) {
|
||||||
|
// v16-: AbortSignal.timeout is not supported
|
||||||
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
setTimeout(() => abortController.abort(), timeoutMs);
|
||||||
|
|
||||||
|
return abortController.signal;
|
||||||
|
} catch (_) {
|
||||||
|
// v15-: AbortController is not supported
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -635,6 +635,10 @@ $shadow-box-padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.zoom-cursor {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="!enabled"
|
:disabled="!enabled"
|
||||||
>
|
>
|
||||||
<a class="btn btn-outline-primary" @click="action()">
|
<button class="btn btn-outline-primary" @click="action()" :aria-label="actionAriaLabel">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -66,6 +66,13 @@ export default {
|
||||||
action: {
|
action: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [ "update:modelValue" ],
|
emits: [ "update:modelValue" ],
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<select ref="select" v-model="model" class="form-select" :disabled="disabled">
|
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
|
||||||
<option v-for="option in options" :key="option" :value="option.value">{{ option.label }}</option>
|
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
<a class="btn btn-outline-primary" @click="action()">
|
<button class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" aria-hidden="true" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -20,6 +20,13 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* The id of the form which will be targeted by a <label for=..
|
||||||
|
*/
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* The value of the select field.
|
* The value of the select field.
|
||||||
*/
|
*/
|
||||||
|
@ -50,6 +57,29 @@ export default {
|
||||||
action: {
|
action: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the action button is disabled.
|
||||||
|
* @example true
|
||||||
|
*/
|
||||||
|
actionDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the select field is required.
|
||||||
|
* @example true
|
||||||
|
*/
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [ "update:modelValue" ],
|
emits: [ "update:modelValue" ],
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
||||||
Confirm,
|
Confirm,
|
||||||
},
|
},
|
||||||
props: {},
|
props: {},
|
||||||
emits: [ "added" ],
|
emits: [ "added", "deleted" ],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
modal: null,
|
modal: null,
|
||||||
|
@ -178,6 +178,7 @@ export default {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
this.$emit("deleted", this.id);
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -163,7 +163,7 @@ export default {
|
||||||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||||
}
|
}
|
||||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://";
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
185
src/components/RemoteBrowserDialog.vue
Normal file
185
src/components/RemoteBrowserDialog.vue
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 id="exampleModalLabel" class="modal-title">
|
||||||
|
{{ $t("Add a Remote Browser") }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="remote-browser-name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||||
|
<input id="remote-browser-name" v-model="remoteBrowser.name" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="remote-browser-url" class="form-label">{{ $t("URL") }}</label>
|
||||||
|
<input id="remote-browser-url" v-model="remoteBrowser.url" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<div class="form-text mt-3">
|
||||||
|
{{ $t("Examples") }}:
|
||||||
|
<ul>
|
||||||
|
<li>ws://chrome.browserless.io/playwright?token=YOUR-API-TOKEN</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||||
|
{{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||||
|
{{ $t("Test") }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||||
|
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
|
||||||
|
{{ $t("deleteRemoteBrowserMessage") }}
|
||||||
|
</Confirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import Confirm from "./Confirm.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Confirm,
|
||||||
|
},
|
||||||
|
props: {},
|
||||||
|
emits: [ "added" ],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modal: null,
|
||||||
|
processing: false,
|
||||||
|
id: null,
|
||||||
|
remoteBrowser: {
|
||||||
|
name: "",
|
||||||
|
url: "",
|
||||||
|
// Do not set default value here, please scroll to show()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm deletion of docker host
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
deleteConfirm() {
|
||||||
|
this.modal.hide();
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show specified docker host
|
||||||
|
* @param {number} remoteBrowserID ID of host to show
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
show(remoteBrowserID) {
|
||||||
|
if (remoteBrowserID) {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
this.id = remoteBrowserID;
|
||||||
|
|
||||||
|
for (let n of this.$root.remoteBrowserList) {
|
||||||
|
if (n.id === remoteBrowserID) {
|
||||||
|
this.remoteBrowser = n;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
this.$root.toastError(this.$t("Remote Browser not found!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.id = null;
|
||||||
|
this.remoteBrowser = {
|
||||||
|
name: "",
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add docker host
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
submit() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("addRemoteBrowser", this.remoteBrowser, this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
|
||||||
|
// Emit added event, doesn't emit edit.
|
||||||
|
if (! this.id) {
|
||||||
|
this.$emit("added", res.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the docker host
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
test() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("testRemoteBrowser", this.remoteBrowser, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete this docker host
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
deleteDockerHost() {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit("deleteRemoteBrowser", this.id, (res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.modal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
52
src/components/ScreenshotDialog.vue
Normal file
52
src/components/ScreenshotDialog.vue
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
{{ $t("Browser Screenshot") }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body"></div>
|
||||||
|
<img :src="imageURL" alt="screenshot of the website">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
imageURL: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modal: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
show() {
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.modal-dialog .form-text, .modal-dialog p {
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -60,13 +60,13 @@
|
||||||
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||||
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
|
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)" :aria-label="$t('Remove the expiry notification')">
|
||||||
<font-awesome-icon class="" icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-xl-6">
|
<div class="col-12 col-xl-6">
|
||||||
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
|
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||||
|
|
53
src/components/settings/RemoteBrowsers.vue
Normal file
53
src/components/settings/RemoteBrowsers.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="dockerHost-list my-4">
|
||||||
|
<p v-if="$root.remoteBrowserList.length === 0">
|
||||||
|
{{ $t("Not available, please setup.") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||||
|
<li v-for="(remoteBrowser, index) in $root.remoteBrowserList" :key="index" class="list-group-item">
|
||||||
|
{{ remoteBrowser.name }}<br>
|
||||||
|
<a href="#" @click="$refs.remoteBrowserDialog.show(remoteBrowser.id)">{{ $t("Edit") }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button class="btn btn-primary me-2" type="button" @click="$refs.remoteBrowserDialog.show()">
|
||||||
|
<font-awesome-icon icon="plus" /> {{ $t("Add Remote Browser") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 pt-4">
|
||||||
|
<h5 class="my-4 settings-subheading">{{ $t("What is a Remote Browser?") }}</h5>
|
||||||
|
<p>{{ $t("remoteBrowsersDescription") }} <a href="https://hub.docker.com/r/browserless/chrome">{{ $t("self-hosted container") }}</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RemoteBrowserDialog ref="remoteBrowserDialog" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import RemoteBrowserDialog from "../../components/RemoteBrowserDialog.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
RemoteBrowserDialog,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$parent.$parent.$parent.settings;
|
||||||
|
},
|
||||||
|
saveSettings() {
|
||||||
|
return this.$parent.$parent.$parent.saveSettings;
|
||||||
|
},
|
||||||
|
settingsLoaded() {
|
||||||
|
return this.$parent.$parent.$parent.settingsLoaded;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -42,7 +42,8 @@ const languageList = {
|
||||||
"yue": "繁體中文 (廣東話 / 粵語)",
|
"yue": "繁體中文 (廣東話 / 粵語)",
|
||||||
"ro": "Limba română",
|
"ro": "Limba română",
|
||||||
"ur": "Urdu",
|
"ur": "Urdu",
|
||||||
"ge": "ქართული"
|
"ge": "ქართული",
|
||||||
|
"uz": "O'zbek tili",
|
||||||
};
|
};
|
||||||
|
|
||||||
let messages = {
|
let messages = {
|
||||||
|
|
|
@ -246,8 +246,8 @@
|
||||||
"Current User": "Current User",
|
"Current User": "Current User",
|
||||||
"topic": "Topic",
|
"topic": "Topic",
|
||||||
"topicExplanation": "MQTT topic to monitor",
|
"topicExplanation": "MQTT topic to monitor",
|
||||||
"successMessage": "Success Message",
|
"successKeyword": "Success Keyword",
|
||||||
"successMessageExplanation": "MQTT message that will be considered as success",
|
"successKeywordExplanation": "MQTT Keyword that will be considered as success",
|
||||||
"recent": "Recent",
|
"recent": "Recent",
|
||||||
"Reset Token": "Reset Token",
|
"Reset Token": "Reset Token",
|
||||||
"Done": "Done",
|
"Done": "Done",
|
||||||
|
@ -348,6 +348,8 @@
|
||||||
"Fingerprint:": "Fingerprint:",
|
"Fingerprint:": "Fingerprint:",
|
||||||
"No status pages": "No status pages",
|
"No status pages": "No status pages",
|
||||||
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
||||||
|
"Add a new expiry notification day": "Add a new expiry notification day",
|
||||||
|
"Remove the expiry notification": "Remove the expiry notification day",
|
||||||
"Proxy": "Proxy",
|
"Proxy": "Proxy",
|
||||||
"Date Created": "Date Created",
|
"Date Created": "Date Created",
|
||||||
"Footer Text": "Footer Text",
|
"Footer Text": "Footer Text",
|
||||||
|
@ -383,6 +385,8 @@
|
||||||
"Setup Docker Host": "Setup Docker Host",
|
"Setup Docker Host": "Setup Docker Host",
|
||||||
"Connection Type": "Connection Type",
|
"Connection Type": "Connection Type",
|
||||||
"Docker Daemon": "Docker Daemon",
|
"Docker Daemon": "Docker Daemon",
|
||||||
|
"noDockerHostMsg": "Not Available. Setup a Docker Host First.",
|
||||||
|
"DockerHostRequired": "Please set the Docker Host for this monitor.",
|
||||||
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
|
"deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?",
|
||||||
"socket": "Socket",
|
"socket": "Socket",
|
||||||
"tcp": "TCP / HTTP",
|
"tcp": "TCP / HTTP",
|
||||||
|
@ -681,6 +685,10 @@
|
||||||
"Notify Channel": "Notify Channel",
|
"Notify Channel": "Notify Channel",
|
||||||
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
||||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||||
|
"setup a new monitor group": "setup a new monitor group",
|
||||||
|
"openModalTo": "open modal to {0}",
|
||||||
|
"Add a domain": "Add a domain",
|
||||||
|
"Remove domain": "Remove domain '{0}'",
|
||||||
"Icon Emoji": "Icon Emoji",
|
"Icon Emoji": "Icon Emoji",
|
||||||
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||||
"aboutWebhooks": "More info about Webhooks on: {0}",
|
"aboutWebhooks": "More info about Webhooks on: {0}",
|
||||||
|
@ -857,5 +865,15 @@
|
||||||
"successEnabled": "Enabled Successfully.",
|
"successEnabled": "Enabled Successfully.",
|
||||||
"tagNotFound": "Tag not found.",
|
"tagNotFound": "Tag not found.",
|
||||||
"foundChromiumVersion": "Found Chromium/Chrome. Version: {0}",
|
"foundChromiumVersion": "Found Chromium/Chrome. Version: {0}",
|
||||||
"GrafanaOncallUrl": "Grafana Oncall URL"
|
"Remote Browsers": "Remote Browsers",
|
||||||
|
"Remote Browser": "Remote Browser",
|
||||||
|
"Add a Remote Browser": "Add a Remote Browser",
|
||||||
|
"Remote Browser not found!": "Remote Browser not found!",
|
||||||
|
"remoteBrowsersDescription": "Remote Browsers are an alternative to running Chromium locally. Setup with a service like browserless.io or connect to your own",
|
||||||
|
"self-hosted container": "self-hosted container",
|
||||||
|
"remoteBrowserToggle": "By default Chromium runs inside the Uptime Kuma container. You can use a remote browser by toggling this switch.",
|
||||||
|
"useRemoteBrowser": "Use a Remote Browser",
|
||||||
|
"deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?",
|
||||||
|
"GrafanaOncallUrl": "Grafana Oncall URL",
|
||||||
|
"Browser Screenshot": "Browser Screenshot"
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ export default {
|
||||||
tlsInfoList: {},
|
tlsInfoList: {},
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
dockerHostList: [],
|
dockerHostList: [],
|
||||||
|
remoteBrowserList: [],
|
||||||
statusPageListLoaded: false,
|
statusPageListLoaded: false,
|
||||||
statusPageList: [],
|
statusPageList: [],
|
||||||
proxyList: [],
|
proxyList: [],
|
||||||
|
@ -174,6 +175,10 @@ export default {
|
||||||
this.dockerHostList = data;
|
this.dockerHostList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("remoteBrowserList", (data) => {
|
||||||
|
this.remoteBrowserList = data;
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("heartbeat", (data) => {
|
socket.on("heartbeat", (data) => {
|
||||||
if (! (data.monitorID in this.heartbeatList)) {
|
if (! (data.monitorID in this.heartbeatList)) {
|
||||||
this.heartbeatList[data.monitorID] = [];
|
this.heartbeatList[data.monitorID] = [];
|
||||||
|
|
|
@ -184,9 +184,10 @@
|
||||||
<!-- Screenshot -->
|
<!-- Screenshot -->
|
||||||
<div v-if="monitor.type === 'real-browser'" class="shadow-box">
|
<div v-if="monitor.type === 'real-browser'" class="shadow-box">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6 zoom-cursor">
|
||||||
<img :src="screenshotURL" alt style="width: 100%;">
|
<img :src="screenshotURL" style="width: 100%;" alt="screenshot of the website" @click="showScreenshotDialog">
|
||||||
</div>
|
</div>
|
||||||
|
<ScreenshotDialog ref="screenshotDialog" :imageURL="screenshotURL" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -283,6 +284,7 @@ import "prismjs/components/prism-javascript";
|
||||||
import "prismjs/components/prism-css";
|
import "prismjs/components/prism-css";
|
||||||
import { PrismEditor } from "vue-prism-editor";
|
import { PrismEditor } from "vue-prism-editor";
|
||||||
import "vue-prism-editor/dist/prismeditor.min.css";
|
import "vue-prism-editor/dist/prismeditor.min.css";
|
||||||
|
import ScreenshotDialog from "../components/ScreenshotDialog.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -297,6 +299,7 @@ export default {
|
||||||
Tag,
|
Tag,
|
||||||
CertificateInfo,
|
CertificateInfo,
|
||||||
PrismEditor,
|
PrismEditor,
|
||||||
|
ScreenshotDialog
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -476,6 +479,14 @@ export default {
|
||||||
this.$refs.confirmDelete.show();
|
this.$refs.confirmDelete.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Screenshot Dialog
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
showScreenshotDialog() {
|
||||||
|
this.$refs.screenshotDialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show dialog to confirm clearing events
|
* Show dialog to confirm clearing events
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
|
|
@ -144,6 +144,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Remote Browser -->
|
||||||
|
<div v-if="monitor.type === 'real-browser'" class="my-3">
|
||||||
|
<!-- Toggle -->
|
||||||
|
<div class="my-3 form-check">
|
||||||
|
<input id="toggle" v-model="remoteBrowsersToggle" class="form-check-input" type="checkbox">
|
||||||
|
<label class="form-check-label" for="toggle">
|
||||||
|
{{ $t("useRemoteBrowser") }}
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
{{ $t("remoteBrowserToggle") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="remoteBrowsersToggle">
|
||||||
|
<label for="remote-browser" class="form-label">{{ $t("Remote Browser") }}</label>
|
||||||
|
<ActionSelect
|
||||||
|
v-model="monitor.remote_browser"
|
||||||
|
:options="remoteBrowsersOptions"
|
||||||
|
icon="plus"
|
||||||
|
:action="() => $refs.remoteBrowserDialog.show()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Json Query -->
|
<!-- Json Query -->
|
||||||
<div v-if="monitor.type === 'json-query'" class="my-3">
|
<div v-if="monitor.type === 'json-query'" class="my-3">
|
||||||
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
||||||
|
@ -288,22 +312,19 @@
|
||||||
<!-- Docker Host -->
|
<!-- Docker Host -->
|
||||||
<!-- For Docker Type -->
|
<!-- For Docker Type -->
|
||||||
<div v-if="monitor.type === 'docker'" class="my-3">
|
<div v-if="monitor.type === 'docker'" class="my-3">
|
||||||
<h2 class="mb-2">{{ $t("Docker Host") }}</h2>
|
<div class="mb-3">
|
||||||
<p v-if="$root.dockerHostList.length === 0">
|
|
||||||
{{ $t("Not available, please setup.") }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-else class="mb-3">
|
|
||||||
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
||||||
<select id="docket-host" v-model="monitor.docker_host" class="form-select">
|
<ActionSelect
|
||||||
<option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
|
id="docker-host"
|
||||||
</select>
|
v-model="monitor.docker_host"
|
||||||
<a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
|
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
|
||||||
|
:options="dockerHostOptionsList"
|
||||||
|
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
|
||||||
|
:icon="'plus'"
|
||||||
|
:action="() => $refs.dockerHostDialog.show()"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
|
|
||||||
{{ $t("Setup Docker Host") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MQTT -->
|
<!-- MQTT -->
|
||||||
|
@ -328,12 +349,34 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="mqttSuccessMessage" class="form-label">MQTT {{ $t("successMessage") }}</label>
|
<label for="mqttCheckType" class="form-label">MQTT {{ $t("Check Type") }}</label>
|
||||||
<input id="mqttSuccessMessage" v-model="monitor.mqttSuccessMessage" type="text" class="form-control">
|
<select id="mqttCheckType" v-model="monitor.mqttCheckType" class="form-select" required>
|
||||||
|
<option value="keyword">{{ $t("Keyword") }}</option>
|
||||||
|
<option value="json-query">{{ $t("Json Query") }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="monitor.mqttCheckType === 'keyword'" class="my-3">
|
||||||
|
<label for="mqttSuccessKeyword" class="form-label">MQTT {{ $t("successKeyword") }}</label>
|
||||||
|
<input id="mqttSuccessKeyword" v-model="monitor.mqttSuccessMessage" type="text" class="form-control">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
{{ $t("successMessageExplanation") }}
|
{{ $t("successKeywordExplanation") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Json Query -->
|
||||||
|
<div v-if="monitor.mqttCheckType === 'json-query'" class="my-3">
|
||||||
|
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
||||||
|
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div class="form-text" v-html="$t('jsonQueryDescription')">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||||
|
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="monitor.type === 'radius'">
|
<template v-if="monitor.type === 'radius'">
|
||||||
|
@ -373,11 +416,20 @@
|
||||||
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
|
<input id="connectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="monitor.type === 'mysql'">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="mysql-password" class="form-label">{{ $t("Password") }}</label>
|
||||||
|
<!-- TODO: Rename monitor.radiusPassword to monitor.password for general use -->
|
||||||
|
<HiddenInput id="mysql-password" v-model="monitor.radiusPassword" autocomplete="false"></HiddenInput>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- SQL Server / PostgreSQL / MySQL -->
|
<!-- SQL Server / PostgreSQL / MySQL -->
|
||||||
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
|
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
|
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
|
||||||
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'select getdate()' ])" required></textarea>
|
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" :placeholder="$t('Example:', [ 'SELECT 1' ])"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -497,9 +549,11 @@
|
||||||
|
|
||||||
<!-- Parent Monitor -->
|
<!-- Parent Monitor -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
|
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
|
id="monitorGroupSelector"
|
||||||
v-model="monitor.parent"
|
v-model="monitor.parent"
|
||||||
|
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
|
||||||
:options="parentMonitorOptionsList"
|
:options="parentMonitorOptionsList"
|
||||||
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
|
@ -830,6 +884,7 @@
|
||||||
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
||||||
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
||||||
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
|
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
|
||||||
|
<RemoteBrowserDialog ref="remoteBrowserDialog" @added="addedRemoteBrowser" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
@ -842,11 +897,13 @@ import CopyableInput from "../components/CopyableInput.vue";
|
||||||
import CreateGroupDialog from "../components/CreateGroupDialog.vue";
|
import CreateGroupDialog from "../components/CreateGroupDialog.vue";
|
||||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||||
import DockerHostDialog from "../components/DockerHostDialog.vue";
|
import DockerHostDialog from "../components/DockerHostDialog.vue";
|
||||||
|
import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue";
|
||||||
import ProxyDialog from "../components/ProxyDialog.vue";
|
import ProxyDialog from "../components/ProxyDialog.vue";
|
||||||
import TagsManager from "../components/TagsManager.vue";
|
import TagsManager from "../components/TagsManager.vue";
|
||||||
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
|
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
|
||||||
import { hostNameRegexPattern } from "../util-frontend";
|
import { hostNameRegexPattern } from "../util-frontend";
|
||||||
import { sleep } from "../util";
|
import { sleep } from "../util";
|
||||||
|
import HiddenInput from "../components/HiddenInput.vue";
|
||||||
|
|
||||||
const toast = useToast;
|
const toast = useToast;
|
||||||
|
|
||||||
|
@ -861,7 +918,7 @@ const monitorDefaults = {
|
||||||
interval: 60,
|
interval: 60,
|
||||||
retryInterval: 60,
|
retryInterval: 60,
|
||||||
resendInterval: 0,
|
resendInterval: 0,
|
||||||
maxretries: 1,
|
maxretries: 0,
|
||||||
timeout: 48,
|
timeout: 48,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
ignoreTls: false,
|
ignoreTls: false,
|
||||||
|
@ -879,6 +936,7 @@ const monitorDefaults = {
|
||||||
mqttPassword: "",
|
mqttPassword: "",
|
||||||
mqttTopic: "",
|
mqttTopic: "",
|
||||||
mqttSuccessMessage: "",
|
mqttSuccessMessage: "",
|
||||||
|
mqttCheckType: "keyword",
|
||||||
authMethod: null,
|
authMethod: null,
|
||||||
oauth_auth_method: "client_secret_basic",
|
oauth_auth_method: "client_secret_basic",
|
||||||
httpBodyEncoding: "json",
|
httpBodyEncoding: "json",
|
||||||
|
@ -887,17 +945,21 @@ const monitorDefaults = {
|
||||||
mechanism: "None",
|
mechanism: "None",
|
||||||
},
|
},
|
||||||
kafkaProducerSsl: false,
|
kafkaProducerSsl: false,
|
||||||
|
kafkaProducerAllowAutoTopicCreation: false,
|
||||||
gamedigGivenPortOnly: true,
|
gamedigGivenPortOnly: true,
|
||||||
|
remote_browser: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
HiddenInput,
|
||||||
ActionSelect,
|
ActionSelect,
|
||||||
ProxyDialog,
|
ProxyDialog,
|
||||||
CopyableInput,
|
CopyableInput,
|
||||||
CreateGroupDialog,
|
CreateGroupDialog,
|
||||||
NotificationDialog,
|
NotificationDialog,
|
||||||
DockerHostDialog,
|
DockerHostDialog,
|
||||||
|
RemoteBrowserDialog,
|
||||||
TagsManager,
|
TagsManager,
|
||||||
VueMultiselect,
|
VueMultiselect,
|
||||||
},
|
},
|
||||||
|
@ -925,6 +987,7 @@ export default {
|
||||||
"mongodb": "mongodb://username:password@host:port/database",
|
"mongodb": "mongodb://username:password@host:port/database",
|
||||||
},
|
},
|
||||||
draftGroupName: null,
|
draftGroupName: null,
|
||||||
|
remoteBrowsersEnabled: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -948,7 +1011,31 @@ export default {
|
||||||
}
|
}
|
||||||
return this.$t(name);
|
return this.$t(name);
|
||||||
},
|
},
|
||||||
|
remoteBrowsersOptions() {
|
||||||
|
return this.$root.remoteBrowserList.map(browser => {
|
||||||
|
return {
|
||||||
|
label: browser.name,
|
||||||
|
value: browser.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remoteBrowsersToggle: {
|
||||||
|
get() {
|
||||||
|
return this.remoteBrowsersEnabled || this.monitor.remote_browser != null;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (value) {
|
||||||
|
this.remoteBrowsersEnabled = true;
|
||||||
|
if (this.monitor.remote_browser == null && this.$root.remoteBrowserList.length > 0) {
|
||||||
|
// set a default remote browser if there is one. Otherwise, the user will have to select one manually.
|
||||||
|
this.monitor.remote_browser = this.$root.remoteBrowserList[0].id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.remoteBrowsersEnabled = false;
|
||||||
|
this.monitor.remote_browser = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
isAdd() {
|
isAdd() {
|
||||||
return this.$route.path === "/add";
|
return this.$route.path === "/add";
|
||||||
},
|
},
|
||||||
|
@ -1115,6 +1202,21 @@ message HealthCheckResponse {
|
||||||
return list;
|
return list;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dockerHostOptionsList() {
|
||||||
|
if (this.$root.dockerHostList && this.$root.dockerHostList.length > 0) {
|
||||||
|
return this.$root.dockerHostList.map((host) => {
|
||||||
|
return {
|
||||||
|
label: host.name,
|
||||||
|
value: host.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return [{
|
||||||
|
label: this.$t("noDockerHostMsg"),
|
||||||
|
value: null,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$root.proxyList"() {
|
"$root.proxyList"() {
|
||||||
|
@ -1352,6 +1454,12 @@ message HealthCheckResponse {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.monitor.type === "docker") {
|
||||||
|
if (this.monitor.docker_host == null) {
|
||||||
|
toast.error(this.$t("DockerHostRequired"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -33,10 +33,10 @@ export default {
|
||||||
|
|
||||||
} else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side
|
} else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side
|
||||||
const entryPage = res.entryPage;
|
const entryPage = res.entryPage;
|
||||||
|
if (entryPage?.startsWith("statusPage-")) {
|
||||||
if (entryPage === "statusPage") {
|
this.$router.push("/status/" + entryPage.replace("statusPage-", ""));
|
||||||
this.$router.push("/status");
|
|
||||||
} else {
|
} else {
|
||||||
|
// should the old setting style still exist here?
|
||||||
this.$router.push("/dashboard");
|
this.$router.push("/dashboard");
|
||||||
}
|
}
|
||||||
} else if (res.type === "setup-database") {
|
} else if (res.type === "setup-database") {
|
||||||
|
|
|
@ -104,6 +104,9 @@ export default {
|
||||||
"docker-hosts": {
|
"docker-hosts": {
|
||||||
title: this.$t("Docker Hosts"),
|
title: this.$t("Docker Hosts"),
|
||||||
},
|
},
|
||||||
|
"remote-browsers": {
|
||||||
|
title: this.$t("Remote Browsers"),
|
||||||
|
},
|
||||||
security: {
|
security: {
|
||||||
title: this.$t("Security"),
|
title: this.$t("Security"),
|
||||||
},
|
},
|
||||||
|
|
|
@ -69,13 +69,17 @@
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
{{ $t("Domain Names") }}
|
{{ $t("Domain Names") }}
|
||||||
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField">
|
||||||
|
<font-awesome-icon icon="plus-circle" class="action text-primary" />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<ul class="list-group domain-name-list">
|
<ul class="list-group domain-name-list">
|
||||||
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
||||||
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
||||||
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)">
|
||||||
|
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,6 +31,7 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||||
const Security = () => import("./components/settings/Security.vue");
|
const Security = () => import("./components/settings/Security.vue");
|
||||||
import Proxies from "./components/settings/Proxies.vue";
|
import Proxies from "./components/settings/Proxies.vue";
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
|
import RemoteBrowsers from "./components/settings/RemoteBrowsers.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -113,6 +114,10 @@ const routes = [
|
||||||
path: "docker-hosts",
|
path: "docker-hosts",
|
||||||
component: DockerHosts,
|
component: DockerHosts,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "remote-browsers",
|
||||||
|
component: RemoteBrowsers,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "security",
|
path: "security",
|
||||||
component: Security,
|
component: Security,
|
||||||
|
|
|
@ -123,6 +123,9 @@ class Logger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log(module, msg, level) {
|
log(module, msg, level) {
|
||||||
|
if (level === "DEBUG" && !exports.isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,6 +182,10 @@ class Logger {
|
||||||
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
||||||
*/
|
*/
|
||||||
log(module: string, msg: any, level: string) {
|
log(module: string, msg: any, level: string) {
|
||||||
|
if (level === "DEBUG" && !isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
FROM alpine:3
|
|
||||||
RUN apk add --update nodejs npm git
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN /bin/sh install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
|
@ -1,4 +0,0 @@
|
||||||
FROM centos:7
|
|
||||||
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
|
@ -1,10 +0,0 @@
|
||||||
FROM debian:buster-slim
|
|
||||||
|
|
||||||
# Test invalid node version, these commands install nodejs 10
|
|
||||||
# RUN apt-get update
|
|
||||||
# RUN apt --yes install nodejs
|
|
||||||
# RUN ln -s /usr/bin/nodejs /usr/bin/node
|
|
||||||
# RUN node -v
|
|
||||||
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
|
@ -1,10 +0,0 @@
|
||||||
FROM debian:bookworm-slim
|
|
||||||
|
|
||||||
# Test invalid node version, these commands install nodejs 10
|
|
||||||
# RUN apt-get update
|
|
||||||
# RUN apt --yes install nodejs
|
|
||||||
# RUN ln -s /usr/bin/nodejs /usr/bin/node
|
|
||||||
# RUN node -v
|
|
||||||
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
|
@ -1,4 +0,0 @@
|
||||||
FROM rockylinux:9
|
|
||||||
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
|
@ -1,10 +0,0 @@
|
||||||
FROM ubuntu
|
|
||||||
|
|
||||||
# Test invalid node version, these commands install nodejs 10
|
|
||||||
# RUN apt-get update
|
|
||||||
# RUN apt --yes install nodejs
|
|
||||||
# RUN ln -s /usr/bin/nodejs /usr/bin/node
|
|
||||||
# RUN node -v
|
|
||||||
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
|
@ -1,9 +0,0 @@
|
||||||
FROM ubuntu:16.04
|
|
||||||
|
|
||||||
# Test invalid node version, these commands install nodejs 10
|
|
||||||
#RUN apt --yes install nodejs
|
|
||||||
# RUN ln -s /usr/bin/nodejs /usr/bin/node
|
|
||||||
# RUN node -v
|
|
||||||
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
|
@ -1,4 +0,0 @@
|
||||||
FROM ubuntu:18.04
|
|
||||||
|
|
||||||
COPY ./install.sh .
|
|
||||||
RUN bash install.sh local /opt/uptime-kuma 3000 0.0.0.0
|
|
Loading…
Add table
Reference in a new issue