mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-10 12:56:13 +08:00
Compare commits
580 Commits
1.21.0-bet
...
1.23.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
de83863627 | ||
|
d7e0ff4b8c | ||
|
112cc3d7b8 | ||
|
b44f6e4af2 | ||
|
18a2a8eb1e | ||
|
e9585ccbf4 | ||
|
5e39b0daf6 | ||
|
71fca3f0c3 | ||
|
2921f33c24 | ||
|
4e0bb394db | ||
|
ced576feba | ||
|
ceb5708bfd | ||
|
23fdd32de0 | ||
|
36777c5eff | ||
|
439b6517d1 | ||
|
c6e68fac97 | ||
|
bce4835362 | ||
|
a032e11a2e | ||
|
d231a05526 | ||
|
f4967615c0 | ||
|
67b1974718 | ||
|
062e9db2a7 | ||
|
90e1b4cf56 | ||
|
43698b23c6 | ||
|
f19b27c1c7 | ||
|
42b5d30a33 | ||
|
cda77c1a32 | ||
|
157cf47d38 | ||
|
09c6798a30 | ||
|
587d9e4781 | ||
|
ae2867e305 | ||
|
65ffd77d35 | ||
|
9dd652733e | ||
|
0a59fef7d8 | ||
|
27ce47277b | ||
|
7f68e4a987 | ||
|
149d08ecbb | ||
|
eb6167aaf1 | ||
|
db66195f7d | ||
|
59245e624d | ||
|
c0eb0cb42c | ||
|
573f158f7f | ||
|
6413d4cbdf | ||
|
cf5a04bc5e | ||
|
a0203372ce | ||
|
5ccf2d23fc | ||
|
bf68e0a7bc | ||
|
6984596568 | ||
|
f3562864ee | ||
|
9058a829a1 | ||
|
0684313ec9 | ||
|
d7e12dc92d | ||
|
1d9a28e9ab | ||
|
ddbf367011 | ||
|
50d4091ded | ||
|
66cfbd02c3 | ||
|
688f23035b | ||
|
7701e2ad36 | ||
|
8e72d6f534 | ||
|
278b88a9d9 | ||
|
084cf01fcd | ||
|
f6c1b92fc6 | ||
|
25c8196641 | ||
|
baf5613dfa | ||
|
695691468c | ||
|
4891ec4527 | ||
|
e2a87eb430 | ||
|
80927332cb | ||
|
a0eb733d54 | ||
|
21d556528f | ||
|
357466cc90 | ||
|
b038d09349 | ||
|
5dd4231e56 | ||
|
c6d0c431bd | ||
|
d1b7f4c834 | ||
|
5c4180fb45 | ||
|
345e61abca | ||
|
dd1526deff | ||
|
be26bb75d9 | ||
|
99fb5836e2 | ||
|
2f5a565ce4 | ||
|
973db9d4b2 | ||
|
2e75142fe5 | ||
|
ad3ffacf45 | ||
|
6f4af30701 | ||
|
b1f266ceb1 | ||
|
6bece8796e | ||
|
e7d1b4e14a | ||
|
e5c6783781 | ||
|
ac68a35d3a | ||
|
d825dbf828 | ||
|
cfb4bbc6cb | ||
|
293015ff35 | ||
|
18d8b3a8e0 | ||
|
d55794e1a5 | ||
|
3d50572dd7 | ||
|
cdb38d49eb | ||
|
80b55786a4 | ||
|
fe40d819bd | ||
|
3dbd8277f0 | ||
|
771d21c4ad | ||
|
ed6b4e5ae5 | ||
|
3b9c95a8a8 | ||
|
cdf6922bdd | ||
|
9954ba82e7 | ||
|
19873e5b9e | ||
|
13ae878ee8 | ||
|
4ea5771f97 | ||
|
1774bb86dc | ||
|
c583037dff | ||
|
8223121cd8 | ||
|
ff22010330 | ||
|
a9d691a6a8 | ||
|
7c529d8f83 | ||
|
4fe0891a60 | ||
|
bd5496d267 | ||
|
a0736e04b2 | ||
|
df8fcffb19 | ||
|
9da712054a | ||
|
af78da1dd9 | ||
|
9e041f219b | ||
|
8c60e902e1 | ||
|
de74efb2e6 | ||
|
9ee2780e9e | ||
|
a386f1fc9e | ||
|
35154ef9c5 | ||
|
1baa592824 | ||
|
9882fc65b1 | ||
|
3e5e7e6e32 | ||
|
0e725569e5 | ||
|
afcfb7e19c | ||
|
eaee55fc8f | ||
|
affac0a97b | ||
|
a12e7eba72 | ||
|
4f6035899d | ||
|
dd77baabe1 | ||
|
820f2eec9f | ||
|
4b913c8b4c | ||
|
d01c7c3faa | ||
|
772a946234 | ||
|
f8c9a20afd | ||
|
cea894cc6d | ||
|
79b38e0e7b | ||
|
7cc9783436 | ||
|
21405f71b5 | ||
|
b4b6e07e6b | ||
|
cf4220901b | ||
|
f3996fdef4 | ||
|
1dfe5227ad | ||
|
4ead0609af | ||
|
a8bf52b1e0 | ||
|
ede6d90497 | ||
|
4b8e86efb7 | ||
|
5f706e1921 | ||
|
722c64a4d1 | ||
|
23de52ca5a | ||
|
3d3fb357f9 | ||
|
3b9aa00126 | ||
|
29267e5c2e | ||
|
3e801323b6 | ||
|
b80fd81d24 | ||
|
9cb776405a | ||
|
de7ae3e2db | ||
|
e49ced0524 | ||
|
7e782edf44 | ||
|
43e1e3c272 | ||
|
dc4cf7087f | ||
|
65a0a2b2b5 | ||
|
2d269c3639 | ||
|
11bad53709 | ||
|
9f7782b1c1 | ||
|
9fb8f94e22 | ||
|
7a34103da6 | ||
|
8955c3816b | ||
|
7761e9a05e | ||
|
c9d6e576ab | ||
|
97d38ee1a8 | ||
|
cc94609423 | ||
|
149f8c3646 | ||
|
bdcbd6389b | ||
|
c06b929529 | ||
|
d3ecdb8456 | ||
|
4e420ee3ff | ||
|
a00561ff09 | ||
|
6af44e0780 | ||
|
596402e71f | ||
|
62bbc1cf55 | ||
|
19fc7d31e6 | ||
|
6708eed121 | ||
|
3c56a6f395 | ||
|
2b46693995 | ||
|
c61a3d360f | ||
|
392f95cdd2 | ||
|
dfc6e5ea5b | ||
|
ba4d925374 | ||
|
d37c33ad42 | ||
|
c04194191f | ||
|
de9ad0fe60 | ||
|
8884c2108b | ||
|
ac8ca36895 | ||
|
71c34694b7 | ||
|
2128ed5ce3 | ||
|
09ab6a015b | ||
|
ec858eb67a | ||
|
c4c3fc81b2 | ||
|
8aa577529f | ||
|
cdd5067b17 | ||
|
5fdb01308a | ||
|
eb1a1d0ac7 | ||
|
a1fc283b3c | ||
|
419b684433 | ||
|
8bc139d8c1 | ||
|
91dfd8dfaa | ||
|
1f405cf2a0 | ||
|
cf61077dd8 | ||
|
4396e0d4d8 | ||
|
240db1d173 | ||
|
ef06b5376d | ||
|
186d733134 | ||
|
225ba61e22 | ||
|
11b32ce553 | ||
|
3707919025 | ||
|
d10e378fb1 | ||
|
370328c3b8 | ||
|
1d8a82ae3e | ||
|
7da336e975 | ||
|
b8c12cca2a | ||
|
88fcfcc6fc | ||
|
fcb22f7d05 | ||
|
5be41990bc | ||
|
09149f50e0 | ||
|
8f60274582 | ||
|
7dadac3ebe | ||
|
28c29f755d | ||
|
b426840b5b | ||
|
4f2d39d5fc | ||
|
4012fc6964 | ||
|
d1b52bc098 | ||
|
c9a32f9dbb | ||
|
b884f82de6 | ||
|
65928a26c7 | ||
|
abe00efa7f | ||
|
0c364fc288 | ||
|
0d21529037 | ||
|
37ae8eb44a | ||
|
8897385690 | ||
|
6132a45c7c | ||
|
f68452c47a | ||
|
fea8ef8367 | ||
|
9d71e34a83 | ||
|
37031fb9a7 | ||
|
58ec53fb1d | ||
|
57190b58c6 | ||
|
6c2948d2de | ||
|
68f389868c | ||
|
af5d7cbb0b | ||
|
1fa8c0f9fe | ||
|
9a8bea5761 | ||
|
56f448bfe5 | ||
|
2b46da0f47 | ||
|
9bd76c2795 | ||
|
376d84c742 | ||
|
4b3a2ee71b | ||
|
1634df5a39 | ||
|
039fdb0730 | ||
|
20af2d9d95 | ||
|
04806ba4f3 | ||
|
3ff910a8f8 | ||
|
343a1d3344 | ||
|
f1c184c30c | ||
|
f3fe392ec4 | ||
|
f3c09f2bbd | ||
|
85eb084305 | ||
|
0735f12d19 | ||
|
8ed2b59410 | ||
|
0b8dddba24 | ||
|
2114295381 | ||
|
bc95875aa0 | ||
|
c1efe0f26d | ||
|
a0d0d5b015 | ||
|
8d05d80a5f | ||
|
36942de329 | ||
|
771ca09331 | ||
|
3cb287a40e | ||
|
9c3bb67b6b | ||
|
5200e10aab | ||
|
f1a396b0f7 | ||
|
83a59bd984 | ||
|
446b5fa9e4 | ||
|
0d1b5321ad | ||
|
1e1cc86a10 | ||
|
9825b33ef3 | ||
|
00f733d352 | ||
|
fd10897988 | ||
|
317024ed72 | ||
|
f604d96c5b | ||
|
f30f00655f | ||
|
891f09def7 | ||
|
6b5e179bb0 | ||
|
f653aba735 | ||
|
9dc02bb8e2 | ||
|
bb15fa0179 | ||
|
966066b897 | ||
|
8d24891b8e | ||
|
ba7de3fd37 | ||
|
80c8fd7372 | ||
|
a27386bb92 | ||
|
ce70b3fc62 | ||
|
06fba5b55a | ||
|
f2c294e9e5 | ||
|
332e54937e | ||
|
a1adc30a89 | ||
|
6ce882ad4a | ||
|
e392d12585 | ||
|
253214ad2b | ||
|
33de7bdb1c | ||
|
7f5d0e5490 | ||
|
1a344c1371 | ||
|
28b0f8fc00 | ||
|
0eaaa8b6fa | ||
|
5cd506e340 | ||
|
f0beccf6bf | ||
|
72c16c3aa2 | ||
|
aa8454b73f | ||
|
d23cb0b382 | ||
|
9975050872 | ||
|
f8c2909576 | ||
|
fcfe13e52d | ||
|
9f51115a19 | ||
|
4057ca6e72 | ||
|
8a3bce44ef | ||
|
dfe6f52f6a | ||
|
333a631389 | ||
|
eaa948579b | ||
|
74dd07c3ca | ||
|
f75cf3a186 | ||
|
a3e31b22bc | ||
|
078d1f96a5 | ||
|
8207f16396 | ||
|
ba82abe5f3 | ||
|
eb9c748071 | ||
|
3579520575 | ||
|
030faddd1c | ||
|
0e516a42e5 | ||
|
680dccefea | ||
|
8c9423f4de | ||
|
f433f33418 | ||
|
d4a31cf02a | ||
|
a7588adc52 | ||
|
6356b1e50a | ||
|
af6e01ee3a | ||
|
11f4cb8725 | ||
|
1bf97e701d | ||
|
4c1ac5e870 | ||
|
9e320dc5fb | ||
|
2f3f929fbd | ||
|
b776e88b26 | ||
|
49741bbef2 | ||
|
682f8e52a8 | ||
|
171aff1226 | ||
|
1f7f1f70bf | ||
|
be7d3f6142 | ||
|
7706c29564 | ||
|
9dd1b1ca0f | ||
|
21ad715e6a | ||
|
23af66f618 | ||
|
03aa685d3f | ||
|
84c1baf706 | ||
|
23808efe2a | ||
|
1db25a329f | ||
|
e314d517ad | ||
|
84d1cb73b6 | ||
|
ddd3d3bc92 | ||
|
190e85d2c8 | ||
|
d8511fa201 | ||
|
e76d29dee5 | ||
|
fb38048159 | ||
|
80f1959871 | ||
|
4ddc3b5f5e | ||
|
d173a3c663 | ||
|
45ef7b2f69 | ||
|
6b078b83bd | ||
|
22f730499f | ||
|
1be74e2720 | ||
|
32f84b5e4e | ||
|
97c7ad9cc7 | ||
|
8f449ab738 | ||
|
dbfaddafca | ||
|
511038b45a | ||
|
e8d48561fc | ||
|
aeb2feacd3 | ||
|
c01055efb3 | ||
|
17ae47d091 | ||
|
de0d1edfd4 | ||
|
0f5a243450 | ||
|
b975c24531 | ||
|
524cf7c607 | ||
|
a6acd065bb | ||
|
227cec86a8 | ||
|
ba52e1c885 | ||
|
02291730fe | ||
|
dcc065c86f | ||
|
501dc29e6d | ||
|
d2b09ef042 | ||
|
f608590526 | ||
|
a7f21bffec | ||
|
ebd42444d1 | ||
|
f0f7645c57 | ||
|
3ada6fa99b | ||
|
4fb10a4e3f | ||
|
1aac15bccc | ||
|
cbcd2a1027 | ||
|
17f038767d | ||
|
edd1c6b662 | ||
|
29be8d3ddb | ||
|
2ac1abd424 | ||
|
738a494dcb | ||
|
e575d41f7d | ||
|
8ee4b844fd | ||
|
4ae437dd61 | ||
|
6cb296b07a | ||
|
644c6a872f | ||
|
8c69c18f6d | ||
|
c1a1160767 | ||
|
a0ebd88849 | ||
|
2e9413cf33 | ||
|
278b52ec34 | ||
|
caa757a27c | ||
|
3ce117a943 | ||
|
cefe484b47 | ||
|
a700892709 | ||
|
13d721ccf8 | ||
|
6c66bff518 | ||
|
bea51d048b | ||
|
2e1a0fe4d5 | ||
|
27b0895722 | ||
|
e687698851 | ||
|
fc4312ca1a | ||
|
fbdeb30ce7 | ||
|
41bda4e1d7 | ||
|
4869e6531c | ||
|
302b9cf644 | ||
|
3c3a192943 | ||
|
b64c835cee | ||
|
5266e713e6 | ||
|
86579d245f | ||
|
b6169408be | ||
|
4f05912276 | ||
|
bf525371d9 | ||
|
89bfc3bf33 | ||
|
a2014278b8 | ||
|
70572af1af | ||
|
b31c23a43b | ||
|
f4ee5271af | ||
|
7330db3563 | ||
|
097567e5f0 | ||
|
35f300c8eb | ||
|
4c9d7ac8ca | ||
|
ca52047bf5 | ||
|
d9558833fc | ||
|
776a482a1d | ||
|
d2527d7254 | ||
|
6dfca0c163 | ||
|
df47609671 | ||
|
e63f7562f8 | ||
|
8921ed0cff | ||
|
35a56dd9e0 | ||
|
442f54de84 | ||
|
72317633d9 | ||
|
1973db28bf | ||
|
972ae60cfc | ||
|
56410afc3b | ||
|
df975c2750 | ||
|
0cfd3fa642 | ||
|
0b91391ced | ||
|
02fde56aec | ||
|
da225a225f | ||
|
a40c03f6f3 | ||
|
c238508060 | ||
|
abcbc3c55e | ||
|
350451edc8 | ||
|
b4b2ae55c0 | ||
|
9ecb890f56 | ||
|
4329fc6751 | ||
|
836e125256 | ||
|
0fe98de256 | ||
|
3825dd9f42 | ||
|
c6cd0d9312 | ||
|
82975f8d7b | ||
|
2ebbcc25a9 | ||
|
f2323b012b | ||
|
e5a6238cde | ||
|
61506b1af2 | ||
|
dbe73bd6ae | ||
|
0778549a6d | ||
|
09fa60de55 | ||
|
1e80365b73 | ||
|
491239415e | ||
|
cf59832d51 | ||
|
8f259e1756 | ||
|
29b2809279 | ||
|
16f2701f61 | ||
|
3bbf269da0 | ||
|
56d716cee4 | ||
|
0efabb4e39 | ||
|
b7aad4677d | ||
|
0a9ebf7fa4 | ||
|
09aa7be4f5 | ||
|
d3a3dcbde2 | ||
|
7447a6570f | ||
|
66e80aa6c3 | ||
|
f1c3604ab0 | ||
|
e8a81379bb | ||
|
1f13db26cc | ||
|
1777270bb7 | ||
|
062191d61f | ||
|
86e9c8ade9 | ||
|
35cf31ced1 | ||
|
4608560b1d | ||
|
6a37a2a05b | ||
|
ba7af9b569 | ||
|
6635412980 | ||
|
6e0aa109bc | ||
|
533bc1505b | ||
|
391692a708 | ||
|
a599f5149b | ||
|
f32fcb204f | ||
|
230de63460 | ||
|
e8814e8479 | ||
|
2dedc1cfbd | ||
|
65933de0cd | ||
|
ce8eebc838 | ||
|
ae5a683af8 | ||
|
70bb69fc73 | ||
|
2702335d91 | ||
|
6de57f3283 | ||
|
193a273557 | ||
|
bb7de6aa88 | ||
|
150607cc93 | ||
|
cbbd3e20ad | ||
|
beb22f743d | ||
|
6fc34e44d9 | ||
|
7b4f90ce92 | ||
|
db6b863445 | ||
|
d668812df1 | ||
|
f32d3af62c | ||
|
8a115670cd | ||
|
186ca30508 | ||
|
896e33815d | ||
|
0be8b111e2 | ||
|
3b74f5f359 | ||
|
cef0a0faf4 | ||
|
dfb95dfdcb | ||
|
43941fa2c6 | ||
|
faa78443d6 | ||
|
ab3b2bddba | ||
|
e10ba9ed7e | ||
|
9446c2d102 | ||
|
2c581ade90 | ||
|
f286386f59 | ||
|
9286dcb6ce | ||
|
a6894d36f2 | ||
|
66573934f6 | ||
|
c444d78706 | ||
|
661fa87134 | ||
|
d48eb24046 | ||
|
aee4c22dee | ||
|
9a46b50989 | ||
|
faf3488b1e | ||
|
f3ac351d75 | ||
|
aba515e172 | ||
|
97bd306a09 | ||
|
645fd94bba | ||
|
71f00b3690 | ||
|
a21a47de93 | ||
|
f6d0f28b3a | ||
|
6de0c6a90c | ||
|
94b69935fe | ||
|
3f30feaefb | ||
|
9404efd86d |
28
.devcontainer/README.md
Normal file
28
.devcontainer/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Codespaces
|
||||
|
||||
You can modifiy Uptime Kuma in your browser without setting up a local development.
|
||||
|
||||

|
||||
|
||||
1. Click `Code` -> `Create codespace on master`
|
||||
2. Wait a few minutes until you see there are two exposed ports
|
||||
3. Go to the `3000` url, see if it is working
|
||||
|
||||

|
||||
|
||||
## Frontend
|
||||
|
||||
Since the frontend is using [Vite.js](https://vitejs.dev/), all changes in this area will be hot-reloaded.
|
||||
You don't need to restart the frontend, unless you try to add a new frontend dependency.
|
||||
|
||||
## Backend
|
||||
|
||||
The backend does not automatically hot-reload.
|
||||
You will need to restart the backend after changing something using these steps:
|
||||
|
||||
1. Click `Terminal`
|
||||
2. Click `Codespaces: server-dev` in the right panel
|
||||
3. Press `Ctrl + C` to stop the server
|
||||
4. Press `Up` to run `npm run start-server-dev`
|
||||
|
||||

|
22
.devcontainer/devcontainer.json
Normal file
22
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:dev-18-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"updateContentCommand": "npm ci",
|
||||
"postCreateCommand": "",
|
||||
"postAttachCommand": {
|
||||
"frontend-dev": "npm run start-frontend-devcontainer",
|
||||
"server-dev": "npm run start-server-dev",
|
||||
"open-port": "gh codespace ports visibility 3001:public -c $CODESPACE_NAME"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000, 3001]
|
||||
}
|
@@ -18,6 +18,7 @@ README.md
|
||||
.vscode
|
||||
.eslint*
|
||||
.stylelint*
|
||||
/.devcontainer
|
||||
/.github
|
||||
yarn.lock
|
||||
app.json
|
||||
@@ -35,6 +36,7 @@ tsconfig.json
|
||||
/extra/healthcheck
|
||||
extra/exe-builder
|
||||
|
||||
|
||||
### .gitignore content (commented rules are duplicated)
|
||||
|
||||
#node_modules
|
||||
|
10
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
10
.github/ISSUE_TEMPLATE/ask-for-help.yaml
vendored
@@ -26,6 +26,12 @@ body:
|
||||
label: "📝 Describe your problem"
|
||||
description: "Please walk us through it step by step."
|
||||
placeholder: "Describe what are you asking for..."
|
||||
- type: textarea
|
||||
id: error-msg
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "📝 Error Message(s) or Log"
|
||||
- type: input
|
||||
id: uptime-kuma-version
|
||||
attributes:
|
||||
@@ -38,7 +44,7 @@ body:
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: "💻 Operating System and Arch"
|
||||
description: "Which OS is your server/device running on?"
|
||||
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
|
||||
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||
validations:
|
||||
required: true
|
||||
@@ -46,7 +52,7 @@ body:
|
||||
id: browser-vendor
|
||||
attributes:
|
||||
label: "🌐 Browser"
|
||||
description: "Which browser are you running on?"
|
||||
description: "Which browser are you running on? (For Replit, please do not report this bug)"
|
||||
placeholder: "Ex. Google Chrome 95.0.4638.69"
|
||||
validations:
|
||||
required: true
|
||||
|
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -61,8 +61,8 @@ body:
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: "💻 Operating System and Arch"
|
||||
description: "Which OS is your server/device running on?"
|
||||
placeholder: "Ex. Ubuntu 20.04 x86"
|
||||
description: "Which OS is your server/device running on? (For Replit, please do not report this bug)"
|
||||
placeholder: "Ex. Ubuntu 20.04 x64 "
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
1
.github/config/exclude.txt
vendored
Normal file
1
.github/config/exclude.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# This is a .gitignore style file for 'GrantBirki/json-yaml-validate' Action workflow
|
34
.github/workflows/auto-test.yml
vendored
34
.github/workflows/auto-test.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Auto Test
|
||||
@@ -21,8 +21,8 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node: [ 14, 16, 18, 19 ]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
|
||||
node: [ 14, 20 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: 'npm'
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
@@ -41,6 +41,29 @@ jobs:
|
||||
HEADLESS_TEST: 1
|
||||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
|
||||
|
||||
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
|
||||
armv7-simple-test:
|
||||
needs: [ check-linters ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ARMv7 ]
|
||||
node: [ 14.21.3, 20.5.0 ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install npm@latest -g
|
||||
- run: npm ci --production
|
||||
|
||||
check-linters:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -52,7 +75,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run lint
|
||||
|
||||
@@ -67,7 +89,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:test
|
||||
@@ -83,7 +104,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: npm run cy:run:unit
|
||||
|
26
.github/workflows/json-yaml-validate.yml
vendored
Normal file
26
.github/workflows/json-yaml-validate.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: json-yaml-validate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write # enable write permissions for pull request comments
|
||||
|
||||
jobs:
|
||||
json-yaml-validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: json-yaml-validate
|
||||
id: json-yaml-validate
|
||||
uses: GrantBirki/json-yaml-validate@v1.3.0
|
||||
with:
|
||||
comment: "true" # enable comment mode
|
||||
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,3 +23,6 @@ cypress/screenshots
|
||||
|
||||
extra/exe-builder/bin
|
||||
extra/exe-builder/obj
|
||||
|
||||
.vs
|
||||
.vscode
|
||||
|
@@ -10,6 +10,7 @@
|
||||
"color-function-notation": "legacy",
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"color-hex-length": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"at-rule-no-unknown": null
|
||||
}
|
||||
}
|
||||
|
@@ -34,30 +34,30 @@ Yes or no, it depends on what you will try to do. Since I don't want to waste yo
|
||||
|
||||
Here are some references:
|
||||
|
||||
✅ Usually Accept:
|
||||
### ✅ Usually accepted:
|
||||
- Bug fix
|
||||
- Security fix
|
||||
- Adding notification providers
|
||||
- Adding new language files (You should go to https://weblate.kuma.pet for existing languages)
|
||||
- Adding new language files (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
|
||||
- Adding new language keys: `$t("...")`
|
||||
|
||||
⚠️ Discussion First
|
||||
### ⚠️ Discussion required:
|
||||
- Large pull requests
|
||||
- New features
|
||||
|
||||
❌ Won't Merge
|
||||
- A dedicated pr for translating existing languages (You can now translate on https://weblate.kuma.pet)
|
||||
- Do not pass auto test
|
||||
### ❌ Won't be merged:
|
||||
- A dedicated pr for translating existing languages (see [these instructions](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md))
|
||||
- Do not pass the auto test
|
||||
- Any breaking changes
|
||||
- Duplicated pull request
|
||||
- Duplicated pull requests
|
||||
- Buggy
|
||||
- UI/UX is not close to Uptime Kuma
|
||||
- Existing logic is completely modified or deleted for no reason
|
||||
- A function that is completely out of scope
|
||||
- Convert existing code into other programming languages
|
||||
- Unnecessary large code changes (Hard to review, causes code conflicts to other pull requests)
|
||||
- Modifications or deletions of existing logic without a valid reason.
|
||||
- Adding functions that is completely out of scope
|
||||
- Converting existing code into other programming languages
|
||||
- Unnecessarily large code changes that are hard to review and cause conflicts with other PRs.
|
||||
|
||||
The above cases cannot cover all situations.
|
||||
The above cases may not cover all possible situations.
|
||||
|
||||
I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand.
|
||||
|
||||
@@ -106,11 +106,11 @@ I personally do not like something that requires so many configurations before y
|
||||
|
||||
## Tools
|
||||
|
||||
- Node.js >= 14
|
||||
- NPM >= 8.5
|
||||
- Git
|
||||
- IDE that supports ESLint and EditorConfig (I am using IntelliJ IDEA)
|
||||
- A SQLite GUI tool (SQLite Expert Personal is suggested)
|
||||
- [`Node.js`](https://nodejs.org/) >= 14
|
||||
- [`npm`](https://www.npmjs.com/) >= 8.5
|
||||
- [`git`](https://git-scm.com/)
|
||||
- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/))
|
||||
- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/))
|
||||
|
||||
## Install Dependencies for Development
|
||||
|
||||
@@ -218,7 +218,17 @@ If for maybe security reasons, a library must be updated. Then you must need to
|
||||
|
||||
## Translations
|
||||
|
||||
Please read: https://github.com/louislam/uptime-kuma/tree/master/src/languages
|
||||
Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are ommited, they can not be translated).
|
||||
|
||||
**Don't include any other languages in your inital Pull-Request** (even if this is your mother tounge), to avoid merge-conflicts between weblate and `master`.
|
||||
The translations can then (after merging a PR into `master`) be translated by awesome people donating their language-skills.
|
||||
|
||||
If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
||||
|
||||
## Spelling & Grammar
|
||||
|
||||
Feel free to correct the grammar in the documentation or code.
|
||||
My mother language is not english and my grammar is not that great.
|
||||
|
||||
## Wiki
|
||||
|
||||
|
46
README.md
46
README.md
@@ -1,16 +1,16 @@
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
</div>
|
||||
|
||||
# Uptime Kuma
|
||||
|
||||
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||
|
||||
<a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/stars/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/pulls/louislam/uptime-kuma" /></a> <a target="_blank" href="https://hub.docker.com/r/louislam/uptime-kuma"><img src="https://img.shields.io/docker/v/louislam/uptime-kuma/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://github.com/louislam/uptime-kuma"><img src="https://img.shields.io/github/last-commit/louislam/uptime-kuma" /></a> <a target="_blank" href="https://opencollective.com/uptime-kuma"><img src="https://opencollective.com/uptime-kuma/total/badge.svg?label=Open%20Collective%20Backers&color=brightgreen" /></a>
|
||||
[](https://github.com/sponsors/louislam) <a href="https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/">
|
||||
<img src="https://weblate.kuma.pet/widgets/uptime-kuma/-/svg-badge.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
<div align="center" width="100%">
|
||||
<img src="./public/icon.svg" width="128" alt="" />
|
||||
</div>
|
||||
|
||||
Uptime Kuma is an easy-to-use self-hosted monitoring tool.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg" width="700" alt="" />
|
||||
|
||||
## 🥔 Live Demo
|
||||
@@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
|
||||
* Fancy, Reactive, Fast UI/UX
|
||||
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
|
||||
* 20 second intervals
|
||||
@@ -49,10 +49,14 @@ Uptime Kuma is now running on http://localhost:3001
|
||||
|
||||
### 💪🏻 Non-Docker
|
||||
|
||||
Required Tools:
|
||||
- [Node.js](https://nodejs.org/en/download/) >= 14
|
||||
Requirements:
|
||||
- Platform
|
||||
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
|
||||
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
|
||||
- ❌ Replit / Heroku
|
||||
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 / 20.4
|
||||
- [npm](https://docs.npmjs.com/cli/) >= 7
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
|
||||
|
||||
```bash
|
||||
@@ -67,7 +71,7 @@ npm run setup
|
||||
node server/server.js
|
||||
|
||||
# (Recommended) Option 2. Run in background using PM2
|
||||
# Install PM2 if you don't have it:
|
||||
# Install PM2 if you don't have it:
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
|
||||
# Start Server
|
||||
@@ -87,6 +91,10 @@ pm2 monit
|
||||
pm2 save && pm2 startup
|
||||
```
|
||||
|
||||
### Windows Portable (x64)
|
||||
|
||||
https://github.com/louislam/uptime-kuma/files/11886108/uptime-kuma-win64-portable-1.0.1.zip
|
||||
|
||||
### Advanced Installation
|
||||
|
||||
If you need more options or need to browse via a reverse proxy, please read:
|
||||
@@ -144,17 +152,18 @@ Telegram Notification Sample:
|
||||
|
||||
If you love this project, please consider giving me a ⭐.
|
||||
|
||||
## 🗣️ Discussion
|
||||
## 🗣️ Discussion / Ask for Help
|
||||
|
||||
### Issues Page
|
||||
⚠️ For any general or technical questions, please don't send me an email, as I am unable to provide support in that manner. I will not response if you asked such questions.
|
||||
|
||||
You can discuss or ask for help in [issues](https://github.com/louislam/uptime-kuma/issues).
|
||||
I recommend using Google, GitHub Issues, or Uptime Kuma's Subreddit for finding answers to your question. If you cannot find the information you need, feel free to ask:
|
||||
|
||||
### Subreddit
|
||||
- [GitHub Issues](https://github.com/louislam/uptime-kuma/issues)
|
||||
- [Subreddit r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||
|
||||
My Reddit account: [u/louislamlam](https://reddit.com/u/louislamlam).
|
||||
You can mention me if you ask a question on Reddit.
|
||||
[r/Uptime kuma](https://www.reddit.com/r/UptimeKuma/)
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -175,7 +184,10 @@ If you want to report a bug or request a new feature, feel free to open a [new i
|
||||
### Translations
|
||||
If you want to translate Uptime Kuma into your language, please visit [Weblate Readme](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md).
|
||||
|
||||
Feel free to correct my grammar in this README, source code, or wiki, as my mother language is not English and my grammar is not that great.
|
||||
## Spelling & Grammar
|
||||
|
||||
Feel free to correct the grammar in the documentation or code.
|
||||
My mother language is not english and my grammar is not that great.
|
||||
|
||||
### Create Pull Requests
|
||||
If you want to modify Uptime Kuma, please read this guide and follow the rules here: https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md
|
||||
|
@@ -4,8 +4,4 @@ if (process.env.TEST_FRONTEND) {
|
||||
config.presets = [ "@babel/preset-env" ];
|
||||
}
|
||||
|
||||
if (process.env.TEST_BACKEND) {
|
||||
config.plugins = [ "babel-plugin-rewire" ];
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
|
@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
import visualizer from "rollup-plugin-visualizer";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
import commonjs from "vite-plugin-commonjs";
|
||||
|
||||
const postCssScss = require("postcss-scss");
|
||||
const postcssRTLCSS = require("postcss-rtlcss");
|
||||
@@ -16,8 +17,12 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
|
||||
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
|
||||
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
|
||||
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
|
||||
},
|
||||
plugins: [
|
||||
commonjs(),
|
||||
vue(),
|
||||
legacy({
|
||||
targets: [ "since 2015" ],
|
||||
@@ -42,6 +47,9 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
include: [ /.js$/ ],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id, { getModuleInfo, getModuleIds }) {
|
||||
|
7
db/patch-add-certificate-expiry-status-page.sql
Normal file
7
db/patch-add-certificate-expiry-status-page.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE status_page
|
||||
ADD show_certificate_expiry BOOLEAN default 0 NOT NULL;
|
||||
|
||||
COMMIT;
|
7
db/patch-add-gamedig-given-port.sql
Normal file
7
db/patch-add-gamedig-given-port.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD gamedig_given_port_only BOOLEAN default 1 not null;
|
||||
|
||||
COMMIT;
|
7
db/patch-add-invert-keyword.sql
Normal file
7
db/patch-add-invert-keyword.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD invert_keyword BOOLEAN default 0 not null;
|
||||
|
||||
COMMIT;
|
6
db/patch-add-parent-monitor.sql
Normal file
6
db/patch-add-parent-monitor.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
COMMIT
|
6
db/patch-add-timeout-monitor.sql
Normal file
6
db/patch-add-timeout-monitor.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD timeout DOUBLE default 0 not null;
|
||||
COMMIT;
|
10
db/patch-added-json-query.sql
Normal file
10
db/patch-added-json-query.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD json_path TEXT;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD expected_value VARCHAR(255);
|
||||
|
||||
COMMIT;
|
22
db/patch-added-kafka-producer.sql
Normal file
22
db/patch-added-kafka-producer.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_topic VARCHAR(255);
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_brokers TEXT;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_ssl INTEGER;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_allow_auto_topic_creation VARCHAR(255);
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_sasl_options TEXT;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD kafka_producer_message TEXT;
|
||||
|
||||
COMMIT;
|
11
db/patch-maintenance-cron.sql
Normal file
11
db/patch-maintenance-cron.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DROP TABLE maintenance_timeslot;
|
||||
|
||||
-- 999 characters. https://stackoverflow.com/questions/46134830/maximum-length-for-cron-job
|
||||
ALTER TABLE maintenance ADD cron TEXT;
|
||||
ALTER TABLE maintenance ADD timezone VARCHAR(255);
|
||||
ALTER TABLE maintenance ADD duration INTEGER;
|
||||
|
||||
COMMIT;
|
19
db/patch-monitor-oauth-cc.sql
Normal file
19
db/patch-monitor-oauth-cc.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD oauth_client_id TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD oauth_client_secret TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD oauth_token_url TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD oauth_scopes TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD oauth_auth_method TEXT default null;
|
||||
|
||||
COMMIT;
|
13
db/patch-monitor-tls.sql
Normal file
13
db/patch-monitor-tls.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD tls_ca TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD tls_cert TEXT default null;
|
||||
|
||||
ALTER TABLE monitor
|
||||
ADD tls_key TEXT default null;
|
||||
|
||||
COMMIT;
|
@@ -4,5 +4,5 @@ WORKDIR /app
|
||||
|
||||
# Install apprise, iputils for non-root ping, setpriv
|
||||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib git && \
|
||||
pip3 --no-cache-dir install apprise==1.3.0 && \
|
||||
pip3 --no-cache-dir install apprise==1.4.0 && \
|
||||
rm -rf /root/.cache
|
||||
|
@@ -5,24 +5,44 @@ ARG TARGETPLATFORM
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install Curl
|
||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specify --no-install-recommends to skip them, make the base even smaller than alpine!
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 iputils-ping util-linux dumb-init git && \
|
||||
pip3 --no-cache-dir install apprise==1.3.0 && \
|
||||
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
|
||||
# python3* = apprise's dependencies
|
||||
# sqlite3 = for debugging
|
||||
# iputils-ping = for ping
|
||||
# util-linux = for setpriv (Should be dropped in 2.0.0?)
|
||||
# dumb-init = avoid zombie processes (#480)
|
||||
# curl = for debugging
|
||||
# ca-certificates = keep the cert up-to-date
|
||||
# sudo = for start service nscd with non-root user
|
||||
# nscd = for better DNS caching
|
||||
# (pip) apprise = for notifications
|
||||
RUN apt-get update && \
|
||||
apt-get --yes --no-install-recommends install \
|
||||
python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \
|
||||
sqlite3 \
|
||||
iputils-ping \
|
||||
util-linux \
|
||||
dumb-init \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nscd && \
|
||||
pip3 --no-cache-dir install apprise==1.4.5 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt --yes autoremove
|
||||
|
||||
# Install cloudflared
|
||||
# dpkg --add-architecture arm: cloudflared do not provide armhf, this is workaround. Read more: https://github.com/cloudflare/cloudflared/issues/583
|
||||
COPY extra/download-cloudflared.js ./extra/download-cloudflared.js
|
||||
RUN node ./extra/download-cloudflared.js $TARGETPLATFORM && \
|
||||
dpkg --add-architecture arm && \
|
||||
apt update && \
|
||||
apt --yes --no-install-recommends install ./cloudflared.deb && \
|
||||
RUN set -eux && \
|
||||
mkdir -p --mode=0755 /usr/share/keyrings && \
|
||||
curl --fail --show-error --silent --location --insecure 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 buster main' | tee /etc/apt/sources.list.d/cloudflared.list && \
|
||||
apt-get update && \
|
||||
apt-get install --yes --no-install-recommends cloudflared && \
|
||||
cloudflared version && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
rm -f cloudflared.deb && \
|
||||
apt --yes autoremove
|
||||
|
||||
# For nscd
|
||||
COPY ./docker/etc/nscd.conf /etc/nscd.conf
|
||||
COPY ./docker/etc/sudoers /etc/sudoers
|
||||
|
||||
|
@@ -26,6 +26,8 @@ RUN chmod +x /app/extra/entrypoint.sh
|
||||
FROM louislam/uptime-kuma:base-debian AS release
|
||||
WORKDIR /app
|
||||
|
||||
ENV UPTIME_KUMA_IS_CONTAINER=1
|
||||
|
||||
# Copy app files from build layer
|
||||
COPY --from=build /app /app
|
||||
|
||||
@@ -70,7 +72,6 @@ RUN git clone https://github.com/louislam/uptime-kuma.git .
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 3000 3001
|
||||
VOLUME ["/app/data"]
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD extra/healthcheck
|
||||
CMD ["npm", "run", "start-pr-test"]
|
||||
|
||||
|
90
docker/etc/nscd.conf
Normal file
90
docker/etc/nscd.conf
Normal file
@@ -0,0 +1,90 @@
|
||||
#
|
||||
# /etc/nscd.conf
|
||||
#
|
||||
# An example Name Service Cache config file. This file is needed by nscd.
|
||||
#
|
||||
# Legal entries are:
|
||||
#
|
||||
# logfile <file>
|
||||
# debug-level <level>
|
||||
# threads <initial #threads to use>
|
||||
# max-threads <maximum #threads to use>
|
||||
# server-user <user to run server as instead of root>
|
||||
# server-user is ignored if nscd is started with -S parameters
|
||||
# stat-user <user who is allowed to request statistics>
|
||||
# reload-count unlimited|<number>
|
||||
# paranoia <yes|no>
|
||||
# restart-interval <time in seconds>
|
||||
#
|
||||
# enable-cache <service> <yes|no>
|
||||
# positive-time-to-live <service> <time in seconds>
|
||||
# negative-time-to-live <service> <time in seconds>
|
||||
# suggested-size <service> <prime number>
|
||||
# check-files <service> <yes|no>
|
||||
# persistent <service> <yes|no>
|
||||
# shared <service> <yes|no>
|
||||
# max-db-size <service> <number bytes>
|
||||
# auto-propagate <service> <yes|no>
|
||||
#
|
||||
# Currently supported cache names (services): passwd, group, hosts, services
|
||||
#
|
||||
|
||||
|
||||
# logfile /var/log/nscd.log
|
||||
# threads 4
|
||||
# max-threads 32
|
||||
# server-user node
|
||||
# stat-user somebody
|
||||
debug-level 0
|
||||
# reload-count 5
|
||||
paranoia no
|
||||
# restart-interval 3600
|
||||
|
||||
enable-cache passwd no
|
||||
positive-time-to-live passwd 600
|
||||
negative-time-to-live passwd 20
|
||||
suggested-size passwd 211
|
||||
check-files passwd yes
|
||||
persistent passwd yes
|
||||
shared passwd yes
|
||||
max-db-size passwd 33554432
|
||||
auto-propagate passwd yes
|
||||
|
||||
enable-cache group no
|
||||
positive-time-to-live group 3600
|
||||
negative-time-to-live group 60
|
||||
suggested-size group 211
|
||||
check-files group yes
|
||||
persistent group yes
|
||||
shared group yes
|
||||
max-db-size group 33554432
|
||||
auto-propagate group yes
|
||||
|
||||
enable-cache hosts yes
|
||||
positive-time-to-live hosts 3600
|
||||
negative-time-to-live hosts 20
|
||||
suggested-size hosts 211
|
||||
check-files hosts yes
|
||||
persistent hosts yes
|
||||
# Set shared to "no" to display stats in `nscd -g`
|
||||
# Read more: https://stackoverflow.com/questions/40429245/nscdcentos7curl-0-dns-cache-hit-rate
|
||||
shared hosts no
|
||||
max-db-size hosts 33554432
|
||||
|
||||
enable-cache services no
|
||||
positive-time-to-live services 28800
|
||||
negative-time-to-live services 20
|
||||
suggested-size services 211
|
||||
check-files services yes
|
||||
persistent services yes
|
||||
shared services yes
|
||||
max-db-size services 33554432
|
||||
|
||||
enable-cache netgroup no
|
||||
positive-time-to-live netgroup 28800
|
||||
negative-time-to-live netgroup 20
|
||||
suggested-size netgroup 211
|
||||
check-files netgroup yes
|
||||
persistent netgroup yes
|
||||
shared netgroup yes
|
||||
max-db-size netgroup 33554432
|
31
docker/etc/sudoers
Normal file
31
docker/etc/sudoers
Normal file
@@ -0,0 +1,31 @@
|
||||
#
|
||||
# This file MUST be edited with the 'visudo' command as root.
|
||||
#
|
||||
# Please consider adding local content in /etc/sudoers.d/ instead of
|
||||
# directly modifying this file.
|
||||
#
|
||||
# See the man page for details on how to write a sudoers file.
|
||||
#
|
||||
Defaults env_reset
|
||||
Defaults mail_badpass
|
||||
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
# Host alias specification
|
||||
|
||||
# User alias specification
|
||||
|
||||
# Cmnd alias specification
|
||||
|
||||
# User privilege specification
|
||||
root ALL=(ALL:ALL) ALL
|
||||
|
||||
# Allow members of group sudo to execute any command
|
||||
%sudo ALL=(ALL:ALL) ALL
|
||||
|
||||
# See sudoers(5) for more information on "#include" directives:
|
||||
|
||||
#includedir /etc/sudoers.d
|
||||
|
||||
# Allow `node` to control service (mainly for nscd)
|
||||
node ALL=(root) NOPASSWD: /usr/sbin/nscdservice
|
||||
node ALL=(root) NOPASSWD: /usr/sbin/service
|
@@ -43,10 +43,11 @@ const prompt = (query) => new Promise((resolve) => rl.question(query, resolve));
|
||||
});
|
||||
console.log(result.stdout + result.stderr);
|
||||
|
||||
/*
|
||||
result = await ssh.execCommand("pm2 restart 1", {
|
||||
cwd,
|
||||
});
|
||||
console.log(result.stdout + result.stderr);
|
||||
console.log(result.stdout + result.stderr);*/
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
@@ -1,48 +0,0 @@
|
||||
//
|
||||
|
||||
const http = require("https"); // or 'https' for https:// URLs
|
||||
const fs = require("fs");
|
||||
|
||||
const platform = process.argv[2];
|
||||
|
||||
if (!platform) {
|
||||
console.error("No platform??");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let arch = null;
|
||||
|
||||
if (platform === "linux/amd64") {
|
||||
arch = "amd64";
|
||||
} else if (platform === "linux/arm64") {
|
||||
arch = "arm64";
|
||||
} else if (platform === "linux/arm/v7") {
|
||||
arch = "arm";
|
||||
} else {
|
||||
console.error("Invalid platform?? " + platform);
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream("cloudflared.deb");
|
||||
get("https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-" + arch + ".deb");
|
||||
|
||||
/**
|
||||
* Download specified file
|
||||
* @param {string} url URL to request
|
||||
*/
|
||||
function get(url) {
|
||||
http.get(url, function (res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
console.log("Redirect to " + res.headers.location);
|
||||
get(res.headers.location);
|
||||
} else if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
res.pipe(file);
|
||||
|
||||
res.on("end", function () {
|
||||
console.log("Downloaded");
|
||||
});
|
||||
} else {
|
||||
console.error(res.statusCode);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura />
|
||||
<Costura DisableCompression='true' IncludeDebugSymbols='false' />
|
||||
</Weavers>
|
@@ -6,9 +6,9 @@ using System.Runtime.InteropServices;
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Uptime Kuma")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyDescription("A portable executable for running Uptime Kuma")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyCompany("Uptime Kuma")]
|
||||
[assembly: AssemblyProduct("Uptime Kuma")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2023 Louis Lam")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
@@ -20,7 +20,7 @@ using System.Runtime.InteropServices;
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("2DB53988-1D93-4AC0-90C4-96ADEAAC5C04")]
|
||||
[assembly: Guid("86B40AFB-61FC-433D-8C31-650B0F32EA8F")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyVersion("1.0.1.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.1.0")]
|
||||
|
@@ -5,15 +5,15 @@
|
||||
|
||||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
println("=====================");
|
||||
println("Uptime Kuma Installer");
|
||||
println("Uptime Kuma Install Script");
|
||||
println("=====================");
|
||||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian");
|
||||
println("Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8");
|
||||
println("---------------------------------------");
|
||||
println("This script is designed for Linux and basic usage.");
|
||||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation");
|
||||
println("---------------------------------------");
|
||||
println("");
|
||||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2");
|
||||
println("Local - Install Uptime Kuma on your current machine with git, Node.js and pm2");
|
||||
println("Docker - Install Uptime Kuma Docker container");
|
||||
println("");
|
||||
|
||||
@@ -29,14 +29,10 @@ function checkNode() {
|
||||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')");
|
||||
println("Node Version: " ++ nodeVersion);
|
||||
|
||||
if (nodeVersion < "12") {
|
||||
if (nodeVersion <= "12") {
|
||||
println("Error: Required Node.js 14");
|
||||
call("exit", "1");
|
||||
}
|
||||
|
||||
if (nodeVersion == "12") {
|
||||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested.");
|
||||
}
|
||||
}
|
||||
|
||||
function deb() {
|
||||
@@ -60,8 +56,8 @@ function deb() {
|
||||
bash("apt --yes install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
println("Installing Node.js 16");
|
||||
bash("curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt");
|
||||
bash("apt --yes install nodejs");
|
||||
bash("node -v");
|
||||
|
||||
@@ -91,6 +87,10 @@ if (type == "local") {
|
||||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')");
|
||||
if (os == "Ubuntu") {
|
||||
distribution = "ubuntu";
|
||||
|
||||
// Get ubuntu version
|
||||
bash(". /etc/lsb-release");
|
||||
version = DISTRIB_RELEASE;
|
||||
}
|
||||
if (os == "Debian") {
|
||||
distribution = "debian";
|
||||
@@ -101,6 +101,7 @@ if (type == "local") {
|
||||
|
||||
println("Your OS: " ++ os);
|
||||
println("Distribution: " ++ distribution);
|
||||
println("Version: " ++ version);
|
||||
println("Arch: " ++ arch);
|
||||
|
||||
if ("$3" != "") {
|
||||
@@ -131,15 +132,32 @@ if (type == "local") {
|
||||
checkNode();
|
||||
} else {
|
||||
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("yum -y -q install curl");
|
||||
bash("dnfCheck=$(dnf --version)");
|
||||
|
||||
// Use yum
|
||||
if (dnfCheck == "") {
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("yum -y -q install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 16");
|
||||
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
||||
bash("yum install -y -q nodejs");
|
||||
} else {
|
||||
bash("curlCheck=$(curl --version)");
|
||||
if (curlCheck == "") {
|
||||
println("Installing Curl");
|
||||
bash("dnf -y install curl");
|
||||
}
|
||||
|
||||
println("Installing Node.js 16");
|
||||
bash("curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt");
|
||||
bash("dnf install -y nodejs");
|
||||
}
|
||||
|
||||
println("Installing Node.js 14");
|
||||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt");
|
||||
bash("yum install -y -q nodejs");
|
||||
|
||||
bash("node -v");
|
||||
|
||||
bash("nodeCheckAgain=$(node -v)");
|
||||
@@ -193,6 +211,14 @@ if (type == "local") {
|
||||
bash("pm2 startup");
|
||||
}
|
||||
|
||||
|
||||
// Check again
|
||||
bash("check=$(pm2 --version)");
|
||||
if (check == "") {
|
||||
println("Error: pm2 is not found!");
|
||||
bash("exit 1");
|
||||
}
|
||||
|
||||
bash("mkdir -p $installPath");
|
||||
bash("cd $installPath");
|
||||
bash("git clone https://github.com/louislam/uptime-kuma.git .");
|
||||
|
@@ -13,7 +13,7 @@ lines = lines.filter((line) => line !== "");
|
||||
lines = [ ...new Set(lines) ];
|
||||
|
||||
// Remove @weblate and @UptimeKumaBot
|
||||
lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot");
|
||||
lines = lines.filter((line) => line !== "@weblate" && line !== "@UptimeKumaBot" && line !== "@louislam");
|
||||
|
||||
// Sort the lines
|
||||
lines = lines.sort();
|
||||
|
9
extra/test-docker.js
Normal file
9
extra/test-docker.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// Check if docker is running
|
||||
const { exec } = require("child_process");
|
||||
|
||||
exec("docker ps", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error("Docker is not running. Please start docker and try again.");
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
54
install.sh
54
install.sh
@@ -3,15 +3,15 @@
|
||||
# The command is working on Windows PowerShell and Docker for Windows only.
|
||||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Uptime Kuma Installer"
|
||||
"echo" "-e" "Uptime Kuma Install Script"
|
||||
"echo" "-e" "====================="
|
||||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"
|
||||
"echo" "-e" "Supported OS: Ubuntu >= 16.04, Debian and CentOS/RHEL 7/8"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" "This script is designed for Linux and basic usage."
|
||||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"
|
||||
"echo" "-e" "---------------------------------------"
|
||||
"echo" "-e" ""
|
||||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"
|
||||
"echo" "-e" "Local - Install Uptime Kuma on your current machine with git, Node.js and pm2"
|
||||
"echo" "-e" "Docker - Install Uptime Kuma Docker container"
|
||||
"echo" "-e" ""
|
||||
if [ "$1" != "" ]; then
|
||||
@@ -25,12 +25,9 @@ function checkNode {
|
||||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')
|
||||
"echo" "-e" "Node Version: ""$nodeVersion"
|
||||
_0="12"
|
||||
if [ $(($nodeVersion < $_0)) == 1 ]; then
|
||||
if [ $(($nodeVersion <= $_0)) == 1 ]; then
|
||||
"echo" "-e" "Error: Required Node.js 14"
|
||||
"exit" "1"
|
||||
fi
|
||||
if [ "$nodeVersion" == "12" ]; then
|
||||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested."
|
||||
fi
|
||||
}
|
||||
function deb {
|
||||
@@ -50,8 +47,8 @@ fi
|
||||
"echo" "-e" "Installing Curl"
|
||||
apt --yes install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt
|
||||
"echo" "-e" "Installing Node.js 16"
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - > log.txt
|
||||
apt --yes install nodejs
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
@@ -75,7 +72,10 @@ if [ "$type" == "local" ]; then
|
||||
if [ -e "/etc/issue" ]; then
|
||||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ')
|
||||
if [ "$os" == "Ubuntu" ]; then
|
||||
distribution="ubuntu"
|
||||
distribution="ubuntu"
|
||||
# Get ubuntu version
|
||||
. /etc/lsb-release
|
||||
version="$DISTRIB_RELEASE"
|
||||
fi
|
||||
if [ "$os" == "Debian" ]; then
|
||||
distribution="debian"
|
||||
@@ -85,6 +85,7 @@ fi
|
||||
arch=$(uname -i)
|
||||
"echo" "-e" "Your OS: ""$os"
|
||||
"echo" "-e" "Distribution: ""$distribution"
|
||||
"echo" "-e" "Version: ""$version"
|
||||
"echo" "-e" "Arch: ""$arch"
|
||||
if [ "$3" != "" ]; then
|
||||
port="$3"
|
||||
@@ -108,14 +109,27 @@ fi
|
||||
if [ "$nodeCheck" != "" ]; then
|
||||
"checkNode"
|
||||
else
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
yum -y -q install curl
|
||||
dnfCheck=$(dnf --version)
|
||||
# Use yum
|
||||
if [ "$dnfCheck" == "" ]; then
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
yum -y -q install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 14"
|
||||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt
|
||||
yum install -y -q nodejs
|
||||
"echo" "-e" "Installing Node.js 16"
|
||||
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
||||
yum install -y -q nodejs
|
||||
else
|
||||
curlCheck=$(curl --version)
|
||||
if [ "$curlCheck" == "" ]; then
|
||||
"echo" "-e" "Installing Curl"
|
||||
dnf -y install curl
|
||||
fi
|
||||
"echo" "-e" "Installing Node.js 16"
|
||||
curl -sL https://rpm.nodesource.com/setup_16.x | bash - > log.txt
|
||||
dnf install -y nodejs
|
||||
fi
|
||||
node -v
|
||||
nodeCheckAgain=$(node -v)
|
||||
if [ "$nodeCheckAgain" == "" ]; then
|
||||
@@ -161,6 +175,12 @@ fi
|
||||
"echo" "-e" "Installing PM2"
|
||||
npm install pm2 -g && pm2 install pm2-logrotate
|
||||
pm2 startup
|
||||
fi
|
||||
# Check again
|
||||
check=$(pm2 --version)
|
||||
if [ "$check" == "" ]; then
|
||||
"echo" "-e" "Error: pm2 is not found!"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p $installPath
|
||||
cd $installPath
|
||||
|
24201
package-lock.json
generated
24201
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "1.21.0-beta.0",
|
||||
"version": "1.23.0-beta.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/louislam/uptime-kuma.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.* || >=16.*"
|
||||
"node": "14 || 16 || 18 || >= 20.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"install-legacy": "npm install",
|
||||
@@ -19,6 +19,7 @@
|
||||
"lint": "npm run lint:js && npm run lint:style",
|
||||
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
|
||||
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
|
||||
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
|
||||
"start": "npm run start-server",
|
||||
"start-server": "node server/server.js",
|
||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||
@@ -34,28 +35,31 @@
|
||||
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
|
||||
"build-docker-alpine": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:alpine -t louislam/uptime-kuma:1-alpine -t louislam/uptime-kuma:$VERSION-alpine --target release . --push",
|
||||
"build-docker-debian": "node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma -t louislam/uptime-kuma:1 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:debian -t louislam/uptime-kuma:1-debian -t louislam/uptime-kuma:$VERSION-debian --target release . --push",
|
||||
"build-docker-nightly": "npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly": "node ./extra/test-docker.js && npm run build && docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly --target nightly . --push",
|
||||
"build-docker-nightly-alpine": "docker buildx build -f docker/dockerfile-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:nightly-alpine --target nightly . --push",
|
||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||
"setup": "git checkout 1.20.2 && npm ci --production && npm run download-dist",
|
||||
"setup": "git checkout 1.22.1 && npm ci --production && npm run download-dist",
|
||||
"download-dist": "node extra/download-dist.js",
|
||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||
"reset-password": "node extra/reset-password.js",
|
||||
"remove-2fa": "node extra/remove-2fa.js",
|
||||
"compile-install-script": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./extra/compile-install-script.ps1",
|
||||
"test-install-script-rockylinux": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/rockylinux.dockerfile .",
|
||||
"test-install-script-centos7": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/centos7.dockerfile .",
|
||||
"test-install-script-alpine3": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/alpine3.dockerfile .",
|
||||
"test-install-script-debian": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian.dockerfile .",
|
||||
"test-install-script-debian-buster": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/debian-buster.dockerfile .",
|
||||
"test-install-script-ubuntu": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu.dockerfile .",
|
||||
"test-install-script-ubuntu1804": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1804.dockerfile .",
|
||||
"test-install-script-ubuntu1604": "npm run compile-install-script && docker build --progress plain -f test/test_install_script/ubuntu1604.dockerfile .",
|
||||
"test-nodejs16": "docker build --progress plain -f test/ubuntu-nodejs16.dockerfile .",
|
||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||
"update-language-files": "cd extra/update-language-files && node index.js && cross-env-shell eslint ../../src/languages/$npm_config_language.js --fix",
|
||||
"ncu-patch": "npm-check-updates -u -t patch",
|
||||
"release-final": "node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||
"release-beta": "node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||
"release-final": "node ./extra/test-docker.js && node extra/update-version.js && npm run build-docker && node ./extra/press-any-key.js && npm run upload-artifacts && node ./extra/update-wiki-version.js",
|
||||
"release-beta": "node ./extra/test-docker.js && node extra/beta/update-version.js && npm run build && node ./extra/env2arg.js docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:$VERSION -t louislam/uptime-kuma:beta . --target release --push && node ./extra/press-any-key.js && npm run upload-artifacts",
|
||||
"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",
|
||||
@@ -64,19 +68,18 @@
|
||||
"cy:run:unit": "npx cypress run --browser chrome --headless --config-file ./config/cypress.frontend.config.js",
|
||||
"cypress-open": "concurrently -k -r \"node test/prepare-test-server.js && node server/server.js --port=3002 --data-dir=./data/test/\" \"cypress open --config-file ./config/cypress.config.js\"",
|
||||
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
|
||||
"depoly-demo-server": "node extra/deploy-demo-server.js",
|
||||
"deploy-demo-server": "node extra/deploy-demo-server.js",
|
||||
"sort-contributors": "node extra/sort-contributors.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "~1.7.3",
|
||||
"@louislam/ping": "~0.4.2-mod.2",
|
||||
"@louislam/sqlite3": "15.1.2",
|
||||
"@louislam/ping": "~0.4.4-mod.1",
|
||||
"@louislam/sqlite3": "15.1.6",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.27.0",
|
||||
"axios-ntlm": "1.3.0",
|
||||
"badge-maker": "~3.3.1",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bree": "~7.1.5",
|
||||
"cacheable-lookup": "~6.0.4",
|
||||
"chardet": "~1.4.0",
|
||||
"check-password-strength": "^2.0.5",
|
||||
@@ -85,49 +88,59 @@
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"compression": "~1.7.4",
|
||||
"croner": "~6.0.5",
|
||||
"dayjs": "~1.11.5",
|
||||
"dotenv": "~16.0.3",
|
||||
"express": "~4.17.3",
|
||||
"express-basic-auth": "~1.2.1",
|
||||
"express-static-gzip": "~2.1.7",
|
||||
"form-data": "~4.0.0",
|
||||
"gamedig": "^4.0.5",
|
||||
"gamedig": "~4.0.5",
|
||||
"http-graceful-shutdown": "~3.1.7",
|
||||
"http-proxy-agent": "~5.0.0",
|
||||
"https-proxy-agent": "~5.0.1",
|
||||
"iconv-lite": "~0.6.3",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "^2.0.3",
|
||||
"jsonwebtoken": "~9.0.0",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"kafkajs": "^2.2.4",
|
||||
"limiter": "~2.1.0",
|
||||
"liquidjs": "^10.7.0",
|
||||
"mongodb": "~4.14.0",
|
||||
"mqtt": "~4.3.7",
|
||||
"mssql": "~8.1.4",
|
||||
"mysql2": "~2.3.3",
|
||||
"nanoid": "^3.3.4",
|
||||
"nanoid": "~3.3.4",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"node-radius-client": "~1.0.0",
|
||||
"nodemailer": "~6.6.5",
|
||||
"nostr-tools": "^1.13.1",
|
||||
"notp": "~2.0.3",
|
||||
"openid-client": "^5.4.2",
|
||||
"password-hash": "~1.2.2",
|
||||
"pg": "~8.8.0",
|
||||
"pg-connection-string": "~2.5.0",
|
||||
"playwright-core": "~1.35.1",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.1",
|
||||
"protobufjs": "~7.1.1",
|
||||
"protobufjs": "~7.2.4",
|
||||
"qs": "~6.10.4",
|
||||
"redbean-node": "~0.2.0",
|
||||
"redbean-node": "~0.3.0",
|
||||
"redis": "~4.5.1",
|
||||
"socket.io": "~4.5.3",
|
||||
"socket.io-client": "~4.5.3",
|
||||
"semver": "~7.5.4",
|
||||
"socket.io": "~4.6.1",
|
||||
"socket.io-client": "~4.6.1",
|
||||
"socks-proxy-agent": "6.1.1",
|
||||
"tar": "~6.1.11",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2"
|
||||
"thirty-two": "~1.0.2",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~5.0.1",
|
||||
"@babel/eslint-parser": "~7.17.0",
|
||||
"@babel/eslint-parser": "^7.22.7",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "~5.15.4",
|
||||
@@ -135,28 +148,28 @@
|
||||
"@fortawesome/vue-fontawesome": "~3.0.0-5",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@vitejs/plugin-legacy": "~2.1.0",
|
||||
"@vitejs/plugin-vue": "~3.1.0",
|
||||
"@vue/compiler-sfc": "~3.2.36",
|
||||
"@vitejs/plugin-legacy": "~4.1.0",
|
||||
"@vitejs/plugin-vue": "~4.2.3",
|
||||
"@vue/compiler-sfc": "~3.3.4",
|
||||
"@vuepic/vue-datepicker": "~3.4.8",
|
||||
"aedes": "^0.46.3",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"bootstrap": "5.1.3",
|
||||
"chart.js": "~3.6.2",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"chart.js": "~4.2.1",
|
||||
"chartjs-adapter-dayjs-4": "~1.0.4",
|
||||
"concurrently": "^7.1.0",
|
||||
"core-js": "~3.26.1",
|
||||
"cronstrue": "~2.24.0",
|
||||
"cross-env": "~7.0.3",
|
||||
"cypress": "^10.1.0",
|
||||
"cypress": "^12.17.0",
|
||||
"delay": "^5.0.0",
|
||||
"dns2": "~2.0.1",
|
||||
"dompurify": "~2.4.3",
|
||||
"eslint": "~8.14.0",
|
||||
"eslint-plugin-vue": "~8.7.1",
|
||||
"favico.js": "~0.3.10",
|
||||
"jest": "~27.2.5",
|
||||
"jest": "~29.6.1",
|
||||
"marked": "~4.2.5",
|
||||
"node-ssh": "~13.0.1",
|
||||
"node-ssh": "~13.1.0",
|
||||
"postcss-html": "~1.5.0",
|
||||
"postcss-rtlcss": "~3.7.2",
|
||||
"postcss-scss": "~4.0.4",
|
||||
@@ -164,16 +177,17 @@
|
||||
"qrcode": "~1.5.0",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~14.7.1",
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-standard": "~25.0.0",
|
||||
"terser": "~5.15.0",
|
||||
"timezones-list": "~3.0.1",
|
||||
"typescript": "~4.4.4",
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vite": "~3.1.0",
|
||||
"vite": "~4.4.1",
|
||||
"vite-plugin-commonjs": "^0.8.0",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "next",
|
||||
"vue-chart-3": "3.0.9",
|
||||
"vue": "~3.3.4",
|
||||
"vue-chartjs": "~5.2.0",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
"vue-i18n": "~9.2.2",
|
||||
@@ -184,6 +198,7 @@
|
||||
"vue-router": "~4.0.14",
|
||||
"vue-toastification": "~2.0.0-rc.5",
|
||||
"vuedraggable": "~4.1.0",
|
||||
"wait-on": "^6.0.1"
|
||||
"wait-on": "^6.0.1",
|
||||
"whatwg-url": "~12.0.1"
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ const basicAuth = require("express-basic-auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const { setting } = require("./util-server");
|
||||
const { log } = require("../src/util");
|
||||
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
@@ -81,12 +82,16 @@ function apiAuthorizer(username, password, callback) {
|
||||
apiRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
verifyAPIKey(password).then((valid) => {
|
||||
if (!valid) {
|
||||
log.warn("api-auth", "Failed API auth attempt: invalid API Key");
|
||||
}
|
||||
callback(null, valid);
|
||||
// Only allow a set number of api requests per minute
|
||||
// (currently set to 60)
|
||||
apiRateLimiter.removeTokens(1);
|
||||
});
|
||||
} else {
|
||||
log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
@@ -106,10 +111,12 @@ function userAuthorizer(username, password, callback) {
|
||||
callback(null, user != null);
|
||||
|
||||
if (user == null) {
|
||||
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
|
||||
callback(null, false);
|
||||
}
|
||||
});
|
||||
|
@@ -1,27 +1,33 @@
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const axios = require("axios");
|
||||
const compareVersions = require("compare-versions");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
exports.version = require("../package.json").version;
|
||||
exports.latestVersion = null;
|
||||
|
||||
// How much time in ms to wait between update checks
|
||||
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
|
||||
const UPDATE_CHECKER_LATEST_VERSION_URL = "https://uptime.kuma.pet/version";
|
||||
|
||||
let interval;
|
||||
|
||||
/** Start 48 hour check interval */
|
||||
exports.startInterval = () => {
|
||||
let check = async () => {
|
||||
if (await setting("checkUpdate") === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("update-checker", "Retrieving latest versions");
|
||||
|
||||
try {
|
||||
const res = await axios.get("https://uptime.kuma.pet/version");
|
||||
const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL);
|
||||
|
||||
// For debug
|
||||
if (process.env.TEST_CHECK_VERSION === "1") {
|
||||
res.data.slow = "1000.0.0";
|
||||
}
|
||||
|
||||
if (await setting("checkUpdate") === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
let checkBeta = await setting("checkBeta");
|
||||
|
||||
if (checkBeta && res.data.beta) {
|
||||
@@ -35,12 +41,14 @@ exports.startInterval = () => {
|
||||
exports.latestVersion = res.data.slow;
|
||||
}
|
||||
|
||||
} catch (_) { }
|
||||
} catch (_) {
|
||||
log.info("update-checker", "Failed to check for new versions");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
check();
|
||||
interval = setInterval(check, 3600 * 1000 * 48);
|
||||
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) {
|
||||
/**
|
||||
* Emits the version information to the client.
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @param {boolean} hideVersion
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendInfo(socket) {
|
||||
async function sendInfo(socket, hideVersion = false) {
|
||||
let version;
|
||||
let latestVersion;
|
||||
|
||||
if (!hideVersion) {
|
||||
version = checkVersion.version;
|
||||
latestVersion = checkVersion.latestVersion;
|
||||
}
|
||||
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
latestVersion: checkVersion.latestVersion,
|
||||
version,
|
||||
latestVersion,
|
||||
primaryBaseURL: await setting("primaryBaseURL"),
|
||||
serverTimezone: await server.getTimezone(),
|
||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const args = require("args-parser")(process.argv);
|
||||
// Interop with browser
|
||||
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
const badgeConstants = {
|
||||
|
@@ -2,9 +2,8 @@ const fs = require("fs");
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const { log, sleep } = require("../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const knex = require("knex");
|
||||
const { PluginsManager } = require("./plugins-manager");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
@@ -23,18 +22,17 @@ class Database {
|
||||
*/
|
||||
static uploadDir;
|
||||
|
||||
static screenshotDir;
|
||||
|
||||
static path;
|
||||
|
||||
static dockerTLSDir;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
static patched = false;
|
||||
|
||||
/**
|
||||
* For Backup only
|
||||
*/
|
||||
static backupPath = null;
|
||||
|
||||
/**
|
||||
* Add patch filename in key
|
||||
* Values:
|
||||
@@ -73,6 +71,16 @@ class Database {
|
||||
"patch-http-body-encoding.sql": true,
|
||||
"patch-add-description-monitor.sql": true,
|
||||
"patch-api-key-table.sql": true,
|
||||
"patch-monitor-tls.sql": true,
|
||||
"patch-maintenance-cron.sql": true,
|
||||
"patch-add-parent-monitor.sql": true,
|
||||
"patch-add-invert-keyword.sql": true,
|
||||
"patch-added-json-query.sql": true,
|
||||
"patch-added-kafka-producer.sql": true,
|
||||
"patch-add-certificate-expiry-status-page.sql": true,
|
||||
"patch-monitor-oauth-cc.sql": true,
|
||||
"patch-add-timeout-monitor.sql": true,
|
||||
"patch-add-gamedig-given-port.sql": true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,23 +99,28 @@ class Database {
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
|
||||
// Plugin feature is working only if the dataDir = "./data";
|
||||
if (Database.dataDir !== "./data/") {
|
||||
log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
|
||||
PluginsManager.disable = true;
|
||||
}
|
||||
|
||||
Database.path = Database.dataDir + "kuma.db";
|
||||
Database.path = path.join(Database.dataDir, "kuma.db");
|
||||
if (! fs.existsSync(Database.dataDir)) {
|
||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
Database.uploadDir = Database.dataDir + "upload/";
|
||||
Database.uploadDir = path.join(Database.dataDir, "upload/");
|
||||
|
||||
if (! fs.existsSync(Database.uploadDir)) {
|
||||
fs.mkdirSync(Database.uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create screenshot dir
|
||||
Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
|
||||
if (! fs.existsSync(Database.screenshotDir)) {
|
||||
fs.mkdirSync(Database.screenshotDir, { recursive: true });
|
||||
}
|
||||
|
||||
Database.dockerTLSDir = path.join(Database.dataDir, "docker-tls/");
|
||||
if (! fs.existsSync(Database.dockerTLSDir)) {
|
||||
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
@@ -164,12 +177,12 @@ class Database {
|
||||
await R.exec("PRAGMA journal_mode = WAL");
|
||||
}
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
||||
|
||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||
// FULL synchronous is very safe, but it is also slower.
|
||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||
await R.exec("PRAGMA synchronous = FULL");
|
||||
await R.exec("PRAGMA synchronous = NORMAL");
|
||||
|
||||
if (!noLog) {
|
||||
log.info("db", "SQLite config:");
|
||||
@@ -197,15 +210,7 @@ class Database {
|
||||
} else {
|
||||
log.info("db", "Database patch is needed");
|
||||
|
||||
try {
|
||||
this.backup(version);
|
||||
} catch (e) {
|
||||
log.error("db", e);
|
||||
log.error("db", "Unable to create a backup before patching the database. Please make sure you have enough space and permission.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Try catch anything here, if gone wrong, restore the backup
|
||||
// Try catch anything here
|
||||
try {
|
||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||
const sqlFile = `./db/patch${i}.sql`;
|
||||
@@ -221,7 +226,6 @@ class Database {
|
||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -263,8 +267,6 @@ class Database {
|
||||
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
|
||||
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
|
||||
|
||||
this.restore();
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -366,8 +368,6 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
this.backup(dayjs().format("YYYYMMDDHHmmss"));
|
||||
|
||||
log.info("db", sqlFilename + " is patching");
|
||||
this.patched = true;
|
||||
await this.importSQLFile("./db/" + sqlFilename);
|
||||
@@ -433,6 +433,9 @@ class Database {
|
||||
|
||||
log.info("db", "Closing the database");
|
||||
|
||||
// Flush WAL to main database
|
||||
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
await R.close();
|
||||
@@ -449,100 +452,6 @@ class Database {
|
||||
process.removeListener("unhandledRejection", listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* One backup one time in this process.
|
||||
* Reset this.backupPath if you want to backup again
|
||||
* @param {string} version Version code of backup
|
||||
*/
|
||||
static backup(version) {
|
||||
if (! this.backupPath) {
|
||||
log.info("db", "Backing up the database");
|
||||
this.backupPath = this.dataDir + "kuma.db.bak" + version;
|
||||
fs.copyFileSync(Database.path, this.backupPath);
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
if (fs.existsSync(shmPath)) {
|
||||
this.backupShmPath = shmPath + ".bak" + version;
|
||||
fs.copyFileSync(shmPath, this.backupShmPath);
|
||||
}
|
||||
|
||||
const walPath = Database.path + "-wal";
|
||||
if (fs.existsSync(walPath)) {
|
||||
this.backupWalPath = walPath + ".bak" + version;
|
||||
fs.copyFileSync(walPath, this.backupWalPath);
|
||||
}
|
||||
|
||||
// Double confirm if all files actually backup
|
||||
if (!fs.existsSync(this.backupPath)) {
|
||||
throw new Error("Backup failed! " + this.backupPath);
|
||||
}
|
||||
|
||||
if (fs.existsSync(shmPath)) {
|
||||
if (!fs.existsSync(this.backupShmPath)) {
|
||||
throw new Error("Backup failed! " + this.backupShmPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(walPath)) {
|
||||
if (!fs.existsSync(this.backupWalPath)) {
|
||||
throw new Error("Backup failed! " + this.backupWalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore from most recent backup */
|
||||
static restore() {
|
||||
if (this.backupPath) {
|
||||
log.error("db", "Patching the database failed!!! Restoring the backup");
|
||||
|
||||
const shmPath = Database.path + "-shm";
|
||||
const walPath = Database.path + "-wal";
|
||||
|
||||
// Make sure we have a backup to restore before deleting old db
|
||||
if (
|
||||
!fs.existsSync(this.backupPath)
|
||||
&& !fs.existsSync(shmPath)
|
||||
&& !fs.existsSync(walPath)
|
||||
) {
|
||||
log.error("db", "Backup file not found! Leaving database in failed state.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Delete patch failed db
|
||||
try {
|
||||
if (fs.existsSync(Database.path)) {
|
||||
fs.unlinkSync(Database.path);
|
||||
}
|
||||
|
||||
if (fs.existsSync(shmPath)) {
|
||||
fs.unlinkSync(shmPath);
|
||||
}
|
||||
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.unlinkSync(walPath);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("db", "Restore failed; you may need to restore the backup manually");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Restore backup
|
||||
fs.copyFileSync(this.backupPath, Database.path);
|
||||
|
||||
if (this.backupShmPath) {
|
||||
fs.copyFileSync(this.backupShmPath, shmPath);
|
||||
}
|
||||
|
||||
if (this.backupWalPath) {
|
||||
fs.copyFileSync(this.backupWalPath, walPath);
|
||||
}
|
||||
|
||||
} else {
|
||||
log.info("db", "Nothing to restore");
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the size of the database */
|
||||
static getSize() {
|
||||
log.debug("db", "Database.getSize()");
|
||||
|
@@ -2,8 +2,16 @@ const axios = require("axios");
|
||||
const { R } = require("redbean-node");
|
||||
const version = require("../package.json").version;
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Database = require("./database");
|
||||
|
||||
class DockerHost {
|
||||
|
||||
static CertificateFileNameCA = "ca.pem";
|
||||
static CertificateFileNameCert = "cert.pem";
|
||||
static CertificateFileNameKey = "key.pem";
|
||||
|
||||
/**
|
||||
* Save a docker host
|
||||
* @param {Object} dockerHost Docker host to save
|
||||
@@ -66,10 +74,6 @@ class DockerHost {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
};
|
||||
|
||||
if (dockerHost.dockerType === "socket") {
|
||||
@@ -77,6 +81,7 @@ class DockerHost {
|
||||
} else if (dockerHost.dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||
}
|
||||
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||
|
||||
let res = await axios.request(options);
|
||||
|
||||
@@ -111,6 +116,53 @@ class DockerHost {
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTPS agent options with client side TLS parameters if certificate files
|
||||
* for the given host are available under a predefined directory path.
|
||||
*
|
||||
* The base path where certificates are looked for can be set with the
|
||||
* 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'.
|
||||
*
|
||||
* If a directory in this path exists with a name matching the FQDN of the docker host
|
||||
* (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory
|
||||
* 'data/docker-tls/example.com/' would be searched for certificate files),
|
||||
* then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
|
||||
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
|
||||
*
|
||||
* @param {String} dockerType i.e. "tcp" or "socket"
|
||||
* @param {String} url The docker host URL rewritten to https://
|
||||
* @return {Object}
|
||||
* */
|
||||
static getHttpsAgentOptions(dockerType, url) {
|
||||
let baseOptions = {
|
||||
maxCachedSessions: 0,
|
||||
rejectUnauthorized: true
|
||||
};
|
||||
let certOptions = {};
|
||||
|
||||
let dirName = (new URL(url)).hostname;
|
||||
|
||||
let caPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCA);
|
||||
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
|
||||
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
|
||||
|
||||
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
||||
let ca = fs.readFileSync(caPath);
|
||||
let key = fs.readFileSync(keyPath);
|
||||
let cert = fs.readFileSync(certPath);
|
||||
certOptions = {
|
||||
ca,
|
||||
key,
|
||||
cert
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseOptions,
|
||||
...certOptions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@@ -1,24 +0,0 @@
|
||||
const childProcess = require("child_process");
|
||||
|
||||
class Git {
|
||||
|
||||
static clone(repoURL, cwd, targetDir = ".") {
|
||||
let result = childProcess.spawnSync("git", [
|
||||
"clone",
|
||||
repoURL,
|
||||
targetDir,
|
||||
], {
|
||||
cwd: cwd,
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr.toString("utf-8"));
|
||||
} else {
|
||||
return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Git,
|
||||
};
|
@@ -1,41 +1,51 @@
|
||||
const path = require("path");
|
||||
const Bree = require("bree");
|
||||
const { SHARE_ENV } = require("worker_threads");
|
||||
const { log } = require("../src/util");
|
||||
let bree;
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
const { clearOldData } = require("./jobs/clear-old-data");
|
||||
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
|
||||
const Cron = require("croner");
|
||||
|
||||
const jobs = [
|
||||
{
|
||||
name: "clear-old-data",
|
||||
interval: "at 03:14",
|
||||
interval: "14 03 * * *",
|
||||
jobFunc: clearOldData,
|
||||
croner: null,
|
||||
},
|
||||
{
|
||||
name: "incremental-vacuum",
|
||||
interval: "*/5 * * * *",
|
||||
jobFunc: incrementalVacuum,
|
||||
croner: null,
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Initialize background jobs
|
||||
* @param {Object} args Arguments to pass to workers
|
||||
* @returns {Bree}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const initBackgroundJobs = function (args) {
|
||||
bree = new Bree({
|
||||
root: path.resolve("server", "jobs"),
|
||||
jobs,
|
||||
worker: {
|
||||
env: SHARE_ENV,
|
||||
workerData: args,
|
||||
},
|
||||
workerMessageHandler: (message) => {
|
||||
log.info("jobs", message);
|
||||
}
|
||||
});
|
||||
const initBackgroundJobs = async function () {
|
||||
const timezone = await UptimeKumaServer.getInstance().getTimezone();
|
||||
|
||||
for (const job of jobs) {
|
||||
const cornerJob = new Cron(
|
||||
job.interval,
|
||||
{
|
||||
name: job.name,
|
||||
timezone,
|
||||
},
|
||||
job.jobFunc,
|
||||
);
|
||||
job.croner = cornerJob;
|
||||
}
|
||||
|
||||
bree.start();
|
||||
return bree;
|
||||
};
|
||||
|
||||
/** Stop all background jobs if running */
|
||||
const stopBackgroundJobs = function () {
|
||||
if (bree) {
|
||||
bree.stop();
|
||||
for (const job of jobs) {
|
||||
if (job.croner) {
|
||||
job.croner.stop();
|
||||
job.croner = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,12 +1,15 @@
|
||||
const { log, exit, connectDb } = require("./util-worker");
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
const { setSetting, setting } = require("../util-server");
|
||||
|
||||
const DEFAULT_KEEP_PERIOD = 180;
|
||||
|
||||
(async () => {
|
||||
await connectDb();
|
||||
/**
|
||||
* Clears old data from the heartbeat table of the database.
|
||||
* @return {Promise<void>} A promise that resolves when the data has been cleared.
|
||||
*/
|
||||
|
||||
const clearOldData = async () => {
|
||||
let period = await setting("keepDataPeriodDays");
|
||||
|
||||
// Set Default Period
|
||||
@@ -20,26 +23,30 @@ const DEFAULT_KEEP_PERIOD = 180;
|
||||
try {
|
||||
parsedPeriod = parseInt(period);
|
||||
} catch (_) {
|
||||
log("Failed to parse setting, resetting to default..");
|
||||
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
if (parsedPeriod < 1) {
|
||||
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||
} else {
|
||||
|
||||
log(`Clearing Data older than ${parsedPeriod} days...`);
|
||||
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
|
||||
|
||||
try {
|
||||
await R.exec(
|
||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||
[ parsedPeriod ]
|
||||
);
|
||||
|
||||
await R.exec("PRAGMA optimize;");
|
||||
} catch (e) {
|
||||
log(`Failed to clear old data: ${e.message}`);
|
||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exit();
|
||||
})();
|
||||
module.exports = {
|
||||
clearOldData,
|
||||
};
|
||||
|
21
server/jobs/incremental-vacuum.js
Normal file
21
server/jobs/incremental-vacuum.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
|
||||
/**
|
||||
* Run incremental_vacuum and checkpoint the WAL.
|
||||
* @return {Promise<void>} A promise that resolves when the process is finished.
|
||||
*/
|
||||
|
||||
const incrementalVacuum = async () => {
|
||||
try {
|
||||
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
|
||||
await R.exec("PRAGMA incremental_vacuum(200)");
|
||||
await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
||||
} catch (e) {
|
||||
log.error("incrementalVacuum", `Failed: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
incrementalVacuum,
|
||||
};
|
@@ -1,50 +0,0 @@
|
||||
const { parentPort, workerData } = require("worker_threads");
|
||||
const Database = require("../database");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Send message to parent process for logging
|
||||
* since worker_thread does not have access to stdout, this is used
|
||||
* instead of console.log()
|
||||
* @param {any} any The message to log
|
||||
*/
|
||||
const log = function (any) {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage(any);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Exit the worker process
|
||||
* @param {number} error The status code to exit
|
||||
*/
|
||||
const exit = function (error) {
|
||||
if (error && error !== 0) {
|
||||
process.exit(error);
|
||||
} else {
|
||||
if (parentPort) {
|
||||
parentPort.postMessage("done");
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Connects to the database */
|
||||
const connectDb = async function () {
|
||||
const dbPath = path.join(
|
||||
process.env.DATA_DIR || workerData["data-dir"] || "./data/"
|
||||
);
|
||||
|
||||
Database.init({
|
||||
"data-dir": dbPath,
|
||||
});
|
||||
|
||||
await Database.connect();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
log,
|
||||
exit,
|
||||
connectDb,
|
||||
};
|
@@ -9,12 +9,12 @@ class Group extends BeanModel {
|
||||
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false) {
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
let monitorList = [];
|
||||
|
||||
for (let bean of monitorBeanList) {
|
||||
monitorList.push(await bean.toPublicJSON(showTags));
|
||||
monitorList.push(await bean.toPublicJSON(showTags, certExpiry));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -1,8 +1,10 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
|
||||
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
|
||||
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
const Cron = require("croner");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const apicache = require("../modules/apicache");
|
||||
|
||||
class Maintenance extends BeanModel {
|
||||
|
||||
@@ -15,16 +17,19 @@ class Maintenance extends BeanModel {
|
||||
|
||||
let dateRange = [];
|
||||
if (this.start_date) {
|
||||
dateRange.push(utcToLocal(this.start_date));
|
||||
if (this.end_date) {
|
||||
dateRange.push(utcToLocal(this.end_date));
|
||||
}
|
||||
dateRange.push(this.start_date);
|
||||
} else {
|
||||
dateRange.push(null);
|
||||
}
|
||||
|
||||
if (this.end_date) {
|
||||
dateRange.push(this.end_date);
|
||||
}
|
||||
|
||||
let timeRange = [];
|
||||
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
|
||||
let startTime = parseTimeObject(this.start_time);
|
||||
timeRange.push(startTime);
|
||||
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
|
||||
let endTime = parseTimeObject(this.end_time);
|
||||
timeRange.push(endTime);
|
||||
|
||||
let obj = {
|
||||
@@ -39,12 +44,44 @@ class Maintenance extends BeanModel {
|
||||
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
||||
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
||||
timeslotList: [],
|
||||
cron: this.cron,
|
||||
duration: this.duration,
|
||||
durationMinutes: parseInt(this.duration / 60),
|
||||
timezone: await this.getTimezone(), // Only valid timezone
|
||||
timezoneOption: this.timezone, // Mainly for dropdown menu, because there is a option "SAME_AS_SERVER"
|
||||
timezoneOffset: await this.getTimezoneOffset(),
|
||||
status: await this.getStatus(),
|
||||
};
|
||||
|
||||
const timeslotList = await this.getTimeslotList();
|
||||
if (this.strategy === "manual") {
|
||||
// Do nothing, no timeslots
|
||||
} else if (this.strategy === "single") {
|
||||
obj.timeslotList.push({
|
||||
startDate: this.start_date,
|
||||
endDate: this.end_date,
|
||||
});
|
||||
} else {
|
||||
// Should be cron or recurring here
|
||||
if (this.beanMeta.job) {
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
|
||||
for (let timeslot of timeslotList) {
|
||||
obj.timeslotList.push(await timeslot.toPublicJSON());
|
||||
if (runningTimeslot) {
|
||||
obj.timeslotList.push(runningTimeslot);
|
||||
}
|
||||
|
||||
let nextRunDate = this.beanMeta.job.nextRun();
|
||||
if (nextRunDate) {
|
||||
let startDateDayjs = dayjs(nextRunDate);
|
||||
|
||||
let startDate = startDateDayjs.toISOString();
|
||||
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
|
||||
|
||||
obj.timeslotList.push({
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(obj.weekdays)) {
|
||||
@@ -55,54 +92,9 @@ class Maintenance extends BeanModel {
|
||||
obj.daysOfMonth = [];
|
||||
}
|
||||
|
||||
// Maintenance Status
|
||||
if (!obj.active) {
|
||||
obj.status = "inactive";
|
||||
} else if (obj.strategy === "manual") {
|
||||
obj.status = "under-maintenance";
|
||||
} else if (obj.timeslotList.length > 0) {
|
||||
let currentTimestamp = dayjs().unix();
|
||||
|
||||
for (let timeslot of obj.timeslotList) {
|
||||
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
|
||||
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
|
||||
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
|
||||
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
|
||||
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
|
||||
|
||||
obj.status = "under-maintenance";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!obj.status) {
|
||||
obj.status = "scheduled";
|
||||
}
|
||||
} else if (obj.timeslotList.length === 0) {
|
||||
obj.status = "ended";
|
||||
} else {
|
||||
obj.status = "unknown";
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only get future or current timeslots only
|
||||
* @returns {Promise<[]>}
|
||||
*/
|
||||
async getTimeslotList() {
|
||||
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
|
||||
SELECT maintenance_timeslot.*
|
||||
FROM maintenance_timeslot, maintenance
|
||||
WHERE maintenance_timeslot.maintenance_id = maintenance.id
|
||||
AND maintenance.id = ?
|
||||
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
|
||||
`, [
|
||||
this.id
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||
@@ -126,7 +118,7 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get a list of days in month that maintenance is active for
|
||||
* @returns {number[]} Array of active days in month
|
||||
* @returns {number[]|string[]} Array of active days in month
|
||||
*/
|
||||
getDayOfMonthList() {
|
||||
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
||||
@@ -135,26 +127,10 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start date and time for maintenance
|
||||
* @returns {dayjs.Dayjs} Start date and time
|
||||
*/
|
||||
getStartDateTime() {
|
||||
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
|
||||
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
|
||||
|
||||
// Start Time
|
||||
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
|
||||
log.debug("timeslot", "startTime: " + startTimeSecond);
|
||||
|
||||
// Bake StartDate + StartTime = Start DateTime
|
||||
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duraction of maintenance in seconds
|
||||
* Get the duration of maintenance in seconds
|
||||
* @returns {number} Duration of maintenance
|
||||
*/
|
||||
getDuration() {
|
||||
calcDuration() {
|
||||
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
||||
// Add 24hours if it is across day
|
||||
if (duration < 0) {
|
||||
@@ -169,71 +145,270 @@ class Maintenance extends BeanModel {
|
||||
* @param {Object} obj Data to fill bean with
|
||||
* @returns {Bean} Filled bean
|
||||
*/
|
||||
static jsonToBean(bean, obj) {
|
||||
static async jsonToBean(bean, obj) {
|
||||
if (obj.id) {
|
||||
bean.id = obj.id;
|
||||
}
|
||||
|
||||
// Apply timezone offset to timeRange, as it cannot apply automatically.
|
||||
if (obj.timeRange[0]) {
|
||||
timeObjectToUTC(obj.timeRange[0]);
|
||||
if (obj.timeRange[1]) {
|
||||
timeObjectToUTC(obj.timeRange[1]);
|
||||
}
|
||||
}
|
||||
|
||||
bean.title = obj.title;
|
||||
bean.description = obj.description;
|
||||
bean.strategy = obj.strategy;
|
||||
bean.interval_day = obj.intervalDay;
|
||||
bean.timezone = obj.timezoneOption;
|
||||
bean.active = obj.active;
|
||||
|
||||
if (obj.dateRange[0]) {
|
||||
bean.start_date = localToUTC(obj.dateRange[0]);
|
||||
|
||||
if (obj.dateRange[1]) {
|
||||
bean.end_date = localToUTC(obj.dateRange[1]);
|
||||
}
|
||||
bean.start_date = obj.dateRange[0];
|
||||
} else {
|
||||
bean.start_date = null;
|
||||
}
|
||||
|
||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||
if (obj.dateRange[1]) {
|
||||
bean.end_date = obj.dateRange[1];
|
||||
} else {
|
||||
bean.end_date = null;
|
||||
}
|
||||
|
||||
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||
if (bean.strategy === "cron") {
|
||||
bean.duration = obj.durationMinutes * 60;
|
||||
bean.cron = obj.cron;
|
||||
this.validateCron(bean.cron);
|
||||
}
|
||||
|
||||
if (bean.strategy.startsWith("recurring-")) {
|
||||
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
||||
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
||||
bean.weekdays = JSON.stringify(obj.weekdays);
|
||||
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
||||
await bean.generateCron();
|
||||
this.validateCron(bean.cron);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL conditions for active maintenance
|
||||
* @returns {string}
|
||||
* Throw error if cron is invalid
|
||||
* @param cron
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static getActiveMaintenanceSQLCondition() {
|
||||
return `
|
||||
(
|
||||
(maintenance_timeslot.start_date <= DATETIME('now')
|
||||
AND maintenance_timeslot.end_date >= DATETIME('now')
|
||||
AND maintenance.active = 1)
|
||||
OR
|
||||
(maintenance.strategy = 'manual' AND active = 1)
|
||||
)
|
||||
`;
|
||||
static async validateCron(cron) {
|
||||
let job = new Cron(cron, () => {});
|
||||
job.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL conditions for active and future maintenance
|
||||
* @returns {string}
|
||||
* Run the cron
|
||||
*/
|
||||
static getActiveAndFutureMaintenanceSQLCondition() {
|
||||
return `
|
||||
(
|
||||
((maintenance_timeslot.end_date >= DATETIME('now')
|
||||
AND maintenance.active = 1)
|
||||
OR
|
||||
(maintenance.strategy = 'manual' AND active = 1))
|
||||
)
|
||||
`;
|
||||
async run(throwError = false) {
|
||||
if (this.beanMeta.job) {
|
||||
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
log.debug("maintenance", "Run maintenance id: " + this.id);
|
||||
|
||||
// 1.21.2 migration
|
||||
if (!this.cron) {
|
||||
await this.generateCron();
|
||||
if (!this.timezone) {
|
||||
this.timezone = "UTC";
|
||||
}
|
||||
if (this.cron) {
|
||||
await R.store(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.strategy === "manual") {
|
||||
// Do nothing, because it is controlled by the user
|
||||
} else if (this.strategy === "single") {
|
||||
this.beanMeta.job = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
|
||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
apicache.clear();
|
||||
});
|
||||
} else if (this.cron != null) {
|
||||
// Here should be cron or recurring
|
||||
try {
|
||||
this.beanMeta.status = "scheduled";
|
||||
|
||||
let startEvent = (customDuration = 0) => {
|
||||
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
||||
|
||||
this.beanMeta.status = "under-maintenance";
|
||||
clearTimeout(this.beanMeta.durationTimeout);
|
||||
|
||||
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
||||
let duration;
|
||||
|
||||
if (customDuration > 0) {
|
||||
duration = customDuration;
|
||||
} else if (this.end_date) {
|
||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||
if (d < this.duration) {
|
||||
duration = d * 1000;
|
||||
}
|
||||
} else {
|
||||
duration = this.duration * 1000;
|
||||
}
|
||||
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
|
||||
this.beanMeta.durationTimeout = setTimeout(() => {
|
||||
// End of maintenance for this timeslot
|
||||
this.beanMeta.status = "scheduled";
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Create Cron
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
}, startEvent);
|
||||
|
||||
// Continue if the maintenance is still in the window
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
let current = dayjs();
|
||||
|
||||
if (runningTimeslot) {
|
||||
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
||||
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
|
||||
startEvent(duration);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log.error("maintenance", "Error in maintenance id: " + this.id);
|
||||
log.error("maintenance", "Cron: " + this.cron);
|
||||
log.error("maintenance", e);
|
||||
|
||||
if (throwError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
|
||||
}
|
||||
}
|
||||
|
||||
getRunningTimeslot() {
|
||||
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
|
||||
let end = start.add(this.duration, "second");
|
||||
let current = dayjs();
|
||||
|
||||
if (current.isAfter(start) && current.isBefore(end)) {
|
||||
return {
|
||||
startDate: start.toISOString(),
|
||||
endDate: end.toISOString(),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.beanMeta.job) {
|
||||
this.beanMeta.job.stop();
|
||||
delete this.beanMeta.job;
|
||||
}
|
||||
}
|
||||
|
||||
async isUnderMaintenance() {
|
||||
return (await this.getStatus()) === "under-maintenance";
|
||||
}
|
||||
|
||||
async getTimezone() {
|
||||
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
||||
return await UptimeKumaServer.getInstance().getTimezone();
|
||||
}
|
||||
return this.timezone;
|
||||
}
|
||||
|
||||
async getTimezoneOffset() {
|
||||
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
if (!this.active) {
|
||||
return "inactive";
|
||||
}
|
||||
|
||||
if (this.strategy === "manual") {
|
||||
return "under-maintenance";
|
||||
}
|
||||
|
||||
// Check if the maintenance is started
|
||||
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
|
||||
return "scheduled";
|
||||
}
|
||||
|
||||
// Check if the maintenance is ended
|
||||
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
|
||||
return "ended";
|
||||
}
|
||||
|
||||
if (this.strategy === "single") {
|
||||
return "under-maintenance";
|
||||
}
|
||||
|
||||
if (!this.beanMeta.status) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return this.beanMeta.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Cron for recurring maintenance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async generateCron() {
|
||||
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
||||
|
||||
if (this.strategy === "cron") {
|
||||
// Do nothing for cron
|
||||
} else if (!this.strategy.startsWith("recurring-")) {
|
||||
this.cron = "";
|
||||
} else if (this.strategy === "recurring-interval") {
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
|
||||
this.duration = this.calcDuration();
|
||||
log.debug("maintenance", "Cron: " + this.cron);
|
||||
log.debug("maintenance", "Duration: " + this.duration);
|
||||
} else if (this.strategy === "recurring-weekday") {
|
||||
let list = this.getDayOfWeekList();
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " * * " + list.join(",");
|
||||
this.duration = this.calcDuration();
|
||||
} else if (this.strategy === "recurring-day-of-month") {
|
||||
let list = this.getDayOfMonthList();
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
|
||||
let dayList = [];
|
||||
|
||||
for (let day of list) {
|
||||
if (typeof day === "string" && day.startsWith("lastDay")) {
|
||||
if (day === "lastDay1") {
|
||||
dayList.push("L");
|
||||
}
|
||||
// Unfortunately, lastDay2-4 is not supported by cron
|
||||
} else {
|
||||
dayList.push(day);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate
|
||||
dayList = [ ...new Set(dayList) ];
|
||||
|
||||
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
|
||||
this.duration = this.calcDuration();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,198 +0,0 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
|
||||
class MaintenanceTimeslot extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
|
||||
const obj = {
|
||||
id: this.id,
|
||||
startDate: this.start_date,
|
||||
endDate: this.end_date,
|
||||
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
|
||||
serverTimezoneOffset,
|
||||
};
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toJSON() {
|
||||
return await this.toPublicJSON();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Maintenance} maintenance
|
||||
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
|
||||
* @param {boolean} removeExist Remove existing timeslot before create
|
||||
* @returns {Promise<MaintenanceTimeslot>}
|
||||
*/
|
||||
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
|
||||
if (removeExist) {
|
||||
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
|
||||
maintenance.id
|
||||
]);
|
||||
}
|
||||
|
||||
if (maintenance.strategy === "manual") {
|
||||
log.debug("maintenance", "No need to generate timeslot for manual type");
|
||||
|
||||
} else if (maintenance.strategy === "single") {
|
||||
let bean = R.dispense("maintenance_timeslot");
|
||||
bean.maintenance_id = maintenance.id;
|
||||
bean.start_date = maintenance.start_date;
|
||||
bean.end_date = maintenance.end_date;
|
||||
bean.generated_next = true;
|
||||
return await R.store(bean);
|
||||
|
||||
} else if (maintenance.strategy === "recurring-interval") {
|
||||
// Prevent dead loop, in case interval_day is not set
|
||||
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
|
||||
maintenance.interval_day = 1;
|
||||
}
|
||||
|
||||
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||
return startDateTime.add(maintenance.interval_day, "day");
|
||||
}, () => {
|
||||
return true;
|
||||
});
|
||||
|
||||
} else if (maintenance.strategy === "recurring-weekday") {
|
||||
let dayOfWeekList = maintenance.getDayOfWeekList();
|
||||
log.debug("timeslot", dayOfWeekList);
|
||||
|
||||
if (dayOfWeekList.length <= 0) {
|
||||
log.debug("timeslot", "No weekdays selected?");
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = (startDateTime) => {
|
||||
log.debug("timeslot", "nextDateTime: " + startDateTime);
|
||||
|
||||
let day = startDateTime.local().day();
|
||||
log.debug("timeslot", "nextDateTime.day(): " + day);
|
||||
|
||||
return dayOfWeekList.includes(day);
|
||||
};
|
||||
|
||||
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||
while (true) {
|
||||
startDateTime = startDateTime.add(1, "day");
|
||||
|
||||
if (isValid(startDateTime)) {
|
||||
return startDateTime;
|
||||
}
|
||||
}
|
||||
}, isValid);
|
||||
|
||||
} else if (maintenance.strategy === "recurring-day-of-month") {
|
||||
let dayOfMonthList = maintenance.getDayOfMonthList();
|
||||
if (dayOfMonthList.length <= 0) {
|
||||
log.debug("timeslot", "No day selected?");
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = (startDateTime) => {
|
||||
let day = parseInt(startDateTime.local().format("D"));
|
||||
|
||||
log.debug("timeslot", "day: " + day);
|
||||
|
||||
// Check 1-31
|
||||
if (dayOfMonthList.includes(day)) {
|
||||
return startDateTime;
|
||||
}
|
||||
|
||||
// Check "lastDay1","lastDay2"...
|
||||
let daysInMonth = startDateTime.daysInMonth();
|
||||
let lastDayList = [];
|
||||
|
||||
// Small first, e.g. 28 > 29 > 30 > 31
|
||||
for (let i = 4; i >= 1; i--) {
|
||||
if (dayOfMonthList.includes("lastDay" + i)) {
|
||||
lastDayList.push(daysInMonth - i + 1);
|
||||
}
|
||||
}
|
||||
log.debug("timeslot", lastDayList);
|
||||
return lastDayList.includes(day);
|
||||
};
|
||||
|
||||
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
|
||||
while (true) {
|
||||
startDateTime = startDateTime.add(1, "day");
|
||||
if (isValid(startDateTime)) {
|
||||
return startDateTime;
|
||||
}
|
||||
}
|
||||
}, isValid);
|
||||
} else {
|
||||
throw new Error("Unknown maintenance strategy");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a next timeslot for all recurring types
|
||||
* @param maintenance
|
||||
* @param minDate
|
||||
* @param {function} nextDayCallback The logic how to get the next possible day
|
||||
* @param {function} isValidCallback Check the day whether is matched the current strategy
|
||||
* @returns {Promise<null|MaintenanceTimeslot>}
|
||||
*/
|
||||
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
|
||||
let bean = R.dispense("maintenance_timeslot");
|
||||
|
||||
let duration = maintenance.getDuration();
|
||||
let startDateTime = maintenance.getStartDateTime();
|
||||
let endDateTime;
|
||||
|
||||
// Keep generating from the first possible date, until it is ok
|
||||
while (true) {
|
||||
log.debug("timeslot", "startDateTime: " + startDateTime.format());
|
||||
|
||||
// Handling out of effective date range
|
||||
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||
log.debug("timeslot", "Out of effective date range");
|
||||
return null;
|
||||
}
|
||||
|
||||
endDateTime = startDateTime.add(duration, "second");
|
||||
|
||||
// If endDateTime is out of effective date range, use the end datetime from effective date range
|
||||
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
|
||||
endDateTime = dayjs.utc(maintenance.end_date);
|
||||
}
|
||||
|
||||
// If minDate is set, the endDateTime must be bigger than it.
|
||||
// And the endDateTime must be bigger current time
|
||||
// Is valid under current recurring strategy
|
||||
if (
|
||||
(!minDate || endDateTime.diff(minDate) > 0) &&
|
||||
endDateTime.diff(dayjs()) > 0 &&
|
||||
isValidCallback(startDateTime)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
startDateTime = nextDayCallback(startDateTime);
|
||||
}
|
||||
|
||||
bean.maintenance_id = maintenance.id;
|
||||
bean.start_date = localToUTC(startDateTime);
|
||||
bean.end_date = localToUTC(endDateTime);
|
||||
bean.generated_next = false;
|
||||
return await R.store(bean);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MaintenanceTimeslot;
|
@@ -2,9 +2,11 @@ const https = require("https");
|
||||
const dayjs = require("dayjs");
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing,
|
||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials,
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
@@ -16,9 +18,10 @@ const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
const Maintenance = require("./maintenance");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const Gamedig = require("gamedig");
|
||||
const jsonata = require("jsonata");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@@ -34,11 +37,12 @@ class Monitor extends BeanModel {
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
*/
|
||||
async toPublicJSON(showTags = false) {
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
let obj = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
sendUrl: this.sendUrl,
|
||||
type: this.type,
|
||||
};
|
||||
|
||||
if (this.sendUrl) {
|
||||
@@ -48,6 +52,13 @@ class Monitor extends BeanModel {
|
||||
if (showTags) {
|
||||
obj.tags = await this.getTags();
|
||||
}
|
||||
|
||||
if (certExpiry && this.type === "http") {
|
||||
const { certExpiryDaysRemaining, validCert } = await this.getCertExpiry(this.id);
|
||||
obj.certExpiryDaysRemaining = certExpiryDaysRemaining;
|
||||
obj.validCert = validCert;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -69,22 +80,34 @@ class Monitor extends BeanModel {
|
||||
|
||||
const tags = await this.getTags();
|
||||
|
||||
let screenshot = null;
|
||||
|
||||
if (this.type === "real-browser") {
|
||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||
}
|
||||
|
||||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
pathName: await this.getPathName(),
|
||||
parent: this.parent,
|
||||
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
maxretries: this.maxretries,
|
||||
weight: this.weight,
|
||||
active: this.active,
|
||||
active: await this.isActive(),
|
||||
forceInactive: !await Monitor.isParentActive(this.id),
|
||||
type: this.type,
|
||||
timeout: this.timeout,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
resendInterval: this.resendInterval,
|
||||
keyword: this.keyword,
|
||||
invertKeyword: this.isInvertKeyword(),
|
||||
expiryNotification: this.isEnabledExpiryNotification(),
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
@@ -112,7 +135,16 @@ class Monitor extends BeanModel {
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
game: this.game,
|
||||
httpBodyEncoding: this.httpBodyEncoding
|
||||
gamedigGivenPortOnly: this.getGameDigGivenPortOnly(),
|
||||
httpBodyEncoding: this.httpBodyEncoding,
|
||||
jsonPath: this.jsonPath,
|
||||
expectedValue: this.expectedValue,
|
||||
kafkaProducerTopic: this.kafkaProducerTopic,
|
||||
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
||||
kafkaProducerSsl: this.kafkaProducerSsl === "1" && true || false,
|
||||
kafkaProducerAllowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation === "1" && true || false,
|
||||
kafkaProducerMessage: this.kafkaProducerMessage,
|
||||
screenshot,
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@@ -124,6 +156,11 @@ class Monitor extends BeanModel {
|
||||
grpcMetadata: this.grpcMetadata,
|
||||
basic_auth_user: this.basic_auth_user,
|
||||
basic_auth_pass: this.basic_auth_pass,
|
||||
oauth_client_id: this.oauth_client_id,
|
||||
oauth_client_secret: this.oauth_client_secret,
|
||||
oauth_token_url: this.oauth_token_url,
|
||||
oauth_scopes: this.oauth_scopes,
|
||||
oauth_auth_method: this.oauth_auth_method,
|
||||
pushToken: this.pushToken,
|
||||
databaseConnectionString: this.databaseConnectionString,
|
||||
radiusUsername: this.radiusUsername,
|
||||
@@ -133,6 +170,10 @@ class Monitor extends BeanModel {
|
||||
mqttPassword: this.mqttPassword,
|
||||
authWorkstation: this.authWorkstation,
|
||||
authDomain: this.authDomain,
|
||||
tlsCa: this.tlsCa,
|
||||
tlsCert: this.tlsCert,
|
||||
tlsKey: this.tlsKey,
|
||||
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,6 +181,16 @@ class Monitor extends BeanModel {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the monitor is active based on itself and its parents
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async isActive() {
|
||||
const parentActive = await Monitor.isParentActive(this.id);
|
||||
|
||||
return (this.active === 1) && parentActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags applied to this monitor
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
@@ -148,6 +199,31 @@ class Monitor extends BeanModel {
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets certificate expiry for this monitor
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
*/
|
||||
async getCertExpiry(monitorID) {
|
||||
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
monitorID,
|
||||
]);
|
||||
let tlsInfo;
|
||||
if (tlsInfoBean) {
|
||||
tlsInfo = JSON.parse(tlsInfoBean?.info_json);
|
||||
if (tlsInfo?.valid && tlsInfo?.certInfo?.daysRemaining) {
|
||||
return {
|
||||
certExpiryDaysRemaining: tlsInfo.certInfo.daysRemaining,
|
||||
validCert: true
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
certExpiryDaysRemaining: "",
|
||||
validCert: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode user and password to Base64 encoding
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
@@ -181,6 +257,14 @@ class Monitor extends BeanModel {
|
||||
return Boolean(this.upsideDown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInvertKeyword() {
|
||||
return Boolean(this.invertKeyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
@@ -197,6 +281,10 @@ class Monitor extends BeanModel {
|
||||
return JSON.parse(this.accepted_statuscodes_json);
|
||||
}
|
||||
|
||||
getGameDigGivenPortOnly() {
|
||||
return Boolean(this.gamedigGivenPortOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitor
|
||||
* @param {Server} io Socket server instance
|
||||
@@ -255,7 +343,40 @@ class Monitor extends BeanModel {
|
||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||
bean.msg = "Monitor under maintenance";
|
||||
bean.status = MAINTENANCE;
|
||||
} else if (this.type === "http" || this.type === "keyword") {
|
||||
} else if (this.type === "group") {
|
||||
const children = await Monitor.getChildren(this.id);
|
||||
|
||||
if (children.length > 0) {
|
||||
bean.status = UP;
|
||||
bean.msg = "All children up and running";
|
||||
for (const child of children) {
|
||||
if (!child.active) {
|
||||
// Ignore inactive childs
|
||||
continue;
|
||||
}
|
||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||
|
||||
// Only change state if the monitor is in worse conditions then the ones before
|
||||
// lastBeat.status could be null
|
||||
if (!lastBeat) {
|
||||
bean.status = PENDING;
|
||||
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||
bean.status = lastBeat.status;
|
||||
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
||||
bean.status = lastBeat.status;
|
||||
}
|
||||
}
|
||||
|
||||
if (bean.status !== UP) {
|
||||
bean.msg = "Child inaccessible";
|
||||
}
|
||||
} else {
|
||||
// Set status pending if group is empty
|
||||
bean.status = PENDING;
|
||||
bean.msg = "Group empty";
|
||||
}
|
||||
|
||||
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
|
||||
// Do not do any queries/high loading things before the "bean.ping"
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
@@ -267,6 +388,24 @@ class Monitor extends BeanModel {
|
||||
};
|
||||
}
|
||||
|
||||
// OIDC: Basic client credential flow.
|
||||
// Additional grants might be implemented in the future
|
||||
let oauth2AuthHeader = {};
|
||||
if (this.auth_method === "oauth2-cc") {
|
||||
try {
|
||||
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
|
||||
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
|
||||
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
|
||||
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
|
||||
}
|
||||
oauth2AuthHeader = {
|
||||
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error("The oauth config is invalid. " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const httpsAgentOptions = {
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
@@ -295,12 +434,13 @@ class Monitor extends BeanModel {
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
timeout: this.timeout * 1000,
|
||||
headers: {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(basicAuthHeader),
|
||||
...(oauth2AuthHeader),
|
||||
...(this.headers ? JSON.parse(this.headers) : {})
|
||||
},
|
||||
maxRedirects: this.maxredirects,
|
||||
@@ -331,6 +471,18 @@ class Monitor extends BeanModel {
|
||||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||
}
|
||||
|
||||
if (this.auth_method === "mtls") {
|
||||
if (this.tlsCert !== null && this.tlsCert !== "") {
|
||||
options.httpsAgent.options.cert = Buffer.from(this.tlsCert);
|
||||
}
|
||||
if (this.tlsCa !== null && this.tlsCa !== "") {
|
||||
options.httpsAgent.options.ca = Buffer.from(this.tlsCa);
|
||||
}
|
||||
if (this.tlsKey !== null && this.tlsKey !== "") {
|
||||
options.httpsAgent.options.key = Buffer.from(this.tlsKey);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
|
||||
@@ -349,8 +501,8 @@ class Monitor extends BeanModel {
|
||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||
|
||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||
log.debug("monitor", `[${this.name}] call sendCertNotification`);
|
||||
await this.sendCertNotification(tlsInfoObject);
|
||||
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
||||
await this.checkCertExpiryNotifications(tlsInfoObject);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
@@ -371,7 +523,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
if (this.type === "http") {
|
||||
bean.status = UP;
|
||||
} else {
|
||||
} else if (this.type === "keyword") {
|
||||
|
||||
let data = res.data;
|
||||
|
||||
@@ -380,17 +532,37 @@ class Monitor extends BeanModel {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
if (data.includes(this.keyword)) {
|
||||
bean.msg += ", keyword is found";
|
||||
let keywordFound = data.includes(this.keyword);
|
||||
if (keywordFound === !this.isInvertKeyword()) {
|
||||
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
||||
if (data.length > 50) {
|
||||
data = data.substring(0, 47) + "...";
|
||||
}
|
||||
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
|
||||
throw new Error(bean.msg + ", but keyword is " +
|
||||
(keywordFound ? "present" : "not") + " in [" + data + "]");
|
||||
}
|
||||
|
||||
} else if (this.type === "json-query") {
|
||||
let data = res.data;
|
||||
|
||||
// convert data to object
|
||||
if (typeof data === "string") {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
|
||||
let expression = jsonata(this.jsonPath);
|
||||
|
||||
let result = await expression.evaluate(data);
|
||||
|
||||
if (result.toString() === this.expectedValue) {
|
||||
bean.msg += ", expected value is found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (this.type === "port") {
|
||||
@@ -465,7 +637,7 @@ class Monitor extends BeanModel {
|
||||
// No need to insert successful heartbeat for push type, so end here
|
||||
retries = 0;
|
||||
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
|
||||
this.heartbeatInterval = setTimeout(beat, timeout);
|
||||
this.heartbeatInterval = setTimeout(safeBeat, timeout);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -482,7 +654,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
let res = await axios.get(steamApiUrl, {
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
timeout: this.timeout * 1000,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
@@ -520,7 +692,7 @@ class Monitor extends BeanModel {
|
||||
type: this.game,
|
||||
host: this.hostname,
|
||||
port: this.port,
|
||||
givenPortOnly: true,
|
||||
givenPortOnly: this.getGameDigGivenPortOnly(),
|
||||
});
|
||||
|
||||
bean.msg = state.name;
|
||||
@@ -554,13 +726,22 @@ class Monitor extends BeanModel {
|
||||
options.socketPath = dockerHost._dockerDaemon;
|
||||
} else if (dockerHost._dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
|
||||
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
||||
);
|
||||
}
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
let res = await axios.request(options);
|
||||
|
||||
if (res.data.State.Running) {
|
||||
bean.status = UP;
|
||||
bean.msg = res.data.State.Status;
|
||||
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
|
||||
bean.status = PENDING;
|
||||
bean.msg = res.data.State.Health.Status;
|
||||
} else {
|
||||
bean.status = UP;
|
||||
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
|
||||
}
|
||||
} else {
|
||||
throw Error("Container State is " + res.data.State.Status);
|
||||
}
|
||||
@@ -589,7 +770,6 @@ class Monitor extends BeanModel {
|
||||
grpcEnableTls: this.grpcEnableTls,
|
||||
grpcMethod: this.grpcMethod,
|
||||
grpcBody: this.grpcBody,
|
||||
keyword: this.keyword
|
||||
};
|
||||
const response = await grpcQuery(options);
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
@@ -602,13 +782,14 @@ class Monitor extends BeanModel {
|
||||
bean.status = DOWN;
|
||||
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||
} else {
|
||||
if (response.data.toString().includes(this.keyword)) {
|
||||
let keywordFound = response.data.toString().includes(this.keyword);
|
||||
if (keywordFound === !this.isInvertKeyword()) {
|
||||
bean.status = UP;
|
||||
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
|
||||
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
||||
} else {
|
||||
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
|
||||
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
|
||||
bean.status = DOWN;
|
||||
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
|
||||
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
|
||||
}
|
||||
}
|
||||
} else if (this.type === "postgres") {
|
||||
@@ -622,9 +803,7 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "mysql") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
|
||||
bean.msg = "";
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
@@ -649,28 +828,19 @@ class Monitor extends BeanModel {
|
||||
port = this.port;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await radius(
|
||||
this.hostname,
|
||||
this.radiusUsername,
|
||||
this.radiusPassword,
|
||||
this.radiusCalledStationId,
|
||||
this.radiusCallingStationId,
|
||||
this.radiusSecret,
|
||||
port
|
||||
);
|
||||
if (resp.code) {
|
||||
bean.msg = resp.code;
|
||||
}
|
||||
bean.status = UP;
|
||||
} catch (error) {
|
||||
bean.status = DOWN;
|
||||
if (error.response?.code) {
|
||||
bean.msg = error.response.code;
|
||||
} else {
|
||||
bean.msg = error.message;
|
||||
}
|
||||
}
|
||||
const resp = await radius(
|
||||
this.hostname,
|
||||
this.radiusUsername,
|
||||
this.radiusPassword,
|
||||
this.radiusCalledStationId,
|
||||
this.radiusCallingStationId,
|
||||
this.radiusSecret,
|
||||
port,
|
||||
this.interval * 1000 * 0.4,
|
||||
);
|
||||
|
||||
bean.msg = resp.code;
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
@@ -682,11 +852,29 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||
let startTime = dayjs().valueOf();
|
||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||
await monitorType.check(this, bean);
|
||||
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
|
||||
if (!bean.ping) {
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
}
|
||||
|
||||
} else if (this.type === "kafka-producer") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await kafkaProducerAsync(
|
||||
JSON.parse(this.kafkaProducerBrokers),
|
||||
this.kafkaProducerTopic,
|
||||
this.kafkaProducerMessage,
|
||||
{
|
||||
allowAutoTopicCreation: this.kafkaProducerAllowAutoTopicCreation,
|
||||
ssl: this.kafkaProducerSsl,
|
||||
clientId: `Uptime-Kuma/${version}`,
|
||||
interval: this.interval,
|
||||
},
|
||||
JSON.parse(this.kafkaProducerSaslOptions),
|
||||
);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else {
|
||||
throw new Error("Unknown Monitor Type");
|
||||
}
|
||||
@@ -836,7 +1024,6 @@ class Monitor extends BeanModel {
|
||||
domain: this.authDomain,
|
||||
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||
});
|
||||
|
||||
} else {
|
||||
res = await axios.request(options);
|
||||
}
|
||||
@@ -1165,12 +1352,18 @@ class Monitor extends BeanModel {
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
const heartbeatJSON = bean.toJSON();
|
||||
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
if (!heartbeatJSON["msg"]) {
|
||||
heartbeatJSON["msg"] = "N/A";
|
||||
}
|
||||
|
||||
// Also provide the time in server timezone
|
||||
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
|
||||
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
|
||||
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
|
||||
|
||||
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||
@@ -1193,13 +1386,19 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification about a certificate
|
||||
* checks certificate chain for expiring certificates
|
||||
* @param {Object} tlsInfoObject Information about certificate
|
||||
*/
|
||||
async sendCertNotification(tlsInfoObject) {
|
||||
async checkCertExpiryNotifications(tlsInfoObject) {
|
||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||
const notificationList = await Monitor.getNotificationList(this);
|
||||
|
||||
if (! notificationList.length > 0) {
|
||||
// fail fast. If no notification is set, all the following checks can be skipped.
|
||||
log.debug("monitor", "No notification, no need to send cert notification");
|
||||
return;
|
||||
}
|
||||
|
||||
let notifyDays = await setting("tlsExpiryNotifyDays");
|
||||
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||
// Reset Default
|
||||
@@ -1207,10 +1406,19 @@ class Monitor extends BeanModel {
|
||||
notifyDays = [ 7, 14, 21 ];
|
||||
}
|
||||
|
||||
if (notifyDays != null && Array.isArray(notifyDays)) {
|
||||
for (const day of notifyDays) {
|
||||
log.debug("monitor", "call sendCertNotificationByTargetDays", day);
|
||||
await this.sendCertNotificationByTargetDays(tlsInfoObject.certInfo.daysRemaining, day, notificationList);
|
||||
if (Array.isArray(notifyDays)) {
|
||||
for (const targetDays of notifyDays) {
|
||||
let certInfo = tlsInfoObject.certInfo;
|
||||
while (certInfo) {
|
||||
let subjectCN = certInfo.subject["CN"];
|
||||
if (certInfo.daysRemaining > targetDays) {
|
||||
log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`);
|
||||
} else {
|
||||
log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`);
|
||||
await this.sendCertNotificationByTargetDays(subjectCN, certInfo.certType, certInfo.daysRemaining, targetDays, notificationList);
|
||||
}
|
||||
certInfo = certInfo.issuerCertificate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1219,55 +1427,47 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Send a certificate notification when certificate expires in less
|
||||
* than target days
|
||||
* @param {number} daysRemaining Number of days remaining on certifcate
|
||||
* @param {string} certCN Common Name attribute from the certificate subject
|
||||
* @param {string} certType certificate type
|
||||
* @param {number} daysRemaining Number of days remaining on certificate
|
||||
* @param {number} targetDays Number of days to alert after
|
||||
* @param {LooseObject<any>[]} notificationList List of notification providers
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendCertNotificationByTargetDays(daysRemaining, targetDays, notificationList) {
|
||||
async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) {
|
||||
|
||||
if (daysRemaining > targetDays) {
|
||||
log.debug("monitor", `No need to send cert notification. ${daysRemaining} > ${targetDays}`);
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
log.debug("monitor", "Sent already, no need to send again");
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationList.length > 0) {
|
||||
let sent = false;
|
||||
log.debug("monitor", "Send certificate notification");
|
||||
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
log.debug("monitor", "Sending to " + notification.name);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] ${certType} certificate ${certCN} will be expired in ${daysRemaining} days`);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||
log.error("monitor", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
log.debug("monitor", "Sent already, no need to send again");
|
||||
return;
|
||||
}
|
||||
|
||||
let sent = false;
|
||||
log.debug("monitor", "Send certificate notification");
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
log.debug("monitor", "Sending to " + notification.name);
|
||||
await Notification.send(JSON.parse(notification.config), `[${this.name}][${this.url}] Certificate will be expired in ${daysRemaining} days`);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send cert notification to " + notification.name);
|
||||
log.error("monitor", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
await R.exec("INSERT INTO notification_sent_history (type, monitor_id, days) VALUES(?, ?, ?)", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
log.debug("monitor", "No notification, no need to send cert notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1291,18 +1491,24 @@ class Monitor extends BeanModel {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static async isUnderMaintenance(monitorID) {
|
||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||
const maintenance = await R.getRow(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM monitor_maintenance mm
|
||||
JOIN maintenance
|
||||
ON mm.maintenance_id = maintenance.id
|
||||
AND mm.monitor_id = ?
|
||||
LEFT JOIN maintenance_timeslot
|
||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||
WHERE ${activeCondition}
|
||||
LIMIT 1`, [ monitorID ]);
|
||||
return maintenance.count !== 0;
|
||||
const maintenanceIDList = await R.getCol(`
|
||||
SELECT maintenance_id FROM monitor_maintenance
|
||||
WHERE monitor_id = ?
|
||||
`, [ monitorID ]);
|
||||
|
||||
for (const maintenanceID of maintenanceIDList) {
|
||||
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await Monitor.getParent(monitorID);
|
||||
if (parent != null) {
|
||||
return await Monitor.isUnderMaintenance(parent.id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Make sure monitor interval is between bounds */
|
||||
@@ -1314,6 +1520,105 @@ class Monitor extends BeanModel {
|
||||
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Parent of the monitor
|
||||
* @param {number} monitorID ID of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
*/
|
||||
static async getParent(monitorID) {
|
||||
return await R.getRow(`
|
||||
SELECT parent.* FROM monitor parent
|
||||
LEFT JOIN monitor child
|
||||
ON child.parent = parent.id
|
||||
WHERE child.id = ?
|
||||
`, [
|
||||
monitorID,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all Children of the monitor
|
||||
* @param {number} monitorID ID of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
*/
|
||||
static async getChildren(monitorID) {
|
||||
return await R.getAll(`
|
||||
SELECT * FROM monitor
|
||||
WHERE parent = ?
|
||||
`, [
|
||||
monitorID,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Full Path-Name (Groups and Name)
|
||||
* @returns {Promise<String>}
|
||||
*/
|
||||
async getPathName() {
|
||||
let path = this.name;
|
||||
|
||||
if (this.parent === null) {
|
||||
return path;
|
||||
}
|
||||
|
||||
let parent = await Monitor.getParent(this.id);
|
||||
while (parent !== null) {
|
||||
path = `${parent.name} / ${path}`;
|
||||
parent = await Monitor.getParent(parent.id);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets recursive all child ids
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
static async getAllChildrenIDs(monitorID) {
|
||||
const childs = await Monitor.getChildren(monitorID);
|
||||
|
||||
if (childs === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let childrenIDs = [];
|
||||
|
||||
for (const child of childs) {
|
||||
childrenIDs.push(child.id);
|
||||
childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id));
|
||||
}
|
||||
|
||||
return childrenIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks all children of the the group monitor
|
||||
* @param {number} groupID ID of group to remove children of
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async unlinkAllChildren(groupID) {
|
||||
return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [
|
||||
null, groupID
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks recursive if parent (ancestors) are active
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
static async isParentActive(monitorID) {
|
||||
const parent = await Monitor.getParent(monitorID);
|
||||
|
||||
if (parent === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parentActive = await Monitor.isParentActive(parent.id);
|
||||
return parent.active && parentActive;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
||||
|
@@ -3,7 +3,6 @@ const { R } = require("redbean-node");
|
||||
const cheerio = require("cheerio");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const jsesc = require("jsesc");
|
||||
const Maintenance = require("./maintenance");
|
||||
const googleAnalytics = require("../google-analytics");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
@@ -91,6 +90,8 @@ class StatusPage extends BeanModel {
|
||||
* @param {StatusPage} statusPage
|
||||
*/
|
||||
static async getStatusPageData(statusPage) {
|
||||
const config = await statusPage.toPublicJSON();
|
||||
|
||||
// Incident
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||
statusPage.id,
|
||||
@@ -111,13 +112,13 @@ class StatusPage extends BeanModel {
|
||||
]);
|
||||
|
||||
for (let groupBean of list) {
|
||||
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
|
||||
publicGroupList.push(monitorGroup);
|
||||
}
|
||||
|
||||
// Response
|
||||
return {
|
||||
config: await statusPage.toPublicJSON(),
|
||||
config,
|
||||
incident,
|
||||
publicGroupList,
|
||||
maintenanceList,
|
||||
@@ -235,6 +236,7 @@ class StatusPage extends BeanModel {
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,6 +258,7 @@ class StatusPage extends BeanModel {
|
||||
footerText: this.footer_text,
|
||||
showPoweredBy: !!this.show_powered_by,
|
||||
googleAnalyticsId: this.google_analytics_tag_id,
|
||||
showCertificateExpiry: !!this.show_certificate_expiry,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -290,21 +293,17 @@ class StatusPage extends BeanModel {
|
||||
try {
|
||||
const publicMaintenanceList = [];
|
||||
|
||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
|
||||
SELECT DISTINCT maintenance.*
|
||||
FROM maintenance
|
||||
JOIN maintenance_status_page
|
||||
ON maintenance_status_page.maintenance_id = maintenance.id
|
||||
AND maintenance_status_page.status_page_id = ?
|
||||
LEFT JOIN maintenance_timeslot
|
||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||
WHERE ${activeCondition}
|
||||
ORDER BY maintenance.end_date
|
||||
`, [ statusPageId ]));
|
||||
let maintenanceIDList = await R.getCol(`
|
||||
SELECT DISTINCT maintenance_id
|
||||
FROM maintenance_status_page
|
||||
WHERE status_page_id = ?
|
||||
`, [ statusPageId ]);
|
||||
|
||||
for (const bean of maintenanceBeanList) {
|
||||
publicMaintenanceList.push(await bean.toPublicJSON());
|
||||
for (const maintenanceID of maintenanceIDList) {
|
||||
let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||
publicMaintenanceList.push(await maintenance.toPublicJSON());
|
||||
}
|
||||
}
|
||||
|
||||
return publicMaintenanceList;
|
||||
|
@@ -6,9 +6,10 @@ class MonitorType {
|
||||
*
|
||||
* @param {Monitor} monitor
|
||||
* @param {Heartbeat} heartbeat
|
||||
* @param {UptimeKumaServer} server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
async check(monitor, heartbeat, server) {
|
||||
throw new Error("You need to override check()");
|
||||
}
|
||||
|
||||
|
212
server/monitor-types/real-browser-monitor-type.js
Normal file
212
server/monitor-types/real-browser-monitor-type.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { chromium } = require("playwright-core");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const { Settings } = require("../settings");
|
||||
const commandExistsSync = require("command-exists").sync;
|
||||
const childProcess = require("child_process");
|
||||
const path = require("path");
|
||||
const Database = require("../database");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
|
||||
let browser = null;
|
||||
|
||||
let allowedList = [];
|
||||
let lastAutoDetectChromeExecutable = null;
|
||||
|
||||
if (process.platform === "win32") {
|
||||
allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
|
||||
|
||||
// Allow Chromium too
|
||||
allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
|
||||
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
||||
|
||||
// For Loop A to Z
|
||||
for (let i = 65; i <= 90; i++) {
|
||||
let drive = String.fromCharCode(i);
|
||||
allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
|
||||
allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
|
||||
}
|
||||
|
||||
} else if (process.platform === "linux") {
|
||||
allowedList = [
|
||||
"chromium",
|
||||
"chromium-browser",
|
||||
"google-chrome",
|
||||
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/google-chrome",
|
||||
];
|
||||
} else if (process.platform === "darwin") {
|
||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||
allowedList = [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
];
|
||||
}
|
||||
|
||||
log.debug("chrome", allowedList);
|
||||
|
||||
async function isAllowedChromeExecutable(executablePath) {
|
||||
console.log(config.args);
|
||||
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the executablePath is in the list of allowed executables
|
||||
return allowedList.includes(executablePath);
|
||||
}
|
||||
|
||||
async function getBrowser() {
|
||||
if (!browser) {
|
||||
let executablePath = await Settings.get("chromeExecutable");
|
||||
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
|
||||
browser = await chromium.launch({
|
||||
//headless: false,
|
||||
executablePath,
|
||||
});
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
async function prepareChromeExecutable(executablePath) {
|
||||
// Special code for using the playwright_chromium
|
||||
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
|
||||
// Set to undefined = use playwright_chromium
|
||||
executablePath = undefined;
|
||||
} else if (!executablePath) {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
executablePath = "/usr/bin/chromium";
|
||||
|
||||
// Install chromium in container via apt install
|
||||
if ( !commandExistsSync(executablePath)) {
|
||||
await new Promise((resolve, reject) => {
|
||||
log.info("Chromium", "Installing Chromium...");
|
||||
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
|
||||
|
||||
// On exit
|
||||
child.on("exit", (code) => {
|
||||
log.info("Chromium", "apt install chromium exited with code " + code);
|
||||
|
||||
if (code === 0) {
|
||||
log.info("Chromium", "Installed Chromium");
|
||||
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
|
||||
log.info("Chromium", "Chromium version: " + version);
|
||||
resolve();
|
||||
} else if (code === 100) {
|
||||
reject(new Error("Installing Chromium, please wait..."));
|
||||
} else {
|
||||
reject(new Error("apt install chromium failed with code " + code));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
executablePath = findChrome(allowedList);
|
||||
}
|
||||
} else {
|
||||
// User specified a path
|
||||
// Check if the executablePath is in the list of allowed
|
||||
if (!await isAllowedChromeExecutable(executablePath)) {
|
||||
throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
|
||||
}
|
||||
}
|
||||
return executablePath;
|
||||
}
|
||||
|
||||
function findChrome(executables) {
|
||||
// Use the last working executable, so we don't have to search for it again
|
||||
if (lastAutoDetectChromeExecutable) {
|
||||
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
|
||||
return lastAutoDetectChromeExecutable;
|
||||
}
|
||||
}
|
||||
|
||||
for (let executable of executables) {
|
||||
if (commandExistsSync(executable)) {
|
||||
lastAutoDetectChromeExecutable = executable;
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
|
||||
}
|
||||
|
||||
async function resetChrome() {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
browser = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the chrome executable is valid and return the version
|
||||
* @param executablePath
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function testChrome(executablePath) {
|
||||
try {
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
|
||||
log.info("Chromium", "Testing Chromium executable: " + executablePath);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath,
|
||||
});
|
||||
const version = browser.version();
|
||||
await browser.close();
|
||||
return version;
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
|
||||
*
|
||||
*/
|
||||
class RealBrowserMonitorType extends MonitorType {
|
||||
|
||||
name = "real-browser";
|
||||
|
||||
async check(monitor, heartbeat, server) {
|
||||
const browser = await getBrowser();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
const res = await page.goto(monitor.url, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: monitor.interval * 1000 * 0.8,
|
||||
});
|
||||
|
||||
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(Database.screenshotDir, filename),
|
||||
});
|
||||
|
||||
await context.close();
|
||||
|
||||
if (res.status() >= 200 && res.status() < 400) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = res.status();
|
||||
|
||||
const timing = res.request().timing();
|
||||
heartbeat.ping = timing.responseEnd;
|
||||
} else {
|
||||
throw new Error(res.status() + "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RealBrowserMonitorType,
|
||||
testChrome,
|
||||
resetChrome,
|
||||
};
|
95
server/monitor-types/tailscale-ping.js
Normal file
95
server/monitor-types/tailscale-ping.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, log } = require("../../src/util");
|
||||
const exec = require("child_process").exec;
|
||||
|
||||
/**
|
||||
* A TailscalePing class extends the MonitorType.
|
||||
* It runs Tailscale ping to monitor the status of a specific node.
|
||||
*/
|
||||
class TailscalePing extends MonitorType {
|
||||
|
||||
name = "tailscale-ping";
|
||||
|
||||
/**
|
||||
* Checks the ping status of the URL associated with the monitor.
|
||||
* It then parses the Tailscale ping command output to update the heatrbeat.
|
||||
*
|
||||
* @param {Object} monitor - The monitor object associated with the check.
|
||||
* @param {Object} heartbeat - The heartbeat object to update.
|
||||
* @throws Will throw an error if checking Tailscale ping encounters any error
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
try {
|
||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||
} catch (err) {
|
||||
log.debug("Tailscale", err);
|
||||
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
||||
throw new Error(`Error checking Tailscale ping: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Tailscale ping command to the given URL.
|
||||
*
|
||||
* @param {string} hostname - The hostname to ping.
|
||||
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
||||
* @throws Will throw an error if the command execution encounters any error.
|
||||
*/
|
||||
async runTailscalePing(hostname, interval) {
|
||||
let cmd = `tailscale ping ${hostname}`;
|
||||
|
||||
log.debug("Tailscale", cmd);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeout = interval * 1000 * 0.8;
|
||||
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
|
||||
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
|
||||
if (error) {
|
||||
reject(`Execution error: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
reject(`Error in output: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the output of the Tailscale ping command to update the heartbeat.
|
||||
*
|
||||
* @param {string} tailscaleOutput - The output of the Tailscale ping command.
|
||||
* @param {Object} heartbeat - The heartbeat object to update.
|
||||
* @throws Will throw an eror if the output contains any unexpected string.
|
||||
*/
|
||||
parseTailscaleOutput(tailscaleOutput, heartbeat) {
|
||||
let lines = tailscaleOutput.split("\n");
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.includes("pong from")) {
|
||||
heartbeat.status = UP;
|
||||
let time = line.split(" in ")[1].split(" ")[0];
|
||||
heartbeat.ping = parseInt(time);
|
||||
heartbeat.msg = line;
|
||||
break;
|
||||
} else if (line.includes("timed out")) {
|
||||
throw new Error(`Ping timed out: "${line}"`);
|
||||
// Immediately throws upon "timed out" message, the server is expected to re-call the check function
|
||||
} else if (line.includes("no matching peer")) {
|
||||
throw new Error(`Nonexistant or inaccessible due to ACLs: "${line}"`);
|
||||
} else if (line.includes("is local Tailscale IP")) {
|
||||
throw new Error(`Tailscale only works if used on other machines: "${line}"`);
|
||||
} else if (line !== "") {
|
||||
throw new Error(`Unexpected output: "${line}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TailscalePing,
|
||||
};
|
@@ -15,7 +15,7 @@ class DingDing extends NotificationProvider {
|
||||
msgtype: "markdown",
|
||||
markdown: {
|
||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
|
@@ -59,8 +59,8 @@ class Discord extends NotificationProvider {
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
@@ -94,8 +94,8 @@ class Discord extends NotificationProvider {
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
},
|
||||
{
|
||||
name: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON["localDateTime"],
|
||||
},
|
||||
{
|
||||
name: "Ping",
|
||||
|
@@ -35,8 +35,7 @@ class Feishu extends NotificationProvider {
|
||||
text:
|
||||
"[Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -62,8 +61,7 @@ class Feishu extends NotificationProvider {
|
||||
text:
|
||||
"[Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
"\nTime (UTC): " +
|
||||
heartbeatJSON["time"],
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
98
server/notification-providers/flashduty.js
Normal file
98
server/notification-providers/flashduty.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
const successMessage = "Sent Successfully.";
|
||||
|
||||
class FlashDuty extends NotificationProvider {
|
||||
name = "FlashDuty";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
const title = "Uptime Kuma Alert";
|
||||
const monitor = {
|
||||
type: "ping",
|
||||
url: msg,
|
||||
name: "https://flashcat.cloud"
|
||||
};
|
||||
return this.postNotification(notification, title, msg, monitor);
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === UP) {
|
||||
const title = "Uptime Kuma Monitor ✅ Up";
|
||||
|
||||
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "Ok");
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
const title = "Uptime Kuma Monitor 🔴 Down";
|
||||
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, notification.flashdutySeverity);
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Generate a monitor url from the monitors infomation
|
||||
* @param {Object} monitorInfo Monitor details
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
|
||||
genMonitorUrl(monitorInfo) {
|
||||
if (monitorInfo.type === "port" && monitorInfo.port) {
|
||||
return monitorInfo.hostname + ":" + monitorInfo.port;
|
||||
}
|
||||
if (monitorInfo.hostname != null) {
|
||||
return monitorInfo.hostname;
|
||||
}
|
||||
return monitorInfo.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message
|
||||
* @param {BeanModel} notification Message title
|
||||
* @param {string} title Message
|
||||
* @param {string} body Message
|
||||
* @param {Object} monitorInfo Monitor details
|
||||
* @param {string} eventStatus Monitor status (Info, Warning, Critical, Ok)
|
||||
* @returns {string}
|
||||
*/
|
||||
async postNotification(notification, title, body, monitorInfo, eventStatus) {
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: "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),
|
||||
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
|
||||
[item.name]: item.value
|
||||
}), { resource: this.genMonitorUrl(monitorInfo) }),
|
||||
}
|
||||
};
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorInfo) {
|
||||
options.client = "Uptime Kuma";
|
||||
options.client_url = baseURL + getMonitorRelativeURL(monitorInfo.id);
|
||||
}
|
||||
|
||||
let result = await axios.request(options);
|
||||
if (result.status == null) {
|
||||
throw new Error("FlashDuty notification failed with invalid response!");
|
||||
}
|
||||
if (result.status < 200 || result.status >= 300) {
|
||||
throw new Error("FlashDuty notification failed with status code " + result.status);
|
||||
}
|
||||
if (result.statusText != null) {
|
||||
return "FlashDuty notification succeed: " + result.statusText;
|
||||
}
|
||||
|
||||
return successMessage;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FlashDuty;
|
@@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
||||
{
|
||||
title: "Uptime Kuma",
|
||||
message,
|
||||
|
@@ -33,7 +33,10 @@ class Line extends NotificationProvider {
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"text": "UptimeKuma Alert: [🔴 Down]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -44,7 +47,10 @@ class Line extends NotificationProvider {
|
||||
"messages": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"text": "UptimeKuma Alert: [✅ Up]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@@ -24,12 +24,18 @@ class LineNotify extends NotificationProvider {
|
||||
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"message": "\n[🔴 Down]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
"message": "\n[✅ Up]\n" +
|
||||
"Name: " + monitorJSON["name"] + " \n" +
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||
}
|
||||
|
@@ -28,7 +28,9 @@ class LunaSea extends NotificationProvider {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let downdata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[🔴 Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, downdata);
|
||||
return okMsg;
|
||||
@@ -37,7 +39,9 @@ class LunaSea extends NotificationProvider {
|
||||
if (heartbeatJSON["status"] === UP) {
|
||||
let updata = {
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[✅ Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, updata);
|
||||
return okMsg;
|
||||
|
@@ -10,7 +10,7 @@ class Mattermost extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let mattermostTestData = {
|
||||
username: mattermostUserName,
|
||||
@@ -27,97 +27,79 @@ class Mattermost extends NotificationProvider {
|
||||
}
|
||||
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
let mattermostIconEmojiOnline = "";
|
||||
let mattermostIconEmojiOffline = "";
|
||||
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let mattermostdowndata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went down.",
|
||||
color: "#FF0000",
|
||||
title:
|
||||
"❌ " +
|
||||
monitorJSON["name"] +
|
||||
" service went down. ❌",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON["msg"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let mattermostupdata = {
|
||||
username: mattermostUserName,
|
||||
text: "Uptime Kuma Alert",
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: mattermostIconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON["name"] +
|
||||
" service went up!",
|
||||
color: "#32CD32",
|
||||
title:
|
||||
"✅ " +
|
||||
monitorJSON["name"] +
|
||||
" service went up! ✅",
|
||||
title_link: monitorJSON["url"],
|
||||
fields: [
|
||||
{
|
||||
short: true,
|
||||
title: "Service Name",
|
||||
value: monitorJSON["name"],
|
||||
},
|
||||
{
|
||||
short: true,
|
||||
title: "Time (UTC)",
|
||||
value: heartbeatJSON["time"],
|
||||
},
|
||||
{
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON["ping"] + "ms",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostupdata
|
||||
);
|
||||
return okMsg;
|
||||
if (mattermostIconEmoji && typeof mattermostIconEmoji === "string") {
|
||||
const emojiArray = mattermostIconEmoji.split(" ");
|
||||
if (emojiArray.length >= 2) {
|
||||
mattermostIconEmojiOnline = emojiArray[0];
|
||||
mattermostIconEmojiOffline = emojiArray[1];
|
||||
}
|
||||
}
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
let iconEmoji = mattermostIconEmoji;
|
||||
let statusField = {
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON.msg,
|
||||
};
|
||||
let statusText = "unknown";
|
||||
let color = "#000000";
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
iconEmoji = mattermostIconEmojiOffline || mattermostIconEmoji;
|
||||
statusField = {
|
||||
short: false,
|
||||
title: "Error",
|
||||
value: heartbeatJSON.msg,
|
||||
};
|
||||
statusText = "down.";
|
||||
color = "#FF0000";
|
||||
} else if (heartbeatJSON.status === UP) {
|
||||
iconEmoji = mattermostIconEmojiOnline || mattermostIconEmoji;
|
||||
statusField = {
|
||||
short: false,
|
||||
title: "Ping",
|
||||
value: heartbeatJSON.ping + "ms",
|
||||
};
|
||||
statusText = "up!";
|
||||
color = "#32CD32";
|
||||
}
|
||||
|
||||
let mattermostdata = {
|
||||
username: monitorJSON.name + " " + mattermostUserName,
|
||||
channel: mattermostChannel,
|
||||
icon_emoji: iconEmoji,
|
||||
icon_url: mattermostIconUrl,
|
||||
attachments: [
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON.name +
|
||||
" service went " +
|
||||
statusText,
|
||||
color: color,
|
||||
title:
|
||||
monitorJSON.name +
|
||||
" service went " +
|
||||
statusText,
|
||||
title_link: monitorJSON.url,
|
||||
fields: [
|
||||
statusField,
|
||||
{
|
||||
short: true,
|
||||
title: `Time (${heartbeatJSON["timezone"]})`,
|
||||
value: heartbeatJSON.localDateTime,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdata
|
||||
);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
119
server/notification-providers/nostr.js
Normal file
119
server/notification-providers/nostr.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const { log } = require("../../src/util");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const {
|
||||
relayInit,
|
||||
getPublicKey,
|
||||
getEventHash,
|
||||
getSignature,
|
||||
nip04,
|
||||
nip19
|
||||
} = require("nostr-tools");
|
||||
|
||||
// polyfills for node versions
|
||||
const semver = require("semver");
|
||||
const nodeVersion = process.version;
|
||||
if (semver.lt(nodeVersion, "16.0.0")) {
|
||||
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
|
||||
} else if (semver.lt(nodeVersion, "18.0.0")) {
|
||||
// polyfills for node 16
|
||||
global.crypto = require("crypto");
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
|
||||
crypto.subtle = crypto.webcrypto.subtle;
|
||||
}
|
||||
} else if (semver.lt(nodeVersion, "20.0.0")) {
|
||||
// polyfills for node 18
|
||||
global.crypto = require("crypto");
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
} else {
|
||||
// polyfills for node 20
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
}
|
||||
|
||||
class Nostr extends NotificationProvider {
|
||||
name = "nostr";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
// All DMs should have same timestamp
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
|
||||
const senderPrivateKey = await this.getPrivateKey(notification.sender);
|
||||
const senderPublicKey = getPublicKey(senderPrivateKey);
|
||||
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
|
||||
|
||||
// Create NIP-04 encrypted direct message event for each recipient
|
||||
const events = [];
|
||||
for (const recipientPublicKey of recipientsPublicKeys) {
|
||||
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
|
||||
let event = {
|
||||
kind: 4,
|
||||
pubkey: senderPublicKey,
|
||||
created_at: createdAt,
|
||||
tags: [[ "p", recipientPublicKey ]],
|
||||
content: ciphertext,
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = getSignature(event, senderPrivateKey);
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
// Publish events to each relay
|
||||
const relays = notification.relays.split("\n");
|
||||
let successfulRelays = 0;
|
||||
|
||||
// Connect to each relay
|
||||
for (const relayUrl of relays) {
|
||||
const relay = relayInit(relayUrl);
|
||||
try {
|
||||
await relay.connect();
|
||||
successfulRelays++;
|
||||
|
||||
// Publish events
|
||||
for (const event of events) {
|
||||
relay.publish(event);
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
} finally {
|
||||
relay.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Report success or failure
|
||||
if (successfulRelays === 0) {
|
||||
throw Error("Failed to connect to any relays.");
|
||||
}
|
||||
return `${successfulRelays}/${relays.length} relays connected.`;
|
||||
}
|
||||
|
||||
async getPrivateKey(sender) {
|
||||
try {
|
||||
const senderDecodeResult = await nip19.decode(sender);
|
||||
const { data } = senderDecodeResult;
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get private key: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getPublicKeys(recipients) {
|
||||
const recipientsList = recipients.split("\n");
|
||||
const publicKeys = [];
|
||||
for (const recipient of recipientsList) {
|
||||
try {
|
||||
const recipientDecodeResult = await nip19.decode(recipient);
|
||||
const { type, data } = recipientDecodeResult;
|
||||
if (type === "npub") {
|
||||
publicKeys.push(data);
|
||||
} else {
|
||||
throw new Error("not an npub");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Error decoding recipient: ${error}`);
|
||||
}
|
||||
}
|
||||
return publicKeys;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Nostr;
|
@@ -1,5 +1,6 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Ntfy extends NotificationProvider {
|
||||
|
||||
@@ -9,16 +10,54 @@ class Ntfy extends NotificationProvider {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let headers = {};
|
||||
if (notification.ntfyusername) {
|
||||
if (notification.ntfyAuthenticationMethod === "usernamePassword") {
|
||||
headers = {
|
||||
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
|
||||
};
|
||||
} else if (notification.ntfyAuthenticationMethod === "accessToken") {
|
||||
headers = {
|
||||
"Authorization": "Bearer " + notification.ntfyaccesstoken,
|
||||
};
|
||||
}
|
||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
||||
if (heartbeatJSON == null) {
|
||||
let ntfyTestData = {
|
||||
"topic": notification.ntfytopic,
|
||||
"title": (monitorJSON?.name || notification.ntfytopic) + " [Uptime-Kuma]",
|
||||
"message": msg,
|
||||
"priority": notification.ntfyPriority,
|
||||
"tags": [ "test_tube" ],
|
||||
};
|
||||
await axios.post(`${notification.ntfyserverurl}`, ntfyTestData, { headers: headers });
|
||||
return okMsg;
|
||||
}
|
||||
let tags = [];
|
||||
let status = "unknown";
|
||||
let priority = notification.ntfyPriority || 4;
|
||||
if ("status" in heartbeatJSON) {
|
||||
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;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
tags = [ "green_circle" ];
|
||||
status = "Up";
|
||||
}
|
||||
}
|
||||
let data = {
|
||||
"topic": notification.ntfytopic,
|
||||
"message": msg,
|
||||
"priority": notification.ntfyPriority || 4,
|
||||
"title": "Uptime-Kuma",
|
||||
"message": heartbeatJSON.msg,
|
||||
"priority": priority,
|
||||
"title": monitorJSON.name + " " + status + " [Uptime-Kuma]",
|
||||
"tags": tags,
|
||||
"actions": [
|
||||
{
|
||||
"action": "view",
|
||||
"label": "Open " + monitorJSON.name,
|
||||
"url": monitorJSON.url,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (notification.ntfyIcon) {
|
||||
|
97
server/notification-providers/opsgenie.js
Normal file
97
server/notification-providers/opsgenie.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
|
||||
const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts";
|
||||
const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts";
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
class Opsgenie extends NotificationProvider {
|
||||
|
||||
name = "Opsgenie";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let opsgenieAlertsUrl;
|
||||
let priority = (!notification.opsgeniePriority) ? 3 : notification.opsgeniePriority;
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
|
||||
try {
|
||||
switch (notification.opsgenieRegion) {
|
||||
case "US":
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
|
||||
break;
|
||||
case "EU":
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlEU;
|
||||
break;
|
||||
default:
|
||||
opsgenieAlertsUrl = opsgenieAlertsUrlUS;
|
||||
}
|
||||
|
||||
if (heartbeatJSON == null) {
|
||||
let notificationTestAlias = "uptime-kuma-notification-test";
|
||||
let data = {
|
||||
"message": msg,
|
||||
"alias": notificationTestAlias,
|
||||
"source": "Uptime Kuma",
|
||||
"priority": "P5"
|
||||
};
|
||||
|
||||
return this.post(notification, opsgenieAlertsUrl, data);
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
let data = {
|
||||
"message": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
|
||||
"alias": monitorJSON.name,
|
||||
"description": msg,
|
||||
"source": "Uptime Kuma",
|
||||
"priority": `P${priority}`
|
||||
};
|
||||
|
||||
return this.post(notification, opsgenieAlertsUrl, data);
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === UP) {
|
||||
let opsgenieAlertsCloseUrl = `${opsgenieAlertsUrl}/${encodeURIComponent(monitorJSON.name)}/close?identifierType=alias`;
|
||||
let data = {
|
||||
"source": "Uptime Kuma",
|
||||
};
|
||||
|
||||
return this.post(notification, opsgenieAlertsCloseUrl, data);
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {BeanModel} notification
|
||||
* @param {string} url Request url
|
||||
* @param {Object} data Request body
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async post(notification, url, data) {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `GenieKey ${notification.opsgenieApiKey}`,
|
||||
}
|
||||
};
|
||||
|
||||
let res = await axios.post(url, data, config);
|
||||
if (res.status == null) {
|
||||
return "Opsgenie notification failed with invalid response!";
|
||||
}
|
||||
if (res.status < 200 || res.status >= 300) {
|
||||
return `Opsgenie notification failed with status code ${res.status}`;
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Opsgenie;
|
@@ -29,14 +29,18 @@ class Pushbullet extends NotificationProvider {
|
||||
let downData = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[🔴 Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, downData, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upData = {
|
||||
"type": "note",
|
||||
"title": "UptimeKuma Alert: " + monitorJSON["name"],
|
||||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"],
|
||||
"body": "[✅ Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, upData, config);
|
||||
}
|
||||
|
@@ -8,7 +8,9 @@ class PushDeer extends NotificationProvider {
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let pushdeerlink = "https://api2.pushdeer.com/message/push";
|
||||
let endpoint = "/message/push";
|
||||
let serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
|
||||
let pushdeerlink = `${serverUrl.trim().replace(/\/*$/, "")}${endpoint}`;
|
||||
|
||||
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
|
||||
|
||||
|
@@ -24,13 +24,16 @@ class Pushover extends NotificationProvider {
|
||||
if (notification.pushoverdevice) {
|
||||
data.device = notification.pushoverdevice;
|
||||
}
|
||||
if (notification.pushoverttl) {
|
||||
data.ttl = notification.pushoverttl;
|
||||
}
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
} else {
|
||||
data.message += "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"];
|
||||
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`;
|
||||
await axios.post(pushoverlink, data);
|
||||
return okMsg;
|
||||
}
|
||||
|
@@ -22,8 +22,6 @@ class RocketChat extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
|
||||
let data = {
|
||||
"text": "Uptime Kuma Alert",
|
||||
"channel": notification.rocketchannel,
|
||||
@@ -31,7 +29,7 @@ class RocketChat extends NotificationProvider {
|
||||
"icon_emoji": notification.rocketiconemo,
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time,
|
||||
"title": `Uptime Kuma Alert *Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
"text": "*Message*\n" + msg,
|
||||
}
|
||||
]
|
||||
|
@@ -27,6 +27,11 @@ class Slack extends NotificationProvider {
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
if (notification.slackchannelnotify) {
|
||||
msg += " <!channel>";
|
||||
}
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
@@ -39,7 +44,6 @@ class Slack extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const time = heartbeatJSON["time"];
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": `${textMsg}\n${msg}`,
|
||||
@@ -54,7 +58,7 @@ class Slack extends NotificationProvider {
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Uptime Kuma Alert",
|
||||
"text": textMsg,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -65,7 +69,7 @@ class Slack extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Time (UTC)*\n" + time,
|
||||
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
}],
|
||||
}
|
||||
],
|
||||
|
42
server/notification-providers/smsc.js
Normal file
42
server/notification-providers/smsc.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSC extends NotificationProvider {
|
||||
name = "smsc";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "text/json",
|
||||
}
|
||||
};
|
||||
|
||||
let getArray = [
|
||||
"fmt=3",
|
||||
"translit=" + notification.smscTranslit,
|
||||
"login=" + notification.smscLogin,
|
||||
"psw=" + notification.smscPassword,
|
||||
"phones=" + notification.smscToNumber,
|
||||
"mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")),
|
||||
];
|
||||
if (notification.smscSenderName !== "") {
|
||||
getArray.push("sender=" + notification.smscSenderName);
|
||||
}
|
||||
|
||||
let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
|
||||
if (resp.data.id === undefined) {
|
||||
let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SMSC;
|
@@ -13,7 +13,7 @@ class SMTP extends NotificationProvider {
|
||||
port: notification.smtpPort,
|
||||
secure: notification.smtpSecure,
|
||||
tls: {
|
||||
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
|
||||
rejectUnauthorized: !notification.smtpIgnoreTLSError || false,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
@@ -91,7 +91,7 @@ class SMTP extends NotificationProvider {
|
||||
|
||||
let bodyTextContent = msg;
|
||||
if (heartbeatJSON) {
|
||||
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`;
|
||||
bodyTextContent = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
|
||||
}
|
||||
|
||||
// send mail with defined transport object
|
||||
|
@@ -25,8 +25,11 @@ class Telegram extends NotificationProvider {
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description";
|
||||
throw new Error(msg);
|
||||
if (error.response && error.response.data && error.response.data.description) {
|
||||
throw new Error(error.response.data.description);
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
42
server/notification-providers/twilio.js
Normal file
42
server/notification-providers/twilio.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Twilio extends NotificationProvider {
|
||||
|
||||
name = "twilio";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
let accountSID = notification.twilioAccountSID;
|
||||
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
|
||||
let authToken = notification.twilioAuthToken;
|
||||
|
||||
try {
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
"Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"),
|
||||
}
|
||||
};
|
||||
|
||||
let data = new URLSearchParams();
|
||||
data.append("To", notification.twilioToNumber);
|
||||
data.append("From", notification.twilioFromNumber);
|
||||
data.append("Body", msg);
|
||||
|
||||
let url = "https://api.twilio.com/2010-04-01/Accounts/" + accountSID + "/Messages.json";
|
||||
|
||||
await axios.post(url, data, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Twilio;
|
@@ -1,6 +1,7 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const FormData = require("form-data");
|
||||
const { Liquid } = require("liquidjs");
|
||||
|
||||
class Webhook extends NotificationProvider {
|
||||
|
||||
@@ -15,17 +16,27 @@ class Webhook extends NotificationProvider {
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let finalData;
|
||||
let config = {
|
||||
headers: {}
|
||||
};
|
||||
|
||||
if (notification.webhookContentType === "form-data") {
|
||||
finalData = new FormData();
|
||||
finalData.append("data", JSON.stringify(data));
|
||||
config.headers = finalData.getHeaders();
|
||||
} else {
|
||||
finalData = data;
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(data));
|
||||
config.headers = formData.getHeaders();
|
||||
data = formData;
|
||||
} else if (notification.webhookContentType === "custom") {
|
||||
// Initialize LiquidJS and parse the custom Body Template
|
||||
const engine = new Liquid();
|
||||
const tpl = engine.parse(notification.webhookCustomBody);
|
||||
|
||||
// Insert templated values into Body
|
||||
data = await engine.render(tpl,
|
||||
{
|
||||
msg,
|
||||
heartbeatJSON,
|
||||
monitorJSON
|
||||
});
|
||||
}
|
||||
|
||||
if (notification.webhookAdditionalHeaders) {
|
||||
@@ -39,7 +50,7 @@ class Webhook extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, finalData, config);
|
||||
await axios.post(notification.webhookURL, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
|
@@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const Apprise = require("./notification-providers/apprise");
|
||||
const Bark = require("./notification-providers/bark");
|
||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||
const SMSC = require("./notification-providers/smsc");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
const Discord = require("./notification-providers/discord");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
@@ -20,10 +21,13 @@ const LineNotify = require("./notification-providers/linenotify");
|
||||
const LunaSea = require("./notification-providers/lunasea");
|
||||
const Matrix = require("./notification-providers/matrix");
|
||||
const Mattermost = require("./notification-providers/mattermost");
|
||||
const Nostr = require("./notification-providers/nostr");
|
||||
const Ntfy = require("./notification-providers/ntfy");
|
||||
const Octopush = require("./notification-providers/octopush");
|
||||
const OneBot = require("./notification-providers/onebot");
|
||||
const Opsgenie = require("./notification-providers/opsgenie");
|
||||
const PagerDuty = require("./notification-providers/pagerduty");
|
||||
const FlashDuty = require("./notification-providers/flashduty");
|
||||
const PagerTree = require("./notification-providers/pagertree");
|
||||
const PromoSMS = require("./notification-providers/promosms");
|
||||
const Pushbullet = require("./notification-providers/pushbullet");
|
||||
@@ -41,6 +45,7 @@ const Stackfield = require("./notification-providers/stackfield");
|
||||
const Teams = require("./notification-providers/teams");
|
||||
const TechulusPush = require("./notification-providers/techulus-push");
|
||||
const Telegram = require("./notification-providers/telegram");
|
||||
const Twilio = require("./notification-providers/twilio");
|
||||
const Splunk = require("./notification-providers/splunk");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const WeCom = require("./notification-providers/wecom");
|
||||
@@ -66,6 +71,7 @@ class Notification {
|
||||
new Apprise(),
|
||||
new Bark(),
|
||||
new ClickSendSMS(),
|
||||
new SMSC(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Feishu(),
|
||||
@@ -80,10 +86,13 @@ class Notification {
|
||||
new LunaSea(),
|
||||
new Matrix(),
|
||||
new Mattermost(),
|
||||
new Nostr(),
|
||||
new Ntfy(),
|
||||
new Octopush(),
|
||||
new OneBot(),
|
||||
new Opsgenie(),
|
||||
new PagerDuty(),
|
||||
new FlashDuty(),
|
||||
new PagerTree(),
|
||||
new PromoSMS(),
|
||||
new Pushbullet(),
|
||||
@@ -103,13 +112,13 @@ class Notification {
|
||||
new Teams(),
|
||||
new TechulusPush(),
|
||||
new Telegram(),
|
||||
new Twilio(),
|
||||
new Splunk(),
|
||||
new Webhook(),
|
||||
new WeCom(),
|
||||
new GoAlert(),
|
||||
new ZohoCliq()
|
||||
];
|
||||
|
||||
for (let item of list) {
|
||||
if (! item.name) {
|
||||
throw new Error("Notification provider without name");
|
||||
|
@@ -1,13 +0,0 @@
|
||||
class Plugin {
|
||||
async load() {
|
||||
|
||||
}
|
||||
|
||||
async unload() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Plugin,
|
||||
};
|
@@ -1,256 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const { log } = require("../src/util");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const { Git } = require("./git");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
class PluginsManager {
|
||||
|
||||
static disable = false;
|
||||
|
||||
/**
|
||||
* Plugin List
|
||||
* @type {PluginWrapper[]}
|
||||
*/
|
||||
pluginList = [];
|
||||
|
||||
/**
|
||||
* Plugins Dir
|
||||
*/
|
||||
pluginsDir;
|
||||
|
||||
server;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UptimeKumaServer} server
|
||||
*/
|
||||
constructor(server) {
|
||||
this.server = server;
|
||||
|
||||
if (!PluginsManager.disable) {
|
||||
this.pluginsDir = "./data/plugins/";
|
||||
|
||||
if (! fs.existsSync(this.pluginsDir)) {
|
||||
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.debug("plugin", "Scanning plugin directory");
|
||||
let list = fs.readdirSync(this.pluginsDir);
|
||||
|
||||
this.pluginList = [];
|
||||
for (let item of list) {
|
||||
this.loadPlugin(item);
|
||||
}
|
||||
|
||||
} else {
|
||||
log.warn("PLUGIN", "Skip scanning plugin directory");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a Plugin
|
||||
*/
|
||||
async loadPlugin(name) {
|
||||
log.info("plugin", "Load " + name);
|
||||
let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
|
||||
|
||||
try {
|
||||
await plugin.load();
|
||||
this.pluginList.push(plugin);
|
||||
} catch (e) {
|
||||
log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
|
||||
log.error("plugin", "Reason: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a Plugin
|
||||
* @param {string} repoURL Git repo url
|
||||
* @param {string} name Directory name, also known as plugin unique name
|
||||
*/
|
||||
downloadPlugin(repoURL, name) {
|
||||
if (fs.existsSync(this.pluginsDir + name)) {
|
||||
log.info("plugin", "Plugin folder already exists? Removing...");
|
||||
fs.rmSync(this.pluginsDir + name, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
log.info("plugin", "Installing plugin: " + name + " " + repoURL);
|
||||
let result = Git.clone(repoURL, this.pluginsDir, name);
|
||||
log.info("plugin", "Install result: " + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a plugin
|
||||
* @param {string} name
|
||||
*/
|
||||
async removePlugin(name) {
|
||||
log.info("plugin", "Removing plugin: " + name);
|
||||
for (let plugin of this.pluginList) {
|
||||
if (plugin.info.name === name) {
|
||||
await plugin.unload();
|
||||
|
||||
// Delete the plugin directory
|
||||
fs.rmSync(this.pluginsDir + name, {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.warn("plugin", "Plugin not found: " + name);
|
||||
throw new Error("Plugin not found: " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Update a plugin
|
||||
* Only available for plugins which were downloaded from the official list
|
||||
* @param pluginID
|
||||
*/
|
||||
updatePlugin(pluginID) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin list from server + local installed plugin list
|
||||
* Item will be merged if the `name` is the same.
|
||||
* @returns {Promise<[]>}
|
||||
*/
|
||||
async fetchPluginList() {
|
||||
let remotePluginList;
|
||||
try {
|
||||
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
|
||||
remotePluginList = res.data.pluginList;
|
||||
} catch (e) {
|
||||
log.error("plugin", "Failed to fetch plugin list: " + e.message);
|
||||
remotePluginList = [];
|
||||
}
|
||||
|
||||
for (let plugin of this.pluginList) {
|
||||
let find = false;
|
||||
// Try to merge
|
||||
for (let remotePlugin of remotePluginList) {
|
||||
if (remotePlugin.name === plugin.info.name) {
|
||||
find = true;
|
||||
remotePlugin.installed = true;
|
||||
remotePlugin.name = plugin.info.name;
|
||||
remotePlugin.fullName = plugin.info.fullName;
|
||||
remotePlugin.description = plugin.info.description;
|
||||
remotePlugin.version = plugin.info.version;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Local plugin
|
||||
if (!find) {
|
||||
plugin.info.local = true;
|
||||
remotePluginList.push(plugin.info);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort Installed first, then sort by name
|
||||
return remotePluginList.sort((a, b) => {
|
||||
if (a.installed === b.installed) {
|
||||
if (a.fullName < b.fullName) {
|
||||
return -1;
|
||||
}
|
||||
if (a.fullName > b.fullName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} else if (a.installed) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PluginWrapper {
|
||||
|
||||
server = undefined;
|
||||
pluginDir = undefined;
|
||||
|
||||
/**
|
||||
* Must be an `new-able` class.
|
||||
* @type {function}
|
||||
*/
|
||||
pluginClass = undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Plugin}
|
||||
*/
|
||||
object = undefined;
|
||||
info = {};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UptimeKumaServer} server
|
||||
* @param {string} pluginDir
|
||||
*/
|
||||
constructor(server, pluginDir) {
|
||||
this.server = server;
|
||||
this.pluginDir = pluginDir;
|
||||
}
|
||||
|
||||
async load() {
|
||||
let indexFile = this.pluginDir + "/index.js";
|
||||
let packageJSON = this.pluginDir + "/package.json";
|
||||
|
||||
log.info("plugin", "Installing dependencies");
|
||||
|
||||
if (fs.existsSync(indexFile)) {
|
||||
// Install dependencies
|
||||
let result = childProcess.spawnSync("npm", [ "install" ], {
|
||||
cwd: this.pluginDir,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor
|
||||
}
|
||||
});
|
||||
|
||||
if (result.stdout) {
|
||||
log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
|
||||
} else {
|
||||
log.warn("plugin", "Install dependencies result: no output");
|
||||
}
|
||||
|
||||
this.pluginClass = require(path.join(process.cwd(), indexFile));
|
||||
|
||||
let pluginClassType = typeof this.pluginClass;
|
||||
|
||||
if (pluginClassType === "function") {
|
||||
this.object = new this.pluginClass(this.server);
|
||||
await this.object.load();
|
||||
} else {
|
||||
throw new Error("Invalid plugin, it does not export a class");
|
||||
}
|
||||
|
||||
if (fs.existsSync(packageJSON)) {
|
||||
this.info = require(path.join(process.cwd(), packageJSON));
|
||||
} else {
|
||||
this.info.fullName = this.pluginDir;
|
||||
this.info.name = "[unknown]";
|
||||
this.info.version = "[unknown-version]";
|
||||
}
|
||||
|
||||
this.info.installed = true;
|
||||
log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
|
||||
}
|
||||
}
|
||||
|
||||
async unload() {
|
||||
await this.object.unload();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PluginsManager,
|
||||
PluginWrapper
|
||||
};
|
@@ -28,7 +28,7 @@ const monitorResponseTime = new PrometheusClient.Gauge({
|
||||
|
||||
const monitorStatus = new PrometheusClient.Gauge({
|
||||
name: "monitor_status",
|
||||
help: "Monitor Status (1 = UP, 0= DOWN)",
|
||||
help: "Monitor Status (1 = UP, 0= DOWN, 2= PENDING, 3= MAINTENANCE)",
|
||||
labelNames: commonLabels
|
||||
});
|
||||
|
||||
|
@@ -132,6 +132,9 @@ class Proxy {
|
||||
...httpAgentOptions,
|
||||
...httpsAgentOptions,
|
||||
...proxyOptions,
|
||||
tls: {
|
||||
rejectUnauthorized: httpsAgentOptions.rejectUnauthorized,
|
||||
},
|
||||
});
|
||||
|
||||
httpAgent = agent;
|
||||
|
@@ -10,6 +10,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
@@ -37,7 +38,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
|
||||
let pushToken = request.params.pushToken;
|
||||
let msg = request.query.msg || "OK";
|
||||
let ping = request.query.ping || null;
|
||||
let ping = parseInt(request.query.ping) || null;
|
||||
let statusString = request.query.status || "up";
|
||||
let status = (statusString === "up") ? UP : DOWN;
|
||||
|
||||
@@ -89,6 +90,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
||||
UptimeCacheList.clearCache(monitor.id);
|
||||
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
||||
new Prometheus(monitor).update(bean, undefined);
|
||||
|
||||
response.json({
|
||||
ok: true,
|
||||
@@ -147,7 +149,11 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
|
||||
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||
const state = overrideValue !== undefined ? overrideValue : heartbeat.status;
|
||||
|
||||
badgeValues.label = label ?? "Status";
|
||||
if (label === undefined) {
|
||||
badgeValues.label = "Status";
|
||||
} else {
|
||||
badgeValues.label = label;
|
||||
}
|
||||
switch (state) {
|
||||
case DOWN:
|
||||
badgeValues.color = downColor;
|
||||
@@ -224,7 +230,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||
);
|
||||
|
||||
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||
const cleanUptime = parseFloat(uptime.toPrecision(4));
|
||||
const cleanUptime = (uptime * 100).toPrecision(4);
|
||||
|
||||
// use a given, custom color or calculate one based on the uptime value
|
||||
badgeValues.color = color ?? percentageToColor(uptime);
|
||||
@@ -235,7 +241,7 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||
labelPrefix,
|
||||
label ?? `Uptime (${requestedDuration}${labelSuffix})`,
|
||||
]);
|
||||
badgeValues.message = filterAndJoin([ prefix, `${cleanUptime * 100}`, suffix ]);
|
||||
badgeValues.message = filterAndJoin([ prefix, cleanUptime, suffix ]);
|
||||
}
|
||||
|
||||
// build the SVG based on given values
|
||||
@@ -436,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
|
||||
if (!tlsInfo.valid) {
|
||||
// return a "Bad Cert" badge in naColor (grey), when cert is not valid
|
||||
badgeValues.message = "Bad Cert";
|
||||
badgeValues.color = badgeConstants.downColor;
|
||||
badgeValues.color = downColor;
|
||||
} else {
|
||||
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);
|
||||
|
||||
|
@@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page");
|
||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const Monitor = require("../model/monitor");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
@@ -139,4 +141,100 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
|
||||
}
|
||||
});
|
||||
|
||||
// overall status-page status badge
|
||||
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
const slug = request.params.slug;
|
||||
const statusPageID = await StatusPage.slugToID(slug);
|
||||
const {
|
||||
label,
|
||||
upColor = badgeConstants.defaultUpColor,
|
||||
downColor = badgeConstants.defaultDownColor,
|
||||
partialColor = "#F6BE00",
|
||||
maintenanceColor = "#808080",
|
||||
style = badgeConstants.defaultStyle
|
||||
} = request.query;
|
||||
|
||||
try {
|
||||
let monitorIDList = await R.getCol(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND public = 1
|
||||
AND \`group\`.status_page_id = ?
|
||||
`, [
|
||||
statusPageID
|
||||
]);
|
||||
|
||||
let hasUp = false;
|
||||
let hasDown = false;
|
||||
let hasMaintenance = false;
|
||||
|
||||
for (let monitorID of monitorIDList) {
|
||||
// retrieve the latest heartbeat
|
||||
let beat = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY time DESC
|
||||
LIMIT 1
|
||||
`, [
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
// to be sure, when corresponding monitor not found
|
||||
if (beat.length === 0) {
|
||||
continue;
|
||||
}
|
||||
// handle status of beat
|
||||
if (beat[0].status === 3) {
|
||||
hasMaintenance = true;
|
||||
} else if (beat[0].status === 2) {
|
||||
// ignored
|
||||
} else if (beat[0].status === 1) {
|
||||
hasUp = true;
|
||||
} else {
|
||||
hasDown = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
if (!hasUp && !hasDown && !hasMaintenance) {
|
||||
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
|
||||
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
|
||||
} else {
|
||||
if (hasMaintenance) {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = maintenanceColor;
|
||||
badgeValues.message = "Maintenance";
|
||||
} else if (hasUp && !hasDown) {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = upColor;
|
||||
badgeValues.message = "Up";
|
||||
} else if (hasUp && hasDown) {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = partialColor;
|
||||
badgeValues.message = "Degraded";
|
||||
} else {
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = downColor;
|
||||
badgeValues.message = "Down";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// build the svg based on given values
|
||||
const svg = makeBadge(badgeValues);
|
||||
|
||||
response.type("image/svg+xml");
|
||||
response.send(svg);
|
||||
|
||||
} catch (error) {
|
||||
sendHttpError(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
126
server/server.js
126
server/server.js
@@ -15,15 +15,27 @@ dayjs.extend(require("dayjs/plugin/customParseFormat"));
|
||||
require("dotenv").config();
|
||||
|
||||
// Check Node.js Version
|
||||
const nodeVersion = parseInt(process.versions.node.split(".")[0]);
|
||||
const requiredVersion = 14;
|
||||
const nodeVersion = process.versions.node;
|
||||
|
||||
// Get the required Node.js version from package.json
|
||||
const requiredNodeVersions = require("../package.json").engines.node;
|
||||
const bannedNodeVersions = " < 14 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
|
||||
console.log(`Your Node.js version: ${nodeVersion}`);
|
||||
|
||||
if (nodeVersion < requiredVersion) {
|
||||
console.error(`Error: Your Node.js version is not supported, please upgrade to Node.js >= ${requiredVersion}.`);
|
||||
const semver = require("semver");
|
||||
const requiredNodeVersionsComma = requiredNodeVersions.split("||").map((version) => version.trim()).join(", ");
|
||||
|
||||
// Exit Uptime Kuma immediately if the Node.js version is banned
|
||||
if (semver.satisfies(nodeVersion, bannedNodeVersions)) {
|
||||
console.error("\x1b[31m%s\x1b[0m", `Error: Your Node.js version: ${nodeVersion} is not supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
// Warning if the Node.js version is not in the support list, but it maybe still works
|
||||
if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
|
||||
console.warn("\x1b[31m%s\x1b[0m", `Warning: Your Node.js version: ${nodeVersion} is not officially supported, please upgrade your Node.js to ${requiredNodeVersionsComma}.`);
|
||||
}
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
|
||||
const config = require("./config");
|
||||
@@ -37,6 +49,7 @@ if (! process.env.NODE_ENV) {
|
||||
}
|
||||
|
||||
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
||||
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
||||
|
||||
log.info("server", "Importing Node libraries");
|
||||
const fs = require("fs");
|
||||
@@ -142,7 +155,8 @@ const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handle
|
||||
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
|
||||
const { Settings } = require("./settings");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
|
||||
const apicache = require("./modules/apicache");
|
||||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -155,12 +169,6 @@ app.use(function (req, res, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
let jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
@@ -171,7 +179,6 @@ let needSetup = false;
|
||||
Database.init(args);
|
||||
await initDatabase(testMode);
|
||||
await server.initAfterDatabaseReady();
|
||||
server.loadPlugins();
|
||||
server.entryPage = await Settings.get("entryPage");
|
||||
await StatusPage.loadDomainMappingList();
|
||||
|
||||
@@ -209,6 +216,7 @@ let needSetup = false;
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.post("/test-webhook", async (request, response) => {
|
||||
log.debug("test", request.headers);
|
||||
log.debug("test", request.body);
|
||||
@@ -263,7 +271,7 @@ let needSetup = false;
|
||||
log.info("server", "Adding socket handler");
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
sendInfo(socket);
|
||||
sendInfo(socket, true);
|
||||
|
||||
if (needSetup) {
|
||||
log.info("server", "Redirect to setup page");
|
||||
@@ -280,7 +288,7 @@ let needSetup = false;
|
||||
log.info("auth", `Login by token. IP=${clientIP}`);
|
||||
|
||||
try {
|
||||
let decoded = jwt.verify(token, jwtSecret);
|
||||
let decoded = jwt.verify(token, server.jwtSecret);
|
||||
|
||||
log.info("auth", "Username from JWT: " + decoded.username);
|
||||
|
||||
@@ -351,7 +359,7 @@ let needSetup = false;
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -381,7 +389,7 @@ let needSetup = false;
|
||||
ok: true,
|
||||
token: jwt.sign({
|
||||
username: data.username,
|
||||
}, jwtSecret),
|
||||
}, server.jwtSecret),
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -633,9 +641,16 @@ let needSetup = false;
|
||||
let notificationIDList = monitor.notificationIDList;
|
||||
delete monitor.notificationIDList;
|
||||
|
||||
// Ensure status code ranges are strings
|
||||
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
|
||||
throw new Error("Accepted status codes are not all strings");
|
||||
}
|
||||
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
delete monitor.accepted_statuscodes;
|
||||
|
||||
monitor.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
||||
monitor.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||
|
||||
bean.import(monitor);
|
||||
bean.user_id = socket.userID;
|
||||
|
||||
@@ -646,7 +661,10 @@ let needSetup = false;
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
await server.sendMonitorList(socket);
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
|
||||
if (monitor.active !== false) {
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
||||
|
||||
@@ -670,6 +688,7 @@ let needSetup = false;
|
||||
// Edit a monitor
|
||||
socket.on("editMonitor", async (monitor, callback) => {
|
||||
try {
|
||||
let removeGroupChildren = false;
|
||||
checkLogin(socket);
|
||||
|
||||
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]);
|
||||
@@ -678,8 +697,27 @@ let needSetup = false;
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
// Check if Parent is Descendant (would cause endless loop)
|
||||
if (monitor.parent !== null) {
|
||||
const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
|
||||
if (childIDs.includes(monitor.parent)) {
|
||||
throw new Error("Invalid Monitor Group");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove children if monitor type has changed (from group to non-group)
|
||||
if (bean.type === "group" && monitor.type !== bean.type) {
|
||||
removeGroupChildren = true;
|
||||
}
|
||||
|
||||
// Ensure status code ranges are strings
|
||||
if (!monitor.accepted_statuscodes.every((code) => typeof code === "string")) {
|
||||
throw new Error("Accepted status codes are not all strings");
|
||||
}
|
||||
|
||||
bean.name = monitor.name;
|
||||
bean.description = monitor.description;
|
||||
bean.parent = monitor.parent;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.method = monitor.method;
|
||||
@@ -687,6 +725,15 @@ let needSetup = false;
|
||||
bean.headers = monitor.headers;
|
||||
bean.basic_auth_user = monitor.basic_auth_user;
|
||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||
bean.timeout = monitor.timeout;
|
||||
bean.oauth_client_id = monitor.oauth_client_id,
|
||||
bean.oauth_client_secret = monitor.oauth_client_secret,
|
||||
bean.oauth_auth_method = this.oauth_auth_method,
|
||||
bean.oauth_token_url = monitor.oauth_token_url,
|
||||
bean.oauth_scopes = monitor.oauth_scopes,
|
||||
bean.tlsCa = monitor.tlsCa;
|
||||
bean.tlsCert = monitor.tlsCert;
|
||||
bean.tlsKey = monitor.tlsKey;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.resendInterval = monitor.resendInterval;
|
||||
@@ -695,6 +742,7 @@ let needSetup = false;
|
||||
bean.maxretries = monitor.maxretries;
|
||||
bean.port = parseInt(monitor.port);
|
||||
bean.keyword = monitor.keyword;
|
||||
bean.invertKeyword = monitor.invertKeyword;
|
||||
bean.ignoreTls = monitor.ignoreTls;
|
||||
bean.expiryNotification = monitor.expiryNotification;
|
||||
bean.upsideDown = monitor.upsideDown;
|
||||
@@ -729,14 +777,26 @@ let needSetup = false;
|
||||
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
||||
bean.radiusSecret = monitor.radiusSecret;
|
||||
bean.httpBodyEncoding = monitor.httpBodyEncoding;
|
||||
bean.expectedValue = monitor.expectedValue;
|
||||
bean.jsonPath = monitor.jsonPath;
|
||||
bean.kafkaProducerTopic = monitor.kafkaProducerTopic;
|
||||
bean.kafkaProducerBrokers = JSON.stringify(monitor.kafkaProducerBrokers);
|
||||
bean.kafkaProducerAllowAutoTopicCreation = monitor.kafkaProducerAllowAutoTopicCreation;
|
||||
bean.kafkaProducerSaslOptions = JSON.stringify(monitor.kafkaProducerSaslOptions);
|
||||
bean.kafkaProducerMessage = monitor.kafkaProducerMessage;
|
||||
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
|
||||
|
||||
bean.validate();
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
if (removeGroupChildren) {
|
||||
await Monitor.unlinkAllChildren(monitor.id);
|
||||
}
|
||||
|
||||
await updateMonitorNotification(bean.id, monitor.notificationIDList);
|
||||
|
||||
if (bean.active) {
|
||||
if (bean.isActive()) {
|
||||
await restartMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
@@ -879,11 +939,20 @@ let needSetup = false;
|
||||
delete server.monitorList[monitorID];
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
// Fix #2880
|
||||
apicache.clear();
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
@@ -1127,6 +1196,8 @@ let needSetup = false;
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||
|
||||
await setSettings("general", data);
|
||||
server.entryPage = data.entryPage;
|
||||
|
||||
@@ -1137,6 +1208,12 @@ let needSetup = false;
|
||||
await server.setTimezone(data.serverTimezone);
|
||||
}
|
||||
|
||||
// If Chrome Executable is changed, need to reset the browser
|
||||
if (previousChromeExecutable !== data.chromeExecutable) {
|
||||
log.info("settings", "Chrome executable is changed. Resetting Chrome...");
|
||||
await resetChrome();
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved"
|
||||
@@ -1306,6 +1383,7 @@ let needSetup = false;
|
||||
|
||||
// Define default values
|
||||
let retryInterval = 0;
|
||||
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
|
||||
|
||||
/*
|
||||
Only replace the default value with the backup file data for the specific version, where it appears the first time
|
||||
@@ -1331,6 +1409,7 @@ let needSetup = false;
|
||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||
authWorkstation: monitorListData[i].authWorkstation,
|
||||
authDomain: monitorListData[i].authDomain,
|
||||
timeout,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
resendInterval: monitorListData[i].resendInterval || 0,
|
||||
@@ -1338,13 +1417,14 @@ let needSetup = false;
|
||||
maxretries: monitorListData[i].maxretries,
|
||||
port: monitorListData[i].port,
|
||||
keyword: monitorListData[i].keyword,
|
||||
invertKeyword: monitorListData[i].invertKeyword,
|
||||
ignoreTls: monitorListData[i].ignoreTls,
|
||||
upsideDown: monitorListData[i].upsideDown,
|
||||
maxredirects: monitorListData[i].maxredirects,
|
||||
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
|
||||
dns_resolve_type: monitorListData[i].dns_resolve_type,
|
||||
dns_resolve_server: monitorListData[i].dns_resolve_server,
|
||||
notificationIDList: {},
|
||||
notificationIDList: monitorListData[i].notificationIDList,
|
||||
proxy_id: monitorListData[i].proxy_id || null,
|
||||
};
|
||||
|
||||
@@ -1506,7 +1586,6 @@ let needSetup = false;
|
||||
maintenanceSocketHandler(socket);
|
||||
apiKeySocketHandler(socket);
|
||||
generalSocketHandler(socket, server);
|
||||
pluginsHandler(socket, server);
|
||||
|
||||
log.debug("server", "added all socket handlers");
|
||||
|
||||
@@ -1532,6 +1611,8 @@ let needSetup = false;
|
||||
await shutdownFunction();
|
||||
});
|
||||
|
||||
server.start();
|
||||
|
||||
server.httpServer.listen(port, hostname, () => {
|
||||
if (hostname) {
|
||||
log.info("server", `Listening on ${hostname}:${port}`);
|
||||
@@ -1550,7 +1631,7 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
initBackgroundJobs(args);
|
||||
await initBackgroundJobs();
|
||||
|
||||
// Start cloudflared at the end if configured
|
||||
await cloudflaredAutoStart(cloudflaredToken);
|
||||
@@ -1609,6 +1690,7 @@ async function afterLogin(socket, user) {
|
||||
socket.join(user.id);
|
||||
|
||||
let monitorList = await server.sendMonitorList(socket);
|
||||
sendInfo(socket);
|
||||
server.sendMaintenanceList(socket);
|
||||
sendNotificationList(socket);
|
||||
sendProxyList(socket);
|
||||
@@ -1676,7 +1758,7 @@ async function initDatabase(testMode = false) {
|
||||
needSetup = true;
|
||||
}
|
||||
|
||||
jwtSecret = jwtSecretBean.value;
|
||||
server.jwtSecret = jwtSecretBean.value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -3,6 +3,7 @@ const { Settings } = require("../settings");
|
||||
const { sendInfo } = require("../client");
|
||||
const { checkLogin } = require("../util-server");
|
||||
const GameResolver = require("gamedig/lib/GameResolver");
|
||||
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
||||
|
||||
let gameResolver = new GameResolver();
|
||||
let gameList = null;
|
||||
@@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("testChrome", (executable, callback) => {
|
||||
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
||||
testChrome(executable).then((version) => {
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Found Chromium/Chrome. Version: " + version,
|
||||
});
|
||||
}).catch((e) => {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@@ -5,7 +5,6 @@ const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const Maintenance = require("../model/maintenance");
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
const MaintenanceTimeslot = require("../model/maintenance_timeslot");
|
||||
|
||||
/**
|
||||
* Handlers for Maintenance
|
||||
@@ -19,10 +18,12 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", maintenance);
|
||||
|
||||
let bean = Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
|
||||
let bean = await Maintenance.jsonToBean(R.dispense("maintenance"), maintenance);
|
||||
bean.user_id = socket.userID;
|
||||
let maintenanceID = await R.store(bean);
|
||||
await MaintenanceTimeslot.generateTimeslot(bean);
|
||||
|
||||
server.maintenanceList[maintenanceID] = bean;
|
||||
await bean.run(true);
|
||||
|
||||
await server.sendMaintenanceList(socket);
|
||||
|
||||
@@ -45,17 +46,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let bean = await R.findOne("maintenance", " id = ? ", [ maintenance.id ]);
|
||||
let bean = server.getMaintenance(maintenance.id);
|
||||
|
||||
if (bean.user_id !== socket.userID) {
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
Maintenance.jsonToBean(bean, maintenance);
|
||||
|
||||
await Maintenance.jsonToBean(bean, maintenance);
|
||||
await R.store(bean);
|
||||
await MaintenanceTimeslot.generateTimeslot(bean, null, true);
|
||||
|
||||
await bean.run(true);
|
||||
await server.sendMaintenanceList(socket);
|
||||
|
||||
callback({
|
||||
@@ -187,7 +186,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
|
||||
let monitors = await R.getAll("SELECT monitor.id FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
|
||||
maintenanceID,
|
||||
]);
|
||||
|
||||
@@ -236,6 +235,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
log.debug("maintenance", `Delete Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
if (maintenanceID in server.maintenanceList) {
|
||||
server.maintenanceList[maintenanceID].stop();
|
||||
delete server.maintenanceList[maintenanceID];
|
||||
}
|
||||
|
||||
@@ -267,9 +267,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", `Pause Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE maintenance SET active = 0 WHERE id = ? ", [
|
||||
maintenanceID,
|
||||
]);
|
||||
let maintenance = server.getMaintenance(maintenanceID);
|
||||
|
||||
if (!maintenance) {
|
||||
throw new Error("Maintenance not found");
|
||||
}
|
||||
|
||||
maintenance.active = false;
|
||||
await R.store(maintenance);
|
||||
maintenance.stop();
|
||||
|
||||
apicache.clear();
|
||||
|
||||
@@ -294,9 +300,15 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", `Resume Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE maintenance SET active = 1 WHERE id = ? ", [
|
||||
maintenanceID,
|
||||
]);
|
||||
let maintenance = server.getMaintenance(maintenanceID);
|
||||
|
||||
if (!maintenance) {
|
||||
throw new Error("Maintenance not found");
|
||||
}
|
||||
|
||||
maintenance.active = true;
|
||||
await R.store(maintenance);
|
||||
await maintenance.run();
|
||||
|
||||
apicache.clear();
|
||||
|
||||
|
@@ -1,69 +0,0 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const { PluginsManager } = require("../plugins-manager");
|
||||
const { log } = require("../../src/util.js");
|
||||
|
||||
/**
|
||||
* Handlers for plugins
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {UptimeKumaServer} server
|
||||
*/
|
||||
module.exports.pluginsHandler = (socket, server) => {
|
||||
|
||||
const pluginManager = server.getPluginManager();
|
||||
|
||||
// Get Plugin List
|
||||
socket.on("getPluginList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
|
||||
|
||||
if (PluginsManager.disable) {
|
||||
throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
|
||||
}
|
||||
|
||||
let pluginList = await pluginManager.fetchPluginList();
|
||||
callback({
|
||||
ok: true,
|
||||
pluginList,
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn("plugin", "Error: " + error.message);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("installPlugin", async (repoURL, name, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
pluginManager.downloadPlugin(repoURL, name);
|
||||
await pluginManager.loadPlugin(name);
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("uninstallPlugin", async (name, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await pluginManager.removePlugin(name);
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@@ -162,6 +162,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
statusPage.footer_text = config.footerText;
|
||||
statusPage.custom_css = config.customCSS;
|
||||
statusPage.show_powered_by = config.showPoweredBy;
|
||||
statusPage.show_certificate_expiry = config.showCertificateExpiry;
|
||||
statusPage.modified_date = R.isoDateTime();
|
||||
statusPage.google_analytics_tag_id = config.googleAnalyticsId;
|
||||
|
||||
@@ -276,7 +277,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
let statusPage = R.dispense("status_page");
|
||||
statusPage.slug = slug;
|
||||
statusPage.title = title;
|
||||
statusPage.theme = "light";
|
||||
statusPage.theme = "auto";
|
||||
statusPage.icon = "";
|
||||
await R.store(statusPage);
|
||||
|
||||
|
@@ -10,8 +10,9 @@ const util = require("util");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
const { PluginsManager } = require("./plugins-manager");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
|
||||
const childProcess = require("child_process");
|
||||
const path = require("path");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||
|
||||
/**
|
||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||
@@ -47,14 +48,6 @@ class UptimeKumaServer {
|
||||
*/
|
||||
indexHTML = "";
|
||||
|
||||
generateMaintenanceTimeslotsInterval = undefined;
|
||||
|
||||
/**
|
||||
* Plugins Manager
|
||||
* @type {PluginsManager}
|
||||
*/
|
||||
pluginsManager = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {{}}
|
||||
@@ -63,6 +56,12 @@ class UptimeKumaServer {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Use for decode the auth object
|
||||
* @type {null}
|
||||
*/
|
||||
jwtSecret = null;
|
||||
|
||||
static getInstance(args) {
|
||||
if (UptimeKumaServer.instance == null) {
|
||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||
@@ -74,6 +73,7 @@ class UptimeKumaServer {
|
||||
// SSL
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
||||
|
||||
log.info("server", "Creating express and socket.io instance");
|
||||
this.app = express();
|
||||
@@ -81,7 +81,8 @@ class UptimeKumaServer {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert)
|
||||
cert: fs.readFileSync(sslCert),
|
||||
passphrase: sslKeyPassphrase,
|
||||
}, this.app);
|
||||
} else {
|
||||
log.info("server", "Server Type: HTTP");
|
||||
@@ -98,11 +99,18 @@ class UptimeKumaServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Set Monitor Types
|
||||
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
|
||||
/** Initialise app after the database has been set up */
|
||||
async initAfterDatabaseReady() {
|
||||
// Static
|
||||
this.app.use("/screenshots", express.static(Database.screenshotDir));
|
||||
|
||||
await CacheableDnsHttpAgent.update();
|
||||
|
||||
process.env.TZ = await this.getTimezone();
|
||||
@@ -110,8 +118,7 @@ class UptimeKumaServer {
|
||||
log.debug("DEBUG", "Timezone: " + process.env.TZ);
|
||||
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
|
||||
|
||||
await this.generateMaintenanceTimeslots();
|
||||
this.generateMaintenanceTimeslotsInterval = setInterval(this.generateMaintenanceTimeslots, 60 * 1000);
|
||||
await this.loadMaintenanceList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,16 +180,33 @@ class UptimeKumaServer {
|
||||
*/
|
||||
async getMaintenanceJSONList(userID) {
|
||||
let result = {};
|
||||
for (let maintenanceID in this.maintenanceList) {
|
||||
result[maintenanceID] = await this.maintenanceList[maintenanceID].toJSON();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load maintenance list and run
|
||||
* @param userID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadMaintenanceList(userID) {
|
||||
let maintenanceList = await R.findAll("maintenance", " ORDER BY end_date DESC, title", [
|
||||
|
||||
let maintenanceList = await R.find("maintenance", " user_id = ? ORDER BY end_date DESC, title", [
|
||||
userID,
|
||||
]);
|
||||
|
||||
for (let maintenance of maintenanceList) {
|
||||
result[maintenance.id] = await maintenance.toJSON();
|
||||
this.maintenanceList[maintenance.id] = maintenance;
|
||||
maintenance.run(this);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
getMaintenance(maintenanceID) {
|
||||
if (this.maintenanceList[maintenanceID]) {
|
||||
return this.maintenanceList[maintenanceID];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,7 +215,7 @@ class UptimeKumaServer {
|
||||
* @param {boolean} outputToConsole Should the error also be output to console?
|
||||
*/
|
||||
static errorLog(error, outputToConsole = true) {
|
||||
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
|
||||
const errorLogStream = fs.createWriteStream(path.join(Database.dataDir, "/error.log"), {
|
||||
flags: "a"
|
||||
});
|
||||
|
||||
@@ -228,9 +252,9 @@ class UptimeKumaServer {
|
||||
|
||||
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
||||
|| socket.client.conn.request.headers["x-real-ip"]
|
||||
|| clientIP.replace(/^.*:/, "");
|
||||
|| clientIP.replace(/^::ffff:/, "");
|
||||
} else {
|
||||
return clientIP.replace(/^.*:/, "");
|
||||
return clientIP.replace(/^::ffff:/, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,16 +262,46 @@ class UptimeKumaServer {
|
||||
* Attempt to get the current server timezone
|
||||
* If this fails, fall back to environment variables and then make a
|
||||
* guess.
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async getTimezone() {
|
||||
// From process.env.TZ
|
||||
try {
|
||||
if (process.env.TZ) {
|
||||
this.checkTimezone(process.env.TZ);
|
||||
return process.env.TZ;
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("timezone", e.message + " in process.env.TZ");
|
||||
}
|
||||
|
||||
let timezone = await Settings.get("serverTimezone");
|
||||
if (timezone) {
|
||||
return timezone;
|
||||
} else if (process.env.TZ) {
|
||||
return process.env.TZ;
|
||||
} else {
|
||||
return dayjs.tz.guess();
|
||||
|
||||
// From Settings
|
||||
try {
|
||||
log.debug("timezone", "Using timezone from settings: " + timezone);
|
||||
if (timezone) {
|
||||
this.checkTimezone(timezone);
|
||||
return timezone;
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("timezone", e.message + " in settings");
|
||||
}
|
||||
|
||||
// Guess
|
||||
try {
|
||||
let guess = dayjs.tz.guess();
|
||||
log.debug("timezone", "Guessing timezone: " + guess);
|
||||
if (guess) {
|
||||
this.checkTimezone(guess);
|
||||
return guess;
|
||||
} else {
|
||||
return "UTC";
|
||||
}
|
||||
} catch (e) {
|
||||
// Guess failed, fall back to UTC
|
||||
log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
|
||||
return "UTC";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,80 +313,79 @@ class UptimeKumaServer {
|
||||
return dayjs().format("Z");
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an error if the timezone is invalid
|
||||
* @param timezone
|
||||
*/
|
||||
checkTimezone(timezone) {
|
||||
try {
|
||||
dayjs.utc("2013-11-18 11:55").tz(timezone).format();
|
||||
} catch (e) {
|
||||
throw new Error("Invalid timezone:" + timezone);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current server timezone and environment variables
|
||||
* @param {string} timezone
|
||||
*/
|
||||
async setTimezone(timezone) {
|
||||
this.checkTimezone(timezone);
|
||||
await Settings.set("serverTimezone", timezone, "general");
|
||||
process.env.TZ = timezone;
|
||||
dayjs.tz.setDefault(timezone);
|
||||
}
|
||||
|
||||
/** Load the timeslots for maintenance */
|
||||
async generateMaintenanceTimeslots() {
|
||||
|
||||
let list = await R.find("maintenance_timeslot", " generated_next = 0 AND start_date <= DATETIME('now') ");
|
||||
|
||||
for (let maintenanceTimeslot of list) {
|
||||
let maintenance = await maintenanceTimeslot.maintenance;
|
||||
await MaintenanceTimeslot.generateTimeslot(maintenance, maintenanceTimeslot.end_date, false);
|
||||
maintenanceTimeslot.generated_next = true;
|
||||
await R.store(maintenanceTimeslot);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Listen logic should be moved to here
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start() {
|
||||
this.startServices();
|
||||
}
|
||||
|
||||
/** Stop the server */
|
||||
/**
|
||||
* Stop the server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async stop() {
|
||||
clearTimeout(this.generateMaintenanceTimeslotsInterval);
|
||||
}
|
||||
|
||||
loadPlugins() {
|
||||
this.pluginsManager = new PluginsManager(this);
|
||||
this.stopServices();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {PluginsManager}
|
||||
* Start all system services (e.g. nscd)
|
||||
* For now, only used in Docker
|
||||
*/
|
||||
getPluginManager() {
|
||||
return this.pluginsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {MonitorType} monitorType
|
||||
*/
|
||||
addMonitorType(monitorType) {
|
||||
if (monitorType instanceof MonitorType && monitorType.name) {
|
||||
if (monitorType.name in UptimeKumaServer.monitorTypeList) {
|
||||
log.error("", "Conflict Monitor Type name");
|
||||
startServices() {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
try {
|
||||
log.info("services", "Starting nscd");
|
||||
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
log.info("services", "Failed to start nscd");
|
||||
}
|
||||
UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
|
||||
} else {
|
||||
log.error("", "Invalid Monitor Type: " + monitorType.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {MonitorType} monitorType
|
||||
* Stop all system services
|
||||
*/
|
||||
removeMonitorType(monitorType) {
|
||||
if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
|
||||
delete UptimeKumaServer.monitorTypeList[monitorType.name];
|
||||
} else {
|
||||
log.error("", "Remove MonitorType failed: " + monitorType.name);
|
||||
stopServices() {
|
||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||
try {
|
||||
log.info("services", "Stopping nscd");
|
||||
childProcess.execSync("sudo service nscd stop");
|
||||
} catch (e) {
|
||||
log.info("services", "Failed to stop nscd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UptimeKumaServer
|
||||
};
|
||||
|
||||
// Must be at the end
|
||||
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
|
||||
const { MonitorType } = require("./monitor-types/monitor-type");
|
||||
// Must be at the end to avoid circular dependencies
|
||||
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
|
||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||
|
@@ -21,6 +21,8 @@ const grpc = require("@grpc/grpc-js");
|
||||
const protojs = require("protobufjs");
|
||||
const radiusClient = require("node-radius-client");
|
||||
const redis = require("redis");
|
||||
const oidc = require("openid-client");
|
||||
|
||||
const {
|
||||
dictionaries: {
|
||||
rfc2865: { file, attributes },
|
||||
@@ -28,8 +30,11 @@ const {
|
||||
} = require("node-radius-utils");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
const isWindows = process.platform === /^win/.test(process.platform);
|
||||
// SASLOptions used in JSDoc
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { Kafka, SASLOptions } = require("kafkajs");
|
||||
|
||||
const isWindows = process.platform === /^win/.test(process.platform);
|
||||
/**
|
||||
* Init or reset JWT secret
|
||||
* @returns {Promise<Bean>}
|
||||
@@ -49,6 +54,43 @@ exports.initJWTSecret = async () => {
|
||||
return jwtSecretBean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decodes a jwt and returns the payload portion without verifying the jqt.
|
||||
* @param {string} jwt The input jwt as a string
|
||||
* @returns {Object} Decoded jwt payload object
|
||||
*/
|
||||
exports.decodeJwt = (jwt) => {
|
||||
return JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a Access Token form a oidc/oauth2 provider
|
||||
* @param {string} tokenEndpoint The token URI form the auth service provider
|
||||
* @param {string} clientId The oidc/oauth application client id
|
||||
* @param {string} clientSecret The oidc/oauth application client secret
|
||||
* @param {string} scope The scope the for which the token should be issued for
|
||||
* @param {string} authMethod The method on how to sent the credentials. Default client_secret_basic
|
||||
* @returns {Promise<oidc.TokenSet>} TokenSet promise if the token request was successful
|
||||
*/
|
||||
exports.getOidcTokenClientCredentials = async (tokenEndpoint, clientId, clientSecret, scope, authMethod = "client_secret_basic") => {
|
||||
const oauthProvider = new oidc.Issuer({ token_endpoint: tokenEndpoint });
|
||||
let client = new oauthProvider.Client({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
token_endpoint_auth_method: authMethod
|
||||
});
|
||||
|
||||
// Increase default timeout and clock tolerance
|
||||
client[oidc.custom.http_options] = () => ({ timeout: 10000 });
|
||||
client[oidc.custom.clock_tolerance] = 5;
|
||||
|
||||
let grantParams = { grant_type: "client_credentials" };
|
||||
if (scope) {
|
||||
grantParams.scope = scope;
|
||||
}
|
||||
return await client.grant(grantParams);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send TCP request to specified hostname and port
|
||||
* @param {string} hostname Hostname / address of machine
|
||||
@@ -196,6 +238,94 @@ exports.mqttAsync = function (hostname, topic, okMessage, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Monitor Kafka using Producer
|
||||
* @param {string} topic Topic name to produce into
|
||||
* @param {string} message Message to produce
|
||||
* @param {Object} [options={interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma"}]
|
||||
* Kafka client options. Contains ssl, clientId, allowAutoTopicCreation and
|
||||
* interval (interval defaults to 20, allowAutoTopicCreation defaults to false, clientId defaults to "Uptime-Kuma"
|
||||
* and ssl defaults to false)
|
||||
* @param {string[]} brokers List of kafka brokers to connect, host and port joined by ':'
|
||||
* @param {SASLOptions} [saslOptions={}] Options for kafka client Authentication (SASL) (defaults to
|
||||
* {})
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
exports.kafkaProducerAsync = function (brokers, topic, message, options = {}, saslOptions = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { interval = 20, allowAutoTopicCreation = false, ssl = false, clientId = "Uptime-Kuma" } = options;
|
||||
|
||||
let connectedToKafka = false;
|
||||
|
||||
const timeoutID = setTimeout(() => {
|
||||
log.debug("kafkaProducer", "KafkaProducer timeout triggered");
|
||||
connectedToKafka = true;
|
||||
reject(new Error("Timeout"));
|
||||
}, interval * 1000 * 0.8);
|
||||
|
||||
if (saslOptions.mechanism === "None") {
|
||||
saslOptions = undefined;
|
||||
}
|
||||
|
||||
let client = new Kafka({
|
||||
brokers: brokers,
|
||||
clientId: clientId,
|
||||
sasl: saslOptions,
|
||||
retry: {
|
||||
retries: 0,
|
||||
},
|
||||
ssl: ssl,
|
||||
});
|
||||
|
||||
let producer = client.producer({
|
||||
allowAutoTopicCreation: allowAutoTopicCreation,
|
||||
retry: {
|
||||
retries: 0,
|
||||
}
|
||||
});
|
||||
|
||||
producer.connect().then(
|
||||
() => {
|
||||
try {
|
||||
producer.send({
|
||||
topic: topic,
|
||||
messages: [{
|
||||
value: message,
|
||||
}],
|
||||
});
|
||||
connectedToKafka = true;
|
||||
clearTimeout(timeoutID);
|
||||
resolve("Message sent successfully");
|
||||
} catch (e) {
|
||||
connectedToKafka = true;
|
||||
producer.disconnect();
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("Error sending message: " + e.message));
|
||||
}
|
||||
}
|
||||
).catch(
|
||||
(e) => {
|
||||
connectedToKafka = true;
|
||||
producer.disconnect();
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("Error in producer connection: " + e.message));
|
||||
}
|
||||
);
|
||||
|
||||
producer.on("producer.network.request_timeout", (_) => {
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("producer.network.request_timeout"));
|
||||
});
|
||||
|
||||
producer.on("producer.disconnect", (_) => {
|
||||
if (!connectedToKafka) {
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("producer.disconnect"));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Use NTLM Auth for a http request.
|
||||
* @param {Object} options The http request options
|
||||
@@ -322,21 +452,33 @@ exports.postgresQuery = function (connectionString, query) {
|
||||
* Run a query on MySQL/MariaDB
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to validate the database with
|
||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||
* @returns {Promise<(string)>}
|
||||
*/
|
||||
exports.mysqlQuery = function (connectionString, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = mysql.createConnection(connectionString);
|
||||
connection.promise().query(query)
|
||||
.then(res => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
connection.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
connection.query(query, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
} else {
|
||||
if (Array.isArray(res)) {
|
||||
resolve("Rows: " + res.length);
|
||||
} else {
|
||||
resolve("No Error, but the result is not an array. Type: " + typeof res);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
connection.end();
|
||||
} catch (_) {
|
||||
connection.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -366,6 +508,7 @@ exports.mongodbPing = async function (connectionString) {
|
||||
* @param {string} callingStationId ID of calling station
|
||||
* @param {string} secret Secret to use
|
||||
* @param {number} [port=1812] Port to contact radius server on
|
||||
* @param {number} [timeout=2500] Timeout for connection to use
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
exports.radius = function (
|
||||
@@ -376,10 +519,13 @@ exports.radius = function (
|
||||
callingStationId,
|
||||
secret,
|
||||
port = 1812,
|
||||
timeout = 2500,
|
||||
) {
|
||||
const client = new radiusClient({
|
||||
host: hostname,
|
||||
hostPort: port,
|
||||
timeout: timeout,
|
||||
retries: 1,
|
||||
dictionaries: [ file ],
|
||||
});
|
||||
|
||||
@@ -391,6 +537,12 @@ exports.radius = function (
|
||||
[ attributes.CALLING_STATION_ID, callingStationId ],
|
||||
[ attributes.CALLED_STATION_ID, calledStationId ],
|
||||
],
|
||||
}).catch((error) => {
|
||||
if (error.response?.code) {
|
||||
throw Error(error.response.code);
|
||||
} else {
|
||||
throw Error(error.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -401,19 +553,28 @@ exports.radius = function (
|
||||
exports.redisPingAsync = function (dsn) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = redis.createClient({
|
||||
url: dsn,
|
||||
url: dsn
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
client.connect().then(() => {
|
||||
if (!client.isOpen) {
|
||||
client.emit("error", new Error("connection isn't open"));
|
||||
}
|
||||
client.ping().then((res, err) => {
|
||||
if (client.isOpen) {
|
||||
client.disconnect();
|
||||
}
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -509,12 +670,16 @@ const parseCertificateInfo = function (info) {
|
||||
|
||||
// Move up the chain until loop is encountered
|
||||
if (link.issuerCertificate == null) {
|
||||
link.certType = (i === 0) ? "self-signed" : "root CA";
|
||||
break;
|
||||
} else if (link.issuerCertificate.fingerprint in existingList) {
|
||||
// a root CA certificate is typically "signed by itself" (=> "self signed certificate") and thus the "issuerCertificate" is a reference to itself.
|
||||
log.debug("cert", `[Last] ${link.issuerCertificate.fingerprint}`);
|
||||
link.certType = (i === 0) ? "self-signed" : "root CA";
|
||||
link.issuerCertificate = null;
|
||||
break;
|
||||
} else {
|
||||
link.certType = (i === 0) ? "server" : "intermediate CA";
|
||||
link = link.issuerCertificate;
|
||||
}
|
||||
|
||||
@@ -555,7 +720,6 @@ exports.checkCertificate = function (res) {
|
||||
* @param {number} status The status code to check
|
||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||
* @returns {boolean} True if status code within range, false otherwise
|
||||
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
||||
*/
|
||||
exports.checkStatusCode = function (status, acceptedCodes) {
|
||||
if (acceptedCodes == null || acceptedCodes.length === 0) {
|
||||
@@ -563,6 +727,11 @@ exports.checkStatusCode = function (status, acceptedCodes) {
|
||||
}
|
||||
|
||||
for (const codeRange of acceptedCodes) {
|
||||
if (typeof codeRange !== "string") {
|
||||
log.error("monitor", `Accepted status code not a string. ${codeRange} is of type ${typeof codeRange}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const codeRangeSplit = codeRange.split("-").map(string => parseInt(string));
|
||||
if (codeRangeSplit.length === 1) {
|
||||
if (status === codeRangeSplit[0]) {
|
||||
@@ -573,7 +742,8 @@ exports.checkStatusCode = function (status, acceptedCodes) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid status code range");
|
||||
log.error("monitor", `${codeRange} is not a valid status code range`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,3 +1052,13 @@ module.exports.grpcQuery = async (options) => {
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
// For unit test, export functions
|
||||
if (process.env.TEST_BACKEND) {
|
||||
module.exports.__test = {
|
||||
parseCertificateInfo,
|
||||
};
|
||||
module.exports.__getPrivateFunction = (functionName) => {
|
||||
return module.exports.__test[functionName];
|
||||
};
|
||||
}
|
||||
|
@@ -111,6 +111,10 @@ optgroup {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: white;
|
||||
|
||||
@@ -158,6 +162,26 @@ optgroup {
|
||||
background-color: #161B22;
|
||||
}
|
||||
|
||||
.btn-outline-normal {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 25px;
|
||||
background-color: transparent;
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
border: 1px solid $dark-font-color2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $highlight-white;
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-font-color2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.table-shadow-box {
|
||||
padding: 10px !important;
|
||||
@@ -266,6 +290,11 @@ optgroup {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
.form-select:disabled {
|
||||
color: rgba($dark-font-color, 0.7);
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
border-color: $dark-border-color;
|
||||
}
|
||||
@@ -431,12 +460,11 @@ optgroup {
|
||||
.monitor-list {
|
||||
&.scrollbar {
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 65px);
|
||||
}
|
||||
|
||||
@media (max-width: 770px) {
|
||||
&.scrollbar {
|
||||
height: calc(100% - 40px);
|
||||
height: calc(100% - 97px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,6 +584,31 @@ h5.settings-subheading::after {
|
||||
border-bottom: 1px solid $dark-border-color;
|
||||
}
|
||||
|
||||
|
||||
$shadow-box-padding: 20px;
|
||||
|
||||
.shadow-box-with-fixed-bottom-bar {
|
||||
padding-top: $shadow-box-padding;
|
||||
padding-bottom: 0;
|
||||
padding-right: $shadow-box-padding;
|
||||
padding-left: $shadow-box-padding;
|
||||
}
|
||||
|
||||
.fixed-bottom-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-left: -$shadow-box-padding;
|
||||
margin-right: -$shadow-box-padding;
|
||||
z-index: 100;
|
||||
background-color: rgba(white, 0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: 0 0 10px 10px;
|
||||
|
||||
.dark & {
|
||||
background-color: rgba($dark-header-bg, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
// Localization
|
||||
|
||||
@import "localization.scss";
|
||||
|
@@ -1,6 +1,12 @@
|
||||
@import "vars.scss";
|
||||
@import "node_modules/vue-multiselect/dist/vue-multiselect";
|
||||
|
||||
.multiselect {
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid #ced4da;
|
||||
@@ -14,10 +20,12 @@
|
||||
|
||||
.multiselect__option--highlight {
|
||||
background: $primary !important;
|
||||
color: $dark-font-color2 !important;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight::after {
|
||||
background: $primary !important;
|
||||
color: $dark-font-color2 !important;
|
||||
}
|
||||
|
||||
.multiselect__tag {
|
||||
@@ -61,6 +69,7 @@
|
||||
.multiselect__content-wrapper {
|
||||
background-color: $dark-bg2;
|
||||
border-color: $dark-border-color;
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
|
@@ -48,15 +48,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
{{ $t("Generate") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
{{ $t("Generate") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,6 +158,16 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Clear Form inputs */
|
||||
clearForm() {
|
||||
this.key = {
|
||||
name: "",
|
||||
expires: this.minDate,
|
||||
active: 1,
|
||||
};
|
||||
this.noExpire = false;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
70
src/components/ActionSelect.vue
Normal file
70
src/components/ActionSelect.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="input-group mb-3">
|
||||
<select ref="select" v-model="model" class="form-select" :disabled="disabled">
|
||||
<option v-for="option in options" :key="option" :value="option.value">{{ option.label }}</option>
|
||||
</select>
|
||||
<a class="btn btn-outline-primary" @click="action()">
|
||||
<font-awesome-icon :icon="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Generic select field with a customizable action on the right.
|
||||
* Action is passed in as a function.
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* The value of the select field.
|
||||
*/
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* Whether the select field is enabled / disabled.
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* The icon displayed in the right button of the select field.
|
||||
* Accepts a Font Awesome icon string identifier.
|
||||
* @example "plus"
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* The action to be performed when the button is clicked.
|
||||
* Action is passed in as a function.
|
||||
*/
|
||||
action: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
emits: [ "update:modelValue" ],
|
||||
computed: {
|
||||
/**
|
||||
* Send value update to parent on change.
|
||||
*/
|
||||
model: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:modelValue", value);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
305
src/components/BadgeGeneratorDialog.vue
Normal file
305
src/components/BadgeGeneratorDialog.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div ref="BadgeGeneratorModal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ $t("Badge Generator", [monitor.name]) }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="type" class="form-label">{{ $t("Badge Type") }}</label>
|
||||
<select id="type" v-model="badge.type" class="form-select">
|
||||
<option value="status">status</option>
|
||||
<option value="uptime">uptime</option>
|
||||
<option value="ping">ping</option>
|
||||
<option value="avg-response">avg-response</option>
|
||||
<option value="cert-exp">cert-exp</option>
|
||||
<option value="response">response</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('duration') " class="mb-3">
|
||||
<label for="duration" class="form-label">{{ $t("Badge Duration (in hours)") }}</label>
|
||||
<input id="duration" v-model="badge.duration" type="number" min="0" placeholder="24" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('label') " class="mb-3">
|
||||
<label for="label" class="form-label">{{ $t("Badge Label") }}</label>
|
||||
<input id="label" v-model="badge.label" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('prefix') " class="mb-3">
|
||||
<label for="prefix" class="form-label">{{ $t("Badge Prefix") }}</label>
|
||||
<input id="prefix" v-model="badge.prefix" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('suffix') " class="mb-3">
|
||||
<label for="suffix" class="form-label">{{ $t("Badge Suffix") }}</label>
|
||||
<input id="suffix" v-model="badge.suffix" type="text" placeholder="%" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelColor') " class="mb-3">
|
||||
<label for="labelColor" class="form-label">{{ $t("Badge Label Color") }}</label>
|
||||
<input id="labelColor" v-model="badge.labelColor" type="text" placeholder="#555" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('color') " class="mb-3">
|
||||
<label for="color" class="form-label">{{ $t("Badge Color") }}</label>
|
||||
<input id="color" v-model="badge.color" type="text" :placeholder="badgeConstants.defaultUpColor" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelPrefix') " class="mb-3">
|
||||
<label for="labelPrefix" class="form-label">{{ $t("Badge Label Prefix") }}</label>
|
||||
<input id="labelPrefix" v-model="badge.labelPrefix" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('labelSuffix') " class="mb-3">
|
||||
<label for="labelSuffix" class="form-label">{{ $t("Badge Label Suffix") }}</label>
|
||||
<input id="labelSuffix" v-model="badge.labelSuffix" type="text" placeholder="h" class="form-control">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('upColor') " class="mb-3">
|
||||
<label for="upColor" class="form-label">{{ $t("Badge Up Color") }}</label>
|
||||
<input id="upColor" v-model="badge.upColor" type="text" class="form-control" :placeholder="badgeConstants.defaultUpColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downColor') " class="mb-3">
|
||||
<label for="downColor" class="form-label">{{ $t("Badge Down Color") }}</label>
|
||||
<input id="downColor" v-model="badge.downColor" type="text" class="form-control" :placeholder="badgeConstants.defaultDownColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('pendingColor') " class="mb-3">
|
||||
<label for="pendingColor" class="form-label">{{ $t("Badge Pending Color") }}</label>
|
||||
<input id="pendingColor" v-model="badge.pendingColor" type="text" class="form-control" :placeholder="badgeConstants.defaultPendingColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('maintenanceColor') " class="mb-3">
|
||||
<label for="maintenanceColor" class="form-label">{{ $t("Badge Maintenance Color") }}</label>
|
||||
<input id="maintenanceColor" v-model="badge.maintenanceColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnColor') " class="mb-3">
|
||||
<label for="warnColor" class="form-label">{{ $t("Badge Warn Color") }}</label>
|
||||
<input id="warnColor" v-model="badge.warnColor" type="text" class="form-control" :placeholder="badgeConstants.defaultMaintenanceColor">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('warnDays') " class="mb-3">
|
||||
<label for="warnDays" class="form-label">{{ $t("Badge Warn Days") }}</label>
|
||||
<input id="warnDays" v-model="badge.warnDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireWarnDays">
|
||||
</div>
|
||||
|
||||
<div v-if=" (parameters[badge.type || 'null'] || [] ).includes('downDays') " class="mb-3">
|
||||
<label for="downDays" class="form-label">{{ $t("Badge Down Days") }}</label>
|
||||
<input id="downDays" v-model="badge.downDays" type="number" min="0" class="form-control" :placeholder="badgeConstants.defaultCertExpireDownDays">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="style" class="form-label">{{ $t("Badge Style") }}</label>
|
||||
<select id="style" v-model="badge.style" class="form-select">
|
||||
<option value="plastic">plastic</option>
|
||||
<option value="flat">flat</option>
|
||||
<option value="flat-square">flat-square</option>
|
||||
<option value="for-the-badge">for-the-badge</option>
|
||||
<option value="social">social</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">{{ $t("Badge value (For Testing only.)") }}</label>
|
||||
<input id="value" v-model="badge.value" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 pt-3 d-flex justify-content-center">
|
||||
<img :src="badgeURL" :alt="$t('Badge Preview')">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="badge-url" class="form-label">{{ $t("Badge URL") }}</label>
|
||||
<CopyableInput id="badge-url" v-model="badgeURL" type="url" disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
{{ $t("Close") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import CopyableInput from "./CopyableInput.vue";
|
||||
import { default as serverConfig } from "../../server/config.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CopyableInput
|
||||
},
|
||||
props: {},
|
||||
emits: [],
|
||||
data() {
|
||||
return {
|
||||
model: null,
|
||||
processing: false,
|
||||
monitor: {
|
||||
id: null,
|
||||
name: null,
|
||||
},
|
||||
badge: {
|
||||
type: "status",
|
||||
duration: null,
|
||||
label: null,
|
||||
prefix: null,
|
||||
suffix: null,
|
||||
labelColor: null,
|
||||
color: null,
|
||||
labelPrefix: null,
|
||||
labelSuffix: null,
|
||||
upColor: null,
|
||||
downColor: null,
|
||||
pendingColor: null,
|
||||
maintenanceColor: null,
|
||||
warnColor: null,
|
||||
warnDays: null,
|
||||
downDays: null,
|
||||
style: "flat",
|
||||
value: null,
|
||||
},
|
||||
parameters: {
|
||||
status: [
|
||||
"upLabel",
|
||||
"downLabel",
|
||||
"pendingLabel",
|
||||
"maintenanceLabel",
|
||||
"upColor",
|
||||
"downColor",
|
||||
"pendingColor",
|
||||
"maintenanceColor",
|
||||
],
|
||||
uptime: [
|
||||
"duration",
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
ping: [
|
||||
"duration",
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
"avg-response": [
|
||||
"duration",
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
"cert-exp": [
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"upColor",
|
||||
"warnColor",
|
||||
"downColor",
|
||||
"warnDays",
|
||||
"downDays",
|
||||
"labelColor",
|
||||
],
|
||||
response: [
|
||||
"labelPrefix",
|
||||
"labelSuffix",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"color",
|
||||
"labelColor",
|
||||
],
|
||||
},
|
||||
badgeConstants: serverConfig.badgeConstants,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
badgeURL() {
|
||||
if (!this.monitor.id || !this.badge.type) {
|
||||
return;
|
||||
}
|
||||
let badgeURL = this.$root.baseURL + "/api/badge/" + this.monitor.id + "/" + this.badge.type;
|
||||
|
||||
let parameterList = {};
|
||||
|
||||
for (let parameter of this.parameters[this.badge.type] || []) {
|
||||
if (parameter === "duration" && this.badge.duration) {
|
||||
badgeURL += "/" + this.badge.duration;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.badge[parameter]) {
|
||||
parameterList[parameter] = this.badge[parameter];
|
||||
}
|
||||
}
|
||||
|
||||
for (let parameter of [ "label", "style", "value" ]) {
|
||||
if (parameter === "style" && this.badge.style === "flat") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.badge[parameter]) {
|
||||
parameterList[parameter] = this.badge[parameter];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(parameterList).length > 0) {
|
||||
return badgeURL + "?" + new URLSearchParams(parameterList);
|
||||
}
|
||||
|
||||
return badgeURL;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.BadgeGeneratorModal = new Modal(this.$refs.BadgeGeneratorModal);
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Setting monitor
|
||||
* @param {number} monitorId ID of monitor
|
||||
* @param {string} monitorName Name of monitor
|
||||
*/
|
||||
show(monitorId, monitorName) {
|
||||
this.monitor = {
|
||||
id: monitorId,
|
||||
name: monitorName,
|
||||
};
|
||||
|
||||
this.BadgeGeneratorModal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user