UNPKG

steam-server-query

Version:

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

125 lines (124 loc) 4.98 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.queryMasterServer = void 0; const promiseSocket_1 = require("../promiseSocket"); const ZERO_IP = '0.0.0.0:0'; const RESPONSE_START = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0x66, 0x0A]); /** * Fetch a Steam master server to retrieve a list of game server hosts. * * Read more [here](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol). * @param masterServer Host and port of the master server to call. * @param region The region of the world where you wish to find servers in. Use REGIONS.ALL for all regions. * @param filters Optional. Object which contains filters to be sent with the query. Default is { }. Read more [here](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter). * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1 second. * @param maxHosts Optional. Return a limited amount of hosts. Stops calling the master server after this limit is reached. Can be used to prevent getting rate limited. * @returns A promise including an array of game server hosts. */ async function queryMasterServer(masterServer, region, filters = {}, timeout = 1000, maxHosts) { const splitMasterServer = masterServer.split(':'); const host = splitMasterServer[0]; const port = parseInt(splitMasterServer[1]); const masterServerQuery = new MasterServerQuery(host, port, region, filters, timeout, maxHosts); const hosts = await masterServerQuery.fetchServers(); return hosts; } exports.queryMasterServer = queryMasterServer; class MasterServerQuery { constructor(_host, _port, _region, _filters, timeout, _maxHosts) { this._host = _host; this._port = _port; this._region = _region; this._filters = _filters; this._maxHosts = _maxHosts; this._seedId = ZERO_IP; this._hosts = []; this._promiseSocket = new promiseSocket_1.PromiseSocket(1, timeout); } ; async fetchServers() { do { let resultBuffer; try { resultBuffer = await this._promiseSocket.send(this._buildPacket(), this._host, this._port); // catch promise rejections and throw error } catch (err) { this._promiseSocket.closeSocket(); throw new Error(err); } const parsedHosts = this._parseBuffer(resultBuffer); this._seedId = parsedHosts[parsedHosts.length - 1]; this._hosts.push(...parsedHosts); if (this._maxHosts && this._hosts.length >= this._maxHosts && this._hosts[this._maxHosts - 1] !== ZERO_IP) { this._promiseSocket.closeSocket(); return this._hosts.slice(0, this._maxHosts); } } while (this._seedId !== ZERO_IP); this._promiseSocket.closeSocket(); // remove ZERO_IP from end of host list this._hosts.pop(); return this._hosts; } _buildPacket() { return Buffer.concat([ Buffer.from([0x31]), Buffer.from([this._region]), Buffer.from(this._seedId, 'ascii'), Buffer.from([0x00]), Buffer.from(this.formatFilters(), 'ascii'), ]); } formatFilters() { let str = ''; for (const key of Object.keys(this._filters)) { // @ts-ignore let val = this._filters[key]; str += '\\' + key + '\\'; if (key === 'nor' || key === 'nand') { str += Object.keys(val).length + this._slashifyObject(val); } else if (Array.isArray(val)) { str += val.join(','); } else { str += val; } } str += '\x00'; return str; } _slashifyObject(object) { let str = ''; for (const key of Object.keys(object)) { let val = object[key]; str += '\\' + key + '\\' + val; } return str; } _parseBuffer(buffer) { const hosts = []; if (buffer.compare(RESPONSE_START, 0, 6, 0, 6) === 0) { buffer = buffer.slice(6); } let i = 0; while (i < buffer.length) { const ip = this._numberToIp(buffer.readInt32BE(i)); const port = buffer[i + 4] << 8 | buffer[i + 5]; hosts.push(`${ip}:${port}`); i += 6; } return hosts; } _numberToIp(number) { var nbuffer = new ArrayBuffer(4); var ndv = new DataView(nbuffer); ndv.setUint32(0, number); var a = new Array(); for (var i = 0; i < 4; i++) { a[i] = ndv.getUint8(i); } return a.join('.'); } }