diff --git a/server/model/monitor.js b/server/model/monitor.js index e01977133c..a90fdceae5 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -770,56 +770,6 @@ class Monitor extends BeanModel { bean.duration = beatInterval; throw new Error("No heartbeat in the time window"); } - } else if (this.type === "steam") { - const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/"; - const steamAPIKey = await setting("steamAPIKey"); - const filter = `addr\\${this.hostname}:${this.port}`; - - if (!steamAPIKey) { - throw new Error("Steam API Key not found"); - } - - let res = await axios.get(steamApiUrl, { - timeout: this.timeout * 1000, - headers: { - Accept: "*/*", - }, - httpsAgent: new https.Agent({ - maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) - rejectUnauthorized: !this.getIgnoreTls(), - secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, - }), - httpAgent: new http.Agent({ - maxCachedSessions: 0, - }), - maxRedirects: this.maxredirects, - validateStatus: (status) => { - return checkStatusCode(status, this.getAcceptedStatuscodes()); - }, - params: { - filter: filter, - key: steamAPIKey, - }, - }); - - if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) { - bean.status = UP; - bean.msg = res.data.response.servers[0].name; - - try { - bean.ping = await ping( - this.hostname, - PING_COUNT_DEFAULT, - "", - true, - this.packetSize, - PING_GLOBAL_TIMEOUT_DEFAULT, - PING_PER_REQUEST_TIMEOUT_DEFAULT - ); - } catch (_) {} - } else { - throw new Error("Server not found on Steam"); - } } else if (this.type === "docker") { log.debug("monitor", `[${this.name}] Prepare Options for Axios`); diff --git a/server/monitor-types/steam.js b/server/monitor-types/steam.js new file mode 100644 index 0000000000..7936610d58 --- /dev/null +++ b/server/monitor-types/steam.js @@ -0,0 +1,84 @@ +const { MonitorType } = require("./monitor-type"); +const { + UP, + PING_COUNT_DEFAULT, + PING_GLOBAL_TIMEOUT_DEFAULT, + PING_PER_REQUEST_TIMEOUT_DEFAULT, +} = require("../../src/util"); +const { ping, checkStatusCode, setting } = require("../util-server"); +const axios = require("axios"); +const https = require("https"); +const http = require("http"); +const crypto = require("crypto"); + +class SteamMonitorType extends MonitorType { + name = "steam"; + steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/"; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, _server) { + const res = await this.getServerList(monitor); + if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) { + heartbeat.status = UP; + heartbeat.msg = res.data.response.servers[0].name; + + try { + heartbeat.ping = await ping( + monitor.hostname, + PING_COUNT_DEFAULT, + "", + true, + monitor.packetSize, + PING_GLOBAL_TIMEOUT_DEFAULT, + PING_PER_REQUEST_TIMEOUT_DEFAULT + ); + } catch (_) {} + } else { + throw new Error("Server not found on Steam"); + } + } + + /** + * Get server list from Steam API + * @param {Monitor} monitor Monitor object + * @returns {Promise} Axios response object containing server list data + * @throws {Error} If Steam API Key is not configured + */ + async getServerList(monitor) { + const steamAPIKey = await setting("steamAPIKey"); + const filter = `addr\\${monitor.hostname}:${monitor.port}`; + + if (!steamAPIKey) { + throw new Error("Steam API Key not found"); + } + const options = { + timeout: monitor.timeout * 1000, + headers: { + Accept: "*/*", + }, + httpsAgent: new https.Agent({ + maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940) + rejectUnauthorized: !monitor.ignoreTls, + secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, + }), + httpAgent: new http.Agent({ + maxCachedSessions: 0, + }), + maxRedirects: monitor.maxredirects, + validateStatus: (status) => { + return checkStatusCode(status, monitor.getAcceptedStatuscodes()); + }, + params: { + filter: filter, + key: steamAPIKey, + }, + }; + return await axios.get(this.steamApiUrl, options); + } +} + +module.exports = { + SteamMonitorType, +}; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 030ee07874..aa73f97461 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -114,6 +114,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); UptimeKumaServer.monitorTypeList["websocket-upgrade"] = new WebSocketMonitorType(); UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType(); + UptimeKumaServer.monitorTypeList["steam"] = new SteamMonitorType(); UptimeKumaServer.monitorTypeList["postgres"] = new PostgresMonitorType(); UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType(); UptimeKumaServer.monitorTypeList["smtp"] = new SMTPMonitorType(); @@ -564,6 +565,7 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor const { TailscalePing } = require("./monitor-types/tailscale-ping"); const { WebSocketMonitorType } = require("./monitor-types/websocket-upgrade"); const { DnsMonitorType } = require("./monitor-types/dns"); +const { SteamMonitorType } = require("./monitor-types/steam"); const { PostgresMonitorType } = require("./monitor-types/postgres"); const { MqttMonitorType } = require("./monitor-types/mqtt"); const { SMTPMonitorType } = require("./monitor-types/smtp"); diff --git a/test/backend-test/monitors/test-steam.js b/test/backend-test/monitors/test-steam.js new file mode 100644 index 0000000000..e785dd961e --- /dev/null +++ b/test/backend-test/monitors/test-steam.js @@ -0,0 +1,104 @@ +process.env.UPTIME_KUMA_HIDE_LOG = ["info_db", "info_server"].join(","); + +const { describe, test, before, after } = require("node:test"); +const assert = require("node:assert"); +const express = require("express"); +const { UP, PENDING } = require("../../../src/util"); +const { SteamMonitorType } = require("../../../server/monitor-types/steam"); +const { setSetting } = require("../../../server/util-server"); +const TestDB = require("../../mock-testdb"); + +const testDb = new TestDB(); +const TEST_PORT = 30158; +let mockServer; + +describe("Steam Monitor", () => { + before(async () => { + await testDb.create(); + await setSetting("steamAPIKey", "test-steam-api-key"); + + // Create shared mock Steam API server with different endpoints + const app = express(); + app.use(express.json()); + app.get("/GetServerList/", (req, res) => { + res.json({ + response: { + servers: [ + { + name: "Test Game Server", + addr: "127.0.0.1:27015", + }, + ], + }, + }); + }); + app.get("/EmptyGetServerList/", (req, res) => { + res.json({ + response: { + servers: [], + }, + }); + }); + + mockServer = await new Promise((resolve) => { + const server = app.listen(TEST_PORT, () => resolve(server)); + }); + }); + + after(async () => { + if (mockServer) { + await new Promise((resolve) => mockServer.close(resolve)); + } + await testDb.destroy(); + }); + + test("check() sets status to UP when Steam API returns valid server response", async () => { + const steamMonitor = new SteamMonitorType(); + steamMonitor.steamApiUrl = `http://127.0.0.1:${TEST_PORT}/GetServerList/`; + + const monitor = { + hostname: "127.0.0.1", + port: 27015, + timeout: 2, + packetSize: 56, + ignoreTls: false, + maxredirects: 10, + getAcceptedStatuscodes: () => ["200-299"], + }; + + const heartbeat = { + msg: "", + status: PENDING, + ping: null, + }; + + await steamMonitor.check(monitor, heartbeat, {}); + + assert.strictEqual(heartbeat.status, UP); + assert.strictEqual(heartbeat.msg, "Test Game Server"); + // Note: ping may be null or a value depending on if ICMP ping succeeds + }); + + test("check() throws error when Steam API returns empty server list", async () => { + const steamMonitor = new SteamMonitorType(); + steamMonitor.steamApiUrl = `http://127.0.0.1:${TEST_PORT}/EmptyGetServerList/`; + + const monitor = { + hostname: "127.0.0.1", + port: 27015, + timeout: 2, + ignoreTls: false, + maxredirects: 10, + getAcceptedStatuscodes: () => ["200-299"], + }; + + const heartbeat = { + msg: "", + status: PENDING, + }; + + await assert.rejects(steamMonitor.check(monitor, heartbeat, {}), { + message: "Server not found on Steam", + }); + }); +});