Merge branch 'louislam:master' into feature/local-service-monitor

pull/6488/head
iotux 4 months ago committed by GitHub
commit b3f84f0bf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

18
package-lock.json generated

@ -62,7 +62,7 @@
"node-cloudflared-tunnel": "~1.0.9",
"node-fetch-cache": "^5.1.0",
"node-radius-utils": "~1.2.0",
"nodemailer": "~6.9.13",
"nodemailer": "~7.0.12",
"nostr-tools": "^2.10.4",
"notp": "~2.0.3",
"openid-client": "^5.4.2",
@ -74,7 +74,7 @@
"prometheus-api-metrics": "~3.2.1",
"promisify-child-process": "~4.1.2",
"protobufjs": "~7.2.4",
"qs": "~6.10.4",
"qs": "~6.14.1",
"radius": "~1.1.4",
"redbean-node": "~0.3.0",
"redis": "~5.9.0",
@ -14338,9 +14338,9 @@
}
},
"node_modules/nodemailer": {
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz",
"integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@ -15657,12 +15657,12 @@
}
},
"node_modules/qs": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
"integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.4"
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"

@ -122,8 +122,8 @@
"net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9",
"node-fetch-cache": "^5.1.0",
"nodemailer": "~7.0.12",
"node-radius-utils": "~1.2.0",
"nodemailer": "~6.9.13",
"nostr-tools": "^2.10.4",
"notp": "~2.0.3",
"openid-client": "^5.4.2",
@ -135,7 +135,7 @@
"prometheus-api-metrics": "~3.2.1",
"promisify-child-process": "~4.1.2",
"protobufjs": "~7.2.4",
"qs": "~6.10.4",
"qs": "~6.14.1",
"radius": "~1.1.4",
"redbean-node": "~0.3.0",
"redis": "~5.9.0",

@ -20,7 +20,6 @@ const version = require("../../package.json").version;
const apicache = require("../modules/apicache");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { DockerHost } = require("../docker");
const Gamedig = require("gamedig");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const { UptimeCalculator } = require("../uptime-calculator");
@ -718,21 +717,6 @@ class Monitor extends BeanModel {
} else {
throw new Error("Server not found on Steam");
}
} else if (this.type === "gamedig") {
try {
const state = await Gamedig.query({
type: this.game,
host: this.hostname,
port: this.port,
givenPortOnly: this.getGameDigGivenPortOnly(),
});
bean.msg = state.name;
bean.status = UP;
bean.ping = state.ping;
} catch (e) {
throw new Error(e.message);
}
} else if (this.type === "docker") {
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);

@ -106,7 +106,7 @@ function createNTLMv2Response(type2message, username, ntlmhash, nonce, targetNam
function createPseudoRandomValue(length) {
var str = '';
while (str.length < length) {
str += Math.floor(Math.random() * 16).toString(16);
str += crypto.randomInt(16).toString(16);
}
return str;
}

@ -0,0 +1,55 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN } = require("../../src/util");
const Gamedig = require("gamedig");
const dns = require("dns").promises;
const net = require("net");
class GameDigMonitorType extends MonitorType {
name = "gamedig";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
heartbeat.status = DOWN;
let host = monitor.hostname;
if (net.isIP(host) === 0) {
host = await this.resolveHostname(host);
}
try {
const state = await Gamedig.query({
type: monitor.game,
host: host,
port: monitor.port,
givenPortOnly: Boolean(monitor.gamedigGivenPortOnly),
});
heartbeat.msg = state.name;
heartbeat.status = UP;
heartbeat.ping = state.ping;
} catch (e) {
throw new Error(e.message);
}
}
/**
* Resolves a domain name to its IPv4 address.
* @param {string} hostname - The domain name to resolve (e.g., "example.dyndns.org").
* @returns {Promise<string>} - The resolved IP address.
* @throws Will throw an error if the DNS resolution fails.
*/
async resolveHostname(hostname) {
try {
const result = await dns.lookup(hostname);
return result.address;
} catch (err) {
throw new Error(`DNS resolution failed for ${hostname}: ${err.message}`);
}
}
}
module.exports = {
GameDigMonitorType,
};

@ -121,6 +121,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["grpc-keyword"] = new GrpcKeywordMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
UptimeKumaServer.monitorTypeList["gamedig"] = new GameDigMonitorType();
UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType();
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType();
@ -571,6 +572,7 @@ const { SNMPMonitorType } = require("./monitor-types/snmp");
const { GrpcKeywordMonitorType } = require("./monitor-types/grpc");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const { GameDigMonitorType } = require("./monitor-types/gamedig");
const { TCPMonitorType } = require("./monitor-types/tcp.js");
const { ManualMonitorType } = require("./monitor-types/manual");
const { RedisMonitorType } = require("./monitor-types/redis");

@ -0,0 +1,264 @@
const { describe, test, mock } = require("node:test");
const assert = require("node:assert");
const { GameDigMonitorType } = require("../../../server/monitor-types/gamedig");
const { UP, DOWN, PENDING } = require("../../../src/util");
const net = require("net");
const Gamedig = require("gamedig");
describe("GameDig Monitor", () => {
test("check() sets status to UP when Gamedig.query returns valid server response", async () => {
const gamedigMonitor = new GameDigMonitorType();
mock.method(Gamedig, "query", async () => {
return {
name: "Test Minecraft Server",
ping: 42,
players: [],
};
});
const monitor = {
hostname: "127.0.0.1",
port: 25565,
game: "minecraft",
gamedigGivenPortOnly: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await gamedigMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Test Minecraft Server");
assert.strictEqual(heartbeat.ping, 42);
} finally {
mock.restoreAll();
}
});
test("check() resolves hostname to IP address when hostname is not an IP", async () => {
const gamedigMonitor = new GameDigMonitorType();
mock.method(Gamedig, "query", async (options) => {
assert.ok(
net.isIP(options.host) !== 0,
`Expected IP address, got ${options.host}`
);
return {
name: "Test Server",
ping: 50,
};
});
const monitor = {
hostname: "localhost",
port: 25565,
game: "minecraft",
gamedigGivenPortOnly: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await gamedigMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Test Server");
assert.strictEqual(heartbeat.ping, 50);
} finally {
mock.restoreAll();
}
});
test("check() uses IP address directly without DNS resolution when hostname is IPv4", async () => {
const gamedigMonitor = new GameDigMonitorType();
let capturedOptions = null;
mock.method(Gamedig, "query", async (options) => {
capturedOptions = options;
return {
name: "Test Server",
ping: 30,
};
});
const monitor = {
hostname: "192.168.1.100",
port: 27015,
game: "valve",
gamedigGivenPortOnly: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await gamedigMonitor.check(monitor, heartbeat, {});
assert.strictEqual(capturedOptions.host, "192.168.1.100");
assert.strictEqual(heartbeat.status, UP);
} finally {
mock.restoreAll();
}
});
test("check() uses IP address directly without DNS resolution when hostname is IPv6", async () => {
const gamedigMonitor = new GameDigMonitorType();
let capturedOptions = null;
mock.method(Gamedig, "query", async (options) => {
capturedOptions = options;
return {
name: "Test Server",
ping: 30,
};
});
const monitor = {
hostname: "::1",
port: 27015,
game: "valve",
gamedigGivenPortOnly: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await gamedigMonitor.check(monitor, heartbeat, {});
assert.strictEqual(capturedOptions.host, "::1");
assert.strictEqual(heartbeat.status, UP);
} finally {
mock.restoreAll();
}
});
test("check() passes correct parameters to Gamedig.query", async () => {
const gamedigMonitor = new GameDigMonitorType();
let capturedOptions = null;
mock.method(Gamedig, "query", async (options) => {
capturedOptions = options;
return {
name: "Test Server",
ping: 25,
};
});
const monitor = {
hostname: "192.168.1.100",
port: 27015,
game: "valve",
gamedigGivenPortOnly: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await gamedigMonitor.check(monitor, heartbeat, {});
assert.strictEqual(capturedOptions.type, "valve");
assert.strictEqual(capturedOptions.host, "192.168.1.100");
assert.strictEqual(capturedOptions.port, 27015);
assert.strictEqual(capturedOptions.givenPortOnly, true);
} finally {
mock.restoreAll();
}
});
test("check() converts gamedigGivenPortOnly to boolean when value is truthy non-boolean", async () => {
const gamedigMonitor = new GameDigMonitorType();
let capturedOptions = null;
mock.method(Gamedig, "query", async (options) => {
capturedOptions = options;
return {
name: "Test Server",
ping: 30,
};
});
const monitor = {
hostname: "127.0.0.1",
port: 25565,
game: "minecraft",
gamedigGivenPortOnly: 1,
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await gamedigMonitor.check(monitor, heartbeat, {});
assert.strictEqual(capturedOptions.givenPortOnly, true);
assert.strictEqual(typeof capturedOptions.givenPortOnly, "boolean");
} finally {
mock.restoreAll();
}
});
test("check() sets status to DOWN and rejects when game server is unreachable", async () => {
const gamedigMonitor = new GameDigMonitorType();
const monitor = {
hostname: "127.0.0.1",
port: 54321,
game: "minecraft",
gamedigGivenPortOnly: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
await assert.rejects(
gamedigMonitor.check(monitor, heartbeat, {}),
/Error/
);
assert.strictEqual(heartbeat.status, DOWN);
});
test("resolveHostname() returns IP address when given valid hostname", async () => {
const gamedigMonitor = new GameDigMonitorType();
const resolvedIP = await gamedigMonitor.resolveHostname("localhost");
assert.ok(
net.isIP(resolvedIP) !== 0,
`Expected valid IP address, got ${resolvedIP}`
);
});
test("resolveHostname() rejects when DNS resolution fails for invalid hostname", async () => {
const gamedigMonitor = new GameDigMonitorType();
await assert.rejects(
gamedigMonitor.resolveHostname("this-domain-definitely-does-not-exist-12345.invalid"),
/DNS resolution failed/
);
});
});

@ -0,0 +1,27 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const hash = require("../../../server/modules/axios-ntlm/lib/hash");
describe("createPseudoRandomValue()", () => {
test("returns a hexadecimal string with the requested length", () => {
for (const length of [ 0, 8, 16, 32, 64 ]) {
const result = hash.createPseudoRandomValue(length);
assert.strictEqual(typeof result, "string");
assert.strictEqual(result.length, length);
assert.ok(/^[0-9a-f]*$/.test(result));
}
});
test("returns unique values across multiple calls with the same length", () => {
const length = 16;
const iterations = 10;
const results = new Set();
for (let i = 0; i < iterations; i++) {
results.add(hash.createPseudoRandomValue(length));
}
assert.strictEqual(results.size, iterations);
});
});

@ -73,7 +73,7 @@ test.describe("Monitor Form", () => {
await page.getByTestId("save-button").click();
await page.waitForURL("/dashboard/*");
expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
await expect(page.getByTestId("monitor-status")).toHaveText("up", { ignoreCase: true });
await screenshot(testInfo, page);
});
@ -101,7 +101,7 @@ test.describe("Monitor Form", () => {
await page.getByTestId("save-button").click();
await page.waitForURL("/dashboard/*");
expect(page.getByTestId("monitor-status")).toHaveText("down", { ignoreCase: true });
await expect(page.getByTestId("monitor-status")).toHaveText("down", { ignoreCase: true });
await screenshot(testInfo, page);
});

Loading…
Cancel
Save