steam-server-query
Version:
Module which implements the Master Server Query Protocol and Game Server Queries.
125 lines (124 loc) • 4.98 kB
JavaScript
"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('.');
}
}