mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-08 22:44:24 +08:00
Merge branch 'louislam:master' into master
This commit is contained in:
@@ -34,6 +34,13 @@ exports.login = async function (username, password) {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that checks if a user is logged in.
|
||||
* @param {string} username The username of the user to check for.
|
||||
* @param {function} callback The callback to call when done, with an error and result parameter.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function myAuthorizer(username, password, callback) {
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
|
@@ -17,7 +17,7 @@ exports.startInterval = () => {
|
||||
res.data.slow = "1000.0.0";
|
||||
}
|
||||
|
||||
if (!await setting("checkUpdate")) {
|
||||
if (await setting("checkUpdate") === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,12 @@ const { io } = require("./server");
|
||||
const { setting } = require("./util-server");
|
||||
const checkVersion = require("./check-version");
|
||||
|
||||
/**
|
||||
* Send a list of notifications to the user.
|
||||
* @param {Socket} socket The socket object that is connected to the client.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
@@ -100,6 +106,12 @@ async function sendProxyList(socket) {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the version information to the client.
|
||||
* @param {Socket} socket The socket object that is connected to the client.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function sendInfo(socket) {
|
||||
socket.emit("info", {
|
||||
version: checkVersion.version,
|
||||
|
@@ -56,6 +56,7 @@ class Database {
|
||||
"patch-add-docker-columns.sql": true,
|
||||
"patch-status-page.sql": true,
|
||||
"patch-proxy.sql": true,
|
||||
"patch-monitor-expiry-notification.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +84,7 @@ class Database {
|
||||
console.log(`Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
static async connect(testMode = false) {
|
||||
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
@@ -113,7 +114,10 @@ class Database {
|
||||
|
||||
// Auto map the model to a bean object
|
||||
R.freeze(true);
|
||||
await R.autoloadModels("./server/model");
|
||||
|
||||
if (autoloadModels) {
|
||||
await R.autoloadModels("./server/model");
|
||||
}
|
||||
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
if (testMode) {
|
||||
@@ -126,10 +130,17 @@ class Database {
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||
|
||||
console.log("SQLite config:");
|
||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||
console.log(await R.getAll("PRAGMA cache_size"));
|
||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||
// 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");
|
||||
|
||||
if (!noLog) {
|
||||
console.log("SQLite config:");
|
||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||
console.log(await R.getAll("PRAGMA cache_size"));
|
||||
console.log("SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||
}
|
||||
}
|
||||
|
||||
static async patch() {
|
||||
|
@@ -6,6 +6,12 @@ let fs = require("fs");
|
||||
|
||||
let ImageDataURI = (() => {
|
||||
|
||||
/**
|
||||
* @param {string} dataURI - A string that is a valid Data URI.
|
||||
* @returns {?Object} An object with properties "imageType" and "dataBase64". The former is the image type, e.g., "png", and the latter is a base64 encoded string of the image's binary data. If it fails to parse, returns null instead of an object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function decode(dataURI) {
|
||||
if (!/data:image\//.test(dataURI)) {
|
||||
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\"");
|
||||
@@ -20,6 +26,13 @@ let ImageDataURI = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data - The image data to be encoded.
|
||||
* @param {String} mediaType - The type of the image, e.g., "image/png".
|
||||
* @returns {String|null} A string representing the base64-encoded version of the given Buffer object or null if an error occurred.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function encode(data, mediaType) {
|
||||
if (!data || !mediaType) {
|
||||
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType ");
|
||||
@@ -33,6 +46,13 @@ let ImageDataURI = (() => {
|
||||
return dataImgBase64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a data URI to a file path.
|
||||
* @param {string} dataURI The Data URI of the image.
|
||||
* @param {string} [filePath] The path where the image will be saved, defaults to "./".
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function outputFile(dataURI, filePath) {
|
||||
filePath = filePath || "./";
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const path = require("path");
|
||||
const Bree = require("bree");
|
||||
const { SHARE_ENV } = require("worker_threads");
|
||||
|
||||
let bree;
|
||||
const jobs = [
|
||||
{
|
||||
name: "clear-old-data",
|
||||
@@ -10,7 +10,7 @@ const jobs = [
|
||||
];
|
||||
|
||||
const initBackgroundJobs = function (args) {
|
||||
const bree = new Bree({
|
||||
bree = new Bree({
|
||||
root: path.resolve("server", "jobs"),
|
||||
jobs,
|
||||
worker: {
|
||||
@@ -26,6 +26,13 @@ const initBackgroundJobs = function (args) {
|
||||
return bree;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initBackgroundJobs
|
||||
const stopBackgroundJobs = function () {
|
||||
if (bree) {
|
||||
bree.stop();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initBackgroundJobs,
|
||||
stopBackgroundJobs
|
||||
};
|
||||
|
@@ -74,6 +74,7 @@ class Monitor extends BeanModel {
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
keyword: this.keyword,
|
||||
expiryNotification: this.isEnabledExpiryNotification(),
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
maxredirects: this.maxredirects,
|
||||
@@ -104,6 +105,10 @@ class Monitor extends BeanModel {
|
||||
return Buffer.from(user + ":" + pass).toString("base64");
|
||||
}
|
||||
|
||||
isEnabledExpiryNotification() {
|
||||
return Boolean(this.expiryNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
@@ -243,7 +248,7 @@ class Monitor extends BeanModel {
|
||||
let tlsInfoObject = checkCertificate(res);
|
||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||
|
||||
if (!this.getIgnoreTls()) {
|
||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||
debug(`[${this.name}] call sendCertNotification`);
|
||||
await this.sendCertNotification(tlsInfoObject);
|
||||
}
|
||||
|
@@ -3,6 +3,20 @@ const { R } = require("redbean-node");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
|
||||
static domainMappingList = { };
|
||||
|
||||
/**
|
||||
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async loadDomainMappingList() {
|
||||
StatusPage.domainMappingList = await R.getAssoc(`
|
||||
SELECT domain, slug
|
||||
FROM status_page, status_page_cname
|
||||
WHERE status_page.id = status_page_cname.status_page_id
|
||||
`);
|
||||
}
|
||||
|
||||
static async sendStatusPageList(io, socket) {
|
||||
let result = {};
|
||||
|
||||
@@ -16,6 +30,57 @@ class StatusPage extends BeanModel {
|
||||
return list;
|
||||
}
|
||||
|
||||
async updateDomainNameList(domainNameList) {
|
||||
|
||||
if (!Array.isArray(domainNameList)) {
|
||||
throw new Error("Invalid array");
|
||||
}
|
||||
|
||||
let trx = await R.begin();
|
||||
|
||||
await trx.exec("DELETE FROM status_page_cname WHERE status_page_id = ?", [
|
||||
this.id,
|
||||
]);
|
||||
|
||||
try {
|
||||
for (let domain of domainNameList) {
|
||||
if (typeof domain !== "string") {
|
||||
throw new Error("Invalid domain");
|
||||
}
|
||||
|
||||
if (domain.trim() === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the domain name is used in another status page, delete it
|
||||
await trx.exec("DELETE FROM status_page_cname WHERE domain = ?", [
|
||||
domain,
|
||||
]);
|
||||
|
||||
let mapping = trx.dispense("status_page_cname");
|
||||
mapping.status_page_id = this.id;
|
||||
mapping.domain = domain;
|
||||
await trx.store(mapping);
|
||||
}
|
||||
await trx.commit();
|
||||
} catch (error) {
|
||||
await trx.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getDomainNameList() {
|
||||
let domainList = [];
|
||||
for (let domain in StatusPage.domainMappingList) {
|
||||
let s = StatusPage.domainMappingList[domain];
|
||||
|
||||
if (this.slug === s) {
|
||||
domainList.push(domain);
|
||||
}
|
||||
}
|
||||
return domainList;
|
||||
}
|
||||
|
||||
async toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
@@ -26,6 +91,7 @@ class StatusPage extends BeanModel {
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
domainNameList: this.getDomainNameList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -68,6 +68,15 @@ function ApiCache() {
|
||||
instances.push(this);
|
||||
this.id = instances.length;
|
||||
|
||||
/**
|
||||
* Logs a message to the console if the `DEBUG` environment variable is set.
|
||||
* @param {string} a - The first argument to log.
|
||||
* @param {string} b - The second argument to log.
|
||||
* @param {string} c - The third argument to log.
|
||||
* @param {string} d - The fourth argument to log, and so on... (optional)
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function debug(a, b, c, d) {
|
||||
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) {
|
||||
return arg !== undefined;
|
||||
@@ -77,6 +86,13 @@ function ApiCache() {
|
||||
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given request and response should be logged.
|
||||
* @param {Object} request The HTTP request object.
|
||||
* @param {Object} response The HTTP response object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function shouldCacheResponse(request, response, toggle) {
|
||||
let opt = globalOptions;
|
||||
let codes = opt.statusCodes;
|
||||
@@ -99,6 +115,12 @@ function ApiCache() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a key to the index.
|
||||
* @param {string} key The key to add.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function addIndexEntries(key, req) {
|
||||
let groupName = req.apicacheGroup;
|
||||
|
||||
@@ -111,6 +133,13 @@ function ApiCache() {
|
||||
index.all.unshift(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new object containing only the whitelisted headers.
|
||||
* @param {Object} headers The original object of header names and values.
|
||||
* @param {Array.<string>} globalOptions.headerWhitelist An array of strings representing the whitelisted header names to keep in the output object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function filterBlacklistedHeaders(headers) {
|
||||
return Object.keys(headers)
|
||||
.filter(function (key) {
|
||||
@@ -122,6 +151,12 @@ function ApiCache() {
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} headers The response headers to filter.
|
||||
* @returns {Object} A new object containing only the whitelisted response headers.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function createCacheObject(status, headers, data, encoding) {
|
||||
return {
|
||||
status: status,
|
||||
@@ -132,6 +167,14 @@ function ApiCache() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a cache value for the given key.
|
||||
* @param {string} key The cache key to set.
|
||||
* @param {*} value The cache value to set.
|
||||
* @param {number} duration How long in milliseconds the cached response should be valid for (defaults to 1 hour).
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function cacheResponse(key, value, duration) {
|
||||
let redis = globalOptions.redisClient;
|
||||
let expireCallback = globalOptions.events.expire;
|
||||
@@ -154,6 +197,12 @@ function ApiCache() {
|
||||
}, Math.min(duration, 2147483647));
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends content to the response.
|
||||
* @param {string|Buffer} content The content to append.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function accumulateContent(res, content) {
|
||||
if (content) {
|
||||
if (typeof content == "string") {
|
||||
@@ -179,6 +228,13 @@ function ApiCache() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monkeypatches the response object to add cache control headers and create a cache object.
|
||||
* @param {Object} req - The request object.
|
||||
* @param {Object} res - The response object.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
|
||||
// monkeypatch res.end to create cache object
|
||||
res._apicache = {
|
||||
@@ -245,6 +301,13 @@ function ApiCache() {
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Request} request
|
||||
* @param {Response} response
|
||||
* @returns {boolean|undefined} true if the request should be cached, false otherwise. If undefined, defaults to true.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
|
||||
if (toggle && !toggle(request, response)) {
|
||||
return next();
|
||||
@@ -365,6 +428,13 @@ function ApiCache() {
|
||||
return this.getIndex();
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a duration string to an integer number of milliseconds.
|
||||
* @param {string} duration - The string to convert.
|
||||
* @returns {number} The converted value in milliseconds, or the defaultDuration if it can't be parsed.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function parseDuration(duration, defaultDuration) {
|
||||
if (typeof duration === "number") {
|
||||
return duration;
|
||||
|
@@ -14,7 +14,7 @@ class Alerta extends NotificationProvider {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Authorization": "Key " + notification.alertaapiKey,
|
||||
"Authorization": "Key " + notification.alertaApiKey,
|
||||
}
|
||||
};
|
||||
let data = {
|
||||
|
@@ -15,12 +15,17 @@ class Mattermost extends NotificationProvider {
|
||||
let mattermostTestData = {
|
||||
username: mattermostUserName,
|
||||
text: msg,
|
||||
}
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData)
|
||||
};
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const mattermostChannel = notification.mattermostchannel.toLowerCase();
|
||||
let mattermostChannel;
|
||||
|
||||
if (typeof notification.mattermostchannel === "string") {
|
||||
mattermostChannel = notification.mattermostchannel.toLowerCase();
|
||||
}
|
||||
|
||||
const mattermostIconEmoji = notification.mattermosticonemo;
|
||||
const mattermostIconUrl = notification.mattermosticonurl;
|
||||
|
||||
|
@@ -154,6 +154,13 @@ class Notification {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new monitor to the database.
|
||||
* @param {number} userID The ID of the user that owns this monitor.
|
||||
* @param {string} name The name of this monitor.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function applyNotificationEveryMonitor(notificationID, userID) {
|
||||
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
|
||||
userID
|
||||
|
@@ -8,6 +8,13 @@ const util = require("./util-server");
|
||||
|
||||
module.exports = Ping;
|
||||
|
||||
/**
|
||||
* @param {string} host - The host to ping
|
||||
* @param {object} [options] - Options for the ping command
|
||||
* @param {array|string} [options.args] - Arguments to pass to the ping command
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function Ping(host, options) {
|
||||
if (!host) {
|
||||
throw new Error("You must specify a host to ping!");
|
||||
@@ -125,6 +132,11 @@ Ping.prototype.send = function (callback) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Function} callback
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
function onEnd() {
|
||||
let stdout = this.stdout._stdout;
|
||||
let stderr = this.stderr._stderr;
|
||||
|
@@ -3,6 +3,7 @@ const HttpProxyAgent = require("http-proxy-agent");
|
||||
const HttpsProxyAgent = require("https-proxy-agent");
|
||||
const SocksProxyAgent = require("socks-proxy-agent");
|
||||
const { debug } = require("../src/util");
|
||||
const server = require("./server");
|
||||
|
||||
class Proxy {
|
||||
|
||||
@@ -144,6 +145,22 @@ class Proxy {
|
||||
httpsAgent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload proxy settings for current monitors
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async reloadProxy() {
|
||||
let updatedList = await R.getAssoc("SELECT id, proxy_id FROM monitor");
|
||||
|
||||
for (let monitorID in server.monitorList) {
|
||||
let monitor = server.monitorList[monitorID];
|
||||
|
||||
if (updatedList[monitorID]) {
|
||||
monitor.proxy_id = updatedList[monitorID].proxy_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -12,9 +12,19 @@ let router = express.Router();
|
||||
let cache = apicache.middleware;
|
||||
let io = server.io;
|
||||
|
||||
router.get("/api/entry-page", async (_, response) => {
|
||||
router.get("/api/entry-page", async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
response.json(server.entryPage);
|
||||
|
||||
let result = { };
|
||||
|
||||
if (request.hostname in StatusPage.domainMappingList) {
|
||||
result.type = "statusPageMatchedDomain";
|
||||
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
|
||||
} else {
|
||||
result.type = "entryPage";
|
||||
result.entryPage = server.entryPage;
|
||||
}
|
||||
response.json(result);
|
||||
});
|
||||
|
||||
router.get("/api/push/:pushToken", async (request, response) => {
|
||||
|
241
server/server.js
241
server/server.js
@@ -48,6 +48,27 @@ debug("Importing 2FA Modules");
|
||||
const notp = require("notp");
|
||||
const base32 = require("thirty-two");
|
||||
|
||||
/**
|
||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||
* @type {UptimeKumaServer}
|
||||
*/
|
||||
class UptimeKumaServer {
|
||||
/**
|
||||
* Main monitor list
|
||||
* @type {{}}
|
||||
*/
|
||||
monitorList = {};
|
||||
entryPage = "dashboard";
|
||||
|
||||
async sendMonitorList(socket) {
|
||||
let list = await getMonitorJSONList(socket.userID);
|
||||
io.to(socket.userID).emit("monitorList", list);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
const server = module.exports = new UptimeKumaServer();
|
||||
|
||||
console.log("Importing this project modules");
|
||||
debug("Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
@@ -65,7 +86,7 @@ debug("Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
debug("Importing Background Jobs");
|
||||
const { initBackgroundJobs } = require("./jobs");
|
||||
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
|
||||
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
|
||||
|
||||
const { basicAuth } = require("./auth");
|
||||
@@ -77,23 +98,22 @@ console.info("Version: " + checkVersion.version);
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
let hostname = process.env.UPTIME_KUMA_HOST || args.host;
|
||||
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
if (!hostname && !FBSD) {
|
||||
hostname = process.env.HOST;
|
||||
}
|
||||
let hostEnv = FBSD ? null : process.env.HOST;
|
||||
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
|
||||
if (hostname) {
|
||||
console.log("Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
|
||||
const port = [args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
|
||||
// SSL
|
||||
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
|
||||
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
|
||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||
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 disableFrameSameOrigin = args["disable-frame-sameorigin"] || !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || false;
|
||||
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||
|
||||
// 2FA / notp verification defaults
|
||||
@@ -115,20 +135,20 @@ if (config.demoMode) {
|
||||
console.log("Creating express and socket.io instance");
|
||||
const app = express();
|
||||
|
||||
let server;
|
||||
let httpServer;
|
||||
|
||||
if (sslKey && sslCert) {
|
||||
console.log("Server Type: HTTPS");
|
||||
server = https.createServer({
|
||||
httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
cert: fs.readFileSync(sslCert)
|
||||
}, app);
|
||||
} else {
|
||||
console.log("Server Type: HTTP");
|
||||
server = http.createServer(app);
|
||||
httpServer = http.createServer(app);
|
||||
}
|
||||
|
||||
const io = new Server(server);
|
||||
const io = new Server(httpServer);
|
||||
module.exports.io = io;
|
||||
|
||||
// Must be after io instantiation
|
||||
@@ -137,7 +157,8 @@ const { statusPageSocketHandler } = require("./socket-handlers/status-page-socke
|
||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
const StatusPage = require("./model/status_page");
|
||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart } = require("./socket-handlers/cloudflared-socket-handler");
|
||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
||||
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -162,12 +183,6 @@ let totalClient = 0;
|
||||
*/
|
||||
let jwtSecret = null;
|
||||
|
||||
/**
|
||||
* Main monitor list
|
||||
* @type {{}}
|
||||
*/
|
||||
let monitorList = {};
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
@@ -190,13 +205,12 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
exports.entryPage = "dashboard";
|
||||
|
||||
(async () => {
|
||||
Database.init(args);
|
||||
await initDatabase(testMode);
|
||||
|
||||
exports.entryPage = await setting("entryPage");
|
||||
await StatusPage.loadDomainMappingList();
|
||||
|
||||
console.log("Adding route");
|
||||
|
||||
@@ -205,8 +219,13 @@ exports.entryPage = "dashboard";
|
||||
// ***************************
|
||||
|
||||
// Entry Page
|
||||
app.get("/", async (_request, response) => {
|
||||
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||
app.get("/", async (request, response) => {
|
||||
debug(`Request Domain: ${request.hostname}`);
|
||||
|
||||
if (request.hostname in StatusPage.domainMappingList) {
|
||||
debug("This is a status page domain");
|
||||
response.send(indexHTML);
|
||||
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||
} else {
|
||||
response.redirect("/dashboard");
|
||||
@@ -600,7 +619,7 @@ exports.entryPage = "dashboard";
|
||||
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
await sendMonitorList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
|
||||
callback({
|
||||
@@ -629,7 +648,7 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
|
||||
// Reset Prometheus labels
|
||||
monitorList[monitor.id]?.prometheus()?.remove();
|
||||
server.monitorList[monitor.id]?.prometheus()?.remove();
|
||||
|
||||
bean.name = monitor.name;
|
||||
bean.type = monitor.type;
|
||||
@@ -646,6 +665,7 @@ exports.entryPage = "dashboard";
|
||||
bean.port = monitor.port;
|
||||
bean.keyword = monitor.keyword;
|
||||
bean.ignoreTls = monitor.ignoreTls;
|
||||
bean.expiryNotification = monitor.expiryNotification;
|
||||
bean.upsideDown = monitor.upsideDown;
|
||||
bean.maxredirects = monitor.maxredirects;
|
||||
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
@@ -665,7 +685,7 @@ exports.entryPage = "dashboard";
|
||||
await restartMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
await sendMonitorList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -685,7 +705,7 @@ exports.entryPage = "dashboard";
|
||||
socket.on("getMonitorList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await sendMonitorList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
@@ -759,7 +779,7 @@ exports.entryPage = "dashboard";
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await startMonitor(socket.userID, monitorID);
|
||||
await sendMonitorList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -778,7 +798,7 @@ exports.entryPage = "dashboard";
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await pauseMonitor(socket.userID, monitorID);
|
||||
await sendMonitorList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -799,9 +819,9 @@ exports.entryPage = "dashboard";
|
||||
|
||||
console.log(`Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
if (monitorID in monitorList) {
|
||||
monitorList[monitorID].stop();
|
||||
delete monitorList[monitorID];
|
||||
if (monitorID in server.monitorList) {
|
||||
server.monitorList[monitorID].stop();
|
||||
delete server.monitorList[monitorID];
|
||||
}
|
||||
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
@@ -814,7 +834,7 @@ exports.entryPage = "dashboard";
|
||||
msg: "Deleted Successfully.",
|
||||
});
|
||||
|
||||
await sendMonitorList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
// Clear heartbeat list on client
|
||||
await sendImportantHeartbeatList(socket, monitorID, true, true);
|
||||
|
||||
@@ -1114,52 +1134,6 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
const proxyBean = await Proxy.save(proxy, proxyID, socket.userID);
|
||||
await sendProxyList(socket);
|
||||
|
||||
if (proxy.applyExisting) {
|
||||
await restartMonitors(socket.userID);
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
id: proxyBean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("deleteProxy", async (proxyID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await Proxy.delete(proxyID, socket.userID);
|
||||
await sendProxyList(socket);
|
||||
await restartMonitors(socket.userID);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("checkApprise", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
@@ -1186,8 +1160,8 @@ exports.entryPage = "dashboard";
|
||||
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
|
||||
if (importHandle == "overwrite") {
|
||||
// Stops every monitor first, so it doesn't execute any heartbeat while importing
|
||||
for (let id in monitorList) {
|
||||
let monitor = monitorList[id];
|
||||
for (let id in server.monitorList) {
|
||||
let monitor = server.monitorList[id];
|
||||
await monitor.stop();
|
||||
}
|
||||
await R.exec("DELETE FROM heartbeat");
|
||||
@@ -1350,7 +1324,7 @@ exports.entryPage = "dashboard";
|
||||
}
|
||||
|
||||
await sendNotificationList(socket);
|
||||
await sendMonitorList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
}
|
||||
|
||||
callback({
|
||||
@@ -1440,6 +1414,7 @@ exports.entryPage = "dashboard";
|
||||
statusPageSocketHandler(socket);
|
||||
cloudflaredSocketHandler(socket);
|
||||
databaseSocketHandler(socket);
|
||||
proxySocketHandler(socket);
|
||||
|
||||
debug("added all socket handlers");
|
||||
|
||||
@@ -1460,12 +1435,12 @@ exports.entryPage = "dashboard";
|
||||
|
||||
console.log("Init the server");
|
||||
|
||||
server.once("error", async (err) => {
|
||||
httpServer.once("error", async (err) => {
|
||||
console.error("Cannot listen: " + err.message);
|
||||
await Database.close();
|
||||
await shutdownFunction();
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
httpServer.listen(port, hostname, () => {
|
||||
if (hostname) {
|
||||
console.log(`Listening on ${hostname}:${port}`);
|
||||
} else {
|
||||
@@ -1486,6 +1461,13 @@ exports.entryPage = "dashboard";
|
||||
|
||||
})();
|
||||
|
||||
/**
|
||||
* Adds or removes notifications from a monitor.
|
||||
* @param {number} monitorID The ID of the monitor to add/remove notifications from.
|
||||
* @param {Array.<number>} notificationIDList An array of IDs for the notifications to add/remove.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
await R.exec("DELETE FROM monitor_notification WHERE monitor_id = ? ", [
|
||||
monitorID,
|
||||
@@ -1501,6 +1483,13 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks if the user owns a monitor with the given ID.
|
||||
* @param {number} monitorID - The ID of the monitor to check ownership for.
|
||||
* @param {number} userID - The ID of the user who is trying to access this data.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function checkOwner(userID, monitorID) {
|
||||
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [
|
||||
monitorID,
|
||||
@@ -1512,17 +1501,15 @@ async function checkOwner(userID, monitorID) {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMonitorList(socket) {
|
||||
let list = await getMonitorJSONList(socket.userID);
|
||||
io.to(socket.userID).emit("monitorList", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to send the heartbeat list of a monitor.
|
||||
* @param {Socket} socket - The socket object that will be used to send the data.
|
||||
*/
|
||||
async function afterLogin(socket, user) {
|
||||
socket.userID = user.id;
|
||||
socket.join(user.id);
|
||||
|
||||
let monitorList = await sendMonitorList(socket);
|
||||
let monitorList = await server.sendMonitorList(socket);
|
||||
sendNotificationList(socket);
|
||||
sendProxyList(socket);
|
||||
|
||||
@@ -1543,6 +1530,13 @@ async function afterLogin(socket, user) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of monitors for the given user.
|
||||
* @param {string} userID - The ID of the user to get monitors for.
|
||||
* @returns {Promise<Object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function getMonitorJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
@@ -1557,6 +1551,11 @@ async function getMonitorJSONList(userID) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database and patch it if necessary.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function initDatabase(testMode = false) {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database");
|
||||
@@ -1591,6 +1590,13 @@ async function initDatabase(testMode = false) {
|
||||
jwtSecret = jwtSecretBean.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a monitor.
|
||||
* @param {string} userID - The ID of the user who owns the monitor.
|
||||
* @param {string} monitorID - The ID of the monitor to resume.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function startMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
|
||||
@@ -1605,11 +1611,11 @@ async function startMonitor(userID, monitorID) {
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
if (monitor.id in monitorList) {
|
||||
monitorList[monitor.id].stop();
|
||||
if (monitor.id in server.monitorList) {
|
||||
server.monitorList[monitor.id].stop();
|
||||
}
|
||||
|
||||
monitorList[monitor.id] = monitor;
|
||||
server.monitorList[monitor.id] = monitor;
|
||||
monitor.start(io);
|
||||
}
|
||||
|
||||
@@ -1617,19 +1623,13 @@ async function restartMonitor(userID, monitorID) {
|
||||
return await startMonitor(userID, monitorID);
|
||||
}
|
||||
|
||||
async function restartMonitors(userID) {
|
||||
// Fetch all active monitors for user
|
||||
const monitors = await R.getAll("SELECT id FROM monitor WHERE active = 1 AND user_id = ?", [userID]);
|
||||
|
||||
for (const monitor of monitors) {
|
||||
// Start updated monitor
|
||||
await startMonitor(userID, monitor.id);
|
||||
|
||||
// Give some delays, so all monitors won't make request at the same moment when just start the server.
|
||||
await sleep(getRandomInt(300, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a monitor.
|
||||
* @param {string} userID - The ID of the user who owns the monitor.
|
||||
* @param {string} monitorID - The ID of the monitor to pause.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function pauseMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
|
||||
@@ -1640,8 +1640,8 @@ async function pauseMonitor(userID, monitorID) {
|
||||
userID,
|
||||
]);
|
||||
|
||||
if (monitorID in monitorList) {
|
||||
monitorList[monitorID].stop();
|
||||
if (monitorID in server.monitorList) {
|
||||
server.monitorList[monitorID].stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1652,7 +1652,7 @@ async function startMonitors() {
|
||||
let list = await R.find("monitor", " active = 1 ");
|
||||
|
||||
for (let monitor of list) {
|
||||
monitorList[monitor.id] = monitor;
|
||||
server.monitorList[monitor.id] = monitor;
|
||||
}
|
||||
|
||||
for (let monitor of list) {
|
||||
@@ -1662,24 +1662,33 @@ async function startMonitors() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all monitors and closes the database connection.
|
||||
* @param {string} signal The signal that triggered this function to be called.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async function shutdownFunction(signal) {
|
||||
console.log("Shutdown requested");
|
||||
console.log("Called signal: " + signal);
|
||||
|
||||
console.log("Stopping all monitors");
|
||||
for (let id in monitorList) {
|
||||
let monitor = monitorList[id];
|
||||
for (let id in server.monitorList) {
|
||||
let monitor = server.monitorList[id];
|
||||
monitor.stop();
|
||||
}
|
||||
await sleep(2000);
|
||||
await Database.close();
|
||||
|
||||
stopBackgroundJobs();
|
||||
await cloudflaredStop();
|
||||
}
|
||||
|
||||
function finalFunction() {
|
||||
console.log("Graceful shutdown successful!");
|
||||
}
|
||||
|
||||
gracefulShutdown(server, {
|
||||
gracefulShutdown(httpServer, {
|
||||
signals: "SIGINT SIGTERM",
|
||||
timeout: 30000, // timeout: 30 secs
|
||||
development: false, // not in dev mode
|
||||
|
@@ -83,3 +83,8 @@ module.exports.autoStart = async (token) => {
|
||||
cloudflared.start();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.stop = async () => {
|
||||
console.log("Stop cloudflared");
|
||||
cloudflared.stop();
|
||||
};
|
||||
|
53
server/socket-handlers/proxy-socket-handler.js
Normal file
53
server/socket-handlers/proxy-socket-handler.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const { Proxy } = require("../proxy");
|
||||
const { sendProxyList } = require("../client");
|
||||
const server = require("../server");
|
||||
|
||||
module.exports.proxySocketHandler = (socket) => {
|
||||
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
const proxyBean = await Proxy.save(proxy, proxyID, socket.userID);
|
||||
await sendProxyList(socket);
|
||||
|
||||
if (proxy.applyExisting) {
|
||||
await Proxy.reloadProxy();
|
||||
await server.sendMonitorList(socket);
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
id: proxyBean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("deleteProxy", async (proxyID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await Proxy.delete(proxyID, socket.userID);
|
||||
await sendProxyList(socket);
|
||||
await Proxy.reloadProxy();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@@ -85,15 +85,35 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getStatusPage", async (slug, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (!statusPage) {
|
||||
throw new Error("No slug?");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
config: await statusPage.toJSON(),
|
||||
});
|
||||
} catch (error) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Save Status Page
|
||||
// imgDataUrl Only Accept PNG!
|
||||
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||
|
||||
try {
|
||||
checkSlug(config.slug);
|
||||
|
||||
checkLogin(socket);
|
||||
apicache.clear();
|
||||
|
||||
// Save Config
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
@@ -104,6 +124,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
throw new Error("No slug?");
|
||||
}
|
||||
|
||||
checkSlug(config.slug);
|
||||
|
||||
const header = "data:image/png;base64,";
|
||||
|
||||
// Check logo format
|
||||
@@ -137,6 +159,9 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
await R.store(statusPage);
|
||||
|
||||
await statusPage.updateDomainNameList(config.domainNameList);
|
||||
await StatusPage.loadDomainMappingList();
|
||||
|
||||
// Save Public Group List
|
||||
const groupIDList = [];
|
||||
let groupOrder = 1;
|
||||
@@ -193,6 +218,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
await setSetting("entryPage", server.entryPage, "general");
|
||||
}
|
||||
|
||||
apicache.clear();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
publicGroupList,
|
||||
|
Reference in New Issue
Block a user