Merge remote-tracking branch 'origin/master' into test/add-cypress-tests

# Conflicts:
#	package.json
This commit is contained in:
Louis Lam
2022-09-09 16:32:23 +08:00
54 changed files with 4229 additions and 1899 deletions

View File

@@ -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
};

View File

@@ -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

106
server/docker.js Normal file
View File

@@ -0,0 +1,106 @@
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.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?");
}
}
}
module.exports = {
DockerHost,
};

View 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;

View File

@@ -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");
@@ -79,6 +79,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(),
@@ -88,6 +89,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,
@@ -100,6 +104,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) {
@@ -206,6 +215,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);
@@ -468,6 +478,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._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,
@@ -484,6 +523,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;
@@ -525,12 +596,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) {
@@ -541,7 +627,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`);

View File

@@ -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 {
@@ -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;
// 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;
}

View 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
};
let resp = 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;

View 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;

View 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;

View File

@@ -12,7 +12,9 @@ const Feishu = require("./notification-providers/feishu");
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");
@@ -36,6 +38,7 @@ 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");
class Notification {
@@ -60,7 +63,9 @@ class Notification {
new GoogleChat(),
new Gorush(),
new Gotify(),
new HomeAssistant(),
new Line(),
new LineNotify(),
new LunaSea(),
new Matrix(),
new Mattermost(),
@@ -84,6 +89,7 @@ class Notification {
new Telegram(),
new Webhook(),
new WeCom(),
new GoAlert(),
];
for (let item of list) {

View File

@@ -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;
}

View File

@@ -119,13 +119,14 @@ if (config.demoMode) {
}
// 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());
@@ -165,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-")) {
@@ -247,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);
@@ -263,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,
@@ -279,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,
@@ -290,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") {
@@ -303,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;
}
@@ -313,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,
@@ -325,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,
@@ -343,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,
@@ -353,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,
@@ -363,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,
@@ -435,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;
@@ -447,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,
@@ -455,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,
@@ -465,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;
@@ -474,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,
@@ -482,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,
@@ -653,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);
@@ -665,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;
@@ -675,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);
@@ -1255,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,
@@ -1423,6 +1449,7 @@ let needSetup = false;
cloudflaredSocketHandler(socket);
databaseSocketHandler(socket);
proxySocketHandler(socket);
dockerSocketHandler(socket);
log.debug("server", "added all socket handlers");
@@ -1527,6 +1554,7 @@ async function afterLogin(socket, user) {
let monitorList = await server.sendMonitorList(socket);
sendNotificationList(socket);
sendProxyList(socket);
sendDockerHostList(socket);
await sleep(500);
@@ -1681,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!");

View 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,
});
}
});
};

View File

@@ -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);
}

View File

@@ -8,6 +8,7 @@ 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.
@@ -50,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({
@@ -129,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 = {

View File

@@ -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);
@@ -251,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);