mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-06-27 22:35:53 +00:00
fix: resolve Steam monitor hostnames (#7542)
This commit is contained in:
@@ -32,7 +32,6 @@ const {
|
||||
checkCertificate,
|
||||
checkStatusCode,
|
||||
getTotalClientInRoom,
|
||||
setting,
|
||||
httpNtlm,
|
||||
radius,
|
||||
kafkaProducerAsync,
|
||||
@@ -772,56 +771,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`);
|
||||
|
||||
|
||||
134
server/monitor-types/steam.js
Normal file
134
server/monitor-types/steam.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const {
|
||||
UP,
|
||||
PING_COUNT_DEFAULT,
|
||||
PING_GLOBAL_TIMEOUT_DEFAULT,
|
||||
PING_PER_REQUEST_TIMEOUT_DEFAULT,
|
||||
} = require("../../src/util");
|
||||
const { Settings } = require("../settings");
|
||||
const { ping, checkStatusCode } = require("../util-server");
|
||||
const axios = require("axios");
|
||||
const crypto = require("crypto");
|
||||
const dns = require("node:dns/promises");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const net = require("node:net");
|
||||
|
||||
class SteamMonitorType extends MonitorType {
|
||||
name = "steam";
|
||||
|
||||
/**
|
||||
* Creates a Steam monitor type.
|
||||
* @param {object} options Optional dependencies for tests.
|
||||
* @param {object} options.steamApiClient Axios-compatible Steam API client.
|
||||
* @param {Function} options.lookup DNS lookup function.
|
||||
* @param {Function} options.getSteamAPIKey Steam API key provider.
|
||||
* @param {Function} options.ping Steam server ping function.
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this.steamApiClient = options.steamApiClient || axios;
|
||||
this.lookup = options.lookup || dns.lookup;
|
||||
this.getSteamAPIKey = options.getSteamAPIKey || (() => Settings.get("steamAPIKey"));
|
||||
this.ping = options.ping || ping;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
|
||||
const steamAPIKey = await this.getSteamAPIKey();
|
||||
|
||||
if (!steamAPIKey) {
|
||||
throw new Error("Steam API Key not found");
|
||||
}
|
||||
|
||||
const filter = await this.buildServerFilter(monitor.hostname, monitor.port);
|
||||
|
||||
let res = await this.steamApiClient.get(steamApiUrl, {
|
||||
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.getIgnoreTls(),
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
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 this.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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Steam API server filter.
|
||||
* @param {string} hostname Steam server hostname or IP address.
|
||||
* @param {number} port Steam server port.
|
||||
* @returns {Promise<string>} Steam API addr filter.
|
||||
*/
|
||||
async buildServerFilter(hostname, port) {
|
||||
const resolvedHostname = await this.resolveSteamHostname(hostname);
|
||||
return `addr\\${resolvedHostname}:${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves hostnames before passing them to Steam's addr filter.
|
||||
* @param {string} hostname Steam server hostname or IP address.
|
||||
* @returns {Promise<string>} IP address accepted by the Steam API.
|
||||
* @throws {Error} When the hostname cannot be resolved.
|
||||
*/
|
||||
async resolveSteamHostname(hostname) {
|
||||
if (net.isIP(hostname)) {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
try {
|
||||
const lookupResult = await this.lookup(hostname, { all: true });
|
||||
const addresses = Array.isArray(lookupResult) ? lookupResult : [lookupResult];
|
||||
const ipv4Address = addresses.find(({ address }) => net.isIP(address) === 4);
|
||||
const resolvedAddress = ipv4Address?.address || addresses[0]?.address;
|
||||
|
||||
if (!resolvedAddress) {
|
||||
throw new Error("DNS lookup returned no addresses");
|
||||
}
|
||||
|
||||
return resolvedAddress;
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to resolve Steam server hostname "${hostname}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SteamMonitorType,
|
||||
};
|
||||
@@ -124,6 +124,7 @@ class UptimeKumaServer {
|
||||
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["sip-options"] = new SIPMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["gamedig"] = new GameDigMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["steam"] = new SteamMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["globalping"] = new GlobalpingMonitorType(this.getUserAgent());
|
||||
@@ -576,6 +577,7 @@ const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
|
||||
const { SIPMonitorType } = require("./monitor-types/sip-options");
|
||||
const { GameDigMonitorType } = require("./monitor-types/gamedig");
|
||||
const { SteamMonitorType } = require("./monitor-types/steam");
|
||||
const { TCPMonitorType } = require("./monitor-types/tcp.js");
|
||||
const { ManualMonitorType } = require("./monitor-types/manual");
|
||||
const { GlobalpingMonitorType } = require("./monitor-types/globalping");
|
||||
|
||||
140
test/backend-test/monitors/test-steam.js
Normal file
140
test/backend-test/monitors/test-steam.js
Normal file
@@ -0,0 +1,140 @@
|
||||
const { describe, test } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { SteamMonitorType } = require("../../../server/monitor-types/steam");
|
||||
const { UP, PENDING } = require("../../../src/util");
|
||||
|
||||
describe("Steam Monitor", () => {
|
||||
test("resolveSteamHostname() returns IP addresses without DNS lookup", async () => {
|
||||
let lookupCalled = false;
|
||||
const steamMonitor = new SteamMonitorType({
|
||||
lookup: async () => {
|
||||
lookupCalled = true;
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(await steamMonitor.resolveSteamHostname("192.0.2.10"), "192.0.2.10");
|
||||
assert.strictEqual(await steamMonitor.resolveSteamHostname("2001:db8::10"), "2001:db8::10");
|
||||
assert.strictEqual(lookupCalled, false);
|
||||
});
|
||||
|
||||
test("buildServerFilter() resolves hostnames before building the Steam API addr filter", async () => {
|
||||
let capturedHostname = null;
|
||||
let capturedOptions = null;
|
||||
const steamMonitor = new SteamMonitorType({
|
||||
lookup: async (hostname, options) => {
|
||||
capturedHostname = hostname;
|
||||
capturedOptions = options;
|
||||
return [
|
||||
{
|
||||
address: "203.0.113.10",
|
||||
family: 4,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const filter = await steamMonitor.buildServerFilter("server.example.com", 27015);
|
||||
|
||||
assert.strictEqual(filter, "addr\\203.0.113.10:27015");
|
||||
assert.strictEqual(capturedHostname, "server.example.com");
|
||||
assert.deepStrictEqual(capturedOptions, { all: true });
|
||||
});
|
||||
|
||||
test("resolveSteamHostname() prefers IPv4 addresses returned by DNS lookup", async () => {
|
||||
const steamMonitor = new SteamMonitorType({
|
||||
lookup: async () => {
|
||||
return [
|
||||
{
|
||||
address: "2001:db8::20",
|
||||
family: 6,
|
||||
},
|
||||
{
|
||||
address: "203.0.113.20",
|
||||
family: 4,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(await steamMonitor.resolveSteamHostname("server.example.com"), "203.0.113.20");
|
||||
});
|
||||
|
||||
test("check() uses the resolved IP address in the Steam API filter", async () => {
|
||||
let capturedUrl = null;
|
||||
let capturedOptions = null;
|
||||
const steamMonitor = new SteamMonitorType({
|
||||
lookup: async () => {
|
||||
return [
|
||||
{
|
||||
address: "203.0.113.30",
|
||||
family: 4,
|
||||
},
|
||||
];
|
||||
},
|
||||
getSteamAPIKey: async () => "test-steam-api-key",
|
||||
steamApiClient: {
|
||||
get: async (url, options) => {
|
||||
capturedUrl = url;
|
||||
capturedOptions = options;
|
||||
return {
|
||||
data: {
|
||||
response: {
|
||||
servers: [
|
||||
{
|
||||
name: "Test Steam Server",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
ping: async () => 42,
|
||||
});
|
||||
|
||||
const monitor = {
|
||||
hostname: "server.example.com",
|
||||
port: 27015,
|
||||
timeout: 30,
|
||||
maxredirects: 10,
|
||||
packetSize: 56,
|
||||
getIgnoreTls: () => false,
|
||||
getAcceptedStatuscodes: () => ["200"],
|
||||
};
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
await steamMonitor.check(monitor, heartbeat);
|
||||
|
||||
assert.strictEqual(capturedUrl, "https://api.steampowered.com/IGameServersService/GetServerList/v1/");
|
||||
assert.strictEqual(capturedOptions.params.filter, "addr\\203.0.113.30:27015");
|
||||
assert.strictEqual(capturedOptions.params.key, "test-steam-api-key");
|
||||
assert.strictEqual(heartbeat.status, UP);
|
||||
assert.strictEqual(heartbeat.msg, "Test Steam Server");
|
||||
assert.strictEqual(heartbeat.ping, 42);
|
||||
});
|
||||
|
||||
test("check() does not resolve hostnames when the Steam API key is missing", async () => {
|
||||
let lookupCalled = false;
|
||||
const steamMonitor = new SteamMonitorType({
|
||||
lookup: async () => {
|
||||
lookupCalled = true;
|
||||
},
|
||||
getSteamAPIKey: async () => "",
|
||||
});
|
||||
|
||||
const monitor = {
|
||||
hostname: "server.example.com",
|
||||
port: 27015,
|
||||
};
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
await assert.rejects(steamMonitor.check(monitor, heartbeat), /Steam API Key not found/);
|
||||
assert.strictEqual(lookupCalled, false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user