Merge branch 'master' into feature/umami-analytics-status-page

This commit is contained in:
Frank Elsinga
2025-10-27 13:16:29 +01:00
committed by GitHub
217 changed files with 9844 additions and 3062 deletions

View File

@@ -3,7 +3,7 @@ name: ❓ Ask for help
description: |
Submit any question related to Uptime Kuma
#title: "[Help]"
labels: ["help", "P3-low"]
labels: ["help"]
body:
- type: markdown
attributes:

View File

@@ -3,7 +3,7 @@ name: 🐛 Bug Report
description: |
Submit a bug report to help us improve
#title: "[Bug]"
labels: ["bug", "P2-medium"]
labels: ["bug"]
body:
- type: markdown
attributes:

View File

@@ -3,28 +3,15 @@ name: 🚀 Feature Request
description: |
Submit a proposal for a new feature
# title: "[Feature]"
labels: ["feature-request", "P3-low"]
labels: ["feature-request"]
body:
- type: markdown
attributes:
value: |
## ❗Important Announcement
### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews
**At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.**
- **Reason**: Our current focus is on addressing bugs, improving system performance, and implementing essential updates. This will help stabilize the project and ensure smoother management.
- **Impact**: While no new feature requests or pull requests are being outright rejected, there may be significant delays in reviews. We encourage the community to help by reviewing PRs or assisting other users in the meantime.
- **What You Can Do**: If you're interested in contributing, reviewing open PRs by following our [Review Guidelines](https://github.com/louislam/uptime-kuma/blob/master/.github/REVIEW_GUIDELINES.md) or offering help to other users is greatly appreciated. All feature requests and PRs will be revisited once the suspension period is lifted.
We appreciate your patience and understanding as we continue to improve Uptime Kuma.
### 🚫 Please Avoid Unnecessary Pinging of Maintainers
**We kindly ask you to refrain from pinging maintainers unless absolutely necessary. Pings are reserved for critical/urgent pull requests that require immediate attention.**
**Why**: Reserving pings for urgent matters ensures maintainers can prioritize critical tasks effectively.
We kindly ask you to refrain from pinging maintainers unless absolutely necessary.
Pings are for critical/urgent pull requests that require immediate attention.
- type: textarea
id: related-issues
validations:

View File

@@ -3,7 +3,7 @@ name: 🛡️ Security Issue
description: |
Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue.
# title: "Security Issue"
labels: ["security", "P1-high"]
labels: ["security"]
assignees: [louislam]
body:
- type: markdown

View File

@@ -1,44 +1,35 @@
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we dont waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
## ❗ Important Announcement
## ❗ Important Announcements
<details><summary>Click here for more details:</summary>
</p>
### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews
**At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.**
- **Reason**: Our current focus is on addressing bugs, improving system performance, and implementing essential updates. This will help stabilize the project and ensure smoother management.
- **Impact**: While no new feature requests or pull requests are being outright rejected, there may be significant delays in reviews. We encourage the community to help by reviewing PRs or assisting other users in the meantime.
- **What You Can Do**: If you're interested in contributing, reviewing open PRs by following our [Review Guidelines](https://github.com/louislam/uptime-kuma/blob/master/.github/REVIEW_GUIDELINES.md) or offering support to other users is greatly appreciated. All feature requests and PRs will be revisited once the suspension period is lifted.
We appreciate your patience and understanding as we continue to improve Uptime Kuma.
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we dont waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
### 🚫 Please Avoid Unnecessary Pinging of Maintainers
**We kindly ask you to refrain from pinging maintainers unless absolutely necessary. Pings are reserved for critical/urgent pull requests that require immediate attention.**
**Why**: Reserving pings for urgent matters ensures maintainers can prioritize critical tasks effectively.
We kindly ask you to refrain from pinging maintainers unless absolutely necessary. Pings are for critical/urgent pull requests that require immediate attention.
</p>
</details>
## 📋 Overview
Provide a clear summary of the purpose and scope of this pull request:
<!-- Provide a clear summary of the purpose and scope of this pull request:-->
- **What problem does this pull request address?**
- Please provide a detailed explanation here.
- **What features or functionality does this pull request introduce or enhance?**
- Please provide a detailed explanation here.
## 🔄 Changes
<!--
Please link any GitHub issues or tasks that this pull request addresses.
Use the appropriate issue numbers or links to enable auto-closing.
-->
### 🛠️ Type of change
- Relates to #issue-number
- Resolves #issue-number
## 🛠️ Type of change
<!-- Please select all options that apply -->
@@ -52,23 +43,12 @@ Provide a clear summary of the purpose and scope of this pull request:
- [ ] 🔧 Other (please specify):
- Provide additional details here.
## 🔗 Related Issues
<!--
Please link any GitHub issues or tasks that this pull request addresses. Use the appropriate issue numbers or links.
**Note**: Include only issues directly related to this PR. Remove any irrelevant reference.
-->
- Relates to #issue-number
- Resolves #issue-number
- Fixes #issue-number
## 📄 Checklist *
## 📄 Checklist
<!-- Please select all options that apply -->
- [ ] 🔍 My code adheres to the style guidelines of this project.
- [ ] 🦿 I have indicated where (if any) I used an LLM for the contributions
- [ ] ✅ I ran ESLint and other code linters for modified files.
- [ ] 🛠️ I have reviewed and tested my code.
- [ ] 📝 I have commented my code, especially in hard-to-understand areas (e.g., using JSDoc for methods).
@@ -82,10 +62,11 @@ Please link any GitHub issues or tasks that this pull request addresses. Use the
## 📷 Screenshots or Visual Changes
<!--
Please upload the image directly here by pasting it or dragging and dropping. Avoid using external image services as the image will be uploaded automatically.
If this pull request introduces visual changes, please provide the following details.
If not, remove this section.
Please upload the image directly here by pasting it or dragging and dropping.
Avoid using external image services as the image will be uploaded automatically.
-->
- **UI Modifications**: Highlight any changes made to the user interface.
@@ -97,26 +78,3 @@ If not, remove this section.
| `DOWN` | ![Before](image-link) | ![After](image-link) |
| Certificate-expiry | ![Before](image-link) | ![After](image-link) |
| Testing | ![Before](image-link) | ![After](image-link) |
## Additional Context
Provide any relevant details to assist reviewers in understanding the changes.
<details><summary>Click here for more details:</summary>
</p>
**Key Considerations**:
- **Design decisions** Key choices or trade-offs made during development.
- **Alternative solutions** Approaches considered but not implemented, along with reasons.
- **Relevant links** Specifications, discussions, or resources that provide context.
- **Dependencies** Related pull requests or issues that must be resolved before merging.
- **Additional context** Any other details that may help reviewers understand the changes.
Provide details here
## 💬 Requested Feedback
<!-- If a part of our docs is unclear, you are unsure how to do something/.. this is where we would appreciate your feedback -->
- `Mention documents needing feedback here`

View File

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

View File

@@ -86,7 +86,7 @@ to review the appropriate one for your contribution.
PR:
- A text may not be currently localisable. In this case, **adding a new
language key** via `$t("languageKey")` might be nessesary
language key** via `$t("languageKey")` might be necessary
- language keys need to be **added to `en.json`** to be visible in weblate. If
this has not happened, a PR is appreciated.
- **Adding a new language** requires a new file see

View File

@@ -17,7 +17,7 @@ Uptime Kuma is an easy-to-use self-hosted monitoring tool.
Try it!
Demo Server (Location: Frankfurt - Germany): https://demo.kuma.pet/start-demo
Demo Server (Location: Frankfurt - Germany): <https://demo.kuma.pet/start-demo>
It is a temporary live demo, all data will be deleted after 10 minutes. Sponsored by [Uptime Kuma Sponsors](https://github.com/louislam/uptime-kuma#%EF%B8%8F-sponsors).
@@ -37,10 +37,13 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Sponsore
## 🔧 How to Install
### 🐳 Docker
### 🐳 Docker Compose
```bash
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
mkdir uptime-kuma
cd uptime-kuma
curl -o compose.yaml https://raw.githubusercontent.com/louislam/uptime-kuma/master/compose.yaml
docker compose up -d
```
Uptime Kuma is now running on <http://0.0.0.0:3001>.
@@ -48,11 +51,19 @@ Uptime Kuma is now running on <http://0.0.0.0:3001>.
> [!WARNING]
> File Systems like **NFS** (Network File System) are **NOT** supported. Please map to a local directory or volume.
### 🐳 Docker Command
```bash
docker run -d --restart=always -p 3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:2
```
Uptime Kuma is now running on <http://0.0.0.0:3001>.
> [!NOTE]
> If you want to limit exposure to localhost (without exposing port for other users or to use a [reverse proxy](https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy)), you can expose the port like this:
>
>
> ```bash
> docker run -d --restart=always -p 127.0.0.1:3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1
> docker run -d --restart=always -p 127.0.0.1:3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:2
> ```
### 💪🏻 Non-Docker
@@ -64,8 +75,7 @@ Requirements:
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ FreeBSD / OpenBSD / NetBSD
- ❌ Replit / Heroku
- [Node.js](https://nodejs.org/en/download/) 18 / 20.4
- [npm](https://docs.npmjs.com/cli/) 9
- [Node.js](https://nodejs.org/en/download/) >= 20.4
- [Git](https://git-scm.com/downloads)
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
@@ -85,7 +95,7 @@ npm install pm2 -g && pm2 install pm2-logrotate
pm2 start server/server.js --name uptime-kuma
```
Uptime Kuma is now running on http://localhost:3001
Uptime Kuma is now running on <http://localhost:3001>
More useful PM2 Commands
@@ -94,26 +104,26 @@ More useful PM2 Commands
pm2 monit
# If you want to add it to startup
pm2 save && pm2 startup
pm2 startup && pm2 save
```
### Advanced Installation
If you need more options or need to browse via a reverse proxy, please read:
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install
<https://github.com/louislam/uptime-kuma/wiki/%F0%9F%94%A7-How-to-Install>
## 🆙 How to Update
Please read:
https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update
<https://github.com/louislam/uptime-kuma/wiki/%F0%9F%86%99-How-to-Update>
## 🆕 What's Next?
I will assign requests/issues to the next milestone.
https://github.com/louislam/uptime-kuma/milestones
<https://github.com/louislam/uptime-kuma/milestones>
## ❤️ Sponsors
@@ -174,11 +184,11 @@ We DO NOT accept all types of pull requests and do not want to waste your time.
There are a lot of pull requests right now, but I don't have time to test them all.
If you want to help, you can check this:
https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests
<https://github.com/louislam/uptime-kuma/wiki/Test-Pull-Requests>
### Test Beta Version
Check out the latest beta release here: https://github.com/louislam/uptime-kuma/releases
Check out the latest beta release here: <https://github.com/louislam/uptime-kuma/releases>
### Bug Reports / Feature Requests
@@ -192,5 +202,3 @@ If you want to translate Uptime Kuma into your language, please visit [Weblate R
Feel free to correct the grammar in the documentation or code.
My mother language is not English and my grammar is not that great.

View File

@@ -22,17 +22,21 @@ Advisories only. I will ignore all 3rd-party bug bounty platforms emails.
### Uptime Kuma Versions
You should use or upgrade to the latest version of Uptime Kuma. All `1.X.X`
versions are upgradable to the latest version.
You should use or upgrade to the latest version of Uptime Kuma.
All versions are upgradable to the latest version.
### Upgradable Docker Tags
| Tag | Supported |
| -------------- | ------------------ |
| 1 | :white_check_mark: |
| 1-debian | :white_check_mark: |
| latest | :white_check_mark: |
| debian | :white_check_mark: |
| 1-alpine | ⚠️ Deprecated |
| alpine | ⚠️ Deprecated |
| All other tags | ❌ |
| Tag | Supported |
| --------------- | ------------------------------------------------------------------------------------- |
| 2 | :white_check_mark: |
| 2-slim | :white_check_mark: |
| next | :white_check_mark: |
| next-slim | :white_check_mark: |
| 2-rootless | :white_check_mark: |
| 2-slim-rootless | :white_check_mark: |
| 1 | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
| 1-debian | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
| latest | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
| debian | [⚠️ Deprecated](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) |
| All other tags | ❌ |

View File

@@ -1,9 +1,9 @@
services:
uptime-kuma:
image: louislam/uptime-kuma:1
image: louislam/uptime-kuma:2
restart: unless-stopped
volumes:
- ./data:/app/data
ports:
# <Host Port>:<Container Port>
- 3001:3001
restart: unless-stopped
- "3001:3001"

View File

@@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("smtp_security").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("smtp_security");
});
};

View File

@@ -0,0 +1,24 @@
/* SQL:
ALTER TABLE monitor ADD ping_count INTEGER default 1 not null;
ALTER TABLE monitor ADD ping_numeric BOOLEAN default true not null;
ALTER TABLE monitor ADD ping_per_request_timeout INTEGER default 2 not null;
*/
exports.up = function (knex) {
// Add new columns to table monitor
return knex.schema
.alterTable("monitor", function (table) {
table.integer("ping_count").defaultTo(1).notNullable();
table.boolean("ping_numeric").defaultTo(true).notNullable();
table.integer("ping_per_request_timeout").defaultTo(2).notNullable();
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("ping_count");
table.dropColumn("ping_numeric");
table.dropColumn("ping_per_request_timeout");
});
};

View File

@@ -0,0 +1,13 @@
// Add column custom_url to monitor_group table
exports.up = function (knex) {
return knex.schema
.alterTable("monitor_group", function (table) {
table.text("custom_url", "text");
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor_group", function (table) {
table.dropColumn("custom_url");
});
};

View File

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

View File

@@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("manual_status").defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("manual_status");
});
};

View File

@@ -0,0 +1,34 @@
// Add column last_start_date to maintenance table
exports.up = async function (knex) {
await knex.schema
.alterTable("maintenance", function (table) {
table.datetime("last_start_date");
});
// Perform migration for recurring-interval strategy
const recurringMaintenances = await knex("maintenance").where({
strategy: "recurring-interval",
cron: "* * * * *"
}).select("id", "start_time");
// eslint-disable-next-line camelcase
const maintenanceUpdates = recurringMaintenances.map(async ({ start_time, id }) => {
// eslint-disable-next-line camelcase
const [ hourStr, minuteStr ] = start_time.split(":");
const hour = parseInt(hourStr, 10);
const minute = parseInt(minuteStr, 10);
const cron = `${minute} ${hour} * * *`;
await knex("maintenance")
.where({ id })
.update({ cron });
});
await Promise.all(maintenanceUpdates);
};
exports.down = function (knex) {
return knex.schema.alterTable("maintenance", function (table) {
table.dropColumn("last_start_date");
});
};

View File

@@ -0,0 +1,13 @@
// Fix: Change manual_status column type to smallint
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.smallint("manual_status").alter();
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.string("manual_status").alter();
});
};

View File

@@ -0,0 +1,12 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
table.string("oauth_audience").nullable().defaultTo(null);
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.string("oauth_audience").alter();
});
};

View File

@@ -0,0 +1,15 @@
exports.up = function (knex) {
// Add new column monitor.mqtt_websocket_path
return knex.schema
.alterTable("monitor", function (table) {
table.string("mqtt_websocket_path", 255).nullable();
});
};
exports.down = function (knex) {
// Drop column monitor.mqtt_websocket_path
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("mqtt_websocket_path");
});
};

View File

@@ -0,0 +1,16 @@
exports.up = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
// Fix ip_family, change to varchar instead of boolean
// possible values are "ipv4" and "ipv6"
table.string("ip_family", 4).defaultTo(null).alter();
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("monitor", function (table) {
// Rollback to boolean
table.boolean("ip_family").defaultTo(null).alter();
});
};

View File

@@ -0,0 +1,27 @@
// Fix for #4315. Logically, setting it to 0 ping may not be correct, but it is better than throwing errors
exports.up = function (knex) {
return knex.schema
.alterTable("stat_daily", function (table) {
table.integer("ping").defaultTo(0).alter();
})
.alterTable("stat_hourly", function (table) {
table.integer("ping").defaultTo(0).alter();
})
.alterTable("stat_minutely", function (table) {
table.integer("ping").defaultTo(0).alter();
});
};
exports.down = function (knex) {
return knex.schema
.alterTable("stat_daily", function (table) {
table.integer("ping").alter();
})
.alterTable("stat_hourly", function (table) {
table.integer("ping").alter();
})
.alterTable("stat_minutely", function (table) {
table.integer("ping").alter();
});
};

View File

@@ -2,15 +2,24 @@
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
############################################
FROM golang:1.19-buster
FROM golang:1-buster
WORKDIR /app
ARG TARGETPLATFORM
COPY ./extra/ ./extra/
## Switch to archive.debian.org
RUN sed -i '/^deb/s/^/#/' /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian buster main contrib non-free" | tee -a /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian-security buster/updates main contrib non-free" | tee -a /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian buster-updates main contrib non-free" | tee -a /etc/apt/sources.list
# Compile healthcheck.go
RUN apt update && \
apt --yes --no-install-recommends install curl && \
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
apt --yes --no-install-recommends install nodejs && \
node ./extra/build-healthcheck.js $TARGETPLATFORM && \
apt --yes remove nodejs
apt --yes remove nodejs && \
apt autoremove -y --purge && \
apt clean && \
rm -rf /var/lib/apt/lists/*

View File

@@ -5,7 +5,10 @@ COPY ./extra/download-apprise.mjs ./download-apprise.mjs
RUN apt update && \
apt --yes --no-install-recommends install curl && \
npm install cheerio semver && \
node ./download-apprise.mjs
node ./download-apprise.mjs && \
apt autoremove -y --purge && \
apt clean && \
rm -rf /var/lib/apt/lists/*
# Base Image (Slim)
# If the image changed, the second stage image should be changed too
@@ -31,8 +34,9 @@ RUN apt update && \
curl \
sudo \
nscd && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
apt autoremove -y --purge && \
apt clean && \
rm -rf /var/lib/apt/lists/*
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
@@ -41,24 +45,25 @@ RUN apt update && \
COPY --from=download-apprise /app/apprise.deb ./apprise.deb
RUN apt update && \
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
rm -rf /var/lib/apt/lists/* && \
rm -f apprise.deb && \
apt --yes autoremove
apt autoremove -y --purge && \
apt clean && \
rm -rf /var/lib/apt/lists/*
# Install cloudflared
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | tee /etc/apt/sources.list.d/cloudflared.list && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' | tee /etc/apt/sources.list.d/cloudflared.list && \
apt update && \
apt install --yes --no-install-recommends -t stable cloudflared && \
apt install --yes --no-install-recommends cloudflared && \
cloudflared version && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove
apt autoremove -y --purge && \
apt clean && \
rm -rf /var/lib/apt/lists/*
# For nscd
COPY ./docker/etc/nscd.conf /etc/nscd.conf
COPY ./docker/etc/sudoers /etc/sudoers
# Full Base Image
# MariaDB, Chromium and fonts
# Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch.
@@ -67,6 +72,7 @@ FROM louislam/uptime-kuma:base2-slim AS base2
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1
RUN apt update && \
apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \
apt autoremove -y --purge && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
apt --yes autoremove && \
chown -R node:node /var/lib/mysql

View File

@@ -70,7 +70,10 @@ RUN apt update \
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& apt update \
&& apt --yes --no-install-recommends install git
&& apt --yes --no-install-recommends install git \
&& apt autoremove -y --purge \
&& apt clean \
&& rm -rf /var/lib/apt/lists/*
## Empty the directory, because we have to clone the Git repo.
RUN rm -rf ./* && chown node /app
@@ -79,6 +82,10 @@ USER node
RUN git config --global user.email "no-reply@no-reply.com"
RUN git config --global user.name "PR Tester"
RUN git clone https://github.com/louislam/uptime-kuma.git .
# Hide the warning when running in detached head state
RUN git config --global advice.detachedHead false
RUN npm ci
EXPOSE 3000 3001
@@ -91,7 +98,10 @@ CMD ["npm", "run", "start-pr-test"]
FROM louislam/uptime-kuma:base2 AS upload-artifact
WORKDIR /
RUN apt update && \
apt --yes install curl file
apt --yes install curl file && \
apt autoremove -y --purge && \
apt clean && \
rm -rf /var/lib/apt/lists/*
COPY --from=build /app /app
@@ -111,4 +121,3 @@ RUN chmod +x /app/extra/upload-github-release-asset.sh
# Dist only
RUN cd /app && tar -zcvf $DIST dist
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST

View File

@@ -24,9 +24,7 @@ if (! exists) {
// Also update package-lock.json
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
childProcess.spawnSync(npm, [ "install" ]);
commit(version);
tag(version);
} else {
console.log("version tag exists, please delete the tag or use another tag");
@@ -54,19 +52,6 @@ function commit(version) {
console.log(res.stdout.toString().trim());
}
/**
* Create a tag with the specified version
* @param {string} version Tag to create
* @returns {void}
*/
function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
res = childProcess.spawnSync("git", [ "push", "origin", version ]);
console.log(res.stdout.toString().trim());
}
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check

View File

@@ -1,33 +0,0 @@
const childProcess = require("child_process");
if (!process.env.UPTIME_KUMA_GH_REPO) {
console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
process.exit(1);
}
let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
if (inputArray.length !== 2) {
console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
}
let name = inputArray[0];
let branch = inputArray[1];
console.log("Checkout pr");
// Checkout the pr
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());
result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());

34
extra/checkout-pr.mjs Normal file
View File

@@ -0,0 +1,34 @@
import childProcess from "child_process";
import { parsePrName } from "./kuma-pr/pr-lib.mjs";
let { name, branch } = parsePrName(process.env.UPTIME_KUMA_GH_REPO);
console.log(`Checking out PR from ${name}:${branch}`);
// Checkout the pr
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to add remote repository.");
process.exit(1);
}
result = childProcess.spawnSync("git", [ "fetch", name, branch ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to fetch the branch.");
process.exit(1);
}
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to checkout the branch.");
process.exit(1);
}

View File

@@ -37,7 +37,7 @@ const github = require("@actions/github");
owner: issue.owner,
repo: issue.repo,
issue_number: issue.number,
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context nessesary for a good discussions.`
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context necessary for a good discussions.`
});
// Close the issue

View File

@@ -0,0 +1,201 @@
// Script to generate changelog
// Usage: node generate-changelog.mjs <previous-version-tag>
// GitHub CLI (gh command) is required
import * as childProcess from "child_process";
const ignoreList = [
"louislam",
"CommanderStorm",
"UptimeKumaBot",
"weblate",
"Copilot"
];
const mergeList = [
"Translations Update from Weblate",
"Update dependencies",
];
const template = `
LLM Task: Please help to put above PRs into the following sections based on their content. If a PR fits multiple sections, choose the most relevant one. If a PR doesn't fit any section, place it in "Others". If there are grammatical errors in the PR titles, please correct them. Don't change the PR numbers and authors, and keep the format. Output as markdown.
Changelog:
### 🆕 New Features
### 💇‍♀️ Improvements
### 🐞 Bug Fixes
### ⬆️ Security Fixes
### 🦎 Translation Contributions
### Others
- Other small changes, code refactoring and comment/doc updates in this repo:
`;
await main();
/**
* Main Function
* @returns {Promise<void>}
*/
async function main() {
const previousVersion = process.argv[2];
if (!previousVersion) {
console.error("Please provide the previous version as the first argument.");
process.exit(1);
}
console.log(`Generating changelog since version ${previousVersion}...`);
try {
const prList = await getPullRequestList(previousVersion);
const list = [];
let i = 1;
for (const pr of prList) {
console.log(`Progress: ${i++}/${prList.length}`);
let authorSet = await getAuthorList(pr.number);
authorSet = await mainAuthorToFront(pr.author.login, authorSet);
if (mergeList.includes(pr.title)) {
// Check if it is already in the list
const existingItem = list.find(item => item.title === pr.title);
if (existingItem) {
existingItem.numbers.push(pr.number);
for (const author of authorSet) {
existingItem.authors.add(author);
// Sort the authors
existingItem.authors = new Set([ ...existingItem.authors ].sort((a, b) => a.localeCompare(b)));
}
continue;
}
}
const item = {
numbers: [ pr.number ],
title: pr.title,
authors: authorSet,
};
list.push(item);
}
for (const item of list) {
// Concat pr numbers into a string like #123 #456
const prPart = item.numbers.map(num => `#${num}`).join(" ");
// Concat authors into a string like @user1 @user2
let authorPart = [ ...item.authors ].map(author => `@${author}`).join(" ");
if (authorPart) {
authorPart = `(Thanks ${authorPart})`;
}
console.log(`- ${prPart} ${item.title} ${authorPart}`);
}
console.log(template);
} catch (e) {
console.error("Failed to get pull request list:", e);
process.exit(1);
}
}
/**
* @param {string} previousVersion Previous Version Tag
* @returns {Promise<object>} List of Pull Requests merged since previousVersion
*/
async function getPullRequestList(previousVersion) {
// Get the date of previousVersion in YYYY-MM-DD format from git
const previousVersionDate = childProcess.execSync(`git log -1 --format=%cd --date=short ${previousVersion}`).toString().trim();
if (!previousVersionDate) {
throw new Error(`Unable to find the date of version ${previousVersion}. Please make sure the version tag exists.`);
}
const ghProcess = childProcess.spawnSync("gh", [
"pr",
"list",
"--state",
"merged",
"--base",
"master",
"--search",
`merged:>=${previousVersionDate}`,
"--json",
"number,title,author",
"--limit",
"1000"
], {
encoding: "utf-8"
});
if (ghProcess.error) {
throw ghProcess.error;
}
if (ghProcess.status !== 0) {
throw new Error(`gh command failed with status ${ghProcess.status}: ${ghProcess.stderr}`);
}
return JSON.parse(ghProcess.stdout);
}
/**
* @param {number} prID Pull Request ID
* @returns {Promise<Set<string>>} Set of Authors' GitHub Usernames
*/
async function getAuthorList(prID) {
const ghProcess = childProcess.spawnSync("gh", [
"pr",
"view",
prID,
"--json",
"commits"
], {
encoding: "utf-8"
});
if (ghProcess.error) {
throw ghProcess.error;
}
if (ghProcess.status !== 0) {
throw new Error(`gh command failed with status ${ghProcess.status}: ${ghProcess.stderr}`);
}
const prInfo = JSON.parse(ghProcess.stdout);
const commits = prInfo.commits;
const set = new Set();
for (const commit of commits) {
for (const author of commit.authors) {
if (author.login && !ignoreList.includes(author.login)) {
set.add(author.login);
}
}
}
// Sort the set
return new Set([ ...set ].sort((a, b) => a.localeCompare(b)));
}
/**
* @param {string} mainAuthor Main Author
* @param {Set<string>} authorSet Set of Authors
* @returns {Set<string>} New Set with mainAuthor at the front
*/
async function mainAuthorToFront(mainAuthor, authorSet) {
if (ignoreList.includes(mainAuthor)) {
return authorSet;
}
return new Set([ mainAuthor, ...authorSet ]);
}

26
extra/kuma-pr/index.mjs Normal file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env node
import { spawn } from "child_process";
import { parsePrName } from "./pr-lib.mjs";
const prName = process.argv[2];
// Pre-check the prName here, so testers don't need to wait until the Docker image is pulled to see the error.
try {
parsePrName(prName);
} catch (error) {
console.error(error.message);
process.exit(1);
}
spawn("docker", [
"run",
"--rm",
"-it",
"-p", "3000:3000",
"-p", "3001:3001",
"--pull", "always",
"-e", `UPTIME_KUMA_GH_REPO=${prName}`,
"louislam/uptime-kuma:pr-test2"
], {
stdio: "inherit",
});

View File

@@ -0,0 +1,8 @@
{
"name": "kuma-pr",
"version": "1.0.0",
"type": "module",
"bin": {
"kuma-pr": "./index.mjs"
}
}

39
extra/kuma-pr/pr-lib.mjs Normal file
View File

@@ -0,0 +1,39 @@
/**
* Parse <name>:<branch> to an object.
* @param {string} prName <name>:<branch>
* @returns {object} An object with name and branch properties.
*/
export function parsePrName(prName) {
let name = "louislam";
let branch;
const errorMessage = "Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)";
if (!prName) {
throw new Error(errorMessage);
}
prName = prName.trim();
if (prName === "") {
throw new Error(errorMessage);
}
let inputArray = prName.split(":");
// Just realized that owner's prs are not prefixed with "louislam:"
if (inputArray.length === 1) {
branch = inputArray[0];
} else if (inputArray.length === 2) {
name = inputArray[0];
branch = inputArray[1];
} else {
throw new Error("Invalid format. The format is like this: mhkarimi1383:goalert-notification");
}
return {
name,
branch
};
}

View File

@@ -1,44 +0,0 @@
// 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);

View File

@@ -8,7 +8,7 @@ import {
checkVersionFormat,
getRepoNames,
pressAnyKey,
execSync, uploadArtifacts,
execSync, uploadArtifacts, checkReleaseBranch,
} from "./lib.mjs";
import semver from "semver";
@@ -23,6 +23,9 @@ if (!githubToken) {
process.exit(1);
}
// Check if the current branch is "release"
checkReleaseBranch();
// Check if the version is a valid semver
checkVersionFormat(version);

View File

@@ -7,7 +7,7 @@ import {
checkTagExists,
checkVersionFormat,
getRepoNames,
pressAnyKey, execSync, uploadArtifacts
pressAnyKey, execSync, uploadArtifacts, checkReleaseBranch
} from "./lib.mjs";
const repoNames = getRepoNames();
@@ -21,6 +21,9 @@ if (!githubToken) {
process.exit(1);
}
// Check if the current branch is "release"
checkReleaseBranch();
// Check if the version is a valid semver
checkVersionFormat(version);

View File

@@ -249,3 +249,16 @@ export function execSync(cmd) {
console.info(`[DRY RUN] ${cmd}`);
}
}
/**
* Check if the current branch is "release"
* @returns {void}
*/
export function checkReleaseBranch() {
const res = childProcess.spawnSync("git", [ "rev-parse", "--abbrev-ref", "HEAD" ]);
const branch = res.stdout.toString().trim();
if (branch !== "release") {
console.error(`Current branch is ${branch}, please switch to "release" branch`);
process.exit(1);
}
}

View File

@@ -27,10 +27,19 @@ if (! exists) {
// Also update package-lock.json
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
childProcess.spawnSync(npm, [ "install" ]);
const resultVersion = childProcess.spawnSync(npm, [ "--no-git-tag-version", "version", newVersion ], { shell: true });
if (resultVersion.error) {
console.error(resultVersion.error);
console.error("error npm version!");
process.exit(1);
}
const resultInstall = childProcess.spawnSync(npm, [ "install" ], { shell: true });
if (resultInstall.error) {
console.error(resultInstall.error);
console.error("error update package-lock!");
process.exit(1);
}
commit(newVersion);
tag(newVersion);
} else {
console.log("version exists");
@@ -54,16 +63,6 @@ function commit(version) {
}
}
/**
* Create a tag with the specified version
* @param {string} version Tag to create
* @returns {void}
*/
function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
}
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check

3264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "uptime-kuma",
"version": "2.0.0-beta.2",
"version": "2.0.2",
"license": "MIT",
"repository": {
"type": "git",
@@ -32,7 +32,7 @@
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
"playwright-show-report": "playwright show-report ./private/playwright-report",
"tsc": "tsc",
"tsc": "tsc --project ./tsconfig-backend.json",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
@@ -41,7 +41,7 @@
"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": "node extra/release/upload-artifacts.mjs",
"upload-artifacts-beta": "node extra/release/upload-artifacts-beta.mjs",
"setup": "git checkout 1.23.16 && npm ci --omit dev && npm run download-dist",
"setup": "git checkout 2.0.2 && npm ci --omit dev && npm run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
@@ -57,14 +57,15 @@
"release-nightly": "node ./extra/release/nightly.mjs",
"git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev",
"start-pr-test": "node extra/checkout-pr.mjs && npm install && npm run dev",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js"
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js",
"generate-changelog": "node ./extra/generate-changelog.mjs"
},
"dependencies": {
"@grpc/grpc-js": "~1.8.22",
@@ -81,7 +82,7 @@
"chroma-js": "~2.4.2",
"command-exists": "~1.2.9",
"compare-versions": "~3.6.0",
"compression": "~1.7.4",
"compression": "~1.8.1",
"country-flag-emoji-polyfill": "^0.1.8",
"croner": "~8.1.0",
"dayjs": "~1.11.5",
@@ -99,6 +100,7 @@
"http-proxy-agent": "~7.0.2",
"https-proxy-agent": "~7.0.6",
"iconv-lite": "~0.6.3",
"is-url": "^1.2.4",
"isomorphic-ws": "^5.0.0",
"jsesc": "~3.0.2",
"jsonata": "^2.0.3",
@@ -117,7 +119,6 @@
"nanoid": "~3.3.4",
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.9.13",
"nostr-tools": "^2.10.4",
"notp": "~2.0.3",
@@ -131,12 +132,15 @@
"promisify-child-process": "~4.1.2",
"protobufjs": "~7.2.4",
"qs": "~6.10.4",
"radius": "~1.1.4",
"node-radius-utils": "~1.2.0",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"semver": "~7.5.4",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",
"socks-proxy-agent": "~8.0.5",
"sqlstring": "~2.3.3",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",

View File

@@ -18,15 +18,15 @@ exports.login = async function (username, password) {
return null;
}
let user = await R.findOne("user", " username = ? AND active = 1 ", [
username,
let user = await R.findOne("user", "TRIM(username) = ? AND active = 1 ", [
username.trim(),
]);
if (user && passwordHash.verify(password, user.password)) {
// Upgrade the hash to bcrypt
if (passwordHash.needRehash(user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password),
await passwordHash.generate(password),
user.id,
]);
}

View File

@@ -1,4 +1,5 @@
const fs = require("fs");
const fsAsync = fs.promises;
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
@@ -11,6 +12,7 @@ const { UptimeCalculator } = require("./uptime-calculator");
const dayjs = require("dayjs");
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
const SqlString = require("sqlstring");
/**
* Database & App Data Folder
@@ -18,7 +20,7 @@ const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysq
class Database {
/**
* Boostrap database for SQLite
* Bootstrap database for SQLite
* @type {string}
*/
static templatePath = "./db/kuma.db";
@@ -255,10 +257,6 @@ class Database {
}
};
} else if (dbConfig.type === "mariadb") {
if (!/^\w+$/.test(dbConfig.dbName)) {
throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores");
}
const connection = await mysql.createConnection({
host: dbConfig.hostname,
port: dbConfig.port,
@@ -266,7 +264,11 @@ class Database {
password: dbConfig.password,
});
await connection.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName + " CHARACTER SET utf8mb4");
// Set to true, so for example "uptime.kuma", becomes `uptime.kuma`, not `uptime`.`kuma`
// Doc: https://github.com/mysqljs/sqlstring?tab=readme-ov-file#escaping-query-identifiers
const escapedDBName = SqlString.escapeId(dbConfig.dbName, true);
await connection.execute("CREATE DATABASE IF NOT EXISTS " + escapedDBName + " CHARACTER SET utf8mb4");
connection.end();
config = {
@@ -707,12 +709,12 @@ class Database {
/**
* Get the size of the database (SQLite only)
* @returns {number} Size of database
* @returns {Promise<number>} Size of database
*/
static getSize() {
static async getSize() {
if (Database.dbConfig.type === "sqlite") {
log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.sqlitePath);
let stats = await fsAsync.stat(Database.sqlitePath);
log.debug("db", stats);
return stats.size;
}
@@ -736,7 +738,7 @@ class Database {
if (Database.dbConfig.type === "sqlite") {
return "DATETIME('now', ? || ' hours')";
} else {
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
return "DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? HOUR)";
}
}

View File

@@ -1,10 +1,10 @@
const axios = require("axios");
const { R } = require("redbean-node");
const https = require("https");
const fs = require("fs");
const fsAsync = require("fs").promises;
const path = require("path");
const Database = require("./database");
const { axiosAbortSignal } = require("./util-server");
const { axiosAbortSignal, fsExists } = require("./util-server");
class DockerHost {
@@ -81,7 +81,7 @@ class DockerHost {
options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
options.httpsAgent = new https.Agent(await DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
}
try {
@@ -141,9 +141,9 @@ class DockerHost {
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
* @param {string} dockerType i.e. "tcp" or "socket"
* @param {string} url The docker host URL rewritten to https://
* @returns {object} HTTP agent options
* @returns {Promise<object>} HTTP agent options
*/
static getHttpsAgentOptions(dockerType, url) {
static async getHttpsAgentOptions(dockerType, url) {
let baseOptions = {
maxCachedSessions: 0,
rejectUnauthorized: true
@@ -156,10 +156,10 @@ class DockerHost {
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
let ca = fs.readFileSync(caPath);
let key = fs.readFileSync(keyPath);
let cert = fs.readFileSync(certPath);
if (dockerType === "tcp" && await fsExists(caPath) && await fsExists(certPath) && await fsExists(keyPath)) {
let ca = await fsAsync.readFile(caPath);
let key = await fsAsync.readFile(keyPath);
let cert = await fsAsync.readFile(certPath);
certOptions = {
ca,
key,

View File

@@ -33,7 +33,7 @@ class Group extends BeanModel {
*/
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
SELECT monitor.*, monitor_group.send_url, monitor_group.custom_url FROM monitor, monitor_group
WHERE monitor.id = monitor_group.monitor_id
AND group_id = ?
ORDER BY monitor_group.weight

View File

@@ -158,12 +158,22 @@ class Maintenance extends BeanModel {
bean.active = obj.active;
if (obj.dateRange[0]) {
const parsedDate = new Date(obj.dateRange[0]);
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
throw new Error("Invalid start date");
}
bean.start_date = obj.dateRange[0];
} else {
bean.start_date = null;
}
if (obj.dateRange[1]) {
const parsedDate = new Date(obj.dateRange[1]);
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
throw new Error("Invalid end date");
}
bean.end_date = obj.dateRange[1];
} else {
bean.end_date = null;
@@ -192,7 +202,7 @@ class Maintenance extends BeanModel {
* @returns {void}
*/
static validateCron(cron) {
let job = new Cron(cron, () => {});
let job = new Cron(cron, () => { });
job.stop();
}
@@ -229,11 +239,13 @@ class Maintenance extends BeanModel {
apicache.clear();
});
} else if (this.cron != null) {
let current = dayjs();
// Here should be cron or recurring
try {
this.beanMeta.status = "scheduled";
let startEvent = (customDuration = 0) => {
let startEvent = async (customDuration = 0) => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
this.beanMeta.status = "under-maintenance";
@@ -248,6 +260,10 @@ class Maintenance extends BeanModel {
this.beanMeta.status = "scheduled";
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
}, duration);
// Set last start date to current time
this.last_start_date = current.toISOString();
await R.store(this);
};
// Create Cron
@@ -256,11 +272,34 @@ class Maintenance extends BeanModel {
const startDate = dayjs(this.startDate);
const [ hour, minute ] = this.startTime.split(":");
const startDateTime = startDate.hour(hour).minute(minute);
// Fix #6118, since the startDateTime is optional, it will throw error if the date is null when using toISOString()
let startAt = undefined;
try {
startAt = startDateTime.toISOString();
} catch (_) {}
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
interval: this.interval_day * 24 * 60 * 60,
startAt: startDateTime.toISOString(),
}, startEvent);
startAt,
}, () => {
if (!this.lastStartDate || this.interval_day === 1) {
return startEvent();
}
// If last start date is set, it means the maintenance has been started before
let lastStartDate = dayjs(this.lastStartDate)
.subtract(1.1, "hour"); // Subtract 1.1 hour to avoid issues with timezone differences
// Check if the interval is enough
if (current.diff(lastStartDate, "day") < this.interval_day) {
log.debug("maintenance", "Maintenance id: " + this.id + " is still in the window, skipping start event");
return;
}
log.debug("maintenance", "Maintenance id: " + this.id + " is not in the window, starting event");
return startEvent();
});
} else {
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
@@ -269,7 +308,6 @@ class Maintenance extends BeanModel {
// Continue if the maintenance is still in the window
let runningTimeslot = this.getRunningTimeslot();
let current = dayjs();
if (runningTimeslot) {
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
@@ -413,8 +451,11 @@ class Maintenance extends BeanModel {
} else if (!this.strategy.startsWith("recurring-")) {
this.cron = "";
} else if (this.strategy === "recurring-interval") {
// For intervals, the pattern is calculated in the run function as the interval-option is set
this.cron = "* * * * *";
// For intervals, the pattern is used to check if the execution should be started
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = `${minute} ${hour} * * *`;
this.duration = this.calcDuration();
log.debug("maintenance", "Cron: " + this.cron);
log.debug("maintenance", "Duration: " + this.duration);

View File

@@ -2,7 +2,11 @@ const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT, evaluateJsonQuery
SQL_DATETIME_FORMAT, evaluateJsonQuery,
PING_PACKET_SIZE_MIN, PING_PACKET_SIZE_MAX, PING_PACKET_SIZE_DEFAULT,
PING_GLOBAL_TIMEOUT_MIN, PING_GLOBAL_TIMEOUT_MAX, PING_GLOBAL_TIMEOUT_DEFAULT,
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@@ -53,7 +57,7 @@ class Monitor extends BeanModel {
};
if (this.sendUrl) {
obj.url = this.url;
obj.url = this.customUrl ?? this.url;
}
if (showTags) {
@@ -153,8 +157,15 @@ class Monitor extends BeanModel {
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
smtpSecurity: this.smtpSecurity,
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
conditions: JSON.parse(this.conditions),
ipFamily: this.ipFamily,
// ping advanced options
ping_numeric: this.isPingNumeric(),
ping_count: this.ping_count,
ping_per_request_timeout: this.ping_per_request_timeout,
};
if (includeSensitiveData) {
@@ -170,6 +181,7 @@ class Monitor extends BeanModel {
oauth_client_secret: this.oauth_client_secret,
oauth_token_url: this.oauth_token_url,
oauth_scopes: this.oauth_scopes,
oauth_audience: this.oauth_audience,
oauth_auth_method: this.oauth_auth_method,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
@@ -178,6 +190,7 @@ class Monitor extends BeanModel {
radiusSecret: this.radiusSecret,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
mqttWebsocketPath: this.mqttWebsocketPath,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
tlsCa: this.tlsCa,
@@ -247,6 +260,14 @@ class Monitor extends BeanModel {
return Boolean(this.expiryNotification);
}
/**
* Check if ping should use numeric output only
* @returns {boolean} True if IP addresses will be output instead of symbolic hostnames
*/
isPingNumeric() {
return Boolean(this.ping_numeric);
}
/**
* Parse to boolean
* @returns {boolean} Should TLS errors be ignored?
@@ -408,10 +429,26 @@ class Monitor extends BeanModel {
}
}
let agentFamily = undefined;
if (this.ipFamily === "ipv4") {
agentFamily = 4;
}
if (this.ipFamily === "ipv6") {
agentFamily = 6;
}
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
autoSelectFamily: true,
...(agentFamily ? { family: agentFamily } : {})
};
const httpAgentOptions = {
maxCachedSessions: 0,
autoSelectFamily: true,
...(agentFamily ? { family: agentFamily } : {})
};
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
@@ -473,6 +510,7 @@ class Monitor extends BeanModel {
if (proxy && proxy.active) {
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
httpsAgentOptions: httpsAgentOptions,
httpAgentOptions: httpAgentOptions,
});
options.proxy = false;
@@ -481,6 +519,10 @@ class Monitor extends BeanModel {
}
}
if (!options.httpAgent) {
options.httpAgent = new http.Agent(httpAgentOptions);
}
if (!options.httpsAgent) {
let jar = new CookieJar();
let httpsCookieAgentOptions = {
@@ -584,7 +626,7 @@ class Monitor extends BeanModel {
bean.status = UP;
} else if (this.type === "ping") {
bean.ping = await ping(this.hostname, this.packetSize);
bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout);
bean.msg = "";
bean.status = UP;
} else if (this.type === "push") { // Type: Push
@@ -656,7 +698,7 @@ class Monitor extends BeanModel {
bean.msg = res.data.response.servers[0].name;
try {
bean.ping = await ping(this.hostname, this.packetSize);
bean.ping = await ping(this.hostname, PING_COUNT_DEFAULT, "", true, this.packetSize, PING_GLOBAL_TIMEOUT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT);
} catch (_) { }
} else {
throw new Error("Server not found on Steam");
@@ -706,7 +748,7 @@ class Monitor extends BeanModel {
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
options.httpsAgent = new https.Agent(
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
await DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
);
}
@@ -1273,7 +1315,7 @@ class Monitor extends BeanModel {
/**
* Send a notification about a monitor
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
* @param {Monitor} monitor The monitor to send a notificaton about
* @param {Monitor} monitor The monitor to send a notification about
* @param {Bean} bean Status information about monitor
* @returns {void}
*/
@@ -1468,6 +1510,31 @@ class Monitor extends BeanModel {
if (this.interval < MIN_INTERVAL_SECOND) {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
}
if (this.type === "ping") {
// ping parameters validation
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
throw new Error(`Packet size must be between ${PING_PACKET_SIZE_MIN} and ${PING_PACKET_SIZE_MAX} (default: ${PING_PACKET_SIZE_DEFAULT})`);
}
if (this.ping_per_request_timeout && (this.ping_per_request_timeout < PING_PER_REQUEST_TIMEOUT_MIN || this.ping_per_request_timeout > PING_PER_REQUEST_TIMEOUT_MAX)) {
throw new Error(`Per-ping timeout must be between ${PING_PER_REQUEST_TIMEOUT_MIN} and ${PING_PER_REQUEST_TIMEOUT_MAX} seconds (default: ${PING_PER_REQUEST_TIMEOUT_DEFAULT})`);
}
if (this.ping_count && (this.ping_count < PING_COUNT_MIN || this.ping_count > PING_COUNT_MAX)) {
throw new Error(`Echo requests count must be between ${PING_COUNT_MIN} and ${PING_COUNT_MAX} (default: ${PING_COUNT_DEFAULT})`);
}
if (this.timeout) {
const pingGlobalTimeout = Math.round(Number(this.timeout));
if (pingGlobalTimeout < this.ping_per_request_timeout || pingGlobalTimeout < PING_GLOBAL_TIMEOUT_MIN || pingGlobalTimeout > PING_GLOBAL_TIMEOUT_MAX) {
throw new Error(`Timeout must be between ${PING_GLOBAL_TIMEOUT_MIN} and ${PING_GLOBAL_TIMEOUT_MAX} seconds (default: ${PING_GLOBAL_TIMEOUT_DEFAULT})`);
}
this.timeout = pingGlobalTimeout;
}
}
}
/**
@@ -1681,7 +1748,7 @@ class Monitor extends BeanModel {
*/
async makeOidcTokenClientCredentialsRequest() {
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new token`);
const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_audience, this.oauth_auth_method);
if (this.oauthAccessToken?.expires_at) {
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken?.expires_at * 1000)}`);
} else {

View File

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

View File

@@ -89,6 +89,9 @@ function NtlmClient(credentials, AxiosConfig) {
switch (_b.label) {
case 0:
error = err.response;
// The header may look like this: `Negotiate, NTLM, Basic realm="itsahiddenrealm.example.net"`Add commentMore actions
// so extract the 'NTLM' part first
const ntlmheader = error.headers['www-authenticate'].split(',').find(_ => _.match(/ *NTLM/))?.trim() || '';
if (!(error && error.status === 401
&& error.headers['www-authenticate']
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
@@ -96,12 +99,12 @@ function NtlmClient(credentials, AxiosConfig) {
// include the Negotiate option when responding with the T2 message
// There is nore we could do to ensure we are processing correctly,
// but this is the easiest option for now
if (error.headers['www-authenticate'].length < 50) {
if (ntlmheader.length < 50) {
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
error.config.headers["Authorization"] = t1Msg;
}
else {
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
t2Msg = ntlm.decodeType2Message((ntlmheader.match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
error.config.headers["X-retry"] = "false";
error.config.headers["Authorization"] = t3Msg;

View File

@@ -0,0 +1,36 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN, PENDING } = require("../../src/util");
class ManualMonitorType extends MonitorType {
name = "Manual";
type = "manual";
description = "A monitor that allows manual control of the status";
supportsConditions = false;
conditionVariables = [];
/**
* @inheritdoc
*/
async check(monitor, heartbeat) {
if (monitor.manual_status !== null) {
heartbeat.status = monitor.manual_status;
switch (monitor.manual_status) {
case UP:
heartbeat.msg = "Up";
break;
case DOWN:
heartbeat.msg = "Down";
break;
default:
heartbeat.msg = "Pending";
}
} else {
heartbeat.status = PENDING;
heartbeat.msg = "Manual monitoring - No status set";
}
}
}
module.exports = {
ManualMonitorType
};

View File

@@ -10,11 +10,12 @@ class MqttMonitorType extends MonitorType {
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
const [ messageTopic, receivedMessage ] = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
port: monitor.port,
username: monitor.mqttUsername,
password: monitor.mqttPassword,
interval: monitor.interval,
websocketPath: monitor.mqttWebsocketPath,
});
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
@@ -24,7 +25,7 @@ class MqttMonitorType extends MonitorType {
if (monitor.mqttCheckType === "keyword") {
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
heartbeat.msg = `Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`;
heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`;
heartbeat.status = UP;
} else {
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
@@ -52,12 +53,12 @@ class MqttMonitorType extends MonitorType {
* @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)
* password, websocketPath 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;
const { port, username, password, websocketPath, interval = 20 } = options;
// Adds MQTT protocol to the hostname if not already present
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
@@ -70,7 +71,15 @@ class MqttMonitorType extends MonitorType {
reject(new Error("Timeout, Message not received"));
}, interval * 1000 * 0.8);
const mqttUrl = `${hostname}:${port}`;
// Construct the URL based on protocol
let mqttUrl = `${hostname}:${port}`;
if (hostname.startsWith("ws://") || hostname.startsWith("wss://")) {
if (websocketPath && !websocketPath.startsWith("/")) {
mqttUrl = `${hostname}:${port}/${websocketPath || ""}`;
} else {
mqttUrl = `${hostname}:${port}${websocketPath || ""}`;
}
}
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
@@ -101,11 +110,9 @@ class MqttMonitorType extends MonitorType {
});
client.on("message", (messageTopic, message) => {
if (messageTopic === topic) {
client.end();
clearTimeout(timeoutID);
resolve(message.toString("utf8"));
}
client.end();
clearTimeout(timeoutID);
resolve([ messageTopic, message.toString("utf8") ]);
});
});

View File

@@ -0,0 +1,35 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const nodemailer = require("nodemailer");
class SMTPMonitorType extends MonitorType {
name = "smtp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let options = {
port: monitor.port || 25,
host: monitor.hostname,
secure: monitor.smtpSecurity === "secure", // use SMTPS (not STARTTLS)
ignoreTLS: monitor.smtpSecurity === "nostarttls", // don't use STARTTLS even if it's available
requireTLS: monitor.smtpSecurity === "starttls", // use STARTTLS or fail
};
let transporter = nodemailer.createTransport(options);
try {
await transporter.verify();
heartbeat.status = UP;
heartbeat.msg = "SMTP connection verifies successfully";
} catch (e) {
throw new Error(`SMTP connection doesn't verify: ${e}`);
} finally {
transporter.close();
}
}
}
module.exports = {
SMTPMonitorType,
};

View File

@@ -17,12 +17,14 @@ class Elks extends NotificationProvider {
data.append("to", notification.elksToNumber );
data.append("message", msg);
const config = {
let config = {
headers: {
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
}
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(url, data, config);
return okMsg;

View File

@@ -30,6 +30,8 @@ class Alerta extends NotificationProvider {
type: "exceptionAlert",
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let postData = Object.assign({
event: "msg",

View File

@@ -41,7 +41,9 @@ class AlertNow extends NotificationProvider {
"event_id": eventId,
};
await axios.post(notification.alertNowWebhookURL, data);
let config = this.getAxiosConfigWithProxy({});
await axios.post(notification.alertNowWebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -72,6 +72,8 @@ class AliyunSMS extends NotificationProvider {
data: qs.stringify(params),
};
config = this.getAxiosConfigWithProxy(config);
let result = await axios(config);
if (result.data.Message === "OK") {
return true;

View File

@@ -0,0 +1,34 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Bale extends NotificationProvider {
name = "bale";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://tapi.bale.ai";
try {
await axios.post(
`${url}/bot${notification.baleBotToken}/sendMessage`,
{
chat_id: notification.baleChatID,
text: msg
},
{
headers: {
"content-type": "application/json",
},
}
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Bale;

View File

@@ -96,12 +96,13 @@ class Bark extends NotificationProvider {
*/
async postNotification(notification, title, subtitle, endpoint) {
let result;
let config = this.getAxiosConfigWithProxy({});
if (notification.apiVersion === "v1" || notification.apiVersion == null) {
// url encode title and subtitle
title = encodeURIComponent(title);
subtitle = encodeURIComponent(subtitle);
const params = this.additionalParameters(notification);
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`);
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`, config);
} else {
result = await axios.post(`${endpoint}/push`, {
title,
@@ -109,7 +110,7 @@ class Bark extends NotificationProvider {
icon: barkNotificationAvatar,
sound: notification.barkSound || "telegraph", // default sound is telegraph
group: notification.barkGroup || "UptimeKuma", // default group is UptimeKuma
});
}, config);
}
this.checkResult(result);
if (result.statusText != null) {

View File

@@ -19,7 +19,8 @@ class Bitrix24 extends NotificationProvider {
"ATTACH[BLOCKS][0][MESSAGE]": msg
};
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params });
let config = this.getAxiosConfigWithProxy({ params });
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, config);
return okMsg;
} catch (error) {

View File

@@ -0,0 +1,63 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Brevo extends NotificationProvider {
name = "Brevo";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"api-key": notification.brevoApiKey,
},
};
config = this.getAxiosConfigWithProxy(config);
let to = [{ email: notification.brevoToEmail }];
let data = {
sender: {
email: notification.brevoFromEmail.trim(),
name: notification.brevoFromName || "Uptime Kuma"
},
to: to,
subject: notification.brevoSubject || "Notification from Your Uptime Kuma",
htmlContent: `<html><head></head><body><p>${msg.replace(/\n/g, "<br>")}</p></body></html>`
};
if (notification.brevoCcEmail) {
data.cc = notification.brevoCcEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
if (notification.brevoBccEmail) {
data.bcc = notification.brevoBccEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
let result = await axios.post(
"https://api.brevo.com/v3/smtp/email",
data,
config
);
if (result.status === 201) {
return okMsg;
} else {
throw new Error(`Unexpected status code: ${result.status}`);
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Brevo;

View File

@@ -12,7 +12,8 @@ class CallMeBot extends NotificationProvider {
try {
const url = new URL(notification.callMeBotEndpoint);
url.searchParams.set("text", msg);
await axios.get(url.toString());
let config = this.getAxiosConfigWithProxy({});
await axios.get(url.toString(), config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -22,7 +22,8 @@ class Cellsynt extends NotificationProvider {
}
};
try {
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, data);
let config = this.getAxiosConfigWithProxy(data);
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, config);
if (resp.data == null ) {
throw new Error("Could not connect to Cellsynt, please try again.");
} else if (resp.data.includes("Error:")) {

View File

@@ -29,6 +29,7 @@ class ClickSendSMS extends NotificationProvider {
}
]
};
config = this.getAxiosConfigWithProxy(config);
let resp = await axios.post(url, data, config);
if (resp.data.data.messages[0].status !== "SUCCESS") {
let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + ".";

View File

@@ -11,17 +11,23 @@ class DingDing extends NotificationProvider {
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const mentionAll = notification.mentioning === "everyone";
const mobileList = notification.mentioning === "specify-mobiles" ? notification.mobileList : [];
const userList = notification.mentioning === "specify-users" ? notification.userList : [];
const finalList = [ ...mobileList || [], ...userList || [] ];
const mentionStr = finalList.length > 0 ? "\n" : "" + finalList.map(item => `@${item}`).join(" ");
try {
if (heartbeatJSON != null) {
let params = {
msgtype: "markdown",
markdown: {
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}${mentionStr}`,
},
"at": {
"isAtAll": notification.mentioning === "everyone"
at: {
isAtAll: mentionAll,
atUserIds: userList,
atMobiles: mobileList
}
};
if (await this.sendToDingDing(notification, params)) {
@@ -31,7 +37,12 @@ class DingDing extends NotificationProvider {
let params = {
msgtype: "text",
text: {
content: msg
content: `${msg}${mentionStr}`
},
at: {
isAtAll: mentionAll,
atUserIds: userList,
atMobiles: mobileList
}
};
if (await this.sendToDingDing(notification, params)) {
@@ -60,6 +71,7 @@ class DingDing extends NotificationProvider {
url: `${notification.webHookUrl}&timestamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
data: JSON.stringify(params),
};
config = this.getAxiosConfigWithProxy(config);
let result = await axios(config);
if (result.data.errmsg === "ok") {

View File

@@ -12,24 +12,36 @@ class Discord extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
const webhookUrl = new URL(notification.discordWebhookUrl);
if (notification.discordChannelType === "postToThread") {
webhookUrl.searchParams.append("thread_id", notification.threadId);
}
// Check if the webhook has an avatar
let webhookHasAvatar = true;
try {
const webhookInfo = await axios.get(webhookUrl.toString(), config);
webhookHasAvatar = !!webhookInfo.data.avatar;
} catch (e) {
// If we can't verify, we assume he has an avatar to avoid forcing the default avatar
webhookHasAvatar = true;
}
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let discordtestdata = {
username: discordDisplayName,
content: msg,
};
if (!webhookHasAvatar) {
discordtestdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") {
discordtestdata.thread_name = notification.postName;
}
await axios.post(webhookUrl.toString(), discordtestdata);
await axios.post(webhookUrl.toString(), discordtestdata, config);
return okMsg;
}
@@ -46,10 +58,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
{
...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
},
}] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
@@ -61,6 +73,9 @@ class Discord extends NotificationProvider {
],
}],
};
if (!webhookHasAvatar) {
discorddowndata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") {
discorddowndata.thread_name = notification.postName;
}
@@ -68,7 +83,7 @@ class Discord extends NotificationProvider {
discorddowndata.content = notification.discordPrefixMessage;
}
await axios.post(webhookUrl.toString(), discorddowndata);
await axios.post(webhookUrl.toString(), discorddowndata, config);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
@@ -83,10 +98,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
{
...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
},
}] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
@@ -98,6 +113,9 @@ class Discord extends NotificationProvider {
],
}],
};
if (!webhookHasAvatar) {
discordupdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") {
discordupdata.thread_name = notification.postName;
@@ -107,7 +125,7 @@ class Discord extends NotificationProvider {
discordupdata.content = notification.discordPrefixMessage;
}
await axios.post(webhookUrl.toString(), discordupdata);
await axios.post(webhookUrl.toString(), discordupdata, config);
return okMsg;
}
} catch (error) {

View File

@@ -0,0 +1,40 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Evolution extends NotificationProvider {
name = "evolution";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"apikey": notification.evolutionAuthToken,
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"number": notification.evolutionRecipient,
"text": msg,
};
let url = (notification.evolutionApiUrl || "https://evolapicloud.com/").replace(/([^/])\/+$/, "$1") + "/message/sendText/" + encodeURIComponent(notification.evolutionInstanceName);
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Evolution;

View File

@@ -12,6 +12,7 @@ class Feishu extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON == null) {
let testdata = {
msg_type: "text",
@@ -19,7 +20,7 @@ class Feishu extends NotificationProvider {
text: msg,
},
};
await axios.post(notification.feishuWebHookUrl, testdata);
await axios.post(notification.feishuWebHookUrl, testdata, config);
return okMsg;
}
@@ -49,7 +50,7 @@ class Feishu extends NotificationProvider {
]
}
};
await axios.post(notification.feishuWebHookUrl, downdata);
await axios.post(notification.feishuWebHookUrl, downdata, config);
return okMsg;
}
@@ -79,7 +80,7 @@ class Feishu extends NotificationProvider {
]
}
};
await axios.post(notification.feishuWebHookUrl, updata);
await axios.post(notification.feishuWebHookUrl, updata, config);
return okMsg;
}
} catch (error) {

View File

@@ -38,7 +38,7 @@ class FlashDuty extends NotificationProvider {
}
/**
* Generate a monitor url from the monitors infomation
* Generate a monitor url from the monitors information
* @param {object} monitorInfo Monitor details
* @returns {string|undefined} Monitor URL
*/
@@ -73,13 +73,13 @@ class FlashDuty extends NotificationProvider {
}
const options = {
method: "POST",
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
url: notification.flashdutyIntegrationKey.startsWith("http") ? notification.flashdutyIntegrationKey : "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
headers: { "Content-Type": "application/json" },
data: {
description: `[${title}] [${monitorInfo.name}] ${body}`,
title,
event_status: eventStatus || "Info",
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
alert_key: monitorInfo.id ? String(monitorInfo.id) : Math.random().toString(36).substring(7),
labels,
}
};

View File

@@ -11,10 +11,11 @@ class FreeMobile extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
"user": notification.freemobileUser,
"pass": notification.freemobilePass,
});
}, config);
return okMsg;

View File

@@ -24,6 +24,7 @@ class GoAlert extends NotificationProvider {
let config = {
headers: headers
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
return okMsg;
} catch (error) {

View File

@@ -12,8 +12,44 @@ class GoogleChat extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
// If Google Chat Webhook rate limit is reached, retry to configured max retries defaults to 3, delay between 60-180 seconds
const post = async (url, data, config) => {
let retries = notification.googleChatMaxRetries || 1; // Default to 1 retries
retries = (retries > 10) ? 10 : retries; // Enforce maximum retries in backend
while (retries > 0) {
try {
await axios.post(url, data, config);
return;
} catch (error) {
if (error.response && error.response.status === 429) {
retries--;
if (retries === 0) {
throw error;
}
const delay = 60000 + Math.random() * 120000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
try {
let config = this.getAxiosConfigWithProxy({});
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
if (notification.googleChatUseTemplate && notification.googleChatTemplate) {
// Send message using template
const renderedText = await this.renderTemplate(
notification.googleChatTemplate,
msg,
monitorJSON,
heartbeatJSON
);
const data = { "text": renderedText };
await post(notification.googleChatWebhookURL, data, config);
return okMsg;
}
let chatHeader = {
title: "Uptime Kuma Alert",
@@ -83,12 +119,11 @@ class GoogleChat extends NotificationProvider {
],
};
await axios.post(notification.googleChatWebhookURL, data);
await post(notification.googleChatWebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}

View File

@@ -31,8 +31,7 @@ class Gorush extends NotificationProvider {
}
]
};
let config = {};
let config = this.getAxiosConfigWithProxy({});
await axios.post(`${notification.gorushServerURL}/api/push`, data, config);
return okMsg;
} catch (error) {

View File

@@ -11,6 +11,7 @@ class Gotify extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
}
@@ -18,7 +19,7 @@ class Gotify extends NotificationProvider {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
});
}, config);
return okMsg;

View File

@@ -16,13 +16,14 @@ class GrafanaOncall extends NotificationProvider {
}
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON === null) {
let grafanaupdata = {
title: "General notification",
message: msg,
state: "alerting",
};
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
await axios.post(notification.GrafanaOncallURL, grafanaupdata, config);
return okMsg;
} else if (heartbeatJSON["status"] === DOWN) {
let grafanadowndata = {
@@ -30,7 +31,7 @@ class GrafanaOncall extends NotificationProvider {
message: heartbeatJSON["msg"],
state: "alerting",
};
await axios.post(notification.GrafanaOncallURL, grafanadowndata);
await axios.post(notification.GrafanaOncallURL, grafanadowndata, config);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
let grafanaupdata = {
@@ -38,7 +39,7 @@ class GrafanaOncall extends NotificationProvider {
message: heartbeatJSON["msg"],
state: "ok",
};
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
await axios.post(notification.GrafanaOncallURL, grafanaupdata, config);
return okMsg;
}
} catch (error) {

View File

@@ -14,6 +14,7 @@ class GtxMessaging extends NotificationProvider {
const text = msg.replaceAll("🔴 ", "").replaceAll("✅ ", "");
try {
let config = this.getAxiosConfigWithProxy({});
const data = new URLSearchParams();
data.append("from", notification.gtxMessagingFrom.trim());
data.append("to", notification.gtxMessagingTo.trim());
@@ -21,7 +22,7 @@ class GtxMessaging extends NotificationProvider {
const url = `https://rest.gtx-messaging.net/smsc/sendsms/${notification.gtxMessagingApiKey}/json`;
await axios.post(url, data);
await axios.post(url, data, config);
return okMsg;
} catch (error) {

View File

@@ -18,7 +18,7 @@ class HeiiOnCall extends NotificationProvider {
payload["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
}
const config = {
let config = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
@@ -28,6 +28,7 @@ class HeiiOnCall extends NotificationProvider {
const heiiUrl = `https://heiioncall.com/triggers/${notification.heiiOnCallTriggerId}/`;
// docs https://heiioncall.com/docs#manual-triggers
try {
config = this.getAxiosConfigWithProxy(config);
if (!heartbeatJSON) {
// Testing or general notification like certificate expiry
payload["msg"] = msg;

View File

@@ -15,6 +15,13 @@ class HomeAssistant extends NotificationProvider {
const notificationService = notification?.notificationService || defaultNotificationService;
try {
let config = {
headers: {
Authorization: `Bearer ${notification.longLivedAccessToken}`,
"Content-Type": "application/json",
},
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
{
@@ -26,14 +33,7 @@ class HomeAssistant extends NotificationProvider {
channel: "Uptime Kuma",
icon_url: "https://github.com/louislam/uptime-kuma/blob/master/public/icon.png?raw=true",
} }),
},
{
headers: {
Authorization: `Bearer ${notification.longLivedAccessToken}`,
"Content-Type": "application/json",
},
}
);
}, config);
return okMsg;
} catch (error) {

View File

@@ -31,6 +31,8 @@ class Keep extends NotificationProvider {
let webhookURL = url + "/alerts/event/uptimekuma";
config = this.getAxiosConfigWithProxy(config);
await axios.post(webhookURL, data, config);
return okMsg;
} catch (error) {

View File

@@ -22,6 +22,7 @@ class Kook extends NotificationProvider {
},
};
try {
config = this.getAxiosConfigWithProxy(config);
await axios.post(url, data, config);
return okMsg;

View File

@@ -19,6 +19,7 @@ class Line extends NotificationProvider {
"Authorization": "Bearer " + notification.lineChannelAccessToken
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let testMessage = {
"to": notification.lineUserID,

View File

@@ -20,6 +20,7 @@ class LineNotify extends NotificationProvider {
"Authorization": "Bearer " + notification.lineNotifyAccessToken
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let testMessage = {
"message": msg,

View File

@@ -13,13 +13,14 @@ class LunaSea extends NotificationProvider {
const url = "https://notify.lunasea.app/v1";
try {
let config = this.getAxiosConfigWithProxy({});
const target = this.getTarget(notification);
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": msg,
};
await axios.post(`${url}/custom/${target}`, testdata);
await axios.post(`${url}/custom/${target}`, testdata, config);
return okMsg;
}
@@ -30,7 +31,7 @@ class LunaSea extends NotificationProvider {
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(`${url}/custom/${target}`, downdata);
await axios.post(`${url}/custom/${target}`, downdata, config);
return okMsg;
}
@@ -41,7 +42,7 @@ class LunaSea extends NotificationProvider {
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(`${url}/custom/${target}`, updata);
await axios.post(`${url}/custom/${target}`, updata, config);
return okMsg;
}

View File

@@ -37,6 +37,7 @@ class Matrix extends NotificationProvider {
"body": msg
};
config = this.getAxiosConfigWithProxy(config);
await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config);
return okMsg;
} catch (error) {

View File

@@ -12,6 +12,7 @@ class Mattermost extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
@@ -19,7 +20,7 @@ class Mattermost extends NotificationProvider {
username: mattermostUserName,
text: msg,
};
await axios.post(notification.mattermostWebhookUrl, mattermostTestData);
await axios.post(notification.mattermostWebhookUrl, mattermostTestData, config);
return okMsg;
}
@@ -98,7 +99,7 @@ class Mattermost extends NotificationProvider {
},
],
};
await axios.post(notification.mattermostWebhookUrl, mattermostdata);
await axios.post(notification.mattermostWebhookUrl, mattermostdata, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -0,0 +1,66 @@
const { UP, DOWN } = require("../../src/util");
const Crypto = require("crypto");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class NextcloudTalk extends NotificationProvider {
name = "nextcloudtalk";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
// See documentation at https://nextcloud-talk.readthedocs.io/en/latest/bots/#sending-a-chat-message
const okMsg = "Sent Successfully.";
// Create a random string
const talkRandom = encodeURIComponent(
Crypto
.randomBytes(64)
.toString("hex")
.slice(0, 64)
);
// Create the signature over random and message
const talkSignature = Crypto
.createHmac("sha256", Buffer.from(notification.botSecret, "utf8"))
.update(Buffer.from(`${talkRandom}${msg}`, "utf8"))
.digest("hex");
let silentUp = (heartbeatJSON?.status === UP && notification.sendSilentUp);
let silentDown = (heartbeatJSON?.status === DOWN && notification.sendSilentDown);
let silent = (silentUp || silentDown);
let url = `${notification.host}/ocs/v2.php/apps/spreed/api/v1/bot/${notification.conversationToken}/message`;
let config = this.getAxiosConfigWithProxy({});
const data = {
message: msg,
silent
};
const options = {
...config,
headers: {
"X-Nextcloud-Talk-Bot-Random": talkRandom,
"X-Nextcloud-Talk-Bot-Signature": talkSignature,
"OCS-APIRequest": true,
}
};
try {
let result = await axios.post(url, data, options);
if (result?.status === 201) {
return okMsg;
}
throw new Error("Nextcloud Talk Error " + (result?.status ?? "Unknown"));
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = NextcloudTalk;

View File

@@ -0,0 +1,54 @@
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { setting } = require("../util-server");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Notifery extends NotificationProvider {
name = "notifery";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.notifery.com/event";
let data = {
title: notification.notiferyTitle || "Uptime Kuma Alert",
message: msg,
};
if (notification.notiferyGroup) {
data.group = notification.notiferyGroup;
}
// Link to the monitor
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
data.message += `\n\nMonitor: ${baseURL}${getMonitorRelativeURL(monitorJSON.id)}`;
}
if (heartbeatJSON) {
data.code = heartbeatJSON.status === UP ? 0 : 1;
if (heartbeatJSON.ping) {
data.duration = heartbeatJSON.ping;
}
}
try {
const headers = {
"Content-Type": "application/json",
"x-api-key": notification.notiferyApiKey,
};
let config = this.getAxiosConfigWithProxy({ headers });
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Notifery;

View File

@@ -1,5 +1,7 @@
const { Liquid } = require("liquidjs");
const { DOWN } = require("../../src/util");
const { HttpProxyAgent } = require("http-proxy-agent");
const { HttpsProxyAgent } = require("https-proxy-agent");
class NotificationProvider {
@@ -61,7 +63,11 @@ class NotificationProvider {
* @returns {Promise<string>} rendered template
*/
async renderTemplate(template, msg, monitorJSON, heartbeatJSON) {
const engine = new Liquid();
const engine = new Liquid({
root: "./no-such-directory-uptime-kuma",
relativeReference: false,
dynamicPartials: false,
});
const parsedTpl = engine.parse(template);
// Let's start with dummy values to simplify code
@@ -115,6 +121,30 @@ class NotificationProvider {
throw new Error(msg);
}
/**
* Returns axios config with proxy agent if proxy env is set.
* @param {object} axiosConfig - Axios config containing params
* @returns {object} Axios config
*/
getAxiosConfigWithProxy(axiosConfig = {}) {
const proxyEnv = process.env.notification_proxy || process.env.NOTIFICATION_PROXY;
if (proxyEnv) {
const proxyUrl = new URL(proxyEnv);
if (proxyUrl.protocol === "http:") {
axiosConfig.httpAgent = new HttpProxyAgent(proxyEnv);
axiosConfig.httpsAgent = new HttpsProxyAgent(proxyEnv);
} else if (proxyUrl.protocol === "https:") {
const agent = new HttpsProxyAgent(proxyEnv);
axiosConfig.httpAgent = agent;
axiosConfig.httpsAgent = agent;
}
axiosConfig.proxy = false;
}
return axiosConfig;
}
}
module.exports = NotificationProvider;

View File

@@ -22,6 +22,8 @@ class Ntfy extends NotificationProvider {
"Authorization": "Bearer " + notification.ntfyaccesstoken,
};
}
let config = { headers };
config = this.getAxiosConfigWithProxy(config);
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
let ntfyTestData = {
@@ -31,7 +33,7 @@ class Ntfy extends NotificationProvider {
"priority": notification.ntfyPriority,
"tags": [ "test_tube" ],
};
await axios.post(notification.ntfyserverurl, ntfyTestData, { headers: headers });
await axios.post(notification.ntfyserverurl, ntfyTestData, config);
return okMsg;
}
let tags = [];
@@ -41,8 +43,8 @@ class Ntfy extends NotificationProvider {
if (heartbeatJSON.status === DOWN) {
tags = [ "red_circle" ];
status = "Down";
// if priority is not 5, increase priority for down alerts
priority = priority === 5 ? priority : priority + 1;
// defaults to max(priority + 1, 5)
priority = notification.ntfyPriorityDown || (priority === 5 ? priority : priority + 1);
} else if (heartbeatJSON["status"] === UP) {
tags = [ "green_circle" ];
status = "Up";
@@ -70,7 +72,7 @@ class Ntfy extends NotificationProvider {
data.icon = notification.ntfyIcon;
}
await axios.post(notification.ntfyserverurl, data, { headers: headers });
await axios.post(notification.ntfyserverurl, data, config);
return okMsg;

View File

@@ -22,6 +22,7 @@ class Octopush extends NotificationProvider {
"cache-control": "no-cache"
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"recipients": [
{
@@ -53,6 +54,7 @@ class Octopush extends NotificationProvider {
},
params: data
};
config = this.getAxiosConfigWithProxy(config);
// V1 API returns 200 even on error so we must check
// response data

View File

@@ -25,6 +25,7 @@ class OneBot extends NotificationProvider {
"Authorization": "Bearer " + notification.accessToken,
}
};
config = this.getAxiosConfigWithProxy(config);
let pushText = "UptimeKuma Alert: " + msg;
let data = {
"auto_escape": true,

View File

@@ -13,12 +13,13 @@ class OneChat extends NotificationProvider {
const url = "https://chat-api.one.th/message/api/v1/push_message";
try {
const config = {
let config = {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + notification.accessToken,
},
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
const testMessage = {
to: notification.recieverId,

View File

@@ -33,6 +33,7 @@ class Onesender extends NotificationProvider {
"Authorization": "Bearer " + notification.onesenderToken,
}
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(notification.onesenderURL, data, config);
return okMsg;

View File

@@ -80,6 +80,7 @@ class Opsgenie extends NotificationProvider {
"Authorization": `GenieKey ${notification.opsgenieApiKey}`,
}
};
config = this.getAxiosConfigWithProxy(config);
let res = await axios.post(url, data, config);
if (res.status == null) {

View File

@@ -15,7 +15,7 @@ class PromoSMS extends NotificationProvider {
notification.promosmsAllowLongSMS = false;
}
//TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars.
//TODO: Add option for enabling special characters. It will decrease message max length from 160 to 70 chars.
//Lets remove non ascii char
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "");
@@ -27,6 +27,7 @@ class PromoSMS extends NotificationProvider {
"Accept": "text/json",
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"recipients": [ notification.promosmsPhoneNumber ],
//Trim message to maximum length of 1 SMS or 4 if we allowed long messages

View File

@@ -12,6 +12,7 @@ class Pumble extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON === null && monitorJSON === null) {
let data = {
"attachments": [
@@ -23,7 +24,7 @@ class Pumble extends NotificationProvider {
]
};
await axios.post(notification.webhookURL, data);
await axios.post(notification.webhookURL, data, config);
return okMsg;
}
@@ -37,7 +38,7 @@ class Pumble extends NotificationProvider {
]
};
await axios.post(notification.webhookURL, data);
await axios.post(notification.webhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -20,6 +20,7 @@ class Pushbullet extends NotificationProvider {
"Content-Type": "application/json"
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let data = {
"type": "note",

View File

@@ -11,7 +11,7 @@ class PushDeer extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
// capture group below is nessesary to prevent an ReDOS-attack
// capture group below is necessary to prevent an ReDOS-attack
const url = `${serverUrl.trim().replace(/([^/])\/+$/, "$1")}/message/push`;
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
@@ -33,7 +33,8 @@ class PushDeer extends NotificationProvider {
};
try {
let res = await axios.post(url, data);
let config = this.getAxiosConfigWithProxy({});
let res = await axios.post(url, data, config);
if ("error" in res.data) {
let error = res.data.error;

View File

@@ -41,8 +41,9 @@ class Pushover extends NotificationProvider {
}
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON == null) {
await axios.post(url, data);
await axios.post(url, data, config);
return okMsg;
}
@@ -52,7 +53,7 @@ class Pushover extends NotificationProvider {
}
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>: ${heartbeatJSON["localDateTime"]}`;
await axios.post(url, data);
await axios.post(url, data, config);
return okMsg;
} catch (error) {

View File

@@ -17,11 +17,12 @@ class PushPlus extends NotificationProvider {
const okMsg = "Sent Successfully.";
const url = "https://www.pushplus.plus/send";
try {
const config = {
let config = {
headers: {
"Content-Type": "application/json",
},
};
config = this.getAxiosConfigWithProxy(config);
const params = {
"token": notification.pushPlusSendKey,
"title": this.checkStatus(heartbeatJSON, monitorJSON),

View File

@@ -11,6 +11,7 @@ class Pushy extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
"to": notification.pushyToken,
"data": {
@@ -21,7 +22,7 @@ class Pushy extends NotificationProvider {
"badge": 1,
"sound": "ping.aiff"
}
});
}, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -14,6 +14,7 @@ class RocketChat extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON == null) {
let data = {
"text": msg,
@@ -21,7 +22,7 @@ class RocketChat extends NotificationProvider {
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
};
await axios.post(notification.rocketwebhookURL, data);
await axios.post(notification.rocketwebhookURL, data, config);
return okMsg;
}
@@ -55,7 +56,7 @@ class RocketChat extends NotificationProvider {
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);
}
await axios.post(notification.rocketwebhookURL, data);
await axios.post(notification.rocketwebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);

View File

@@ -17,7 +17,7 @@ class SendGrid extends NotificationProvider {
Authorization: `Bearer ${notification.sendgridApiKey}`,
},
};
config = this.getAxiosConfigWithProxy(config);
let personalizations = {
to: [{ email: notification.sendgridToEmail }],
};

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