mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-08 08:28:48 +08:00
Merge branch 'master' of https://github.com/louislam/uptime-kuma into feat/add-auth-header-to-webhook-notification-#1919
This commit is contained in:
54
server/cacheable-dns-http-agent.js
Normal file
54
server/cacheable-dns-http-agent.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const CacheableLookup = require("cacheable-lookup");
|
||||
|
||||
class CacheableDnsHttpAgent {
|
||||
|
||||
static cacheable = new CacheableLookup();
|
||||
|
||||
static httpAgentList = {};
|
||||
static httpsAgentList = {};
|
||||
|
||||
/**
|
||||
* Register cacheable to global agents
|
||||
*/
|
||||
static registerGlobalAgent() {
|
||||
this.cacheable.install(http.globalAgent);
|
||||
this.cacheable.install(https.globalAgent);
|
||||
}
|
||||
|
||||
static install(agent) {
|
||||
this.cacheable.install(agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var {https.AgentOptions} agentOptions
|
||||
* @return {https.Agent}
|
||||
*/
|
||||
static getHttpsAgent(agentOptions) {
|
||||
let key = JSON.stringify(agentOptions);
|
||||
if (!(key in this.httpsAgentList)) {
|
||||
this.httpsAgentList[key] = new https.Agent(agentOptions);
|
||||
this.cacheable.install(this.httpsAgentList[key]);
|
||||
}
|
||||
return this.httpsAgentList[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var {http.AgentOptions} agentOptions
|
||||
* @return {https.Agents}
|
||||
*/
|
||||
static getHttpAgent(agentOptions) {
|
||||
let key = JSON.stringify(agentOptions);
|
||||
if (!(key in this.httpAgentList)) {
|
||||
this.httpAgentList[key] = new http.Agent(agentOptions);
|
||||
this.cacheable.install(this.httpAgentList[key]);
|
||||
}
|
||||
return this.httpAgentList[key];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CacheableDnsHttpAgent,
|
||||
};
|
@@ -125,10 +125,35 @@ async function sendInfo(socket) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send list of docker hosts to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
*/
|
||||
async function sendDockerHostList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let result = [];
|
||||
let list = await R.find("docker_host", " user_id = ? ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
result.push(bean.toJSON());
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("dockerHostList", result);
|
||||
|
||||
timeLogger.print("Send Docker Host List");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendNotificationList,
|
||||
sendImportantHeartbeatList,
|
||||
sendHeartbeatList,
|
||||
sendProxyList,
|
||||
sendInfo,
|
||||
sendDockerHostList
|
||||
};
|
||||
|
@@ -53,6 +53,7 @@ class Database {
|
||||
"patch-2fa-invalidate-used-token.sql": true,
|
||||
"patch-notification_sent_history.sql": true,
|
||||
"patch-monitor-basic-auth.sql": true,
|
||||
"patch-add-docker-columns.sql": true,
|
||||
"patch-status-page.sql": true,
|
||||
"patch-proxy.sql": true,
|
||||
"patch-monitor-expiry-notification.sql": true,
|
||||
@@ -61,6 +62,8 @@ class Database {
|
||||
"patch-add-clickable-status-page-link.sql": true,
|
||||
"patch-add-sqlserver-monitor.sql": true,
|
||||
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
|
||||
"patch-add-radius-monitor.sql": true,
|
||||
"patch-monitor-add-resend-interval.sql": true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -147,6 +150,9 @@ class Database {
|
||||
await R.exec("PRAGMA cache_size = -12000");
|
||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||
|
||||
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
|
||||
await R.exec("PRAGMA busy_timeout = 5000");
|
||||
|
||||
// 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
|
||||
|
118
server/docker.js
Normal file
118
server/docker.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const axios = require("axios");
|
||||
const { R } = require("redbean-node");
|
||||
const version = require("../package.json").version;
|
||||
const https = require("https");
|
||||
|
||||
class DockerHost {
|
||||
/**
|
||||
* Save a docker host
|
||||
* @param {Object} dockerHost Docker host to save
|
||||
* @param {?number} dockerHostID ID of the docker host to update
|
||||
* @param {number} userID ID of the user who adds the docker host
|
||||
* @returns {Promise<Bean>}
|
||||
*/
|
||||
static async save(dockerHost, dockerHostID, userID) {
|
||||
let bean;
|
||||
|
||||
if (dockerHostID) {
|
||||
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("docker host not found");
|
||||
}
|
||||
|
||||
} else {
|
||||
bean = R.dispense("docker_host");
|
||||
}
|
||||
|
||||
bean.user_id = userID;
|
||||
bean.docker_daemon = dockerHost.dockerDaemon;
|
||||
bean.docker_type = dockerHost.dockerType;
|
||||
bean.name = dockerHost.name;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Docker host
|
||||
* @param {number} dockerHostID ID of the Docker host to delete
|
||||
* @param {number} userID ID of the user who created the Docker host
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(dockerHostID, userID) {
|
||||
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("docker host not found");
|
||||
}
|
||||
|
||||
// Delete removed proxy from monitors if exists
|
||||
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);
|
||||
|
||||
await R.trash(bean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the amount of containers on the Docker host
|
||||
* @param {Object} dockerHost Docker host to check for
|
||||
* @returns {number} Total amount of containers on the host
|
||||
*/
|
||||
static async testDockerHost(dockerHost) {
|
||||
const options = {
|
||||
url: "/containers/json?all=true",
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
};
|
||||
|
||||
if (dockerHost.dockerType === "socket") {
|
||||
options.socketPath = dockerHost.dockerDaemon;
|
||||
} else if (dockerHost.dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||
}
|
||||
|
||||
let res = await axios.request(options);
|
||||
|
||||
if (Array.isArray(res.data)) {
|
||||
|
||||
if (res.data.length > 1) {
|
||||
|
||||
if ("ImageID" in res.data[0]) {
|
||||
return res.data.length;
|
||||
} else {
|
||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||
}
|
||||
|
||||
} else {
|
||||
return res.data.length;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Since axios 0.27.X, it does not accept `tcp://` protocol.
|
||||
* Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
|
||||
*/
|
||||
static patchDockerURL(url) {
|
||||
if (typeof url === "string") {
|
||||
// Replace the first occurrence only with g
|
||||
return url.replace(/tcp:\/\//g, "http://");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DockerHost,
|
||||
};
|
19
server/model/docker_host.js
Normal file
19
server/model/docker_host.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class DockerHost extends BeanModel {
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
userID: this.user_id,
|
||||
dockerDaemon: this.docker_daemon,
|
||||
dockerType: this.docker_type,
|
||||
name: this.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DockerHost;
|
@@ -7,7 +7,7 @@ dayjs.extend(timezone);
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mqttAsync, setSetting, httpNtlm } = require("../util-server");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mqttAsync, setSetting, httpNtlm, radius } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
@@ -16,6 +16,8 @@ const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@@ -78,6 +80,7 @@ class Monitor extends BeanModel {
|
||||
type: this.type,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
resendInterval: this.resendInterval,
|
||||
keyword: this.keyword,
|
||||
expiryNotification: this.isEnabledExpiryNotification(),
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
@@ -87,6 +90,9 @@ class Monitor extends BeanModel {
|
||||
dns_resolve_type: this.dns_resolve_type,
|
||||
dns_resolve_server: this.dns_resolve_server,
|
||||
dns_last_result: this.dns_last_result,
|
||||
pushToken: this.pushToken,
|
||||
docker_container: this.docker_container,
|
||||
docker_host: this.docker_host,
|
||||
proxyId: this.proxy_id,
|
||||
notificationIDList,
|
||||
tags: tags,
|
||||
@@ -99,6 +105,11 @@ class Monitor extends BeanModel {
|
||||
authMethod: this.authMethod,
|
||||
authWorkstation: this.authWorkstation,
|
||||
authDomain: this.authDomain,
|
||||
radiusUsername: this.radiusUsername,
|
||||
radiusPassword: this.radiusPassword,
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
radiusSecret: this.radiusSecret,
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@@ -205,6 +216,7 @@ class Monitor extends BeanModel {
|
||||
bean.monitor_id = this.id;
|
||||
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||
bean.status = DOWN;
|
||||
bean.downCount = previousBeat?.downCount || 0;
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
bean.status = flipStatus(bean.status);
|
||||
@@ -440,10 +452,13 @@ class Monitor extends BeanModel {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
maxCachedSessions: 0,
|
||||
}),
|
||||
maxRedirects: this.maxredirects,
|
||||
validateStatus: (status) => {
|
||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||
@@ -464,6 +479,35 @@ class Monitor extends BeanModel {
|
||||
} else {
|
||||
throw new Error("Server not found on Steam");
|
||||
}
|
||||
} else if (this.type === "docker") {
|
||||
log.debug(`[${this.name}] Prepare Options for Axios`);
|
||||
|
||||
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||
|
||||
const options = {
|
||||
url: `/containers/${this.docker_container}/json`,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||
}),
|
||||
};
|
||||
|
||||
if (dockerHost._dockerType === "socket") {
|
||||
options.socketPath = dockerHost._dockerDaemon;
|
||||
} else if (dockerHost._dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||
}
|
||||
|
||||
log.debug(`[${this.name}] Axios Request`);
|
||||
let res = await axios.request(options);
|
||||
if (res.data.State.Running) {
|
||||
bean.status = UP;
|
||||
bean.msg = "";
|
||||
}
|
||||
} else if (this.type === "mqtt") {
|
||||
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
|
||||
port: this.port,
|
||||
@@ -480,6 +524,38 @@ class Monitor extends BeanModel {
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "postgres") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "radius") {
|
||||
let startTime = dayjs().valueOf();
|
||||
try {
|
||||
const resp = await radius(
|
||||
this.hostname,
|
||||
this.radiusUsername,
|
||||
this.radiusPassword,
|
||||
this.radiusCalledStationId,
|
||||
this.radiusCallingStationId,
|
||||
this.radiusSecret
|
||||
);
|
||||
if (resp.code) {
|
||||
bean.msg = resp.code;
|
||||
}
|
||||
bean.status = UP;
|
||||
} catch (error) {
|
||||
bean.status = DOWN;
|
||||
if (error.response?.code) {
|
||||
bean.msg = error.response.code;
|
||||
} else {
|
||||
bean.msg = error.message;
|
||||
}
|
||||
}
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
@@ -521,12 +597,27 @@ class Monitor extends BeanModel {
|
||||
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
|
||||
// Reset down count
|
||||
bean.downCount = 0;
|
||||
|
||||
// Clear Status Page Cache
|
||||
log.debug("monitor", `[${this.name}] apicache clear`);
|
||||
apicache.clear();
|
||||
|
||||
} else {
|
||||
bean.important = false;
|
||||
|
||||
if (bean.status === DOWN && this.resendInterval > 0) {
|
||||
++bean.downCount;
|
||||
if (bean.downCount >= this.resendInterval) {
|
||||
// Send notification again, because we are still DOWN
|
||||
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||
|
||||
// Reset down count
|
||||
bean.downCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bean.status === UP) {
|
||||
@@ -537,7 +628,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Pending: ${bean.msg} | Max retries: ${this.maxretries} | Retry: ${retries} | Retry Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
} else {
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type}`);
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||
}
|
||||
|
||||
log.debug("monitor", `[${this.name}] Send to socket`);
|
||||
|
@@ -12,9 +12,7 @@ const { default: axios } = require("axios");
|
||||
|
||||
// bark is an APN bridge that sends notifications to Apple devices.
|
||||
|
||||
const barkNotificationGroup = "UptimeKuma";
|
||||
const barkNotificationAvatar = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
|
||||
const barkNotificationSound = "telegraph";
|
||||
const successMessage = "Successes!";
|
||||
|
||||
class Bark extends NotificationProvider {
|
||||
@@ -30,17 +28,17 @@ class Bark extends NotificationProvider {
|
||||
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
let title = "UptimeKuma Monitor Up";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||
let title = "UptimeKuma Monitor Down";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||
}
|
||||
|
||||
if (msg != null) {
|
||||
let title = "UptimeKuma Message";
|
||||
return await this.postNotification(title, msg, barkEndpoint);
|
||||
return await this.postNotification(notification, title, msg, barkEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,13 +48,23 @@ class Bark extends NotificationProvider {
|
||||
* @param {string} postUrl URL to append parameters to
|
||||
* @returns {string}
|
||||
*/
|
||||
appendAdditionalParameters(postUrl) {
|
||||
// grouping all our notifications
|
||||
postUrl += "?group=" + barkNotificationGroup;
|
||||
appendAdditionalParameters(notification, postUrl) {
|
||||
// set icon to uptime kuma icon, 11kb should be fine
|
||||
postUrl += "&icon=" + barkNotificationAvatar;
|
||||
postUrl += "?icon=" + barkNotificationAvatar;
|
||||
// grouping all our notifications
|
||||
if (notification.barkGroup != null) {
|
||||
postUrl += "&group=" + notification.barkGroup;
|
||||
} else {
|
||||
// default name
|
||||
postUrl += "&group=" + "UptimeKuma";
|
||||
}
|
||||
// picked a sound, this should follow system's mute status when arrival
|
||||
postUrl += "&sound=" + barkNotificationSound;
|
||||
if (notification.barkSound != null) {
|
||||
postUrl += "&sound=" + notification.barkSound;
|
||||
} else {
|
||||
// default sound
|
||||
postUrl += "&sound=" + "telegraph";
|
||||
}
|
||||
return postUrl;
|
||||
}
|
||||
|
||||
@@ -81,12 +89,12 @@ class Bark extends NotificationProvider {
|
||||
* @param {string} endpoint Endpoint to send request to
|
||||
* @returns {string}
|
||||
*/
|
||||
async postNotification(title, subtitle, endpoint) {
|
||||
async postNotification(notification, title, subtitle, endpoint) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
subtitle = encodeURIComponent(subtitle);
|
||||
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||
postUrl = this.appendAdditionalParameters(postUrl);
|
||||
postUrl = this.appendAdditionalParameters(notification, postUrl);
|
||||
let result = await axios.get(postUrl);
|
||||
this.checkResult(result);
|
||||
if (result.statusText != null) {
|
||||
|
24
server/notification-providers/freemobile.js
Normal file
24
server/notification-providers/freemobile.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class FreeMobile extends NotificationProvider {
|
||||
|
||||
name = "FreeMobile";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
|
||||
"user": notification.freemobileUser,
|
||||
"pass": notification.freemobilePass,
|
||||
});
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FreeMobile;
|
35
server/notification-providers/goalert.js
Normal file
35
server/notification-providers/goalert.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP } = require("../../src/util");
|
||||
|
||||
class GoAlert extends NotificationProvider {
|
||||
|
||||
name = "GoAlert";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let closeAction = "close";
|
||||
let data = {
|
||||
summary: msg,
|
||||
};
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
data["action"] = closeAction;
|
||||
}
|
||||
let headers = {
|
||||
"Content-Type": "multipart/form-data",
|
||||
};
|
||||
let config = {
|
||||
headers: headers
|
||||
};
|
||||
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data) ? error.response.data : "Error without response";
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoAlert;
|
38
server/notification-providers/home-assistant.js
Normal file
38
server/notification-providers/home-assistant.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
const defaultNotificationService = "notify";
|
||||
|
||||
class HomeAssistant extends NotificationProvider {
|
||||
name = "HomeAssistant";
|
||||
|
||||
async send(notification, message, monitor = null, heartbeat = null) {
|
||||
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
|
||||
{
|
||||
title: "Uptime Kuma",
|
||||
message,
|
||||
...(notificationService !== "persistent_notification" && { data: {
|
||||
name: monitor?.name,
|
||||
status: heartbeat?.status,
|
||||
} }),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${notification.longLivedAccessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return "Sent Successfully.";
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HomeAssistant;
|
43
server/notification-providers/linenotify.js
Normal file
43
server/notification-providers/linenotify.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const qs = require("qs");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class LineNotify extends NotificationProvider {
|
||||
|
||||
name = "LineNotify";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
let lineAPIUrl = "https://notify-api.line.me/api/notify";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Authorization": "Bearer " + notification.lineNotifyAccessToken
|
||||
}
|
||||
};
|
||||
if (heartbeatJSON == null) {
|
||||
let testMessage = {
|
||||
"message": msg,
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"message": "\n[🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"message": "\n[✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"]
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LineNotify;
|
@@ -8,12 +8,19 @@ class Ntfy extends NotificationProvider {
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
await axios.post(`${notification.ntfyserverurl}`, {
|
||||
let headers = {};
|
||||
if (notification.ntfyusername.length > 0) {
|
||||
headers = {
|
||||
"Authorization": "Basic " + Buffer.from(notification.ntfyusername + ":" + notification.ntfypassword).toString("base64"),
|
||||
};
|
||||
}
|
||||
let data = {
|
||||
"topic": notification.ntfytopic,
|
||||
"message": msg,
|
||||
"priority": notification.ntfyPriority || 4,
|
||||
"title": "Uptime-Kuma",
|
||||
});
|
||||
};
|
||||
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
|
||||
|
||||
return okMsg;
|
||||
|
||||
|
@@ -10,7 +10,7 @@ class Octopush extends NotificationProvider {
|
||||
|
||||
try {
|
||||
// Default - V2
|
||||
if (notification.octopushVersion === 2 || !notification.octopushVersion) {
|
||||
if (notification.octopushVersion === "2" || !notification.octopushVersion) {
|
||||
let config = {
|
||||
headers: {
|
||||
"api-key": notification.octopushAPIKey,
|
||||
@@ -31,7 +31,7 @@ class Octopush extends NotificationProvider {
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
|
||||
} else if (notification.octopushVersion === 1) {
|
||||
} else if (notification.octopushVersion === "1") {
|
||||
let data = {
|
||||
"user_login": notification.octopushDMLogin,
|
||||
"api_key": notification.octopushDMAPIKey,
|
||||
@@ -49,7 +49,15 @@ class Octopush extends NotificationProvider {
|
||||
},
|
||||
params: data
|
||||
};
|
||||
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
|
||||
|
||||
// V1 API returns 200 even on error so we must check
|
||||
// response data
|
||||
let response = await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
|
||||
if ("error_code" in response.data) {
|
||||
if (response.data.error_code !== "000") {
|
||||
this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unknown Octopush version!");
|
||||
}
|
||||
|
36
server/notification-providers/serverchan.js
Normal file
36
server/notification-providers/serverchan.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class ServerChan extends NotificationProvider {
|
||||
|
||||
name = "ServerChan";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
|
||||
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||
"desp": msg,
|
||||
});
|
||||
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
checkStatus(heartbeatJSON, monitorJSON) {
|
||||
let title = "UptimeKuma Message";
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
title = "UptimeKuma Monitor Up " + monitorJSON["name"];
|
||||
}
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
|
||||
title = "UptimeKuma Monitor Down " + monitorJSON["name"];
|
||||
}
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ServerChan;
|
25
server/notification-providers/smsmanager.js
Normal file
25
server/notification-providers/smsmanager.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSManager extends NotificationProvider {
|
||||
|
||||
name = "SMSManager";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
let data = {
|
||||
apikey: notification.smsmanagerApiKey,
|
||||
endpoint: "https://http-api.smsmanager.cz/Send",
|
||||
message: msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
to: notification.numbers,
|
||||
messageType: notification.messageType,
|
||||
};
|
||||
await axios.get(`${data.endpoint}?apikey=${data.apikey}&message=${data.message}&number=${data.to}&gateway=${data.messageType}`);
|
||||
return "SMS sent sucessfully.";
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SMSManager;
|
76
server/notification-providers/squadcast.js
Normal file
76
server/notification-providers/squadcast.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN } = require("../../src/util");
|
||||
|
||||
class Squadcast extends NotificationProvider {
|
||||
|
||||
name = "squadcast";
|
||||
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
|
||||
let config = {};
|
||||
let data = {
|
||||
message: msg,
|
||||
description: "",
|
||||
tags: {},
|
||||
heartbeat: heartbeatJSON,
|
||||
source: "uptime-kuma"
|
||||
};
|
||||
|
||||
if (heartbeatJSON !== null) {
|
||||
data.description = heartbeatJSON["msg"];
|
||||
data.event_id = heartbeatJSON["monitorID"];
|
||||
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
data.message = `${monitorJSON["name"]} is DOWN`;
|
||||
data.status = "trigger";
|
||||
} else {
|
||||
data.message = `${monitorJSON["name"]} is UP`;
|
||||
data.status = "resolve";
|
||||
}
|
||||
|
||||
let address;
|
||||
switch (monitorJSON["type"]) {
|
||||
case "ping":
|
||||
address = monitorJSON["hostname"];
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
address += ":" + monitorJSON["port"];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
address = monitorJSON["url"];
|
||||
break;
|
||||
}
|
||||
|
||||
data.tags["AlertAddress"] = address;
|
||||
|
||||
monitorJSON["tags"].forEach(tag => {
|
||||
data.tags[tag["name"]] = {
|
||||
value: tag["value"]
|
||||
};
|
||||
if (tag["color"] !== null) {
|
||||
data.tags[tag["name"]]["color"] = tag["color"];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await axios.post(notification.squadcastWebhookURL, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Squadcast;
|
@@ -63,7 +63,7 @@ class Teams extends NotificationProvider {
|
||||
});
|
||||
}
|
||||
|
||||
if (monitorUrl) {
|
||||
if (monitorUrl && monitorUrl !== "https://") {
|
||||
facts.push({
|
||||
name: "URL",
|
||||
value: monitorUrl,
|
||||
@@ -127,13 +127,17 @@ class Teams extends NotificationProvider {
|
||||
|
||||
let url;
|
||||
|
||||
if (monitorJSON["type"] === "port") {
|
||||
url = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
url += ":" + monitorJSON["port"];
|
||||
}
|
||||
} else {
|
||||
url = monitorJSON["url"];
|
||||
switch (monitorJSON["type"]) {
|
||||
case "http":
|
||||
case "keywork":
|
||||
url = monitorJSON["url"];
|
||||
break;
|
||||
case "docker":
|
||||
url = monitorJSON["docker_host"];
|
||||
break;
|
||||
default:
|
||||
url = monitorJSON["hostname"];
|
||||
break;
|
||||
}
|
||||
|
||||
const payload = this._notificationPayloadFactory({
|
||||
|
@@ -9,10 +9,13 @@ const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
const Discord = require("./notification-providers/discord");
|
||||
const Feishu = require("./notification-providers/feishu");
|
||||
const FreeMobile = require("./notification-providers/freemobile");
|
||||
const GoogleChat = require("./notification-providers/google-chat");
|
||||
const Gorush = require("./notification-providers/gorush");
|
||||
const Gotify = require("./notification-providers/gotify");
|
||||
const HomeAssistant = require("./notification-providers/home-assistant");
|
||||
const Line = require("./notification-providers/line");
|
||||
const LineNotify = require("./notification-providers/linenotify");
|
||||
const LunaSea = require("./notification-providers/lunasea");
|
||||
const Matrix = require("./notification-providers/matrix");
|
||||
const Mattermost = require("./notification-providers/mattermost");
|
||||
@@ -30,12 +33,16 @@ const SerwerSMS = require("./notification-providers/serwersms");
|
||||
const Signal = require("./notification-providers/signal");
|
||||
const Slack = require("./notification-providers/slack");
|
||||
const SMTP = require("./notification-providers/smtp");
|
||||
const Squadcast = require("./notification-providers/squadcast");
|
||||
const Stackfield = require("./notification-providers/stackfield");
|
||||
const Teams = require("./notification-providers/teams");
|
||||
const TechulusPush = require("./notification-providers/techulus-push");
|
||||
const Telegram = require("./notification-providers/telegram");
|
||||
const Webhook = require("./notification-providers/webhook");
|
||||
const WeCom = require("./notification-providers/wecom");
|
||||
const GoAlert = require("./notification-providers/goalert");
|
||||
const SMSManager = require("./notification-providers/smsmanager");
|
||||
const ServerChan = require("./notification-providers/serverchan");
|
||||
|
||||
class Notification {
|
||||
|
||||
@@ -57,10 +64,13 @@ class Notification {
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
new Feishu(),
|
||||
new FreeMobile(),
|
||||
new GoogleChat(),
|
||||
new Gorush(),
|
||||
new Gotify(),
|
||||
new HomeAssistant(),
|
||||
new Line(),
|
||||
new LineNotify(),
|
||||
new LunaSea(),
|
||||
new Matrix(),
|
||||
new Mattermost(),
|
||||
@@ -74,16 +84,20 @@ class Notification {
|
||||
new Pushover(),
|
||||
new Pushy(),
|
||||
new RocketChat(),
|
||||
new ServerChan(),
|
||||
new SerwerSMS(),
|
||||
new Signal(),
|
||||
new SMSManager(),
|
||||
new Slack(),
|
||||
new SMTP(),
|
||||
new Squadcast(),
|
||||
new Stackfield(),
|
||||
new Teams(),
|
||||
new TechulusPush(),
|
||||
new Telegram(),
|
||||
new Webhook(),
|
||||
new WeCom(),
|
||||
new GoAlert(),
|
||||
];
|
||||
|
||||
for (let item of list) {
|
||||
|
@@ -105,7 +105,7 @@ Ping.prototype.send = function (callback) {
|
||||
let _exited;
|
||||
let _errored;
|
||||
|
||||
this._ping = spawn(this._bin, this._args); // spawn the binary
|
||||
this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
|
||||
|
||||
this._ping.on("error", function (err) { // handle binary errors
|
||||
_errored = true;
|
||||
|
@@ -136,6 +136,7 @@ router.get("/api/badge/:id/status", cache("5 minutes"), async (request, response
|
||||
const heartbeat = await Monitor.getPreviousHeartbeat(requestedMonitorId);
|
||||
const state = overrideValue !== undefined ? overrideValue : heartbeat.status === 1;
|
||||
|
||||
badgeValues.label = label ? label : "";
|
||||
badgeValues.color = state ? upColor : downColor;
|
||||
badgeValues.message = label ?? state ? upLabel : downLabel;
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ log.info("server", "Importing this project modules");
|
||||
log.debug("server", "Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
log.debug("server", "Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword } = require("./util-server");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests } = require("./util-server");
|
||||
|
||||
log.debug("server", "Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
@@ -112,19 +112,21 @@ const twoFAVerifyOptions = {
|
||||
* @type {boolean}
|
||||
*/
|
||||
const testMode = !!args["test"] || false;
|
||||
const e2eTestMode = !!args["e2e"] || false;
|
||||
|
||||
if (config.demoMode) {
|
||||
log.info("server", "==== Demo Mode ====");
|
||||
}
|
||||
|
||||
// Must be after io instantiation
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
|
||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
const StatusPage = require("./model/status_page");
|
||||
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
|
||||
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
|
||||
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -164,12 +166,20 @@ let needSetup = false;
|
||||
|
||||
// Entry Page
|
||||
app.get("/", async (request, response) => {
|
||||
log.debug("entry", `Request Domain: ${request.hostname}`);
|
||||
let hostname = request.hostname;
|
||||
if (await setting("trustProxy")) {
|
||||
const proxy = request.headers["x-forwarded-host"];
|
||||
if (proxy) {
|
||||
hostname = proxy;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.hostname in StatusPage.domainMappingList) {
|
||||
log.debug("entry", `Request Domain: ${hostname}`);
|
||||
|
||||
if (hostname in StatusPage.domainMappingList) {
|
||||
log.debug("entry", "This is a status page domain");
|
||||
|
||||
let slug = StatusPage.domainMappingList[request.hostname];
|
||||
let slug = StatusPage.domainMappingList[hostname];
|
||||
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
|
||||
|
||||
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||
@@ -246,7 +256,9 @@ let needSetup = false;
|
||||
// ***************************
|
||||
|
||||
socket.on("loginByToken", async (token, callback) => {
|
||||
log.info("auth", `Login by token. IP=${getClientIp(socket)}`);
|
||||
const clientIP = await server.getClientIP(socket);
|
||||
|
||||
log.info("auth", `Login by token. IP=${clientIP}`);
|
||||
|
||||
try {
|
||||
let decoded = jwt.verify(token, jwtSecret);
|
||||
@@ -262,14 +274,14 @@ let needSetup = false;
|
||||
afterLogin(socket, user);
|
||||
log.debug("auth", "afterLogin ok");
|
||||
|
||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -278,7 +290,7 @@ let needSetup = false;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
log.error("auth", `Invalid token. IP=${getClientIp(socket)}`);
|
||||
log.error("auth", `Invalid token. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -289,7 +301,9 @@ let needSetup = false;
|
||||
});
|
||||
|
||||
socket.on("login", async (data, callback) => {
|
||||
log.info("auth", `Login by username + password. IP=${getClientIp(socket)}`);
|
||||
const clientIP = await server.getClientIP(socket);
|
||||
|
||||
log.info("auth", `Login by username + password. IP=${clientIP}`);
|
||||
|
||||
// Checking
|
||||
if (typeof callback !== "function") {
|
||||
@@ -302,7 +316,7 @@ let needSetup = false;
|
||||
|
||||
// Login Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
log.info("auth", `Too many failed requests for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,7 +326,7 @@ let needSetup = false;
|
||||
if (user.twofa_status === 0) {
|
||||
afterLogin(socket, user);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -324,7 +338,7 @@ let needSetup = false;
|
||||
|
||||
if (user.twofa_status === 1 && !data.token) {
|
||||
|
||||
log.info("auth", `2FA token required for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
tokenRequired: true,
|
||||
@@ -342,7 +356,7 @@ let needSetup = false;
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -352,7 +366,7 @@ let needSetup = false;
|
||||
});
|
||||
} else {
|
||||
|
||||
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -362,7 +376,7 @@ let needSetup = false;
|
||||
}
|
||||
} else {
|
||||
|
||||
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${getClientIp(socket)}`);
|
||||
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -434,6 +448,8 @@ let needSetup = false;
|
||||
});
|
||||
|
||||
socket.on("save2FA", async (currentPassword, callback) => {
|
||||
const clientIP = await server.getClientIP(socket);
|
||||
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
@@ -446,7 +462,7 @@ let needSetup = false;
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
log.info("auth", `Saved 2FA token. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `Saved 2FA token. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -454,7 +470,7 @@ let needSetup = false;
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
log.error("auth", `Error changing 2FA token. IP=${getClientIp(socket)}`);
|
||||
log.error("auth", `Error changing 2FA token. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -464,6 +480,8 @@ let needSetup = false;
|
||||
});
|
||||
|
||||
socket.on("disable2FA", async (currentPassword, callback) => {
|
||||
const clientIP = await server.getClientIP(socket);
|
||||
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
@@ -473,7 +491,7 @@ let needSetup = false;
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
await TwoFA.disable2FA(socket.userID);
|
||||
|
||||
log.info("auth", `Disabled 2FA token. IP=${getClientIp(socket)}`);
|
||||
log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -481,7 +499,7 @@ let needSetup = false;
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
log.error("auth", `Error disabling 2FA token. IP=${getClientIp(socket)}`);
|
||||
log.error("auth", `Error disabling 2FA token. IP=${clientIP}`);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
@@ -652,6 +670,7 @@ let needSetup = false;
|
||||
bean.basic_auth_pass = monitor.basic_auth_pass;
|
||||
bean.interval = monitor.interval;
|
||||
bean.retryInterval = monitor.retryInterval;
|
||||
bean.resendInterval = monitor.resendInterval;
|
||||
bean.hostname = monitor.hostname;
|
||||
bean.maxretries = monitor.maxretries;
|
||||
bean.port = parseInt(monitor.port);
|
||||
@@ -664,6 +683,8 @@ let needSetup = false;
|
||||
bean.dns_resolve_type = monitor.dns_resolve_type;
|
||||
bean.dns_resolve_server = monitor.dns_resolve_server;
|
||||
bean.pushToken = monitor.pushToken;
|
||||
bean.docker_container = monitor.docker_container;
|
||||
bean.docker_host = monitor.docker_host;
|
||||
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
|
||||
bean.mqttUsername = monitor.mqttUsername;
|
||||
bean.mqttPassword = monitor.mqttPassword;
|
||||
@@ -674,6 +695,11 @@ let needSetup = false;
|
||||
bean.authMethod = monitor.authMethod;
|
||||
bean.authWorkstation = monitor.authWorkstation;
|
||||
bean.authDomain = monitor.authDomain;
|
||||
bean.radiusUsername = monitor.radiusUsername;
|
||||
bean.radiusPassword = monitor.radiusPassword;
|
||||
bean.radiusCalledStationId = monitor.radiusCalledStationId;
|
||||
bean.radiusCallingStationId = monitor.radiusCallingStationId;
|
||||
bean.radiusSecret = monitor.radiusSecret;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
@@ -1254,6 +1280,7 @@ let needSetup = false;
|
||||
authDomain: monitorListData[i].authDomain,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
resendInterval: monitorListData[i].resendInterval || 0,
|
||||
hostname: monitorListData[i].hostname,
|
||||
maxretries: monitorListData[i].maxretries,
|
||||
port: monitorListData[i].port,
|
||||
@@ -1422,6 +1449,7 @@ let needSetup = false;
|
||||
cloudflaredSocketHandler(socket);
|
||||
databaseSocketHandler(socket);
|
||||
proxySocketHandler(socket);
|
||||
dockerSocketHandler(socket);
|
||||
|
||||
log.debug("server", "added all socket handlers");
|
||||
|
||||
@@ -1459,6 +1487,10 @@ let needSetup = false;
|
||||
if (testMode) {
|
||||
startUnitTest();
|
||||
}
|
||||
|
||||
if (e2eTestMode) {
|
||||
startE2eTests();
|
||||
}
|
||||
});
|
||||
|
||||
initBackgroundJobs(args);
|
||||
@@ -1522,6 +1554,7 @@ async function afterLogin(socket, user) {
|
||||
let monitorList = await server.sendMonitorList(socket);
|
||||
sendNotificationList(socket);
|
||||
sendProxyList(socket);
|
||||
sendDockerHostList(socket);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
@@ -1676,10 +1709,6 @@ async function shutdownFunction(signal) {
|
||||
await cloudflaredStop();
|
||||
}
|
||||
|
||||
function getClientIp(socket) {
|
||||
return socket.client.conn.remoteAddress.replace(/^.*:/, "");
|
||||
}
|
||||
|
||||
/** Final function called before application exits */
|
||||
function finalFunction() {
|
||||
log.info("server", "Graceful shutdown successful!");
|
||||
|
79
server/socket-handlers/docker-socket-handler.js
Normal file
79
server/socket-handlers/docker-socket-handler.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { sendDockerHostList } = require("../client");
|
||||
const { checkLogin } = require("../util-server");
|
||||
const { DockerHost } = require("../docker");
|
||||
const { log } = require("../../src/util");
|
||||
|
||||
/**
|
||||
* Handlers for docker hosts
|
||||
* @param {Socket} socket Socket.io instance
|
||||
*/
|
||||
module.exports.dockerSocketHandler = (socket) => {
|
||||
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
|
||||
await sendDockerHostList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
id: dockerHostBean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("deleteDockerHost", async (dockerHostID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await DockerHost.delete(dockerHostID, socket.userID);
|
||||
await sendDockerHostList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("testDockerHost", async (dockerHost, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let amount = await DockerHost.testDockerHost(dockerHost);
|
||||
let msg;
|
||||
|
||||
if (amount >= 1) {
|
||||
msg = "Connected Successfully. Amount of containers: " + amount;
|
||||
} else {
|
||||
msg = "Connected Successfully, but there are no containers?";
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
log.error("docker", e);
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@@ -202,7 +202,11 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
relationBean.weight = monitorOrder++;
|
||||
relationBean.group_id = groupBean.id;
|
||||
relationBean.monitor_id = monitor.id;
|
||||
relationBean.send_url = monitor.sendUrl;
|
||||
|
||||
if (monitor.sendUrl !== undefined) {
|
||||
relationBean.send_url = monitor.sendUrl;
|
||||
}
|
||||
|
||||
await R.store(relationBean);
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,8 @@ 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");
|
||||
|
||||
/**
|
||||
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
|
||||
@@ -49,7 +51,6 @@ class UptimeKumaServer {
|
||||
|
||||
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({
|
||||
@@ -71,6 +72,8 @@ class UptimeKumaServer {
|
||||
}
|
||||
}
|
||||
|
||||
CacheableDnsHttpAgent.registerGlobalAgent();
|
||||
|
||||
this.io = new Server(this.httpServer);
|
||||
}
|
||||
|
||||
@@ -126,6 +129,22 @@ class UptimeKumaServer {
|
||||
|
||||
errorLogStream.end();
|
||||
}
|
||||
|
||||
async getClientIP(socket) {
|
||||
let clientIP = socket.client.conn.remoteAddress;
|
||||
|
||||
if (clientIP === undefined) {
|
||||
clientIP = "";
|
||||
}
|
||||
|
||||
if (await Settings.get("trustProxy")) {
|
||||
return socket.client.conn.request.headers["x-forwarded-for"]
|
||||
|| socket.client.conn.request.headers["x-real-ip"]
|
||||
|| clientIP.replace(/^.*:/, "");
|
||||
} else {
|
||||
return clientIP.replace(/^.*:/, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@@ -11,8 +11,16 @@ const mqtt = require("mqtt");
|
||||
const chroma = require("chroma-js");
|
||||
const { badgeConstants } = require("./config");
|
||||
const mssql = require("mssql");
|
||||
const { Client } = require("pg");
|
||||
const postgresConParse = require("pg-connection-string").parse;
|
||||
const { NtlmClient } = require("axios-ntlm");
|
||||
const { Settings } = require("./settings");
|
||||
const radiusClient = require("node-radius-client");
|
||||
const {
|
||||
dictionaries: {
|
||||
rfc2865: { file, attributes },
|
||||
},
|
||||
} = require("node-radius-utils");
|
||||
|
||||
// From ping-lite
|
||||
exports.WIN = /^win/.test(process.platform);
|
||||
@@ -238,10 +246,6 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
|
||||
*/
|
||||
exports.mssqlQuery = function (connectionString, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
mssql.on("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
mssql.connect(connectionString).then(pool => {
|
||||
return pool.request()
|
||||
.query(query);
|
||||
@@ -255,10 +259,67 @@ exports.mssqlQuery = function (connectionString, query) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a query on Postgres
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to validate the database with
|
||||
* @returns {Promise<(string[]|Object[]|Object)>}
|
||||
*/
|
||||
exports.postgresQuery = function (connectionString, query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = postgresConParse(connectionString);
|
||||
|
||||
if (config.password === "") {
|
||||
// See https://github.com/brianc/node-postgres/issues/1927
|
||||
return reject(new Error("Password is undefined."));
|
||||
}
|
||||
|
||||
const client = new Client({ connectionString });
|
||||
|
||||
client.connect();
|
||||
|
||||
return client.query(query)
|
||||
.then(res => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
client.end();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.radius = function (
|
||||
hostname,
|
||||
username,
|
||||
password,
|
||||
calledStationId,
|
||||
callingStationId,
|
||||
secret,
|
||||
) {
|
||||
const client = new radiusClient({
|
||||
host: hostname,
|
||||
dictionaries: [ file ],
|
||||
});
|
||||
|
||||
return client.accessRequest({
|
||||
secret: secret,
|
||||
attributes: [
|
||||
[ attributes.USER_NAME, username ],
|
||||
[ attributes.USER_PASSWORD, password ],
|
||||
[ attributes.CALLING_STATION_ID, callingStationId ],
|
||||
[ attributes.CALLED_STATION_ID, calledStationId ],
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve value of setting based on key
|
||||
* @param {string} key Key of setting to retrieve
|
||||
* @returns {Promise<any>} Value
|
||||
* @deprecated Use await Settings.get(key)
|
||||
*/
|
||||
exports.setting = async function (key) {
|
||||
return await Settings.get(key);
|
||||
@@ -384,7 +445,7 @@ exports.checkCertificate = function (res) {
|
||||
|
||||
/**
|
||||
* Check if the provided status code is within the accepted ranges
|
||||
* @param {string} status The status code to check
|
||||
* @param {number} status The status code to check
|
||||
* @param {string[]} acceptedCodes An array of accepted status codes
|
||||
* @returns {boolean} True if status code within range, false otherwise
|
||||
* @throws {Error} Will throw an error if the provided status code is not a valid range string or code string
|
||||
@@ -496,7 +557,27 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
exports.startUnitTest = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
const child = childProcess.spawn(npm, [ "run", "jest" ]);
|
||||
const child = childProcess.spawn(npm, [ "run", "jest-backend" ]);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.on("close", function (code) {
|
||||
console.log("Jest exit code: " + code);
|
||||
process.exit(code);
|
||||
});
|
||||
};
|
||||
|
||||
/** Start end-to-end tests */
|
||||
exports.startE2eTests = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
const child = childProcess.spawn(npm, [ "run", "cy:run" ]);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
|
Reference in New Issue
Block a user