feat: Domain name expiry (#6413)
Co-authored-by: AiroPi <47398145+AiroPi@users.noreply.github.com> Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
parent
f3c76dbc6f
commit
eb0b6cdb09
21
db/knex_migrations/2025-09-02-0000-add-domain-expiry.js
Normal file
21
db/knex_migrations/2025-09-02-0000-add-domain-expiry.js
Normal file
@ -0,0 +1,21 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.boolean("domain_expiry_notification").defaultTo(1);
|
||||
})
|
||||
.createTable("domain_expiry", (table) => {
|
||||
table.increments("id");
|
||||
table.datetime("last_check");
|
||||
table.text("domain").unique().notNullable();
|
||||
table.datetime("expiry");
|
||||
table.integer("last_expiry_notification_sent").defaultTo(null);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.boolean("domain_expiry_notification").alter();
|
||||
})
|
||||
.dropTable("domain_expiry");
|
||||
};
|
||||
318
package-lock.json
generated
318
package-lock.json
generated
@ -60,6 +60,7 @@
|
||||
"nanoid": "~3.3.4",
|
||||
"net-snmp": "^3.11.2",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"node-fetch-cache": "^5.1.0",
|
||||
"node-radius-utils": "~1.2.0",
|
||||
"nodemailer": "~6.9.13",
|
||||
"nostr-tools": "^2.10.4",
|
||||
@ -85,6 +86,7 @@
|
||||
"tar": "~6.2.1",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"tldts": "^7.0.19",
|
||||
"tough-cookie": "~4.1.3",
|
||||
"web-push": "^3.6.7",
|
||||
"ws": "^8.13.0"
|
||||
@ -3646,7 +3648,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@ -3656,7 +3657,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
@ -8783,6 +8783,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||
@ -10467,6 +10476,29 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@ -10674,6 +10706,27 @@
|
||||
"integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz",
|
||||
"integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -11797,7 +11850,6 @@
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.19"
|
||||
@ -12995,6 +13047,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/locko": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/locko/-/locko-1.1.0.tgz",
|
||||
"integrity": "sha512-pYB2dzRY93fJkg2RIl41AMNgTQftEjyTK9vlPrGOJvuGQsOjb267VJBw15BjiN3RBd1oBoKkOu9E2dRdFKIfAA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@ -13546,7 +13604,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
|
||||
"integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
@ -13559,7 +13616,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
|
||||
"integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
@ -13902,6 +13958,26 @@
|
||||
"command-exists": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@ -13922,6 +13998,211 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-cache/-/node-fetch-cache-5.1.0.tgz",
|
||||
"integrity": "sha512-4j3rRHNGIKGX7VzXSrBT0bh7+wFuyJv1DxCfCLDHsnDahJWoD9lXe3BzL3BJg/GEIJiM7KIvqVs3byW1GFtRsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cacache": "^20.0.1",
|
||||
"formdata-node": "^6.0.3",
|
||||
"locko": "^1.1.0",
|
||||
"node-fetch": "3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/@npmcli/fs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz",
|
||||
"integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/cacache": {
|
||||
"version": "20.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz",
|
||||
"integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/fs": "^5.0.0",
|
||||
"fs-minipass": "^3.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"lru-cache": "^11.1.0",
|
||||
"minipass": "^7.0.3",
|
||||
"minipass-collect": "^2.0.1",
|
||||
"minipass-flush": "^1.0.5",
|
||||
"minipass-pipeline": "^1.2.4",
|
||||
"p-map": "^7.0.2",
|
||||
"ssri": "^13.0.0",
|
||||
"unique-filename": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/fs-minipass": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
|
||||
"integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/glob": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
|
||||
"integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/minimatch": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/minipass-collect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
|
||||
"integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/p-map": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
|
||||
"integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/path-scurry": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
|
||||
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/ssri": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz",
|
||||
"integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/unique-filename": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz",
|
||||
"integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"unique-slug": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-cache/node_modules/unique-slug": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz",
|
||||
"integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@ -17987,6 +18268,24 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
|
||||
"integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.19"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
|
||||
"integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
@ -18939,6 +19238,15 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
|
||||
@ -122,6 +122,7 @@
|
||||
"net-snmp": "^3.11.2",
|
||||
"node-cloudflared-tunnel": "~1.0.9",
|
||||
"node-radius-utils": "~1.2.0",
|
||||
"node-fetch-cache": "^5.1.0",
|
||||
"nodemailer": "~6.9.13",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"notp": "~2.0.3",
|
||||
@ -146,6 +147,7 @@
|
||||
"tar": "~6.2.1",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"tldts": "^7.0.19",
|
||||
"tough-cookie": "~4.1.3",
|
||||
"web-push": "^3.6.7",
|
||||
"ws": "^8.13.0"
|
||||
|
||||
270
server/model/domain_expiry.js
Normal file
270
server/model/domain_expiry.js
Normal file
@ -0,0 +1,270 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
const { parse: parseTld } = require("tldts");
|
||||
const { getDaysRemaining, getDaysBetween, setting, setSetting } = require("../util-server");
|
||||
const { Notification } = require("../notification");
|
||||
const { default: NodeFetchCache, MemoryCache } = require("node-fetch-cache");
|
||||
|
||||
const TABLE = "domain_expiry";
|
||||
const urlTypes = [ "websocket-upgrade", "http", "keyword", "json-query", "real-browser" ];
|
||||
const excludeTypes = [ "docker", "group", "push", "manual", "rabbitmq", "redis" ];
|
||||
|
||||
const cachedFetch = process.env.NODE_ENV ? NodeFetchCache.create({
|
||||
// cache for 8h
|
||||
cache: new MemoryCache({ ttl: 1000 * 60 * 60 * 8 })
|
||||
}) : fetch;
|
||||
|
||||
/**
|
||||
* Find the RDAP server for a given TLD
|
||||
* @param {string} tld TLD
|
||||
* @returns {Promise<string>} First RDAP server found
|
||||
*/
|
||||
async function getRdapServer(tld) {
|
||||
let rdapList;
|
||||
try {
|
||||
const res = await cachedFetch("https://data.iana.org/rdap/dns.json");
|
||||
rdapList = await res.json();
|
||||
} catch (error) {
|
||||
log.debug("rdap", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const service of rdapList["services"]) {
|
||||
const [ tlds, urls ] = service;
|
||||
if (tlds.includes(tld)) {
|
||||
return urls[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request RDAP server to retrieve the expiry date of a domain
|
||||
* @param {string} domain Domain to retrieve the expiry date from
|
||||
* @returns {Promise<(Date|null)>} Expiry date from RDAP server
|
||||
*/
|
||||
async function getRdapDomainExpiryDate(domain) {
|
||||
const tld = DomainExpiry.parseTld(domain).publicSuffix;
|
||||
const rdapServer = await getRdapServer(tld);
|
||||
if (rdapServer === null) {
|
||||
log.warn("rdap", `No RDAP server found, TLD ${tld} not supported.`);
|
||||
return null;
|
||||
}
|
||||
const url = `${rdapServer}domain/${domain}`;
|
||||
|
||||
let rdapInfos;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
rdapInfos = await res.json();
|
||||
} catch {
|
||||
log.warn("rdap", "Not able to get expiry date from RDAP");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rdapInfos["events"] === undefined) {
|
||||
return null;
|
||||
}
|
||||
for (const event of rdapInfos["events"]) {
|
||||
if (event["eventAction"] === "expiration") {
|
||||
return new Date(event["eventDate"]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a certificate notification when domain expires in less than target days
|
||||
* @param {string} domain Domain we monitor
|
||||
* @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 function sendDomainNotificationByTargetDays(domain, daysRemaining, targetDays, notificationList) {
|
||||
let sent = false;
|
||||
log.debug("domain", `Send domain expiry notification for ${targetDays} deadline.`);
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
log.debug("domain", `Sending to ${notification.name}`);
|
||||
await Notification.send(
|
||||
JSON.parse(notification.config),
|
||||
`Domain name ${domain} will expire in ${daysRemaining} days`
|
||||
);
|
||||
sent = true;
|
||||
} catch (e) {
|
||||
log.error("domain", `Cannot send domain notification to ${notification.name}`);
|
||||
log.error("domain", e);
|
||||
}
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
class DomainExpiry extends BeanModel {
|
||||
/**
|
||||
* @param {string} domain Domain name
|
||||
* @returns {Promise<DomainExpiry>} Domain bean
|
||||
*/
|
||||
static async findByName(domain) {
|
||||
return R.findOne(TABLE, "domain = ?", [ domain ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} domain Domain name
|
||||
* @returns {DomainExpiry} Domain bean
|
||||
*/
|
||||
static createByName(domain) {
|
||||
const d = R.dispense(TABLE);
|
||||
d.domain = domain;
|
||||
return d;
|
||||
}
|
||||
|
||||
static parseTld = parseTld;
|
||||
|
||||
/**
|
||||
* @returns {(object)} parsed domain components
|
||||
*/
|
||||
parseName() {
|
||||
return parseTld(this.domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {(null|object)} parsed domain tld
|
||||
*/
|
||||
get tld() {
|
||||
return this.parseName().publicSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Monitor} monitor Monitor object
|
||||
* @returns {Promise<DomainExpiry>} Domain expiry bean
|
||||
*/
|
||||
static async forMonitor(monitor) {
|
||||
const m = monitor;
|
||||
if (excludeTypes.includes(m.type) || m.type?.match(/sql$/)) {
|
||||
return false;
|
||||
}
|
||||
const tld = parseTld(urlTypes.includes(m.type) ? m.url : m.type === "grpc-keyword" ? m.grpcUrl : m.hostname);
|
||||
const rdap = await getRdapServer(tld.publicSuffix);
|
||||
if (!rdap) {
|
||||
log.warn("domain", `${tld.publicSuffix} is not supported. File a bug report if you believe it should be.`);
|
||||
return false;
|
||||
}
|
||||
const existing = await DomainExpiry.findByName(tld.domain);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
if (tld.domain) {
|
||||
return await DomainExpiry.createByName(tld.domain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} number of days remaining before expiry
|
||||
*/
|
||||
get daysRemaining() {
|
||||
return getDaysRemaining(new Date(), new Date(this.expiry));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {(Date|null)} Expiry date from RDAP
|
||||
*/
|
||||
getExpiryDate() {
|
||||
return getRdapDomainExpiryDate(this.domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(Monitor)} monitor Monitor object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async checkExpiry(monitor) {
|
||||
|
||||
let bean = await DomainExpiry.forMonitor(monitor);
|
||||
|
||||
let expiryDate;
|
||||
if (bean?.lastCheck && getDaysBetween(new Date(bean.lastCheck), new Date()) < 1) {
|
||||
log.debug("domain", `Domain expiry already checked recently for ${bean.domain}, won't re-check.`);
|
||||
return bean.expiry;
|
||||
} else if (bean) {
|
||||
expiryDate = await bean.getExpiryDate();
|
||||
|
||||
if (new Date(expiryDate) > new Date(bean.expiry)) {
|
||||
bean.lastExpiryNotificationSent = null;
|
||||
}
|
||||
|
||||
bean.expiry = expiryDate;
|
||||
bean.lastCheck = new Date();
|
||||
await R.store(bean);
|
||||
}
|
||||
|
||||
if (expiryDate === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return expiryDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Monitor} monitor Monitor instance
|
||||
* @param {LooseObject<any>[]} notificationList notification List
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async sendNotifications(monitor, notificationList) {
|
||||
const domain = await DomainExpiry.forMonitor(monitor);
|
||||
const name = domain.domain;
|
||||
|
||||
if (!notificationList.length > 0) {
|
||||
// fail fast. If no notification is set, all the following checks can be skipped.
|
||||
log.debug("domain", "No notification, no need to send domain notification");
|
||||
return;
|
||||
}
|
||||
|
||||
const daysRemaining = getDaysRemaining(new Date(), domain.expiry);
|
||||
const lastSent = domain.lastExpiryNotificationSent;
|
||||
log.debug("domain", `${name} expires in ${daysRemaining} days`);
|
||||
|
||||
let notifyDays = await setting("domainExpiryNotifyDays");
|
||||
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||
// Reset Default
|
||||
await setSetting("domainExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
||||
notifyDays = [ 7, 14, 21 ];
|
||||
}
|
||||
if (Array.isArray(notifyDays)) {
|
||||
// Asc sort to avoid sending multiple notifications if daysRemaining is below multiple targetDays
|
||||
notifyDays.sort((a, b) => a - b);
|
||||
for (const targetDays of notifyDays) {
|
||||
if (daysRemaining > targetDays) {
|
||||
log.debug(
|
||||
"domain",
|
||||
`No need to send domain notification for ${name} (${daysRemaining} days valid) on ${targetDays} deadline.`
|
||||
);
|
||||
continue;
|
||||
} else if (lastSent && lastSent <= targetDays) {
|
||||
log.debug(
|
||||
"domain",
|
||||
`Notification for ${name} on ${targetDays} deadline sent already, no need to send again.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const sent = await sendDomainNotificationByTargetDays(
|
||||
name,
|
||||
daysRemaining,
|
||||
targetDays,
|
||||
notificationList
|
||||
);
|
||||
if (sent) {
|
||||
domain.lastExpiryNotificationSent = targetDays;
|
||||
await R.store(domain);
|
||||
return targetDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DomainExpiry;
|
||||
@ -28,6 +28,7 @@ const { CookieJar } = require("tough-cookie");
|
||||
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const DomainExpiry = require("./domain_expiry");
|
||||
|
||||
const rootCertificates = rootCertificatesFingerprints();
|
||||
|
||||
@ -117,6 +118,7 @@ class Monitor extends BeanModel {
|
||||
keyword: this.keyword,
|
||||
invertKeyword: this.isInvertKeyword(),
|
||||
expiryNotification: this.isEnabledExpiryNotification(),
|
||||
domainExpiryNotification: Boolean(this.domainExpiryNotification),
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
packetSize: this.packetSize,
|
||||
@ -934,6 +936,19 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
if (bean.status !== MAINTENANCE && Boolean(this.domainExpiryNotification)) {
|
||||
try {
|
||||
const domainExpiryDate = await DomainExpiry.checkExpiry(this);
|
||||
if (domainExpiryDate) {
|
||||
DomainExpiry.sendNotifications(this, await Monitor.getNotificationList(this) || []);
|
||||
} else {
|
||||
log.debug("monitor", `Failed getting expiration date for domain ${this.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("monitor", `Failed to get domain expiry for ${this.name} : ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (bean.status === UP) {
|
||||
log.debug("monitor", `Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else if (bean.status === PENDING) {
|
||||
@ -1201,6 +1216,9 @@ class Monitor extends BeanModel {
|
||||
|
||||
// Send Cert Info
|
||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
||||
|
||||
// Send domain info
|
||||
await Monitor.sendDomainInfo(io, monitorID, userID);
|
||||
} else {
|
||||
log.debug("monitor", "No clients in the room, no need to send stats");
|
||||
}
|
||||
@ -1222,6 +1240,22 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send domain name information to client
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
* @returns {void}
|
||||
*/
|
||||
static async sendDomainInfo(io, monitorID, userID) {
|
||||
const monitor = await R.findOne("monitor", "id = ?", [ monitorID ]);
|
||||
|
||||
const domain = await DomainExpiry.forMonitor(monitor);
|
||||
if (domain?.expiry) {
|
||||
io.to(userID).emit("domainInfo", monitorID, domain.daysRemaining, new Date(domain.expiry));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has status of monitor changed since last beat?
|
||||
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||
|
||||
@ -843,6 +843,7 @@ let needSetup = false;
|
||||
bean.invertKeyword = monitor.invertKeyword;
|
||||
bean.ignoreTls = monitor.ignoreTls;
|
||||
bean.expiryNotification = monitor.expiryNotification;
|
||||
bean.domainExpiryNotification = monitor.domainExpiryNotification;
|
||||
bean.upsideDown = monitor.upsideDown;
|
||||
bean.packetSize = monitor.packetSize;
|
||||
bean.maxredirects = monitor.maxredirects;
|
||||
@ -981,6 +982,22 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("checkMointor", async (partial, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
const DomainExpiry = require("./model/domain_expiry");
|
||||
callback({
|
||||
ok: true,
|
||||
domain: (await DomainExpiry.forMonitor(partial))?.domain || null
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getMonitorBeats", async (monitorID, period, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
@ -483,6 +483,7 @@ exports.setSettings = async function (type, data) {
|
||||
*/
|
||||
const getDaysBetween = (validFrom, validTo) =>
|
||||
Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
|
||||
exports.getDaysBetween = getDaysBetween;
|
||||
|
||||
/**
|
||||
* Get days remaining from a time range
|
||||
@ -492,11 +493,12 @@ const getDaysBetween = (validFrom, validTo) =>
|
||||
*/
|
||||
const getDaysRemaining = (validFrom, validTo) => {
|
||||
const daysRemaining = getDaysBetween(validFrom, validTo);
|
||||
if (new Date(validTo).getTime() < new Date().getTime()) {
|
||||
if (new Date(validTo).getTime() < new Date(validFrom).getTime()) {
|
||||
return -daysRemaining;
|
||||
}
|
||||
return daysRemaining;
|
||||
};
|
||||
exports.getDaysRemaining = getDaysRemaining;
|
||||
|
||||
/**
|
||||
* Fix certificate info for display
|
||||
|
||||
@ -60,13 +60,35 @@
|
||||
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeExpiryNotifDay(day)">
|
||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeTlsExpiryNotifDay(day)">
|
||||
<font-awesome-icon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||
<ActionInput v-model="tlsExpiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addTlsExpiryNotifDay(tlsExpiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4 pt-4">
|
||||
<h5 class="my-4 settings-subheading">{{ $t("settingsDomainExpiry") }}</h5>
|
||||
<p>{{ $t("domainExpiryDescription") }}</p>
|
||||
<p>{{ $t("notificationDescription") }}</p>
|
||||
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||
<div v-for="day in settings.domainExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeDomainExpiryNotifDay(day)">
|
||||
<font-awesome-icon icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<ActionInput v-model="domainExpiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addDomainExpiryNotifDay(domainExpiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||
@ -96,7 +118,8 @@ export default {
|
||||
/**
|
||||
* Variable to store the input for new certificate expiry day.
|
||||
*/
|
||||
expiryNotifInput: null,
|
||||
tlsExpiryNotifInput: null,
|
||||
domainExpiryNotifInput: null,
|
||||
};
|
||||
},
|
||||
|
||||
@ -134,15 +157,15 @@ export default {
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Remove a day from expiry notification days.
|
||||
* Remove a day from tls expiry notification days.
|
||||
* @param {number} day The day to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
removeExpiryNotifDay(day) {
|
||||
removeTlsExpiryNotifDay(day) {
|
||||
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
|
||||
},
|
||||
/**
|
||||
* Add a new expiry notification day.
|
||||
* Add a new tls expiry notification day.
|
||||
* Will verify:
|
||||
* - day is not null or empty string.
|
||||
* - day is a number.
|
||||
@ -151,14 +174,44 @@ export default {
|
||||
* @param {number} day The day number to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addExpiryNotifDay(day) {
|
||||
addTlsExpiryNotifDay(day) {
|
||||
if (day != null && day !== "") {
|
||||
const parsedDay = parseInt(day);
|
||||
if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
|
||||
if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) {
|
||||
this.settings.tlsExpiryNotifyDays.push(parseInt(day));
|
||||
this.settings.tlsExpiryNotifyDays.sort((a, b) => a - b);
|
||||
this.expiryNotifInput = null;
|
||||
this.tlsExpiryNotifInput = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Remove a day from domain expiry notification days.
|
||||
* @param {number} day The day to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
removeDomainExpiryNotifDay(day) {
|
||||
this.settings.domainExpiryNotifyDays = this.settings.domainExpiryNotifyDays.filter(d => d !== day);
|
||||
},
|
||||
/**
|
||||
* Add a new domain expiry notification day.
|
||||
* Will verify:
|
||||
* - day is not null or empty string.
|
||||
* - day is a number.
|
||||
* - day is > 0.
|
||||
* - The day is not already in the list.
|
||||
* @param {number} day The day number to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addDomainExpiryNotifDay(day) {
|
||||
if (day != null && day !== "") {
|
||||
const parsedDay = parseInt(day);
|
||||
if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) {
|
||||
if (!this.settings.domainExpiryNotifyDays.includes(parsedDay)) {
|
||||
this.settings.domainExpiryNotifyDays.push(parseInt(day));
|
||||
this.settings.domainExpiryNotifyDays.sort((a, b) => a - b);
|
||||
this.domainExpiryNotifInput = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1232,6 +1232,10 @@
|
||||
"Browser not supported": "Browser not supported",
|
||||
"Unable to get permission to notify": "Unable to get permission to notify (request either denied or ignored).",
|
||||
"Webpush Helptext": "Web push only works with SSL (HTTPS) connections. For iOS devices, webpage must be added to homescreen beforehand.",
|
||||
"settingsDomainExpiry": "Domain Expiry",
|
||||
"labelDomainExpiry": "Domain Exp.",
|
||||
"labelDomainNameExpiryNotification": "Domain Name Expiry Notification",
|
||||
"domainExpiryDescription": "Trigger notification when domain names expires in:",
|
||||
"minimumIntervalWarning": "Intervals below 20 seconds may result in poor performance.",
|
||||
"lowIntervalWarning": "Are you sure want to set the interval value below 20 seconds? Performance may be degraded, particularly if there are a large number of monitors."
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ export default {
|
||||
avgPingList: { },
|
||||
uptimeList: { },
|
||||
tlsInfoList: {},
|
||||
domainInfoList: {},
|
||||
notificationList: [],
|
||||
dockerHostList: [],
|
||||
remoteBrowserList: [],
|
||||
@ -250,6 +251,11 @@ export default {
|
||||
this.tlsInfoList[monitorID] = JSON.parse(data);
|
||||
});
|
||||
|
||||
socket.on("domainInfo", (monitorID, daysRemaining, expiresOn) => {
|
||||
this.domainInfoList[monitorID] = { daysRemaining: daysRemaining,
|
||||
expiresOn: expiresOn };
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`);
|
||||
this.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("Reconnecting...")}`;
|
||||
|
||||
@ -298,6 +298,21 @@
|
||||
<font-awesome-icon v-if="tlsInfo.hostnameMatchMonitorUrl === false" class="cert-info-warn" icon="exclamation-triangle" :title="$t('certHostnameMismatch')" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="domainInfo"
|
||||
class="col-12 col-sm col row d-flex align-items-center d-sm-block"
|
||||
>
|
||||
<h4 class="col-4 col-sm-12">{{ $t("labelDomainExpiry") }}</h4>
|
||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">
|
||||
(<Datetime
|
||||
:value="domainInfo.expiresOn"
|
||||
date-only
|
||||
/>)
|
||||
</p>
|
||||
<span class="col-4 col-sm-12 num">
|
||||
{{ domainInfo.daysRemaining }} {{ $tc("day", domainInfo.daysRemaining ) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -626,6 +641,10 @@ export default {
|
||||
return null;
|
||||
},
|
||||
|
||||
domainInfo() {
|
||||
return this.$root.domainInfoList[this.monitor.id] || null;
|
||||
},
|
||||
|
||||
showCertInfoBox() {
|
||||
return this.tlsInfo != null && this.toggleCertInfoBox;
|
||||
},
|
||||
|
||||
@ -797,6 +797,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasDomain" class="my-3 form-check">
|
||||
<input id="domain-expiry-notification" v-model="monitor.domainExpiryNotification" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="domain-expiry-notification">
|
||||
{{ $t("labelDomainNameExpiryNotification") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="monitor.type === 'websocket-upgrade' " class="my-3 form-check">
|
||||
<input id="wsIgnoreSecWebsocketAcceptHeader" v-model="monitor.wsIgnoreSecWebsocketAcceptHeader" class="form-check-input" type="checkbox">
|
||||
<i18n-t tag="label" keypath="Ignore Sec-WebSocket-Accept header" class="form-check-label" for="wsIgnoreSecWebsocketAcceptHeader">
|
||||
@ -1331,6 +1339,7 @@ const monitorDefaults = {
|
||||
ignoreTls: false,
|
||||
upsideDown: false,
|
||||
expiryNotification: false,
|
||||
domainExpiryNotification: true,
|
||||
maxredirects: 10,
|
||||
accepted_statuscodes: [ "200-299" ],
|
||||
dns_resolve_type: "A",
|
||||
@ -1387,6 +1396,7 @@ export default {
|
||||
notificationIDList: {},
|
||||
// Do not add default value here, please check init() method
|
||||
},
|
||||
hasDomain: false,
|
||||
acceptedStatusCodeOptions: [],
|
||||
dnsresolvetypeOptions: [],
|
||||
kafkaSaslMechanismOptions: [],
|
||||
@ -1461,6 +1471,16 @@ export default {
|
||||
return null;
|
||||
},
|
||||
|
||||
monitorTypeUrlHost() {
|
||||
const { type, url, hostname, grpcUrl } = this.monitor;
|
||||
return {
|
||||
type,
|
||||
url,
|
||||
hostname,
|
||||
grpcUrl
|
||||
};
|
||||
},
|
||||
|
||||
pageName() {
|
||||
let name = "Add New Monitor";
|
||||
if (this.isClone) {
|
||||
@ -1738,6 +1758,15 @@ message HealthCheckResponse {
|
||||
}
|
||||
},
|
||||
|
||||
"monitorTypeUrlHost"(data) {
|
||||
this.$root.getSocket().emit("checkMointor", data, (res) => {
|
||||
this.hasDomain = !!res?.domain;
|
||||
if (!res?.domain) {
|
||||
this.monitor.domainExpiryNotification = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
"monitor.type"(newType, oldType) {
|
||||
if (oldType && this.monitor.type === "websocket-upgrade") {
|
||||
this.monitor.url = "wss://";
|
||||
|
||||
@ -179,6 +179,10 @@ export default {
|
||||
this.settings.tlsExpiryNotifyDays = [ 7, 14, 21 ];
|
||||
}
|
||||
|
||||
if (this.settings.domainExpiryNotifyDays === undefined) {
|
||||
this.settings.domainExpiryNotifyDays = [ 7, 14, 21 ];
|
||||
}
|
||||
|
||||
if (this.settings.trustProxy === undefined) {
|
||||
this.settings.trustProxy = false;
|
||||
}
|
||||
|
||||
70
test/backend-test/test-domain.js
Normal file
70
test/backend-test/test-domain.js
Normal file
@ -0,0 +1,70 @@
|
||||
process.env.UPTIME_KUMA_HIDE_LOG = [ "info_db", "info_server" ].join(",");
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const DomainExpiry = require("../../server/model/domain_expiry");
|
||||
const mockWebhook = require("../mock-webhook");
|
||||
const TestDB = require("../mock-testdb");
|
||||
const { R } = require("redbean-node");
|
||||
const { Notification } = require("../../server/notification");
|
||||
const { Settings } = require("../../server/settings");
|
||||
const { setSetting } = require("../../server/util-server");
|
||||
|
||||
const testDb = new TestDB();
|
||||
|
||||
test("Domain Expiry", async (t) => {
|
||||
await testDb.create();
|
||||
Notification.init();
|
||||
|
||||
const monHttpCom = {
|
||||
type: "http",
|
||||
url: "https://www.google.com",
|
||||
domainExpiryNotification: true
|
||||
};
|
||||
await t.test("Should get expiry date for .wiki with no A record", async () => {
|
||||
const d = DomainExpiry.createByName("google.wiki");
|
||||
assert.deepEqual(await d.getExpiryDate(), new Date("2026-11-26T23:59:59.000Z"));
|
||||
});
|
||||
await t.test("Should get expiration date for .com from RDAP", async () => {
|
||||
const domain = await DomainExpiry.forMonitor(monHttpCom);
|
||||
const expiryFromRdap = await domain.getExpiryDate(); // from RDAP
|
||||
assert.deepEqual(expiryFromRdap, new Date("2028-09-14T04:00:00.000Z"));
|
||||
});
|
||||
await t.test("Should have expiration date cached in database", async () => {
|
||||
await DomainExpiry.checkExpiry(monHttpCom); // RDAP -> Cache
|
||||
const domain = await DomainExpiry.findByName("google.com");
|
||||
assert(Date.now() - domain.lastCheck < 5 * 1000);
|
||||
});
|
||||
await t.test("Should trigger notify for expiring domain", async () => {
|
||||
await DomainExpiry.findByName("google.com");
|
||||
const hook = {
|
||||
"port": 3010,
|
||||
"url": "capture"
|
||||
};
|
||||
await setSetting("domainExpiryNotifyDays", [ 1, 2, 1500 ], "general");
|
||||
const notif = R.convertToBean("notification", {
|
||||
"config": JSON.stringify({
|
||||
type: "webhook",
|
||||
httpMethod: "post",
|
||||
webhookContentType: "json",
|
||||
webhookURL: `http://127.0.0.1:${hook.port}/${hook.url}`
|
||||
}),
|
||||
"active": 1,
|
||||
"user_id": 1,
|
||||
"name": "Testhook"
|
||||
});
|
||||
const manyDays = 1500;
|
||||
setSetting("domainExpiryNotifyDays", [ 7, 14, manyDays ], "general");
|
||||
const [ notifRet, data ] = await Promise.all([
|
||||
DomainExpiry.sendNotifications(monHttpCom, [ notif ]),
|
||||
mockWebhook(hook.port, hook.url)
|
||||
]);
|
||||
assert.equal(notifRet, manyDays);
|
||||
assert.match(data.msg, /will expire in/);
|
||||
});
|
||||
}).finally(() => {
|
||||
setTimeout(async () => {
|
||||
Settings.stopCacheCleaner();
|
||||
await testDb.destroy();
|
||||
}, 200);
|
||||
});
|
||||
18
test/backend-test/test-util.js
Normal file
18
test/backend-test/test-util.js
Normal file
@ -0,0 +1,18 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
|
||||
const { getDaysRemaining, getDaysBetween } = require("../../server/util-server");
|
||||
|
||||
test("Test getDaysBetween", async (t) => {
|
||||
let days = getDaysBetween(new Date(2025, 9, 7), new Date(2025, 9, 10));
|
||||
assert.strictEqual(days, 3);
|
||||
days = getDaysBetween(new Date(2024, 9, 7), new Date(2025, 9, 10));
|
||||
assert.strictEqual(days, 368);
|
||||
});
|
||||
|
||||
test("Test getDaysRemaining", async (t) => {
|
||||
let days = getDaysRemaining(new Date(2025, 9, 7), new Date(2025, 9, 10));
|
||||
assert.strictEqual(days, 3);
|
||||
days = getDaysRemaining(new Date(2025, 9, 10), new Date(2025, 9, 7));
|
||||
assert.strictEqual(days, -3);
|
||||
});
|
||||
27
test/mock-testdb.js
Normal file
27
test/mock-testdb.js
Normal file
@ -0,0 +1,27 @@
|
||||
const { sync: rimrafSync } = require("rimraf");
|
||||
const Database = require("../server/database");
|
||||
|
||||
class TestDB {
|
||||
dataDir;
|
||||
|
||||
constructor(dir = "./data/test") {
|
||||
this.dataDir = dir;
|
||||
}
|
||||
|
||||
async create() {
|
||||
Database.initDataDir({ "data-dir": this.dataDir });
|
||||
Database.dbConfig = {
|
||||
type: "sqlite"
|
||||
};
|
||||
Database.writeDBConfig(Database.dbConfig);
|
||||
await Database.connect(true);
|
||||
await Database.patch();
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
await Database.close();
|
||||
this.dataDir && rimrafSync(this.dataDir);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TestDB;
|
||||
28
test/mock-webhook.js
Normal file
28
test/mock-webhook.js
Normal file
@ -0,0 +1,28 @@
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
|
||||
/**
|
||||
* @param {number} port Port number
|
||||
* @param {string} url Webhook URL
|
||||
* @param {number} timeout Timeout
|
||||
* @returns {Promise<object>} Webhook data
|
||||
*/
|
||||
async function mockWebhook(port, url, timeout = 2500) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const app = express();
|
||||
const tmo = setTimeout(() => {
|
||||
server.close();
|
||||
reject({ reason: "Timeout" });
|
||||
}, timeout);
|
||||
app.use(bodyParser.json()); // Middleware to parse JSON bodies
|
||||
app.post(`/${url}`, (req, res) => {
|
||||
res.status(200).send("OK");
|
||||
server.close();
|
||||
tmo && clearTimeout(tmo);
|
||||
resolve(req.body);
|
||||
});
|
||||
const server = app.listen(port);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = mockWebhook;
|
||||
Loading…
Reference in New Issue
Block a user