UNPKG

steam-server-query

Version:

Module which implements the Master Server Query Protocol and Game Server Queries.

279 lines (278 loc) 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.queryGameServerRules = exports.queryGameServerPlayer = exports.queryGameServerInfo = void 0; const promiseSocket_1 = require("../promiseSocket"); /** * Send a A2S_INFO request to a game server. Retrieves information like its name, the current map, the number of players and so on. * * Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO). * @param gameServer Host and port of the game server to call. * @param attempts Optional. Number of call attempts to make. Default is 1 attempt. * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. * @returns A promise including an object of the type `InfoResponse` */ async function queryGameServerInfo(gameServer, attempts = 1, timeout = 1000) { const splitGameServer = gameServer.split(':'); const host = splitGameServer[0]; const port = parseInt(splitGameServer[1]); const gameServerQuery = new GameServerQuery(host, port, attempts, timeout); const result = await gameServerQuery.info(); return result; } exports.queryGameServerInfo = queryGameServerInfo; /** * Send a A2S_PLAYER request to a game server. Retrieves the current playercount and for every player their name, score and duration. * * Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER). * @param gameServer Host and port of the game server to call. * @param attempts Optional. Number of call attempts to make. Default is 1 attempt. * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. * @returns A promise including an object of the type `PlayerResponse` */ async function queryGameServerPlayer(gameServer, attempts = 1, timeout = 1000) { const splitGameServer = gameServer.split(':'); const host = splitGameServer[0]; const port = parseInt(splitGameServer[1]); const gameServerQuery = new GameServerQuery(host, port, attempts, timeout); const result = await gameServerQuery.player(); return result; } exports.queryGameServerPlayer = queryGameServerPlayer; /** * Send a A2S_RULES request to a game server. Retrieves the rule count and for every rule its name and value. * * Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_RULES). * @param gameServer Host and port of the game server to call. * @param attempts Optional. Number of call attempts to make. Default is 1 attempt. * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. * @returns A promise including an object of the type `RulesResponse` */ async function queryGameServerRules(gameServer, attempts = 1, timeout = 1000) { const splitGameServer = gameServer.split(':'); const host = splitGameServer[0]; const port = parseInt(splitGameServer[1]); const gameServerQuery = new GameServerQuery(host, port, attempts, timeout); const result = await gameServerQuery.rules(); return result; } exports.queryGameServerRules = queryGameServerRules; class GameServerQuery { constructor(_host, _port, attempts, timeout) { this._host = _host; this._port = _port; this._promiseSocket = new promiseSocket_1.PromiseSocket(attempts, timeout); } ; async info() { let resultBuffer; try { resultBuffer = await this._promiseSocket.send(this._buildInfoPacket(), this._host, this._port); } catch (err) { this._promiseSocket.closeSocket(); throw new Error(err); } // If the server replied with a challenge, grab challenge number and send request again if (this._isChallengeResponse(resultBuffer)) { resultBuffer = resultBuffer.slice(5); const challenge = resultBuffer; try { resultBuffer = await this._promiseSocket.send(this._buildInfoPacket(challenge), this._host, this._port); } catch (err) { this._promiseSocket.closeSocket(); throw new Error(err); } } this._promiseSocket.closeSocket(); const parsedInfoBuffer = this._parseInfoBuffer(resultBuffer); return parsedInfoBuffer; } async player() { let resultBuffer; let gotPlayerResponse = false; let challengeTries = 0; do { let challengeResultBuffer; try { challengeResultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x55])), this._host, this._port); } catch (err) { this._promiseSocket.closeSocket(); throw new Error(err); } const challenge = challengeResultBuffer.slice(5); try { resultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x55]), challenge), this._host, this._port); } catch (err) { this._promiseSocket.closeSocket(); throw new Error(err); } if (!this._isChallengeResponse(resultBuffer)) { gotPlayerResponse = true; } challengeTries++; } while (!gotPlayerResponse && challengeTries < 5); this._promiseSocket.closeSocket(); if (this._isChallengeResponse(resultBuffer)) { throw new Error('Server kept sending challenge responses.'); } const parsedPlayerBuffer = this._parsePlayerBuffer(resultBuffer); return parsedPlayerBuffer; } async rules() { let challengeResultBuffer; try { challengeResultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x56])), this._host, this._port); } catch (err) { this._promiseSocket.closeSocket(); throw new Error(err); } const challenge = challengeResultBuffer.slice(5); let resultBuffer; try { resultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x56]), challenge), this._host, this._port); } catch (err) { this._promiseSocket.closeSocket(); throw new Error(err); } this._promiseSocket.closeSocket(); const parsedRulesBuffer = this._parseRulesBuffer(resultBuffer); return parsedRulesBuffer; } _buildInfoPacket(challenge) { let packet = Buffer.concat([ Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]), Buffer.from([0x54]), Buffer.from('Source Engine Query', 'ascii'), Buffer.from([0x00]) ]); if (challenge) { packet = Buffer.concat([ packet, challenge ]); } return packet; } _buildPacket(header, challenge) { let packet = Buffer.concat([ Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]), header ]); if (challenge) { packet = Buffer.concat([ packet, challenge ]); } else { packet = Buffer.concat([ packet, Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]) ]); } return packet; } _isChallengeResponse(buffer) { return buffer.compare(Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0x41]), 0, 5, 0, 5) === 0; } _parseInfoBuffer(buffer) { const infoResponse = {}; buffer = buffer.slice(5); [infoResponse.protocol, buffer] = this._readUInt8(buffer); [infoResponse.name, buffer] = this._readString(buffer); [infoResponse.map, buffer] = this._readString(buffer); [infoResponse.folder, buffer] = this._readString(buffer); [infoResponse.game, buffer] = this._readString(buffer); [infoResponse.appId, buffer] = this._readInt16LE(buffer); [infoResponse.players, buffer] = this._readUInt8(buffer); [infoResponse.maxPlayers, buffer] = this._readUInt8(buffer); [infoResponse.bots, buffer] = this._readUInt8(buffer); infoResponse.serverType = buffer.subarray(0, 1).toString('utf-8'); buffer = buffer.slice(1); infoResponse.environment = buffer.subarray(0, 1).toString('utf-8'); buffer = buffer.slice(1); [infoResponse.visibility, buffer] = this._readUInt8(buffer); [infoResponse.vac, buffer] = this._readUInt8(buffer); [infoResponse.version, buffer] = this._readString(buffer); // if the extra data flag (EDF) is present if (buffer.length > 1) { let edf; [edf, buffer] = this._readUInt8(buffer); if (edf & 0x80) { [infoResponse.port, buffer] = this._readInt16LE(buffer); } if (edf & 0x10) { buffer = buffer.slice(8); } if (edf & 0x40) { [infoResponse.spectatorPort, buffer] = this._readUInt8(buffer); [infoResponse.spectatorName, buffer] = this._readString(buffer); } if (edf & 0x20) { [infoResponse.keywords, buffer] = this._readString(buffer); } if (edf & 0x01) { infoResponse.gameId = buffer.readBigInt64LE(); buffer = buffer.slice(8); } } return infoResponse; } _parsePlayerBuffer(buffer) { const playerResponse = {}; buffer = buffer.slice(5); [playerResponse.playerCount, buffer] = this._readUInt8(buffer); playerResponse.players = []; for (let i = 0; i < playerResponse.playerCount; i++) { let player; [player, buffer] = this._readPlayer(buffer); playerResponse.players.push(player); } return playerResponse; } _parseRulesBuffer(buffer) { const rulesResponse = {}; buffer = buffer.slice(5); [rulesResponse.ruleCount, buffer] = this._readInt16LE(buffer); rulesResponse.rules = []; for (let i = 0; i < rulesResponse.ruleCount; i++) { let rule; [rule, buffer] = this._readRule(buffer); rulesResponse.rules.push(rule); } return rulesResponse; } _readString(buffer) { const endOfName = buffer.indexOf(0x00); const stringBuffer = buffer.subarray(0, endOfName); const modifiedBuffer = buffer.slice(endOfName + 1); return [stringBuffer.toString('utf-8'), modifiedBuffer]; } _readUInt8(buffer) { return [buffer.readUInt8(), buffer.slice(1)]; } _readInt16LE(buffer) { return [buffer.readInt16LE(), buffer.slice(2)]; } _readPlayer(buffer) { let player = {}; [player.index, buffer] = this._readUInt8(buffer); [player.name, buffer] = this._readString(buffer); player.score = buffer.readInt32LE(); buffer = buffer.slice(4); player.duration = buffer.readFloatLE(); buffer = buffer.slice(4); return [player, buffer]; } _readRule(buffer) { let rule = {}; [rule.name, buffer] = this._readString(buffer); [rule.value, buffer] = this._readString(buffer); return [rule, buffer]; } }