uptime-kuma/server/uptime-kuma-server.js
SGprooo 1d9a28e9ab
feat: Tailscale ping monitor (#3178)
* Add boilerplate for tailscale ping

* tailscale initial commit draft

* Refactor TailscalePing & better error handling

Split check function into two methods and added async/await syntax for readability/modularity
Switched to promise-based error handling (takes into account different types of error such as "Execution error", "Error in output", "no matching peer", and "is local Tailscale IP") and throws them as JavaScript errors.

* Minor update

* minor update (again)

* Update server/monitor-types/tailscale-ping.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Update server/monitor-types/tailscale-ping.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Update server/monitor-types/tailscale-ping.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Update server/monitor-types/tailscale-ping.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* timeout revision

* JSDoc

* Removed long explainers

* eslint tailscale-ping.js --fix

* reran eslint

* Fix: Use hostname rather than url

* Fixed NaN on monitor interval

now interval value is correctly passed to runTailscalePing

* Add warning message

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2023-07-19 20:58:21 +08:00

350 lines
10 KiB
JavaScript

const express = require("express");
const https = require("https");
const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log } = require("../src/util");
const Database = require("./database");
const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
// 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.
* @type {UptimeKumaServer}
*/
class UptimeKumaServer {
/**
*
* @type {UptimeKumaServer}
*/
static instance = null;
/**
* Main monitor list
* @type {{}}
*/
monitorList = {};
/**
* Main maintenance list
* @type {{}}
*/
maintenanceList = {};
entryPage = "dashboard";
app = undefined;
httpServer = undefined;
io = undefined;
/**
* Cache Index HTML
* @type {string}
*/
indexHTML = "";
/**
*
* @type {{}}
*/
static monitorTypeList = {
};
/**
* Use for decode the auth object
* @type {null}
*/
jwtSecret = null;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
}
return UptimeKumaServer.instance;
}
constructor(args) {
// 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();
if (sslKey && sslCert) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
key: fs.readFileSync(sslKey),
cert: fs.readFileSync(sslCert),
passphrase: sslKeyPassphrase,
}, this.app);
} else {
log.info("server", "Server Type: HTTP");
this.httpServer = http.createServer(this.app);
}
try {
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}
// 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();
dayjs.tz.setDefault(process.env.TZ);
log.debug("DEBUG", "Timezone: " + process.env.TZ);
log.debug("DEBUG", "Current Time: " + dayjs.tz().format());
await this.loadMaintenanceList();
}
/**
* Send list of monitors to client
* @param {Socket} socket
* @returns {Object} List of monitors
*/
async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID);
this.io.to(socket.userID).emit("monitorList", list);
return list;
}
/**
* 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 getMonitorJSONList(userID) {
let result = {};
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
}
return result;
}
/**
* Send maintenance list to client
* @param {Socket} socket Socket.io instance to send to
* @returns {Object}
*/
async sendMaintenanceList(socket) {
return await this.sendMaintenanceListByUserID(socket.userID);
}
/**
* Send list of maintenances to user
* @param {number} userID
* @returns {Object}
*/
async sendMaintenanceListByUserID(userID) {
let list = await this.getMaintenanceJSONList(userID);
this.io.to(userID).emit("maintenanceList", list);
return list;
}
/**
* Get a list of maintenances for the given user.
* @param {string} userID - The ID of the user to get maintenances for.
* @returns {Promise<Object>} A promise that resolves to an object with maintenance IDs as keys and maintenances objects as values.
*/
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", [
]);
for (let maintenance of maintenanceList) {
this.maintenanceList[maintenance.id] = maintenance;
maintenance.run(this);
}
}
getMaintenance(maintenanceID) {
if (this.maintenanceList[maintenanceID]) {
return this.maintenanceList[maintenanceID];
}
return null;
}
/**
* Write error to log file
* @param {any} error The error to write
* @param {boolean} outputToConsole Should the error also be output to console?
*/
static errorLog(error, outputToConsole = true) {
const errorLogStream = fs.createWriteStream(Database.dataDir + "/error.log", {
flags: "a"
});
errorLogStream.on("error", () => {
log.info("", "Cannot write to error.log");
});
if (errorLogStream) {
const dateTime = R.isoDateTime();
errorLogStream.write(`[${dateTime}] ` + util.format(error) + "\n");
if (outputToConsole) {
console.error(error);
}
}
errorLogStream.end();
}
/**
* Get the IP of the client connected to the socket
* @param {Socket} socket
* @returns {string}
*/
async getClientIP(socket) {
let clientIP = socket.client.conn.remoteAddress;
if (clientIP === undefined) {
clientIP = "";
}
if (await Settings.get("trustProxy")) {
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^::ffff:/, "");
} else {
return clientIP.replace(/^::ffff:/, "");
}
}
/**
* Attempt to get the current server timezone
* If this fails, fall back to environment variables and then make a
* guess.
* @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");
// 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";
}
}
/**
* Get the current offset
* @returns {string}
*/
getTimezoneOffset() {
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);
}
/** Stop the server */
async stop() {
}
}
module.exports = {
UptimeKumaServer
};
// 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");