UNPKG

source-server-query

Version:

Query Source game servers using the Source Query Protocol.

253 lines (252 loc) 11.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const dgram_1 = __importDefault(require("dgram")); class SourceQuerySocket { constructor(options = {}) { this.close = () => { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close(); this.socket = undefined; }; this.info = (address, port, timeout = 1000) => __awaiter(this, void 0, void 0, function* () { const query = yield this.solicit({ address, port: parseInt(port, 10), family: '' }, 'T', 'Source Engine Query', timeout); const result = {}; let offset = 4; result.header = query.slice(offset, offset + 1); offset += 1; result.header = result.header.toString(); result.protocol = query.readInt8(offset); offset += 1; result.name = query.slice(offset, query.indexOf(0, offset)); offset += result.name.length + 1; result.name = result.name.toString(); result.map = query.slice(offset, query.indexOf(0, offset)); offset += result.map.length + 1; result.map = result.map.toString(); result.folder = query.slice(offset, query.indexOf(0, offset)); offset += result.folder.length + 1; result.folder = result.folder.toString(); result.game = query.slice(offset, query.indexOf(0, offset)); offset += result.game.length + 1; result.game = result.game.toString(); result.id = query.readInt16LE(offset); offset += 2; result.players = query.readInt8(offset); offset += 1; result.max_players = query.readInt8(offset); offset += 1; result.bots = query.readInt8(offset); offset += 1; result.server_type = query.slice(offset, offset + 1).toString(); offset += 1; result.environment = query.slice(offset, offset + 1).toString(); offset += 1; result.visibility = query.readInt8(offset); offset += 1; result.vac = query.readInt8(offset); offset += 1; result.version = query.slice(offset, query.indexOf(0, offset)); offset += result.version.length + 1; result.version = result.version.toString(); const extra = query.slice(offset); offset = 0; if (extra.length < 1) return result; const edf = extra.readInt8(offset); offset += 1; if (edf & 0x80) { result.port = extra.readInt16LE(offset); offset += 2; } if (edf & 0x10) { result.steamid = extra.readBigUInt64LE(offset); offset += 8; } if (edf & 0x40) { result.tvport = extra.readInt16LE(offset); offset += 2; result.tvname = extra.slice(offset, extra.indexOf(0, offset)); offset += result.tvname.length + 1; result.tvname = result.tvname.toString(); } if (edf & 0x20) { const keywords = extra.slice(offset, extra.indexOf(0, offset)); offset += keywords.length + 1; result.keywords = keywords.toString(); } if (edf & 0x01) { result.gameid = extra.readBigUInt64LE(offset); offset += 8; } return result; }); this.players = (address, port, timeout = 1000) => __awaiter(this, void 0, void 0, function* () { const query = yield this.solicit({ address, port: parseInt(port, 10), family: '' }, 'U', undefined, timeout); let offset = 5; const count = query.readInt8(offset); offset += 1; const result = []; for (let i = 0; i < count; i += 1) { const player = {}; player.index = query.readInt8(offset); offset += 1; player.name = query.slice(offset, query.indexOf(0, offset)); offset += player.name.length + 1; player.name = player.name.toString(); player.score = query.readInt32LE(offset); offset += 4; player.duration = query.readFloatLE(offset); offset += 4; result.push(player); } return result; }); this.rules = (address, port, timeout = 1000) => __awaiter(this, void 0, void 0, function* () { const query = yield this.solicit({ address, port: parseInt(port, 10), family: '' }, 'V', undefined, timeout); let offset = 0; const header = query.readInt32LE(offset); if (header === -2) throw new Error('Unsupported response received.'); offset += 4; offset += 1; const count = query.readInt16LE(offset); offset += 2; const result = []; for (let i = 0; i < count; i += 1) { const rule = {}; rule.name = query.slice(offset, query.indexOf(0, offset)); offset += rule.name.length + 1; rule.name = rule.name.toString(); rule.value = query.slice(offset, query.indexOf(0, offset)); offset += rule.value.length + 1; rule.value = rule.value.toString(); result.push(rule); } return result; }); if (options.port !== undefined) this.port = options.port; if (options.address !== undefined) this.address = options.address; if (options.exclusive !== undefined) this.exclusive = options.exclusive; if (options.fd !== undefined) this.fd = options.fd; } bind() { return new Promise((resolve, reject) => { const error = (err) => { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close(); return reject(err); }; const listening = () => { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeListener('error', error); return resolve(); }; this.socket = dgram_1.default.createSocket('udp4'); this.socket.once('error', error); this.socket.once('listening', listening); this.socket.bind({ port: this.port, address: this.address, exclusive: this.exclusive, fd: this.fd }); }); } assert() { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { var _a, _b; if (this.socket === undefined) { try { yield this.bind(); } catch (err) { return reject(err); } return resolve(); } try { (_a = this.socket) === null || _a === void 0 ? void 0 : _a.address(); } catch (err) { return (_b = this.socket) === null || _b === void 0 ? void 0 : _b.once('listening', () => resolve()); } return resolve(); })); } validate(einfo, rinfo, request, response) { if (rinfo.port !== einfo.port) return false; if (rinfo.address !== einfo.address) return false; return true; } send(einfo, request, duration) { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { var _a, _b; try { yield this.assert(); } catch (err) { return reject(err); } const timeout = setTimeout(() => { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeListener('message', message); return reject(new Error(`Request timed out. [${duration}ms]`)); }, duration); const message = (response, rinfo) => { var _a, _b; if (this.validate(einfo, rinfo, request, response) === false) return; clearTimeout(timeout); (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeListener('message', message); if (((_b = this.socket) === null || _b === void 0 ? void 0 : _b.listenerCount('message')) === 0) this.close(); return resolve(response); }; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on('message', message); (_b = this.socket) === null || _b === void 0 ? void 0 : _b.send(request, einfo.port, einfo.address, (err) => { if (err !== undefined && err !== null) return reject(err); }); })); } pack(header, payload, challenge) { const preamble = Buffer.alloc(4); preamble.writeInt32LE(-1, 0); const request = Buffer.from(header); const data = payload ? Buffer.concat([Buffer.from(payload), Buffer.alloc(1)]) : Buffer.alloc(0); let prologue = Buffer.alloc(0); if (challenge !== undefined) { prologue = Buffer.alloc(4); prologue.writeInt32LE(challenge); } return Buffer.concat([preamble, request, data, prologue, preamble]); } solicit(einfo, header, payload, duration) { return __awaiter(this, void 0, void 0, function* () { const request = this.pack(header, payload); const challenge = yield this.send(einfo, request, duration); const type = challenge.slice(4, 5).toString(); if (type === 'A') { const challenger = challenge.readInt32LE(5); const result = this.pack(header, payload, challenger); return yield this.send(einfo, result, duration); } return challenge; }); } } module.exports = new SourceQuerySocket(); module.exports.SourceQuerySocket = SourceQuerySocket;