Add eslint (#364)

* Add initial prettier and eslint configs

* Modify prettierrc

* Run eslint on everything

* Actually remove prettier

* Fix some eslints

* Remove label in gs2

* Update CHANGELOG

* Update eslintrc to specify es2021
This commit is contained in:
CosminPerRam 2023-09-19 19:52:35 +03:00 committed by GitHub
parent bff9507189
commit 93a9095d99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 6960 additions and 5211 deletions

13
.eslintrc.json Normal file
View file

@ -0,0 +1,13 @@
{
"env": {
"browser": false,
"es2021": true
},
"extends": "standard",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
}
}

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
text=auto

View file

@ -12,6 +12,7 @@
* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package. * Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package.
* Eco (2018) - Added support (requested by @dgibbs64). * Eco (2018) - Added support (requested by @dgibbs64).
* Core Keeper (2022) - Added support (by @dgibbs64). * Core Keeper (2022) - Added support (by @dgibbs64).
* Added eslint which spotted some unused variables and other lints.
### 4.1.0 ### 4.1.0
* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues. * Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues.

View file

@ -1,68 +1,67 @@
#!/usr/bin/env node #!/usr/bin/env node
import Minimist from 'minimist'; import Minimist from 'minimist'
import GameDig from './../lib/index.js'; import GameDig from './../lib/index.js'
const argv = Minimist(process.argv.slice(2), { const argv = Minimist(process.argv.slice(2), {
boolean: ['pretty','debug','givenPortOnly','requestRules'], boolean: ['pretty', 'debug', 'givenPortOnly', 'requestRules'],
string: ['guildId','listenUdpPort','ipFamily'] string: ['guildId', 'listenUdpPort', 'ipFamily']
}); })
const debug = argv.debug; const debug = argv.debug
delete argv.debug; delete argv.debug
const pretty = !!argv.pretty || debug; const pretty = !!argv.pretty || debug
delete argv.pretty; delete argv.pretty
const givenPortOnly = argv.givenPortOnly; const givenPortOnly = argv.givenPortOnly
delete argv.givenPortOnly; delete argv.givenPortOnly
let options = {}; const options = {}
for(const key of Object.keys(argv)) { for (const key of Object.keys(argv)) {
const value = argv[key]; const value = argv[key]
if(key === '_' || key.charAt(0) === '$') if (key === '_' || key.charAt(0) === '$') { continue }
continue;
options[key] = value; options[key] = value
} }
if (argv._.length >= 1) { if (argv._.length >= 1) {
const target = argv._[0]; const target = argv._[0]
const split = target.split(':'); const split = target.split(':')
options.host = split[0]; options.host = split[0]
if (split.length >= 2) { if (split.length >= 2) {
options.port = split[1]; options.port = split[1]
} }
} }
if (debug) { if (debug) {
options.debug = true; options.debug = true
} }
if (givenPortOnly) { if (givenPortOnly) {
options.givenPortOnly = true; options.givenPortOnly = true
} }
const printOnPretty = (object) => { const printOnPretty = (object) => {
if(pretty) { if (pretty) {
console.log(JSON.stringify(object,null,' ')); console.log(JSON.stringify(object, null, ' '))
} else { } else {
console.log(JSON.stringify(object)); console.log(JSON.stringify(object))
} }
} }
const gamedig = new GameDig(options); const gamedig = new GameDig(options)
gamedig.query(options) gamedig.query(options)
.then(printOnPretty) .then(printOnPretty)
.catch((error) => { .catch((error) => {
if (debug) { if (debug) {
if (error instanceof Error) { if (error instanceof Error) {
console.log(error.stack); console.log(error.stack)
} else { } else {
console.log(error); console.log(error)
} }
} else { } else {
if (error instanceof Error) { if (error instanceof Error) {
error = error.message; error = error.message
} }
printOnPretty({error: error}); printOnPretty({ error })
} }
}); })

View file

@ -1,21 +1,21 @@
#!/usr/bin/env node #!/usr/bin/env node
import * as fs from 'fs'; import * as fs from 'fs'
import GameResolver from "../lib/GameResolver"; import GameResolver from '../lib/GameResolver'
const gameResolver = new GameResolver(); const gameResolver = new GameResolver()
const generated = gameResolver.printReadme(); const generated = gameResolver.printReadme()
const readmeFilename = __dirname+'/../README.md'; const readmeFilename = __dirname + '/../README.md'
const readme = fs.readFileSync(readmeFilename, {encoding:'utf8'}); const readme = fs.readFileSync(readmeFilename, { encoding: 'utf8' })
const marker_top = '<!--- BEGIN GENERATED GAMES -->'; const marker_top = '<!--- BEGIN GENERATED GAMES -->'
const marker_bottom = '<!--- END GENERATED GAMES -->'; const marker_bottom = '<!--- END GENERATED GAMES -->'
let start = readme.indexOf(marker_top); let start = readme.indexOf(marker_top)
start += marker_top.length; start += marker_top.length
const end = readme.indexOf(marker_bottom); const end = readme.indexOf(marker_bottom)
const updated = readme.substring(0,start)+"\n\n"+generated+"\n"+readme.substring(end); const updated = readme.substring(0, start) + '\n\n' + generated + '\n' + readme.substring(end)
fs.writeFileSync(readmeFilename, updated); fs.writeFileSync(readmeFilename, updated)

View file

@ -1,20 +1,20 @@
import * as dns from 'dns'; import * as dns from 'dns'
import punycode from "punycode/punycode.js"; import punycode from 'punycode/punycode.js'
import { promisify } from "util"; import { promisify } from 'util'
const dnsLookupAsync = promisify(dns.lookup); const dnsLookupAsync = promisify(dns.lookup)
const dnsResolveAsync = promisify(dns.resolve); const dnsResolveAsync = promisify(dns.resolve)
export default class DnsResolver { export default class DnsResolver {
/** /**
* @param {Logger} logger * @param {Logger} logger
*/ */
constructor(logger) { constructor (logger) {
this.logger = logger; this.logger = logger
} }
isIp(host) { isIp (host) {
return !!host.match(/\d+\.\d+\.\d+\.\d+/); return !!host.match(/\d+\.\d+\.\d+\.\d+/)
} }
/** /**
@ -24,55 +24,55 @@ export default class DnsResolver {
* @param {string=} srvRecordPrefix * @param {string=} srvRecordPrefix
* @returns {Promise<{address:string, port:number=}>} * @returns {Promise<{address:string, port:number=}>}
*/ */
async resolve(host, ipFamily, srvRecordPrefix) { async resolve (host, ipFamily, srvRecordPrefix) {
this.logger.debug("DNS Lookup: " + host); this.logger.debug('DNS Lookup: ' + host)
if(this.isIp(host)) { if (this.isIp(host)) {
this.logger.debug("Raw IP Address: " + host); this.logger.debug('Raw IP Address: ' + host)
return {address: host}; return { address: host }
} }
const asciiForm = punycode.toASCII(host); const asciiForm = punycode.toASCII(host)
if (asciiForm !== host) { if (asciiForm !== host) {
this.logger.debug("Encoded punycode: " + host + " -> " + asciiForm); this.logger.debug('Encoded punycode: ' + host + ' -> ' + asciiForm)
host = asciiForm; host = asciiForm
} }
if (srvRecordPrefix) { if (srvRecordPrefix) {
this.logger.debug("SRV Resolve: " + srvRecordPrefix + '.' + host); this.logger.debug('SRV Resolve: ' + srvRecordPrefix + '.' + host)
let records; let records
try { try {
records = await dnsResolveAsync(srvRecordPrefix + '.' + host, 'SRV'); records = await dnsResolveAsync(srvRecordPrefix + '.' + host, 'SRV')
if (records.length >= 1) { if (records.length >= 1) {
this.logger.debug("Found SRV Records: ", records); this.logger.debug('Found SRV Records: ', records)
const record = records[0]; const record = records[0]
const srvPort = record.port; const srvPort = record.port
const srvHost = record.name; const srvHost = record.name
if (srvHost === host) { if (srvHost === host) {
throw new Error('Loop in DNS SRV records'); throw new Error('Loop in DNS SRV records')
} }
return { return {
port: srvPort, port: srvPort,
...await this.resolve(srvHost, ipFamily, srvRecordPrefix) ...await this.resolve(srvHost, ipFamily, srvRecordPrefix)
};
} }
this.logger.debug("No SRV Record"); }
this.logger.debug('No SRV Record')
} catch (e) { } catch (e) {
this.logger.debug(e); this.logger.debug(e)
} }
} }
this.logger.debug("Standard Resolve: " + host); this.logger.debug('Standard Resolve: ' + host)
const dnsResult = await dnsLookupAsync(host, ipFamily); const dnsResult = await dnsLookupAsync(host, ipFamily)
// For some reason, this sometimes returns a string address rather than an object. // For some reason, this sometimes returns a string address rather than an object.
// I haven't been able to reproduce, but it's been reported on the issue tracker. // I haven't been able to reproduce, but it's been reported on the issue tracker.
let address; let address
if (typeof dnsResult === 'string') { if (typeof dnsResult === 'string') {
address = dnsResult; address = dnsResult
} else { } else {
address = dnsResult.address; address = dnsResult.address
} }
this.logger.debug("Found address: " + address); this.logger.debug('Found address: ' + address)
return {address: address}; return { address }
} }
} }

View file

@ -1,122 +1,114 @@
import * as path from 'path'; import * as path from 'path'
import { fileURLToPath } from "url"; import { fileURLToPath } from 'url'
import * as fs from 'fs'; import * as fs from 'fs'
export default class GameResolver { export default class GameResolver {
constructor() { constructor () {
const loaded = this._readGames(); const loaded = this._readGames()
this.gamesByKey = loaded.gamesByKey; this.gamesByKey = loaded.gamesByKey
this.games = loaded.games; this.games = loaded.games
} }
lookup(type) { lookup (type) {
if(!type) if (!type) { throw Error('No game specified') }
throw Error('No game specified');
if(type.substring(0,9) === 'protocol-') { if (type.substring(0, 9) === 'protocol-') {
return { return {
protocol: type.substring(9) protocol: type.substring(9)
}; }
} }
const game = this.gamesByKey.get(type); const game = this.gamesByKey.get(type)
if(!game) if (!game) { throw Error('Invalid game: ' + type) }
throw Error('Invalid game: '+type);
return game.options; return game.options
} }
printReadme() { printReadme () {
let out = ''; let out = ''
out += '| GameDig Type ID | Name | See Also\n'; out += '| GameDig Type ID | Name | See Also\n'
out += '|---|---|---\n'; out += '|---|---|---\n'
const sorted = this.games const sorted = this.games
.filter(game => game.pretty) .filter(game => game.pretty)
.sort((a,b) => { .sort((a, b) => {
return a.pretty.localeCompare(b.pretty); return a.pretty.localeCompare(b.pretty)
}); })
for(const game of sorted) { for (const game of sorted) {
let keysOut = game.keys.map(key => '`'+key+'`').join('<br>'); const keysOut = game.keys.map(key => '`' + key + '`').join('<br>')
out += "| " + keysOut.padEnd(10, " ") + " " out += '| ' + keysOut.padEnd(10, ' ') + ' ' +
+ "| " + game.pretty; '| ' + game.pretty
let notes = []; const notes = []
if(game.extra.doc_notes) { if (game.extra.doc_notes) {
notes.push("[Notes](#" + game.extra.doc_notes + ")"); notes.push('[Notes](#' + game.extra.doc_notes + ')')
} }
if(game.options.protocol === 'valve') { if (game.options.protocol === 'valve') {
notes.push('[Valve Protocol](#valve)'); notes.push('[Valve Protocol](#valve)')
} }
if(notes.length) { if (notes.length) {
out += " | " + notes.join(', '); out += ' | ' + notes.join(', ')
} }
out += "\n"; out += '\n'
} }
return out; return out
} }
_readGames() { _readGames () {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename)
const gamesFile = path.normalize(__dirname+'/../games.txt'); const gamesFile = path.normalize(__dirname + '/../games.txt')
const lines = fs.readFileSync(gamesFile,'utf8').split('\n'); const lines = fs.readFileSync(gamesFile, 'utf8').split('\n')
const gamesByKey = new Map(); const gamesByKey = new Map()
const games = []; const games = []
for (let line of lines) { for (let line of lines) {
// strip comments // strip comments
const comment = line.indexOf('#'); const comment = line.indexOf('#')
if(comment !== -1) line = line.substring(0,comment); if (comment !== -1) line = line.substring(0, comment)
line = line.trim(); line = line.trim()
if(!line) continue; if (!line) continue
const split = line.split('|'); const split = line.split('|')
const keys = split[0].trim().split(','); const keys = split[0].trim().split(',')
const name = split[1].trim(); const name = split[1].trim()
const options = this._parseList(split[3]); const options = this._parseList(split[3])
options.protocol = split[2].trim(); options.protocol = split[2].trim()
const extra = this._parseList(split[4]); const extra = this._parseList(split[4])
const game = { const game = {
keys: keys, keys,
pretty: name, pretty: name,
options: options, options,
extra: extra extra
}; }
for (const key of keys) { for (const key of keys) {
gamesByKey.set(key, game); gamesByKey.set(key, game)
} }
games.push(game); games.push(game)
} }
return { gamesByKey, games }; return { gamesByKey, games }
} }
_parseList(str) { _parseList (str) {
if(!str) if (!str) { return {} }
return {};
let out = {}; const out = {}
for (const one of str.split(',')) { for (const one of str.split(',')) {
const equals = one.indexOf('='); const equals = one.indexOf('=')
const key = equals === -1 ? one : one.substring(0, equals); const key = equals === -1 ? one : one.substring(0, equals)
/** @type {string|number|boolean} */ /** @type {string|number|boolean} */
let value = equals === -1 ? '' : one.substring(equals + 1); let value = equals === -1 ? '' : one.substring(equals + 1)
if(value === 'true' || value === '') if (value === 'true' || value === '') { value = true } else if (value === 'false') { value = false } else if (!isNaN(parseInt(value))) { value = parseInt(value) }
value = true;
else if(value === 'false')
value = false;
else if(!isNaN(parseInt(value)))
value = parseInt(value);
out[key] = value; out[key] = value
} }
return out; return out
} }
} }

View file

@ -1,69 +1,69 @@
import { createSocket } from 'dgram'
import { createSocket } from "dgram"; import { debugDump } from './HexUtil.js'
import { debugDump } from "./HexUtil.js"; import { promisify } from 'util'
import { promisify } from "util"; import Logger from './Logger.js'
import Logger from "./Logger.js";
export default class GlobalUdpSocket { export default class GlobalUdpSocket {
constructor({port}) { constructor ({ port }) {
this.socket = null; this.socket = null
this.callbacks = new Set(); this.callbacks = new Set()
this.debuggingCallbacks = new Set(); this.debuggingCallbacks = new Set()
this.logger = new Logger(); this.logger = new Logger()
this.port = port; this.port = port
} }
async _getSocket() { async _getSocket () {
if (!this.socket) { if (!this.socket) {
const udpSocket = createSocket({ const udpSocket = createSocket({
type: 'udp4', type: 'udp4',
reuseAddr: true reuseAddr: true
}); })
udpSocket.unref(); udpSocket.unref()
udpSocket.on('message', (buffer, rinfo) => { udpSocket.on('message', (buffer, rinfo) => {
const fromAddress = rinfo.address; const fromAddress = rinfo.address
const fromPort = rinfo.port; const fromPort = rinfo.port
this.logger.debug(log => { this.logger.debug(log => {
log(fromAddress + ':' + fromPort + " <--UDP(" + this.port + ")"); log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')')
log(debugDump(buffer)); log(debugDump(buffer))
}); })
for (const callback of this.callbacks) { for (const callback of this.callbacks) {
callback(fromAddress, fromPort, buffer); callback(fromAddress, fromPort, buffer)
} }
}); })
udpSocket.on('error', e => { udpSocket.on('error', e => {
this.logger.debug("UDP ERROR:", e); this.logger.debug('UDP ERROR:', e)
}); })
await promisify(udpSocket.bind).bind(udpSocket)(this.port); await promisify(udpSocket.bind).bind(udpSocket)(this.port)
this.port = udpSocket.address().port; this.port = udpSocket.address().port
this.socket = udpSocket; this.socket = udpSocket
} }
return this.socket; return this.socket
} }
async send(buffer, address, port, debug) { async send (buffer, address, port, debug) {
const socket = await this._getSocket(); const socket = await this._getSocket()
if (debug) { if (debug) {
this.logger._print(log => { this.logger._print(log => {
log(address + ':' + port + " UDP(" + this.port + ")-->"); log(address + ':' + port + ' UDP(' + this.port + ')-->')
log(debugDump(buffer)); log(debugDump(buffer))
}); })
} }
await promisify(socket.send).bind(socket)(buffer,0,buffer.length,port,address); await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address)
} }
addCallback(callback, debug) { addCallback (callback, debug) {
this.callbacks.add(callback); this.callbacks.add(callback)
if (debug) { if (debug) {
this.debuggingCallbacks.add(callback); this.debuggingCallbacks.add(callback)
this.logger.debugEnabled = true; this.logger.debugEnabled = true
} }
} }
removeCallback(callback) {
this.callbacks.delete(callback); removeCallback (callback) {
this.debuggingCallbacks.delete(callback); this.callbacks.delete(callback)
this.logger.debugEnabled = this.debuggingCallbacks.size > 0; this.debuggingCallbacks.delete(callback)
this.logger.debugEnabled = this.debuggingCallbacks.size > 0
} }
} }

View file

@ -1,21 +1,20 @@
/** @param {Buffer} buffer */ /** @param {Buffer} buffer */
export const debugDump = (buffer) => { export const debugDump = (buffer) => {
let hexLine = ''; let hexLine = ''
let chrLine = ''; let chrLine = ''
let out = ''; let out = ''
out += "Buffer length: " + buffer.length + " bytes\n"; out += 'Buffer length: ' + buffer.length + ' bytes\n'
for(let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
const sliced = buffer.slice(i,i+1); const sliced = buffer.slice(i, i + 1)
hexLine += sliced.toString('hex')+' '; hexLine += sliced.toString('hex') + ' '
let chr = sliced.toString(); let chr = sliced.toString()
if(chr < ' ' || chr > '~') chr = ' '; if (chr < ' ' || chr > '~') chr = ' '
chrLine += chr+' '; chrLine += chr + ' '
if(hexLine.length > 60 || i === buffer.length - 1) { if (hexLine.length > 60 || i === buffer.length - 1) {
out += hexLine + '\n'; out += hexLine + '\n'
out += chrLine + '\n'; out += chrLine + '\n'
hexLine = chrLine = ''; hexLine = chrLine = ''
} }
} }
return out; return out
} }

View file

@ -1,44 +1,44 @@
import {debugDump} from './HexUtil.js'; import { debugDump } from './HexUtil.js'
export default class Logger { export default class Logger {
constructor() { constructor () {
this.debugEnabled = false; this.debugEnabled = false
this.prefix = ''; this.prefix = ''
} }
debug(...args) { debug (...args) {
if (!this.debugEnabled) return; if (!this.debugEnabled) return
this._print(...args); this._print(...args)
} }
_print(...args) { _print (...args) {
try { try {
const strings = this._convertArgsToStrings(...args); const strings = this._convertArgsToStrings(...args)
if (strings.length) { if (strings.length) {
if (this.prefix) { if (this.prefix) {
strings.unshift(this.prefix); strings.unshift(this.prefix)
} }
console.log(...strings); console.log(...strings)
} }
} catch(e) { } catch (e) {
console.log("Error while logging: " + e); console.log('Error while logging: ' + e)
} }
} }
_convertArgsToStrings(...args) { _convertArgsToStrings (...args) {
const out = []; const out = []
for (const arg of args) { for (const arg of args) {
if (arg instanceof Error) { if (arg instanceof Error) {
out.push(arg.stack); out.push(arg.stack)
} else if (arg instanceof Buffer) { } else if (arg instanceof Buffer) {
out.push(debugDump(arg)); out.push(debugDump(arg))
} else if (typeof arg == 'function') { } else if (typeof arg === 'function') {
const result = arg.call(undefined, (...args) => this._print(...args)); const result = arg.call(undefined, (...args) => this._print(...args))
if (result !== undefined) out.push(...this._convertArgsToStrings(result)); if (result !== undefined) out.push(...this._convertArgsToStrings(result))
} else { } else {
out.push(arg); out.push(arg)
} }
} }
return out; return out
} }
} }

View file

@ -1,18 +1,18 @@
export default class Promises { export default class Promises {
static createTimeout(timeoutMs, timeoutMsg) { static createTimeout (timeoutMs, timeoutMsg) {
let cancel = null; let cancel = null
const wrapped = new Promise((res, rej) => { const wrapped = new Promise((resolve, reject) => {
const timeout = setTimeout( const timeout = setTimeout(
() => { () => {
rej(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")); reject(new Error(timeoutMsg + ' - Timed out after ' + timeoutMs + 'ms'))
}, },
timeoutMs timeoutMs
); )
cancel = () => { cancel = () => {
clearTimeout(timeout); clearTimeout(timeout)
}; }
}); })
wrapped.cancel = cancel; wrapped.cancel = cancel
return wrapped; return wrapped
} }
} }

View file

@ -1,8 +1,7 @@
import * as Protocols from '../protocols/index.js' import * as Protocols from '../protocols/index.js'
export const getProtocol = (protocolId) => { export const getProtocol = (protocolId) => {
if(!(protocolId in Protocols)) if (!(protocolId in Protocols)) { throw Error('Protocol definition file missing: ' + protocolId) }
throw Error('Protocol definition file missing: ' + protocolId);
return new Protocols[protocolId]; return new Protocols[protocolId]()
} }

View file

@ -1,27 +1,27 @@
import GameResolver from "./GameResolver.js"; import GameResolver from './GameResolver.js'
import {getProtocol} from './ProtocolResolver.js'; import { getProtocol } from './ProtocolResolver.js'
import GlobalUdpSocket from "./GlobalUdpSocket.js"; import GlobalUdpSocket from './GlobalUdpSocket.js'
const defaultOptions = { const defaultOptions = {
socketTimeout: 2000, socketTimeout: 2000,
attemptTimeout: 10000, attemptTimeout: 10000,
maxAttempts: 1, maxAttempts: 1,
ipFamily: 0 ipFamily: 0
}; }
export default class QueryRunner { export default class QueryRunner {
constructor(runnerOpts = {}) { constructor (runnerOpts = {}) {
this.udpSocket = new GlobalUdpSocket({ this.udpSocket = new GlobalUdpSocket({
port: runnerOpts.listenUdpPort port: runnerOpts.listenUdpPort
}); })
this.gameResolver = new GameResolver(); this.gameResolver = new GameResolver()
} }
async run(userOptions) { async run (userOptions) {
for (const key of Object.keys(userOptions)) { for (const key of Object.keys(userOptions)) {
const value = userOptions[key]; const value = userOptions[key]
if (['port', 'ipFamily'].includes(key)) { if (['port', 'ipFamily'].includes(key)) {
userOptions[key] = parseInt(value); userOptions[key] = parseInt(value)
} }
} }
@ -29,69 +29,67 @@ export default class QueryRunner {
port_query: gameQueryPort, port_query: gameQueryPort,
port_query_offset: gameQueryPortOffset, port_query_offset: gameQueryPortOffset,
...gameOptions ...gameOptions
} = this.gameResolver.lookup(userOptions.type); } = this.gameResolver.lookup(userOptions.type)
let attempts = []; const attempts = []
const optionsCollection = { const optionsCollection = {
...defaultOptions, ...defaultOptions,
...gameOptions, ...gameOptions,
...userOptions ...userOptions
}; }
const addAttemptWithPort = port => { const addAttemptWithPort = port => {
attempts.push({ attempts.push({
...optionsCollection, ...optionsCollection,
port port
}); })
} }
if (userOptions.port) { if (userOptions.port) {
if(!userOptions.givenPortOnly) { if (!userOptions.givenPortOnly) {
if (gameQueryPortOffset) if (gameQueryPortOffset) { addAttemptWithPort(userOptions.port + gameQueryPortOffset) }
addAttemptWithPort(userOptions.port + gameQueryPortOffset);
if (userOptions.port === gameOptions.port && gameQueryPort) if (userOptions.port === gameOptions.port && gameQueryPort) { addAttemptWithPort(gameQueryPort) }
addAttemptWithPort(gameQueryPort);
} }
attempts.push(optionsCollection); attempts.push(optionsCollection)
} else if (gameQueryPort) { } else if (gameQueryPort) {
addAttemptWithPort(gameQueryPort); addAttemptWithPort(gameQueryPort)
} else if (gameOptions.port) { } else if (gameOptions.port) {
addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0)); addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0))
} else { } else {
// Hopefully the request doesn't need a port. If it does, it'll fail when making the request. // Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
attempts.push(optionsCollection); attempts.push(optionsCollection)
} }
const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts; const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts
let attemptNum = 0; let attemptNum = 0
const errors = []; const errors = []
for (const attempt of attempts) { for (const attempt of attempts) {
for (let retry = 0; retry < numRetries; retry++) { for (let retry = 0; retry < numRetries; retry++) {
attemptNum++; attemptNum++
try { try {
return await this._attempt(attempt); return await this._attempt(attempt)
} catch (e) { } catch (e) {
e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack; e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack
errors.push(e); errors.push(e)
} }
} }
} }
const err = new Error('Failed all ' + errors.length + ' attempts'); const err = new Error('Failed all ' + errors.length + ' attempts')
for (const e of errors) { for (const e of errors) {
err.stack += '\n' + e.stack; err.stack += '\n' + e.stack
} }
throw err; throw err
} }
async _attempt(options) { async _attempt (options) {
const core = getProtocol(options.protocol); const core = getProtocol(options.protocol)
core.options = options; core.options = options
core.udpSocket = this.udpSocket; core.udpSocket = this.udpSocket
return await core.runOnceSafe(); return await core.runOnceSafe()
} }
} }

View file

@ -1,43 +1,42 @@
export class Player { export class Player {
name = ''; name = ''
raw = {}; raw = {}
constructor(data) { constructor (data) {
if (typeof data === 'string') { if (typeof data === 'string') {
this.name = data; this.name = data
} else { } else {
const {name, ...raw} = data; const { name, ...raw } = data
if (name) this.name = name; if (name) this.name = name
if (raw) this.raw = raw; if (raw) this.raw = raw
} }
} }
} }
export class Players extends Array { export class Players extends Array {
setNum(num) { setNum (num) {
// If the server specified some ridiculous number of players (billions), we don't want to // If the server specified some ridiculous number of players (billions), we don't want to
// run out of ram allocating these objects. // run out of ram allocating these objects.
num = Math.min(num, 10000); num = Math.min(num, 10000)
while(this.length < num) { while (this.length < num) {
this.push({}); this.push({})
} }
} }
push(data) { push (data) {
super.push(new Player(data)); super.push(new Player(data))
} }
} }
export class Results { export class Results {
name = ''; name = ''
map = ''; map = ''
password = false; password = false
raw = {}; raw = {}
maxplayers = 0; maxplayers = 0
players = new Players(); players = new Players()
bots = new Players(); bots = new Players()
} }

View file

@ -1,24 +1,23 @@
import QueryRunner from './QueryRunner.js'; import QueryRunner from './QueryRunner.js'
let singleton = null; let singleton = null
export default class Gamedig { export default class Gamedig {
constructor(runnerOpts) { constructor (runnerOpts) {
this.queryRunner = new QueryRunner(runnerOpts); this.queryRunner = new QueryRunner(runnerOpts)
} }
async query(userOptions) { async query (userOptions) {
return await this.queryRunner.run(userOptions); return await this.queryRunner.run(userOptions)
} }
static getInstance() { static getInstance () {
if (!singleton) if (!singleton) { singleton = new Gamedig() }
singleton = new Gamedig();
return singleton; return singleton
} }
static async query(...args) { static async query (...args) {
return await Gamedig.getInstance().query(...args); return await Gamedig.getInstance().query(...args)
} }
} }

View file

@ -1,17 +1,17 @@
import Iconv from "iconv-lite"; import Iconv from 'iconv-lite'
import Long from 'long'; import Long from 'long'
import {Buffer} from "buffer"; import { Buffer } from 'buffer'
import Varint from 'varint'; import Varint from 'varint'
function readUInt64BE(buffer,offset) { function readUInt64BE (buffer, offset) {
const high = buffer.readUInt32BE(offset); const high = buffer.readUInt32BE(offset)
const low = buffer.readUInt32BE(offset+4); const low = buffer.readUInt32BE(offset + 4)
return new Long(low,high,true); return new Long(low, high, true)
} }
function readUInt64LE(buffer,offset) { function readUInt64LE (buffer, offset) {
const low = buffer.readUInt32LE(offset); const low = buffer.readUInt32LE(offset)
const high = buffer.readUInt32LE(offset+4); const high = buffer.readUInt32LE(offset + 4)
return new Long(low,high,true); return new Long(low, high, true)
} }
export default class Reader { export default class Reader {
@ -19,154 +19,154 @@ export default class Reader {
* @param {Core} query * @param {Core} query
* @param {Buffer} buffer * @param {Buffer} buffer
**/ **/
constructor(query,buffer) { constructor (query, buffer) {
this.defaultEncoding = query.options.encoding || query.encoding; this.defaultEncoding = query.options.encoding || query.encoding
this.defaultDelimiter = query.delimiter; this.defaultDelimiter = query.delimiter
this.defaultByteOrder = query.byteorder; this.defaultByteOrder = query.byteorder
this.buffer = buffer; this.buffer = buffer
this.i = 0; this.i = 0
} }
setOffset(offset) { setOffset (offset) {
this.i = offset; this.i = offset
} }
offset() { offset () {
return this.i; return this.i
} }
skip(i) { skip (i) {
this.i += i; this.i += i
} }
pascalString(bytesForSize, adjustment=0) { pascalString (bytesForSize, adjustment = 0) {
const length = this.uint(bytesForSize) + adjustment; const length = this.uint(bytesForSize) + adjustment
return this.string(length); return this.string(length)
} }
string(arg) { string (arg) {
let encoding = this.defaultEncoding; let encoding = this.defaultEncoding
let length = null; let length = null
let delimiter = this.defaultDelimiter; let delimiter = this.defaultDelimiter
if(typeof arg === 'string') delimiter = arg; if (typeof arg === 'string') delimiter = arg
else if(typeof arg === 'number') length = arg; else if (typeof arg === 'number') length = arg
else if(typeof arg === 'object') { else if (typeof arg === 'object') {
if ('encoding' in arg) encoding = arg.encoding; if ('encoding' in arg) encoding = arg.encoding
if ('length' in arg) length = arg.length; if ('length' in arg) length = arg.length
if ('delimiter' in arg) delimiter = arg.delimiter; if ('delimiter' in arg) delimiter = arg.delimiter
} }
if(encoding === 'latin1') encoding = 'win1252'; if (encoding === 'latin1') encoding = 'win1252'
const start = this.i; const start = this.i
let end = start; let end = start
if(length === null) { if (length === null) {
// terminated by the delimiter // terminated by the delimiter
let delim = delimiter; let delim = delimiter
if (typeof delim === 'string') delim = delim.charCodeAt(0); if (typeof delim === 'string') delim = delim.charCodeAt(0)
while (true) { while (true) {
if (end >= this.buffer.length) { if (end >= this.buffer.length) {
end = this.buffer.length; end = this.buffer.length
break; break
} }
if (this.buffer.readUInt8(end) === delim) break; if (this.buffer.readUInt8(end) === delim) break
end++; end++
} }
this.i = end + 1; this.i = end + 1
} else if (length <= 0) { } else if (length <= 0) {
return ''; return ''
} else { } else {
end = start+length; end = start + length
if(end >= this.buffer.length) { if (end >= this.buffer.length) {
end = this.buffer.length; end = this.buffer.length
} }
this.i = end; this.i = end
} }
const slice = this.buffer.slice(start, end); const slice = this.buffer.slice(start, end)
const enc = encoding; const enc = encoding
if(enc === 'utf8' || enc === 'ucs2' || enc === 'binary') { if (enc === 'utf8' || enc === 'ucs2' || enc === 'binary') {
return slice.toString(enc); return slice.toString(enc)
} else { } else {
return Iconv.decode(slice,enc); return Iconv.decode(slice, enc)
} }
} }
int(bytes) { int (bytes) {
let r = 0; let r = 0
if(this.remaining() >= bytes) { if (this.remaining() >= bytes) {
if(this.defaultByteOrder === 'be') { if (this.defaultByteOrder === 'be') {
if(bytes === 1) r = this.buffer.readInt8(this.i); if (bytes === 1) r = this.buffer.readInt8(this.i)
else if(bytes === 2) r = this.buffer.readInt16BE(this.i); else if (bytes === 2) r = this.buffer.readInt16BE(this.i)
else if(bytes === 4) r = this.buffer.readInt32BE(this.i); else if (bytes === 4) r = this.buffer.readInt32BE(this.i)
} else { } else {
if(bytes === 1) r = this.buffer.readInt8(this.i); if (bytes === 1) r = this.buffer.readInt8(this.i)
else if(bytes === 2) r = this.buffer.readInt16LE(this.i); else if (bytes === 2) r = this.buffer.readInt16LE(this.i)
else if(bytes === 4) r = this.buffer.readInt32LE(this.i); else if (bytes === 4) r = this.buffer.readInt32LE(this.i)
} }
} }
this.i += bytes; this.i += bytes
return r; return r
} }
/** @returns {number} */ /** @returns {number} */
uint(bytes) { uint (bytes) {
let r = 0; let r = 0
if(this.remaining() >= bytes) { if (this.remaining() >= bytes) {
if(this.defaultByteOrder === 'be') { if (this.defaultByteOrder === 'be') {
if(bytes === 1) r = this.buffer.readUInt8(this.i); if (bytes === 1) r = this.buffer.readUInt8(this.i)
else if(bytes === 2) r = this.buffer.readUInt16BE(this.i); else if (bytes === 2) r = this.buffer.readUInt16BE(this.i)
else if(bytes === 4) r = this.buffer.readUInt32BE(this.i); else if (bytes === 4) r = this.buffer.readUInt32BE(this.i)
else if(bytes === 8) r = readUInt64BE(this.buffer,this.i); else if (bytes === 8) r = readUInt64BE(this.buffer, this.i)
} else { } else {
if(bytes === 1) r = this.buffer.readUInt8(this.i); if (bytes === 1) r = this.buffer.readUInt8(this.i)
else if(bytes === 2) r = this.buffer.readUInt16LE(this.i); else if (bytes === 2) r = this.buffer.readUInt16LE(this.i)
else if(bytes === 4) r = this.buffer.readUInt32LE(this.i); else if (bytes === 4) r = this.buffer.readUInt32LE(this.i)
else if(bytes === 8) r = readUInt64LE(this.buffer,this.i); else if (bytes === 8) r = readUInt64LE(this.buffer, this.i)
} }
} }
this.i += bytes; this.i += bytes
return r; return r
} }
float() { float () {
let r = 0; let r = 0
if(this.remaining() >= 4) { if (this.remaining() >= 4) {
if(this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i); if (this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i)
else r = this.buffer.readFloatLE(this.i); else r = this.buffer.readFloatLE(this.i)
} }
this.i += 4; this.i += 4
return r; return r
} }
varint() { varint () {
const out = Varint.decode(this.buffer, this.i); const out = Varint.decode(this.buffer, this.i)
this.i += Varint.decode.bytes; this.i += Varint.decode.bytes
return out; return out
} }
/** @returns Buffer */ /** @returns Buffer */
part(bytes) { part (bytes) {
let r; let r
if(this.remaining() >= bytes) { if (this.remaining() >= bytes) {
r = this.buffer.slice(this.i,this.i+bytes); r = this.buffer.slice(this.i, this.i + bytes)
} else { } else {
r = Buffer.from([]); r = Buffer.from([])
} }
this.i += bytes; this.i += bytes
return r; return r
} }
remaining() { remaining () {
return this.buffer.length-this.i; return this.buffer.length - this.i
} }
rest() { rest () {
return this.buffer.slice(this.i); return this.buffer.slice(this.i)
} }
done() { done () {
return this.i >= this.buffer.length; return this.i >= this.buffer.length
} }
} }

1730
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,10 @@
{ {
"name": "gamedig", "name": "gamedig",
"description": "Query for the status of any game server in Node.JS", "description": "Query for the status of any game server in Node.JS",
"scripts": {
"lint:check": "eslint .",
"lint:fix": "eslint --fix ."
},
"keywords": [ "keywords": [
"srcds", "srcds",
"query", "query",
@ -35,17 +39,6 @@
"engines": { "engines": {
"node": ">=14.17.0" "node": ">=14.17.0"
}, },
"dependencies": {
"cheerio": "^1.0.0-rc.10",
"gbxremote": "^0.2.1",
"got": "^12.1.0",
"iconv-lite": "^0.6.3",
"long": "^5.2.0",
"minimist": "^1.2.6",
"punycode": "^2.3.0",
"seek-bzip": "^2.0.0",
"varint": "^6.0.0"
},
"bin": { "bin": {
"gamedig": "bin/gamedig.js" "gamedig": "bin/gamedig.js"
}, },
@ -58,8 +51,24 @@
"GAMES_LIST.md", "GAMES_LIST.md",
"README.md" "README.md"
], ],
"dependencies": {
"cheerio": "^1.0.0-rc.10",
"gbxremote": "^0.2.1",
"got": "^12.1.0",
"iconv-lite": "^0.6.3",
"long": "^5.2.0",
"minimist": "^1.2.6",
"punycode": "^2.3.0",
"seek-bzip": "^2.0.0",
"varint": "^6.0.0"
},
"devDependencies": { "devDependencies": {
"@types/cheerio": "^0.22.31", "@types/cheerio": "^0.22.31",
"@types/node": "^14.18.13" "@types/node": "^14.18.13",
"eslint": "^8.49.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-n": "15.7.0",
"eslint-plugin-promise": "^6.1.1"
} }
} }

View file

@ -1,63 +1,65 @@
import Core from './core.js'; import Core from './core.js'
export default class armagetron extends Core { export default class armagetron extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]); const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
const buffer = await this.udpSend(b,b => b); const buffer = await this.udpSend(b, b => b)
const reader = this.reader(buffer); const reader = this.reader(buffer)
reader.skip(6); reader.skip(6)
state.gamePort = this.readUInt(reader); state.gamePort = this.readUInt(reader)
state.raw.hostname = this.readString(reader); state.raw.hostname = this.readString(reader)
state.name = this.stripColorCodes(this.readString(reader)); state.name = this.stripColorCodes(this.readString(reader))
state.raw.numplayers = this.readUInt(reader); state.raw.numplayers = this.readUInt(reader)
state.raw.versionmin = this.readUInt(reader); state.raw.versionmin = this.readUInt(reader)
state.raw.versionmax = this.readUInt(reader); state.raw.versionmax = this.readUInt(reader)
state.raw.version = this.readString(reader); state.raw.version = this.readString(reader)
state.maxplayers = this.readUInt(reader); state.maxplayers = this.readUInt(reader)
const players = this.readString(reader); const players = this.readString(reader)
const list = players.split('\n'); const list = players.split('\n')
for(const name of list) { for (const name of list) {
if(!name) continue; if (!name) continue
state.players.push({ state.players.push({
name: this.stripColorCodes(name) name: this.stripColorCodes(name)
}); })
} }
state.raw.options = this.stripColorCodes(this.readString(reader)); state.raw.options = this.stripColorCodes(this.readString(reader))
state.raw.uri = this.readString(reader); state.raw.uri = this.readString(reader)
state.raw.globalids = this.readString(reader); state.raw.globalids = this.readString(reader)
} }
readUInt(reader) { readUInt (reader) {
const a = reader.uint(2); const a = reader.uint(2)
const b = reader.uint(2); const b = reader.uint(2)
return (b<<16) + a; return (b << 16) + a
}
readString(reader) {
const len = reader.uint(2);
if(!len) return '';
let out = '';
for(let i = 0; i < len; i += 2) {
const hi = reader.uint(1);
const lo = reader.uint(1);
if(i+1<len) out += String.fromCharCode(lo);
if(i+2<len) out += String.fromCharCode(hi);
} }
return out; readString (reader) {
const len = reader.uint(2)
if (!len) return ''
let out = ''
for (let i = 0; i < len; i += 2) {
const hi = reader.uint(1)
const lo = reader.uint(1)
if (i + 1 < len) out += String.fromCharCode(lo)
if (i + 2 < len) out += String.fromCharCode(hi)
} }
stripColorCodes(str) {
return str.replace(/0x[0-9a-f]{6}/g,''); return out
}
stripColorCodes (str) {
return str.replace(/0x[0-9a-f]{6}/g, '')
} }
} }

View file

@ -1,45 +1,45 @@
import Core from './core.js'; import Core from './core.js'
export default class ase extends Core { export default class ase extends Core {
async run(state) { async run (state) {
const buffer = await this.udpSend('s',(buffer) => { const buffer = await this.udpSend('s', (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const header = reader.string(4); const header = reader.string(4)
if (header === 'EYE1') return reader.rest(); if (header === 'EYE1') return reader.rest()
}); })
const reader = this.reader(buffer); const reader = this.reader(buffer)
state.raw.gamename = this.readString(reader); state.raw.gamename = this.readString(reader)
state.gamePort = parseInt(this.readString(reader)); state.gamePort = parseInt(this.readString(reader))
state.name = this.readString(reader); state.name = this.readString(reader)
state.raw.gametype = this.readString(reader); state.raw.gametype = this.readString(reader)
state.map = this.readString(reader); state.map = this.readString(reader)
state.raw.version = this.readString(reader); state.raw.version = this.readString(reader)
state.password = this.readString(reader) === '1'; state.password = this.readString(reader) === '1'
state.raw.numplayers = parseInt(this.readString(reader)); state.raw.numplayers = parseInt(this.readString(reader))
state.maxplayers = parseInt(this.readString(reader)); state.maxplayers = parseInt(this.readString(reader))
while(!reader.done()) { while (!reader.done()) {
const key = this.readString(reader); const key = this.readString(reader)
if(!key) break; if (!key) break
const value = this.readString(reader); const value = this.readString(reader)
state.raw[key] = value; state.raw[key] = value
} }
while(!reader.done()) { while (!reader.done()) {
const flags = reader.uint(1); const flags = reader.uint(1)
const player = {}; const player = {}
if(flags & 1) player.name = this.readString(reader); if (flags & 1) player.name = this.readString(reader)
if(flags & 2) player.team = this.readString(reader); if (flags & 2) player.team = this.readString(reader)
if(flags & 4) player.skin = this.readString(reader); if (flags & 4) player.skin = this.readString(reader)
if(flags & 8) player.score = parseInt(this.readString(reader)); if (flags & 8) player.score = parseInt(this.readString(reader))
if(flags & 16) player.ping = parseInt(this.readString(reader)); if (flags & 16) player.ping = parseInt(this.readString(reader))
if(flags & 32) player.time = parseInt(this.readString(reader)); if (flags & 32) player.time = parseInt(this.readString(reader))
state.players.push(player); state.players.push(player)
} }
} }
readString(reader) { readString (reader) {
return reader.pascalString(1, -1); return reader.pascalString(1, -1)
} }
} }

View file

@ -1,27 +1,27 @@
import Core from './core.js'; import Core from './core.js'
export default class assettocorsa extends Core { export default class assettocorsa extends Core {
async run(state) { async run (state) {
const serverInfo = await this.request({ const serverInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/INFO`, url: `http://${this.options.address}:${this.options.port}/INFO`,
responseType: 'json' responseType: 'json'
}); })
const carInfo = await this.request({ const carInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`, url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`,
responseType: 'json' responseType: 'json'
}); })
if (!serverInfo || !carInfo || !carInfo.Cars) { if (!serverInfo || !carInfo || !carInfo.Cars) {
throw new Error('Query not successful'); throw new Error('Query not successful')
} }
state.maxplayers = serverInfo.maxclients; state.maxplayers = serverInfo.maxclients
state.name = serverInfo.name; state.name = serverInfo.name
state.map = serverInfo.track; state.map = serverInfo.track
state.password = serverInfo.pass; state.password = serverInfo.pass
state.gamePort = serverInfo.port; state.gamePort = serverInfo.port
state.raw.carInfo = carInfo.Cars; state.raw.carInfo = carInfo.Cars
state.raw.serverInfo = serverInfo; state.raw.serverInfo = serverInfo
for (const car of carInfo.Cars) { for (const car of carInfo.Cars) {
if (car.IsConnected) { if (car.IsConnected) {
@ -31,7 +31,7 @@ export default class assettocorsa extends Core {
skin: car.Skin, skin: car.Skin,
nation: car.DriverNation, nation: car.DriverNation,
team: car.DriverTeam team: car.DriverTeam
}); })
} }
} }
} }

View file

@ -1,161 +1,162 @@
import Core from './core.js'; import Core from './core.js'
export default class battlefield extends Core { export default class battlefield extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
} }
async run(state) { async run (state) {
await this.withTcp(async socket => { await this.withTcp(async socket => {
{ {
const data = await this.query(socket, ['serverInfo']); const data = await this.query(socket, ['serverInfo'])
state.name = data.shift(); state.name = data.shift()
state.raw.numplayers = parseInt(data.shift()); state.raw.numplayers = parseInt(data.shift())
state.maxplayers = parseInt(data.shift()); state.maxplayers = parseInt(data.shift())
state.raw.gametype = data.shift(); state.raw.gametype = data.shift()
state.map = data.shift(); state.map = data.shift()
state.raw.roundsplayed = parseInt(data.shift()); state.raw.roundsplayed = parseInt(data.shift())
state.raw.roundstotal = parseInt(data.shift()); state.raw.roundstotal = parseInt(data.shift())
const teamCount = data.shift(); const teamCount = data.shift()
state.raw.teams = []; state.raw.teams = []
for (let i = 0; i < teamCount; i++) { for (let i = 0; i < teamCount; i++) {
const tickets = parseFloat(data.shift()); const tickets = parseFloat(data.shift())
state.raw.teams.push({ state.raw.teams.push({
tickets: tickets tickets
}); })
} }
state.raw.targetscore = parseInt(data.shift()); state.raw.targetscore = parseInt(data.shift())
state.raw.status = data.shift(); state.raw.status = data.shift()
// Seems like the fields end at random places beyond this point // Seems like the fields end at random places beyond this point
// depending on the server version // depending on the server version
if (data.length) state.raw.ranked = (data.shift() === 'true'); if (data.length) state.raw.ranked = (data.shift() === 'true')
if (data.length) state.raw.punkbuster = (data.shift() === 'true'); if (data.length) state.raw.punkbuster = (data.shift() === 'true')
if (data.length) state.password = (data.shift() === 'true'); if (data.length) state.password = (data.shift() === 'true')
if (data.length) state.raw.uptime = parseInt(data.shift()); if (data.length) state.raw.uptime = parseInt(data.shift())
if (data.length) state.raw.roundtime = parseInt(data.shift()); if (data.length) state.raw.roundtime = parseInt(data.shift())
const isBadCompany2 = data[0] === 'BC2'; const isBadCompany2 = data[0] === 'BC2'
if (isBadCompany2) { if (isBadCompany2) {
if (data.length) data.shift(); if (data.length) data.shift()
if (data.length) data.shift(); if (data.length) data.shift()
} }
if (data.length) { if (data.length) {
state.raw.ip = data.shift(); state.raw.ip = data.shift()
const split = state.raw.ip.split(':'); const split = state.raw.ip.split(':')
state.gameHost = split[0]; state.gameHost = split[0]
state.gamePort = split[1]; state.gamePort = split[1]
} else { } else {
// best guess if the server doesn't tell us what the server port is // best guess if the server doesn't tell us what the server port is
// these are just the default game ports for different default query ports // these are just the default game ports for different default query ports
if (this.options.port === 48888) state.gamePort = 7673; if (this.options.port === 48888) state.gamePort = 7673
if (this.options.port === 22000) state.gamePort = 25200; if (this.options.port === 22000) state.gamePort = 25200
} }
if (data.length) state.raw.punkbusterversion = data.shift(); if (data.length) state.raw.punkbusterversion = data.shift()
if (data.length) state.raw.joinqueue = (data.shift() === 'true'); if (data.length) state.raw.joinqueue = (data.shift() === 'true')
if (data.length) state.raw.region = data.shift(); if (data.length) state.raw.region = data.shift()
if (data.length) state.raw.pingsite = data.shift(); if (data.length) state.raw.pingsite = data.shift()
if (data.length) state.raw.country = data.shift(); if (data.length) state.raw.country = data.shift()
if (data.length) state.raw.quickmatch = (data.shift() === 'true'); if (data.length) state.raw.quickmatch = (data.shift() === 'true')
} }
{ {
const data = await this.query(socket, ['version']); const data = await this.query(socket, ['version'])
data.shift(); data.shift()
state.raw.version = data.shift(); state.raw.version = data.shift()
} }
{ {
const data = await this.query(socket, ['listPlayers', 'all']); const data = await this.query(socket, ['listPlayers', 'all'])
const fieldCount = parseInt(data.shift()); const fieldCount = parseInt(data.shift())
const fields = []; const fields = []
for (let i = 0; i < fieldCount; i++) { for (let i = 0; i < fieldCount; i++) {
fields.push(data.shift()); fields.push(data.shift())
} }
const numplayers = data.shift(); const numplayers = data.shift()
for (let i = 0; i < numplayers; i++) { for (let i = 0; i < numplayers; i++) {
const player = {}; const player = {}
for (let key of fields) { for (let key of fields) {
let value = data.shift(); let value = data.shift()
if (key === 'teamId') key = 'team'; if (key === 'teamId') key = 'team'
else if (key === 'squadId') key = 'squad'; else if (key === 'squadId') key = 'squad'
if ( if (
key === 'kills' key === 'kills' ||
|| key === 'deaths' key === 'deaths' ||
|| key === 'score' key === 'score' ||
|| key === 'rank' key === 'rank' ||
|| key === 'team' key === 'team' ||
|| key === 'squad' key === 'squad' ||
|| key === 'ping' key === 'ping' ||
|| key === 'type' key === 'type'
) { ) {
value = parseInt(value); value = parseInt(value)
} }
player[key] = value; player[key] = value
} }
state.players.push(player); state.players.push(player)
} }
} }
}); })
} }
async query(socket, params) { async query (socket, params) {
const outPacket = this.buildPacket(params); const outPacket = this.buildPacket(params)
return await this.tcpSend(socket, outPacket, (data) => { return await this.tcpSend(socket, outPacket, (data) => {
const decoded = this.decodePacket(data); const decoded = this.decodePacket(data)
if(decoded) { if (decoded) {
this.logger.debug(decoded); this.logger.debug(decoded)
if(decoded.shift() !== 'OK') throw new Error('Missing OK'); if (decoded.shift() !== 'OK') throw new Error('Missing OK')
return decoded; return decoded
} }
}); })
} }
buildPacket(params) { buildPacket (params) {
const paramBuffers = []; const paramBuffers = []
for (const param of params) { for (const param of params) {
paramBuffers.push(Buffer.from(param,'utf8')); paramBuffers.push(Buffer.from(param, 'utf8'))
} }
let totalLength = 12; let totalLength = 12
for (const paramBuffer of paramBuffers) { for (const paramBuffer of paramBuffers) {
totalLength += paramBuffer.length+1+4; totalLength += paramBuffer.length + 1 + 4
} }
const b = Buffer.alloc(totalLength); const b = Buffer.alloc(totalLength)
b.writeUInt32LE(0,0); b.writeUInt32LE(0, 0)
b.writeUInt32LE(totalLength,4); b.writeUInt32LE(totalLength, 4)
b.writeUInt32LE(params.length,8); b.writeUInt32LE(params.length, 8)
let offset = 12; let offset = 12
for (const paramBuffer of paramBuffers) { for (const paramBuffer of paramBuffers) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4; b.writeUInt32LE(paramBuffer.length, offset); offset += 4
paramBuffer.copy(b, offset); offset += paramBuffer.length; paramBuffer.copy(b, offset); offset += paramBuffer.length
b.writeUInt8(0, offset); offset += 1; b.writeUInt8(0, offset); offset += 1
} }
return b; return b
} }
decodePacket(buffer) {
if(buffer.length < 8) return false;
const reader = this.reader(buffer);
const header = reader.uint(4);
const totalLength = reader.uint(4);
if(buffer.length < totalLength) return false;
this.logger.debug("Expected " + totalLength + " bytes, have " + buffer.length);
const paramCount = reader.uint(4); decodePacket (buffer) {
const params = []; if (buffer.length < 8) return false
for(let i = 0; i < paramCount; i++) { const reader = this.reader(buffer)
params.push(reader.pascalString(4)); reader.uint(4) // header
const strNull = reader.uint(1); const totalLength = reader.uint(4)
if (buffer.length < totalLength) return false
this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length)
const paramCount = reader.uint(4)
const params = []
for (let i = 0; i < paramCount; i++) {
params.push(reader.pascalString(4))
reader.uint(1) // strNull
} }
return params; return params
} }
} }

View file

@ -1,45 +1,45 @@
import Core from './core.js'; import Core from './core.js'
import * as cheerio from "cheerio"; import * as cheerio from 'cheerio'
export default class buildandshoot extends Core { export default class buildandshoot extends Core {
async run(state) { async run (state) {
const body = await this.request({ const body = await this.request({
url: 'http://'+this.options.address+':'+this.options.port+'/', url: 'http://' + this.options.address + ':' + this.options.port + '/'
}); })
let m; let m
m = body.match(/status server for (.*?)\.?[\r\n]/); m = body.match(/status server for (.*?)\.?[\r\n]/)
if(m) state.name = m[1]; if (m) state.name = m[1]
m = body.match(/Current uptime: (\d+)/); m = body.match(/Current uptime: (\d+)/)
if(m) state.raw.uptime = m[1]; if (m) state.raw.uptime = m[1]
m = body.match(/currently running (.*?) by /); m = body.match(/currently running (.*?) by /)
if(m) state.map = m[1]; if (m) state.map = m[1]
m = body.match(/Current players: (\d+)\/(\d+)/); m = body.match(/Current players: (\d+)\/(\d+)/)
if(m) {
state.raw.numplayers = m[1];
state.maxplayers = m[2];
}
m = body.match(/aos:\/\/[0-9]+:[0-9]+/);
if (m) { if (m) {
state.connect = m[0]; state.raw.numplayers = m[1]
state.maxplayers = m[2]
} }
const $ = cheerio.load(body); m = body.match(/aos:\/\/[0-9]+:[0-9]+/)
$('#playerlist tbody tr').each((i,tr) => { if (m) {
state.connect = m[0]
}
const $ = cheerio.load(body)
$('#playerlist tbody tr').each((i, tr) => {
if (!$(tr).find('td').first().attr('colspan')) { if (!$(tr).find('td').first().attr('colspan')) {
state.players.push({ state.players.push({
name: $(tr).find('td').eq(2).text(), name: $(tr).find('td').eq(2).text(),
ping: $(tr).find('td').eq(3).text().trim(), ping: $(tr).find('td').eq(3).text().trim(),
team: $(tr).find('td').eq(4).text().toLowerCase(), team: $(tr).find('td').eq(4).text().toLowerCase(),
score: parseInt($(tr).find('td').eq(5).text()) score: parseInt($(tr).find('td').eq(5).text())
}); })
} }
}); })
/* /*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) { if(m) {

View file

@ -1,155 +1,156 @@
import {EventEmitter} from "events"; import { EventEmitter } from 'events'
import * as net from "net"; import * as net from 'net'
import Reader from "../lib/reader.js"; import Reader from '../lib/reader.js'
import {debugDump} from '../lib/HexUtil.js'; import { debugDump } from '../lib/HexUtil.js'
import Logger from "../lib/Logger.js"; import Logger from '../lib/Logger.js'
import DnsResolver from "../lib/DnsResolver.js"; import DnsResolver from '../lib/DnsResolver.js'
import {Results} from "../lib/Results.js"; import { Results } from '../lib/Results.js'
import Promises from "../lib/Promises.js"; import Promises from '../lib/Promises.js'
let uid = 0; let uid = 0
export default class Core extends EventEmitter { export default class Core extends EventEmitter {
constructor() { constructor () {
super(); super()
this.encoding = 'utf8'; this.encoding = 'utf8'
this.byteorder = 'le'; this.byteorder = 'le'
this.delimiter = '\0'; this.delimiter = '\0'
this.srvRecord = null; this.srvRecord = null
this.abortedPromise = null; this.abortedPromise = null
this.logger = new Logger(); this.logger = new Logger()
this.dnsResolver = new DnsResolver(this.logger); this.dnsResolver = new DnsResolver(this.logger)
// Sent to us by QueryRunner // Sent to us by QueryRunner
this.options = null; this.options = null
/** @type GlobalUdpSocket */ /** @type GlobalUdpSocket */
this.udpSocket = null; this.udpSocket = null
this.shortestRTT = 0; this.shortestRTT = 0
this.usedTcp = false; this.usedTcp = false
} }
// Runs a single attempt with a timeout and cleans up afterward // Runs a single attempt with a timeout and cleans up afterward
async runOnceSafe() { async runOnceSafe () {
if (this.options.debug) { if (this.options.debug) {
this.logger.debugEnabled = true; this.logger.debugEnabled = true
} }
this.logger.prefix = 'Q#' + (uid++); this.logger.prefix = 'Q#' + (uid++)
this.logger.debug("Starting"); this.logger.debug('Starting')
this.logger.debug("Protocol: " + this.constructor.name); this.logger.debug('Protocol: ' + this.constructor.name)
this.logger.debug("Options:", this.options); this.logger.debug('Options:', this.options)
let abortCall = null; let abortCall = null
this.abortedPromise = new Promise((resolve,reject) => { this.abortedPromise = new Promise((resolve, reject) => {
abortCall = () => reject(new Error("Query is finished -- cancelling outstanding promises")); abortCall = () => reject(new Error('Query is finished -- cancelling outstanding promises'))
}).catch(() => { }).catch(() => {
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection // Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
}); })
let timeout; let timeout
try { try {
const promise = this.runOnce(); const promise = this.runOnce()
timeout = Promises.createTimeout(this.options.attemptTimeout, "Attempt"); timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
const result = await Promise.race([promise, timeout]); const result = await Promise.race([promise, timeout])
this.logger.debug("Query was successful"); this.logger.debug('Query was successful')
return result; return result
} catch(e) { } catch (e) {
this.logger.debug("Query failed with error", e); this.logger.debug('Query failed with error', e)
throw e; throw e
} finally { } finally {
timeout && timeout.cancel(); timeout && timeout.cancel()
try { try {
abortCall(); abortCall()
} catch(e) { } catch (e) {
this.logger.debug("Error during abort cleanup: " + e.stack); this.logger.debug('Error during abort cleanup: ' + e.stack)
} }
} }
} }
async runOnce() { async runOnce () {
const options = this.options; const options = this.options
if (('host' in options) && !('address' in options)) { if (('host' in options) && !('address' in options)) {
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord); const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
options.address = resolved.address; options.address = resolved.address
if (resolved.port) options.port = resolved.port; if (resolved.port) options.port = resolved.port
} }
const state = new Results(); const state = new Results()
await this.run(state); await this.run(state)
// because lots of servers prefix with spaces to try to appear first // because lots of servers prefix with spaces to try to appear first
state.name = (state.name || '').trim(); state.name = (state.name || '').trim()
if (!('connect' in state)) { if (!('connect' in state)) {
state.connect = '' state.connect = '' +
+ (state.gameHost || this.options.host || this.options.address) (state.gameHost || this.options.host || this.options.address) +
+ ':' ':' +
+ (state.gamePort || this.options.port) (state.gamePort || this.options.port)
} }
state.ping = this.shortestRTT; state.ping = this.shortestRTT
delete state.gameHost; delete state.gameHost
delete state.gamePort; delete state.gamePort
this.logger.debug(log => { this.logger.debug(log => {
log("Size of players array: " + state.players.length); log('Size of players array: ' + state.players.length)
log("Size of bots array: " + state.bots.length); log('Size of bots array: ' + state.bots.length)
}); })
return state; return state
} }
async run(/** Results */ state) {} async run (/** Results */ state) {}
/** Param can be a time in ms, or a promise (which will be timed) */ /** Param can be a time in ms, or a promise (which will be timed) */
registerRtt(param) { registerRtt (param) {
if (param.then) { if (param.then) {
const start = Date.now(); const start = Date.now()
param.then(() => { param.then(() => {
const end = Date.now(); const end = Date.now()
const rtt = end - start; const rtt = end - start
this.registerRtt(rtt); this.registerRtt(rtt)
}).catch(() => {}); }).catch(() => {})
} else { } else {
this.logger.debug("Registered RTT: " + param + "ms"); this.logger.debug('Registered RTT: ' + param + 'ms')
if (this.shortestRTT === 0 || param < this.shortestRTT) { if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param; this.shortestRTT = param
} }
} }
} }
// utils // utils
/** @returns {Reader} */ /** @returns {Reader} */
reader(buffer) { reader (buffer) {
return new Reader(this,buffer); return new Reader(this, buffer)
} }
translate(obj,trans) {
for(const from of Object.keys(trans)) { translate (obj, trans) {
const to = trans[from]; for (const from of Object.keys(trans)) {
if(from in obj) { const to = trans[from]
if(to) obj[to] = obj[from]; if (from in obj) {
delete obj[from]; if (to) obj[to] = obj[from]
delete obj[from]
} }
} }
} }
trueTest(str) { trueTest (str) {
if(typeof str === 'boolean') return str; if (typeof str === 'boolean') return str
if(typeof str === 'number') return str !== 0; if (typeof str === 'number') return str !== 0
if(typeof str === 'string') { if (typeof str === 'string') {
if(str.toLowerCase() === 'true') return true; if (str.toLowerCase() === 'true') return true
if(str.toLowerCase() === 'yes') return true; if (str.toLowerCase() === 'yes') return true
if(str === '1') return true; if (str === '1') return true
} }
return false; return false
} }
assertValidPort(port) { assertValidPort (port) {
if (!port) { if (!port) {
throw new Error("Could not determine port to query. Did you provide a port?"); throw new Error('Could not determine port to query. Did you provide a port?')
} }
if (port < 1 || port > 65535) { if (port < 1 || port > 65535) {
throw new Error("Invalid tcp/ip port: " + port); throw new Error('Invalid tcp/ip port: ' + port)
} }
} }
@ -159,52 +160,52 @@ export default class Core extends EventEmitter {
* @param {number=} port * @param {number=} port
* @returns {Promise<T>} * @returns {Promise<T>}
*/ */
async withTcp(fn, port) { async withTcp (fn, port) {
this.usedTcp = true; this.usedTcp = true
const address = this.options.address; const address = this.options.address
if (!port) port = this.options.port; if (!port) port = this.options.port
this.assertValidPort(port); this.assertValidPort(port)
let socket, connectionTimeout; let socket, connectionTimeout
try { try {
socket = net.connect(port,address); socket = net.connect(port, address)
socket.setNoDelay(true); socket.setNoDelay(true)
// Prevent unhandled 'error' events from dumping straight to console // Prevent unhandled 'error' events from dumping straight to console
socket.on('error', () => {}); socket.on('error', () => {})
this.logger.debug(log => { this.logger.debug(log => {
this.logger.debug(address+':'+port+" TCP Connecting"); this.logger.debug(address + ':' + port + ' TCP Connecting')
const writeHook = socket.write; const writeHook = socket.write
socket.write = (...args) => { socket.write = (...args) => {
log(address+':'+port+" TCP-->"); log(address + ':' + port + ' TCP-->')
log(debugDump(args[0])); log(debugDump(args[0]))
writeHook.apply(socket,args); writeHook.apply(socket, args)
}; }
socket.on('error', e => log('TCP Error:', e)); socket.on('error', e => log('TCP Error:', e))
socket.on('close', () => log('TCP Closed')); socket.on('close', () => log('TCP Closed'))
socket.on('data', (data) => { socket.on('data', (data) => {
log(address+':'+port+" <--TCP"); log(address + ':' + port + ' <--TCP')
log(data); log(data)
}); })
socket.on('ready', () => log(address+':'+port+" TCP Connected")); socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
}); })
const connectionPromise = new Promise((resolve,reject) => { const connectionPromise = new Promise((resolve, reject) => {
socket.on('ready', resolve); socket.on('ready', resolve)
socket.on('close', () => reject(new Error('TCP Connection Refused'))); socket.on('close', () => reject(new Error('TCP Connection Refused')))
}); })
this.registerRtt(connectionPromise); this.registerRtt(connectionPromise)
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening'); connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
await Promise.race([ await Promise.race([
connectionPromise, connectionPromise,
connectionTimeout, connectionTimeout,
this.abortedPromise this.abortedPromise
]); ])
return await fn(socket); return await fn(socket)
} finally { } finally {
socket && socket.destroy(); socket && socket.destroy()
connectionTimeout && connectionTimeout.cancel(); connectionTimeout && connectionTimeout.cancel()
} }
} }
@ -215,26 +216,26 @@ export default class Core extends EventEmitter {
* @param {function(Buffer):T} ondata * @param {function(Buffer):T} ondata
* @returns Promise<T> * @returns Promise<T>
*/ */
async tcpSend(socket,buffer,ondata) { async tcpSend (socket, buffer, ondata) {
let timeout; let timeout
try { try {
const promise = new Promise(async (resolve, reject) => { const promise = new Promise((resolve, reject) => {
let received = Buffer.from([]); let received = Buffer.from([])
const onData = (data) => { const onData = (data) => {
received = Buffer.concat([received, data]); received = Buffer.concat([received, data])
const result = ondata(received); const result = ondata(received)
if (result !== undefined) { if (result !== undefined) {
socket.removeListener('data', onData); socket.removeListener('data', onData)
resolve(result); resolve(result)
} }
}; }
socket.on('data', onData); socket.on('data', onData)
socket.write(buffer); socket.write(buffer)
}); })
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP'); timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
return await Promise.race([promise, timeout, this.abortedPromise]); return await Promise.race([promise, timeout, this.abortedPromise])
} finally { } finally {
timeout && timeout.cancel(); timeout && timeout.cancel()
} }
} }
@ -245,105 +246,105 @@ export default class Core extends EventEmitter {
* @returns Promise<T> * @returns Promise<T>
* @template T * @template T
*/ */
async udpSend(buffer,onPacket,onTimeout) { async udpSend (buffer, onPacket, onTimeout) {
const address = this.options.address; const address = this.options.address
const port = this.options.port; const port = this.options.port
this.assertValidPort(port); this.assertValidPort(port)
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary'); if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
const socket = this.udpSocket; const socket = this.udpSocket
await socket.send(buffer, address, port, this.options.debug); await socket.send(buffer, address, port, this.options.debug)
if (!onPacket && !onTimeout) { if (!onPacket && !onTimeout) {
return null; return null
} }
let socketCallback; let socketCallback
let timeout; let timeout
try { try {
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
const start = Date.now(); const start = Date.now()
let end = null; let end = null
socketCallback = (fromAddress, fromPort, buffer) => { socketCallback = (fromAddress, fromPort, buffer) => {
try { try {
if (fromAddress !== address) return; if (fromAddress !== address) return
if (fromPort !== port) return; if (fromPort !== port) return
if (end === null) { if (end === null) {
end = Date.now(); end = Date.now()
const rtt = end-start; const rtt = end - start
this.registerRtt(rtt); this.registerRtt(rtt)
} }
const result = onPacket(buffer); const result = onPacket(buffer)
if (result !== undefined) { if (result !== undefined) {
this.logger.debug("UDP send finished by callback"); this.logger.debug('UDP send finished by callback')
resolve(result); resolve(result)
} }
} catch(e) { } catch (e) {
reject(e); reject(e)
} }
}; }
socket.addCallback(socketCallback, this.options.debug); socket.addCallback(socketCallback, this.options.debug)
}); })
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP'); timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP')
const wrappedTimeout = new Promise((resolve, reject) => { const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => { timeout.catch((e) => {
this.logger.debug("UDP timeout detected"); this.logger.debug('UDP timeout detected')
if (onTimeout) { if (onTimeout) {
try { try {
const result = onTimeout(); const result = onTimeout()
if (result !== undefined) { if (result !== undefined) {
this.logger.debug("UDP timeout resolved by callback"); this.logger.debug('UDP timeout resolved by callback')
resolve(result); resolve(result)
return; return
} }
} catch(e) { } catch (e) {
reject(e); reject(e)
} }
} }
reject(e); reject(e)
}); })
}); })
return await Promise.race([promise, wrappedTimeout, this.abortedPromise]); return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
} finally { } finally {
timeout && timeout.cancel(); timeout && timeout.cancel()
socketCallback && socket.removeCallback(socketCallback); socketCallback && socket.removeCallback(socketCallback)
} }
} }
async tcpPing() { async tcpPing () {
// This will give a much more accurate RTT than using the rtt of an http request. // This will give a much more accurate RTT than using the rtt of an http request.
if (!this.usedTcp) { if (!this.usedTcp) {
await this.withTcp(() => {}); await this.withTcp(() => {})
} }
} }
async request(params) { async request (params) {
await this.tcpPing(); await this.tcpPing()
const got = (await import('got')).got; const got = (await import('got')).got
let requestPromise; let requestPromise
try { try {
requestPromise = got({ requestPromise = got({
...params, ...params,
timeout: { timeout: {
request: this.options.socketTimeout request: this.options.socketTimeout
} }
}); })
this.logger.debug(log => { this.logger.debug(log => {
log(() => params.url + " HTTP-->"); log(() => params.url + ' HTTP-->')
requestPromise requestPromise
.then((response) => log(params.url + " <--HTTP " + response.statusCode)) .then((response) => log(params.url + ' <--HTTP ' + response.statusCode))
.catch(() => {}); .catch(() => {})
}); })
const wrappedPromise = requestPromise.then(response => { const wrappedPromise = requestPromise.then(response => {
if (response.statusCode !== 200) throw new Error("Bad status code: " + response.statusCode); if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode)
return response.body; return response.body
}); })
return await Promise.race([wrappedPromise, this.abortedPromise]); return await Promise.race([wrappedPromise, this.abortedPromise])
} finally { } finally {
requestPromise && requestPromise.cancel(); requestPromise && requestPromise.cancel()
} }
} }
} }

View file

@ -1,67 +1,65 @@
import Core from './core.js'; import Core from './core.js'
export default class cs2d extends Core { export default class cs2d extends Core {
async run(state) { async run (state) {
{
const reader = await this.sendQuery( const reader = await this.sendQuery(
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'), Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
Buffer.from('\x01\x00\xFB\x01', 'binary') Buffer.from('\x01\x00\xFB\x01', 'binary')
); )
const flags = reader.uint(1); const flags = reader.uint(1)
state.raw.flags = flags; state.raw.flags = flags
state.password = this.readFlag(flags, 0); state.password = this.readFlag(flags, 0)
state.raw.registeredOnly = this.readFlag(flags, 1); state.raw.registeredOnly = this.readFlag(flags, 1)
state.raw.fogOfWar = this.readFlag(flags, 2); state.raw.fogOfWar = this.readFlag(flags, 2)
state.raw.friendlyFire = this.readFlag(flags, 3); state.raw.friendlyFire = this.readFlag(flags, 3)
state.raw.botsEnabled = this.readFlag(flags, 5); state.raw.botsEnabled = this.readFlag(flags, 5)
state.raw.luaScripts = this.readFlag(flags, 6); state.raw.luaScripts = this.readFlag(flags, 6)
state.raw.forceLight = this.readFlag(flags, 7); state.raw.forceLight = this.readFlag(flags, 7)
state.name = this.readString(reader); state.name = this.readString(reader)
state.map = this.readString(reader); state.map = this.readString(reader)
state.raw.numplayers = reader.uint(1); state.raw.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1); state.maxplayers = reader.uint(1)
if (flags & 32) { if (flags & 32) {
state.raw.gamemode = reader.uint(1); state.raw.gamemode = reader.uint(1)
} else { } else {
state.raw.gamemode = 0; state.raw.gamemode = 0
} }
state.raw.numbots = reader.uint(1); state.raw.numbots = reader.uint(1)
const flags2 = reader.uint(1); const flags2 = reader.uint(1)
state.raw.flags2 = flags2; state.raw.flags2 = flags2
state.raw.recoil = this.readFlag(flags2, 0); state.raw.recoil = this.readFlag(flags2, 0)
state.raw.offScreenDamage = this.readFlag(flags2, 1); state.raw.offScreenDamage = this.readFlag(flags2, 1)
state.raw.hasDownloads = this.readFlag(flags2, 2); state.raw.hasDownloads = this.readFlag(flags2, 2)
reader.skip(2); reader.skip(2)
const players = reader.uint(1); const players = reader.uint(1)
for (let i = 0; i < players; i++) { for (let i = 0; i < players; i++) {
const player = {} const player = {}
player.id = reader.uint(1); player.id = reader.uint(1)
player.name = this.readString(reader); player.name = this.readString(reader)
player.team = reader.uint(1); player.team = reader.uint(1)
player.score = reader.uint(4); player.score = reader.uint(4)
player.deaths = reader.uint(4); player.deaths = reader.uint(4)
state.players.push(player); state.players.push(player)
}
} }
} }
async sendQuery(request, expectedHeader) { async sendQuery (request, expectedHeader) {
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly // Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
await this.udpSend(request); await this.udpSend(request)
await this.udpSend(request); await this.udpSend(request)
return await this.udpSend(request, (buffer) => { return await this.udpSend(request, (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const header = reader.part(4); const header = reader.part(4)
if (!header.equals(expectedHeader)) return; if (!header.equals(expectedHeader)) return
return reader; return reader
}); })
} }
readFlag(flags, offset) { readFlag (flags, offset) {
return !!(flags & (1 << offset)); return !!(flags & (1 << offset))
} }
readString(reader) { readString (reader) {
return reader.pascalString(1); return reader.pascalString(1)
} }
} }

View file

@ -1,29 +1,29 @@
import Core from './core.js'; import Core from './core.js'
export default class discord extends Core { export default class discord extends Core {
async run(state) { async run (state) {
const guildId = this.options.guildId; const guildId = this.options.guildId
if (typeof guildId !== 'string') { if (typeof guildId !== 'string') {
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
+ " (It's too large of a number for javascript to store without losing precision)"); " (It's too large of a number for javascript to store without losing precision)")
} }
this.usedTcp = true; this.usedTcp = true
const raw = await this.request({ const raw = await this.request({
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json', url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
}); })
const json = JSON.parse(raw); const json = JSON.parse(raw)
state.name = json.name; state.name = json.name
if (json.instant_invite) { if (json.instant_invite) {
state.connect = json.instant_invite; state.connect = json.instant_invite
} else { } else {
state.connect = 'https://discordapp.com/channels/' + guildId; state.connect = 'https://discordapp.com/channels/' + guildId
} }
for (const member of json.members) { for (const member of json.members) {
const {username: name, ...rest} = member; const { username: name, ...rest } = member
state.players.push({ name, ...rest }); state.players.push({ name, ...rest })
} }
delete json.members; delete json.members
state.maxplayers = 500000; state.maxplayers = 500000
state.raw = json; state.raw = json
} }
} }

View file

@ -1,149 +1,147 @@
import Core from './core.js'; import Core from './core.js'
export default class doom3 extends Core { export default class doom3 extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
} }
async run(state) {
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
const reader = this.reader(packet);
const header = reader.uint(2);
if(header !== 0xffff) return;
const header2 = reader.string();
if(header2 !== 'infoResponse') return;
const challengePart1 = reader.string(4);
if (challengePart1 !== "PiNG") return;
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string(4);
if (challengePart2 !== 'PoNg') reader.skip(-4);
return reader.rest();
});
let reader = this.reader(body); async run (state) {
const protoVersion = reader.uint(4); const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff); const reader = this.reader(packet)
const header = reader.uint(2)
if (header !== 0xffff) return
const header2 = reader.string()
if (header2 !== 'infoResponse') return
const challengePart1 = reader.string(4)
if (challengePart1 !== 'PiNG') return
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string(4)
if (challengePart2 !== 'PoNg') reader.skip(-4)
return reader.rest()
})
let reader = this.reader(body)
const protoVersion = reader.uint(4)
state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
// some doom implementations send us a packet size here, some don't (etqw does this) // some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive) // we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2); reader.skip(2)
const packetContainsSize = (reader.uint(2) === 0); const packetContainsSize = (reader.uint(2) === 0)
reader.skip(-4); reader.skip(-4)
if (packetContainsSize) { if (packetContainsSize) {
const size = reader.uint(4); const size = reader.uint(4)
this.logger.debug("Received packet size: " + size); this.logger.debug('Received packet size: ' + size)
} }
while(!reader.done()) { while (!reader.done()) {
const key = reader.string(); const key = reader.string()
let value = this.stripColors(reader.string()); let value = this.stripColors(reader.string())
if(key === 'si_map') { if (key === 'si_map') {
value = value.replace('maps/',''); value = value.replace('maps/', '')
value = value.replace('.entities',''); value = value.replace('.entities', '')
} }
if(!key) break; if (!key) break
state.raw[key] = value; state.raw[key] = value
this.logger.debug(key + "=" + value); this.logger.debug(key + '=' + value)
} }
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw'); const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
const rest = reader.rest(); const rest = reader.rest()
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false); let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false); if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true); if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
if (!playerResult) { if (!playerResult) {
throw new Error("Unable to find a suitable parse strategy for player list"); throw new Error('Unable to find a suitable parse strategy for player list')
} }
let players; let players;
[players,reader] = playerResult; [players, reader] = playerResult
for (const player of players) { for (const player of players) {
if(!player.ping || player.typeflag) if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
state.bots.push(player);
else
state.players.push(player);
} }
state.raw.osmask = reader.uint(4); state.raw.osmask = reader.uint(4)
if (isEtqw) { if (isEtqw) {
state.raw.ranked = reader.uint(1); state.raw.ranked = reader.uint(1)
state.raw.timeleft = reader.uint(4); state.raw.timeleft = reader.uint(4)
state.raw.gamestate = reader.uint(1); state.raw.gamestate = reader.uint(1)
state.raw.servertype = reader.uint(1); state.raw.servertype = reader.uint(1)
// 0 = regular, 1 = tv // 0 = regular, 1 = tv
if(state.raw.servertype === 0) { if (state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1); state.raw.interestedClients = reader.uint(1)
} else if(state.raw.servertype === 1) { } else if (state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4); state.raw.connectedClients = reader.uint(4)
state.raw.maxClients = reader.uint(4); state.raw.maxClients = reader.uint(4)
} }
} }
if (state.raw.si_name) state.name = state.raw.si_name; if (state.raw.si_name) state.name = state.raw.si_name
if (state.raw.si_map) state.map = state.raw.si_map; if (state.raw.si_map) state.map = state.raw.si_map
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers); if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers); if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
if (state.raw.si_usepass === '1') state.password = true; if (state.raw.si_usepass === '1') state.password = true
if (state.raw.si_needPass === '1') state.password = true; if (state.raw.si_needPass === '1') state.password = true
if (this.options.port === 27733) state.gamePort = 3074; // etqw has a different query and game port if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
} }
attemptPlayerParse(rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) { attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
this.logger.debug("starting player parse attempt:"); this.logger.debug('starting player parse attempt:')
this.logger.debug("isEtqw: " + isEtqw); this.logger.debug('isEtqw: ' + isEtqw)
this.logger.debug("hasClanTag: " + hasClanTag); this.logger.debug('hasClanTag: ' + hasClanTag)
this.logger.debug("hasClanTagPos: " + hasClanTagPos); this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
this.logger.debug("hasTypeFlag: " + hasTypeFlag); this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
const reader = this.reader(rest); const reader = this.reader(rest)
let lastId = -1; let lastId = -1
const players = []; const players = []
while(true) { while (true) {
this.logger.debug("---"); this.logger.debug('---')
if (reader.done()) { if (reader.done()) {
this.logger.debug("* aborting attempt, overran buffer *"); this.logger.debug('* aborting attempt, overran buffer *')
return null; return null
} }
const player = {}; const player = {}
player.id = reader.uint(1); player.id = reader.uint(1)
this.logger.debug("id: " + player.id); this.logger.debug('id: ' + player.id)
if (player.id <= lastId || player.id > 0x20) { if (player.id <= lastId || player.id > 0x20) {
this.logger.debug("* aborting attempt, invalid player id *"); this.logger.debug('* aborting attempt, invalid player id *')
return null; return null
} }
lastId = player.id; lastId = player.id
if(player.id === 0x20) { if (player.id === 0x20) {
this.logger.debug("* player parse successful *"); this.logger.debug('* player parse successful *')
break; break
} }
player.ping = reader.uint(2); player.ping = reader.uint(2)
this.logger.debug("ping: " + player.ping); this.logger.debug('ping: ' + player.ping)
if(!isEtqw) { if (!isEtqw) {
player.rate = reader.uint(4); player.rate = reader.uint(4)
this.logger.debug("rate: " + player.rate); this.logger.debug('rate: ' + player.rate)
} }
player.name = this.stripColors(reader.string()); player.name = this.stripColors(reader.string())
this.logger.debug("name: " + player.name); this.logger.debug('name: ' + player.name)
if(hasClanTag) { if (hasClanTag) {
if(hasClanTagPos) { if (hasClanTagPos) {
const clanTagPos = reader.uint(1); const clanTagPos = reader.uint(1)
this.logger.debug("clanTagPos: " + clanTagPos); this.logger.debug('clanTagPos: ' + clanTagPos)
} }
player.clantag = this.stripColors(reader.string()); player.clantag = this.stripColors(reader.string())
this.logger.debug("clan tag: " + player.clantag); this.logger.debug('clan tag: ' + player.clantag)
} }
if(hasTypeFlag) { if (hasTypeFlag) {
player.typeflag = reader.uint(1); player.typeflag = reader.uint(1)
this.logger.debug("type flag: " + player.typeflag); this.logger.debug('type flag: ' + player.typeflag)
} }
players.push(player); players.push(player)
} }
return [players,reader]; return [players, reader]
} }
stripColors(str) { stripColors (str) {
// uses quake 3 color codes // uses quake 3 color codes
return str.replace(/\^(X.{6}|.)/g,''); return str.replace(/\^(X.{6}|.)/g, '')
} }
} }

View file

@ -1,19 +1,19 @@
import Core from './core.js'; import Core from './core.js'
export default class eco extends Core { export default class eco extends Core {
async run(state) { async run (state) {
if (!this.options.port) this.options.port = 3001; if (!this.options.port) this.options.port = 3001
const request = await this.request({ const request = await this.request({
url: `http://${this.options.address}:${this.options.port}/frontpage`, url: `http://${this.options.address}:${this.options.port}/frontpage`,
responseType: 'json' responseType: 'json'
}); })
const serverInfo = request.Info; const serverInfo = request.Info
state.name = serverInfo.Description; state.name = serverInfo.Description
state.maxplayers = serverInfo.TotalPlayers; state.maxplayers = serverInfo.TotalPlayers
state.password = serverInfo.HasPassword; state.password = serverInfo.HasPassword
state.gamePort = serverInfo.GamePort; state.gamePort = serverInfo.GamePort
state.raw = serverInfo; state.raw = serverInfo
} }
} }

View file

@ -1,37 +1,38 @@
import valve from './valve.js'; import valve from './valve.js'
export default class ffow extends valve { export default class ffow extends valve {
constructor() { constructor () {
super(); super()
this.byteorder = 'be'; this.byteorder = 'be'
this.legacyChallenge = true; this.legacyChallenge = true
} }
async queryInfo(state) {
this.logger.debug("Requesting ffow info ..."); async queryInfo (state) {
this.logger.debug('Requesting ffow info ...')
const b = await this.sendPacket( const b = await this.sendPacket(
0x46, 0x46,
'LSQ', 'LSQ',
0x49 0x49
); )
const reader = this.reader(b); const reader = this.reader(b)
state.raw.protocol = reader.uint(1); state.raw.protocol = reader.uint(1)
state.name = reader.string(); state.name = reader.string()
state.map = reader.string(); state.map = reader.string()
state.raw.mod = reader.string(); state.raw.mod = reader.string()
state.raw.gamemode = reader.string(); state.raw.gamemode = reader.string()
state.raw.description = reader.string(); state.raw.description = reader.string()
state.raw.version = reader.string(); state.raw.version = reader.string()
state.gamePort = reader.uint(2); state.gamePort = reader.uint(2)
state.raw.numplayers = reader.uint(1); state.raw.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1); state.maxplayers = reader.uint(1)
state.raw.listentype = String.fromCharCode(reader.uint(1)); state.raw.listentype = String.fromCharCode(reader.uint(1))
state.raw.environment = String.fromCharCode(reader.uint(1)); state.raw.environment = String.fromCharCode(reader.uint(1))
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
state.raw.secure = reader.uint(1); state.raw.secure = reader.uint(1)
state.raw.averagefps = reader.uint(1); state.raw.averagefps = reader.uint(1)
state.raw.round = reader.uint(1); state.raw.round = reader.uint(1)
state.raw.maxrounds = reader.uint(1); state.raw.maxrounds = reader.uint(1)
state.raw.timeleft = reader.uint(2); state.raw.timeleft = reader.uint(2)
} }
} }

View file

@ -1,32 +1,32 @@
import quake2 from './quake2.js'; import quake2 from './quake2.js'
export default class fivem extends quake2 { export default class fivem extends quake2 {
constructor() { constructor () {
super(); super()
this.sendHeader = 'getinfo xxx'; this.sendHeader = 'getinfo xxx'
this.responseHeader = 'infoResponse'; this.responseHeader = 'infoResponse'
this.encoding = 'utf8'; this.encoding = 'utf8'
} }
async run(state) { async run (state) {
await super.run(state); await super.run(state)
{ {
const json = await this.request({ const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json', url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
responseType: 'json' responseType: 'json'
}); })
state.raw.info = json; state.raw.info = json
} }
{ {
const json = await this.request({ const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json', url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
responseType: 'json' responseType: 'json'
}); })
state.raw.players = json; state.raw.players = json
for (const player of json) { for (const player of json) {
state.players.push({name: player.name, ping: player.ping}); state.players.push({ name: player.name, ping: player.ping })
} }
} }
} }

View file

@ -1,179 +1,179 @@
import Core from './core.js'; import Core from './core.js'
const stringKeys = new Set([ const stringKeys = new Set([
'website', 'website',
'gametype', 'gametype',
'gamemode', 'gamemode',
'player' 'player'
]); ])
function normalizeEntry([key,value]) { function normalizeEntry ([key, value]) {
key = key.toLowerCase(); key = key.toLowerCase()
const split = key.split('_'); const split = key.split('_')
let keyType = key; let keyType = key
if (split.length === 2 && !isNaN(Number(split[1]))) { if (split.length === 2 && !isNaN(Number(split[1]))) {
keyType = split[0]; keyType = split[0]
} }
if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope" if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
if (value.toLowerCase() === 'true') { if (value.toLowerCase() === 'true') {
value = true; value = true
} else if (value.toLowerCase() === 'false') { } else if (value.toLowerCase() === 'false') {
value = false; value = false
} else if (value.length && !isNaN(Number(value))) { } else if (value.length && !isNaN(Number(value))) {
value = Number(value); value = Number(value)
} }
} }
return [key,value]; return [key, value]
} }
export default class gamespy1 extends Core { export default class gamespy1 extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
const raw = await this.sendPacket('\\status\\xserverquery'); const raw = await this.sendPacket('\\status\\xserverquery')
// Convert all keys to lowercase and normalize value types // Convert all keys to lowercase and normalize value types
const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry))); const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)))
state.raw = data; state.raw = data
if ('hostname' in data) state.name = data.hostname; if ('hostname' in data) state.name = data.hostname
if ('mapname' in data) state.map = data.mapname; if ('mapname' in data) state.map = data.mapname
if (this.trueTest(data.password)) state.password = true; if (this.trueTest(data.password)) state.password = true
if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers); if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers)
if ('hostport' in data) state.gamePort = Number(data.hostport); if ('hostport' in data) state.gamePort = Number(data.hostport)
const teamOffByOne = data.gamename === 'bfield1942'; const teamOffByOne = data.gamename === 'bfield1942'
const playersById = {}; const playersById = {}
const teamNamesById = {}; const teamNamesById = {}
for (const ident of Object.keys(data)) { for (const ident of Object.keys(data)) {
const split = ident.split('_'); const split = ident.split('_')
if (split.length !== 2) continue; if (split.length !== 2) continue
let key = split[0].toLowerCase(); let key = split[0].toLowerCase()
const id = Number(split[1]); const id = Number(split[1])
if (isNaN(id)) continue; if (isNaN(id)) continue
let value = data[ident]; let value = data[ident]
delete data[ident]; delete data[ident]
if (key !== 'team' && key.startsWith('team')) { if (key !== 'team' && key.startsWith('team')) {
// Info about a team // Info about a team
if (key === 'teamname') { if (key === 'teamname') {
teamNamesById[id] = value; teamNamesById[id] = value
} else { } else {
// other team info which we don't track // other team info which we don't track
} }
} else { } else {
// Info about a player // Info about a player
if (!(id in playersById)) playersById[id] = {}; if (!(id in playersById)) playersById[id] = {}
if (key === 'playername' || key === 'player') { if (key === 'playername' || key === 'player') {
key = 'name'; key = 'name'
} }
if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed. if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed.
key = 'teamId'; key = 'teamId'
value += teamOffByOne ? -1 : 0; value += teamOffByOne ? -1 : 0
} }
playersById[id][key] = value; playersById[id][key] = value
} }
} }
state.raw.teams = teamNamesById; state.raw.teams = teamNamesById
const players = Object.values(playersById); const players = Object.values(playersById)
const seenHashes = new Set(); const seenHashes = new Set()
for (const player of players) { for (const player of players) {
// Some servers (bf1942) report the same player multiple times (bug?) // Some servers (bf1942) report the same player multiple times (bug?)
// Ignore these duplicates // Ignore these duplicates
if (player.keyhash) { if (player.keyhash) {
if (seenHashes.has(player.keyhash)) { if (seenHashes.has(player.keyhash)) {
this.logger.debug("Rejected player with hash " + player.keyhash + " (Duplicate keyhash)"); this.logger.debug('Rejected player with hash ' + player.keyhash + ' (Duplicate keyhash)')
continue; continue
} else { } else {
seenHashes.add(player.keyhash); seenHashes.add(player.keyhash)
} }
} }
// Convert player's team ID to team name if possible // Convert player's team ID to team name if possible
if (player.hasOwnProperty('teamId')) { if (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
if (Object.keys(teamNamesById).length) { if (Object.keys(teamNamesById).length) {
player.team = teamNamesById[player.teamId] || ''; player.team = teamNamesById[player.teamId] || ''
} else { } else {
player.team = player.teamId; player.team = player.teamId
delete player.teamId; delete player.teamId
} }
} }
state.players.push(player); state.players.push(player)
} }
} }
async sendPacket(type) { async sendPacket (type) {
let receivedQueryId; let receivedQueryId
const output = {}; const output = {}
const parts = new Set(); const parts = new Set()
let maxPartNum = 0; let maxPartNum = 0
return await this.udpSend(type, buffer => { return await this.udpSend(type, buffer => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const str = reader.string(buffer.length); const str = reader.string(buffer.length)
const split = str.split('\\'); const split = str.split('\\')
split.shift(); split.shift()
const data = {}; const data = {}
while(split.length) { while (split.length) {
const key = split.shift(); const key = split.shift()
const value = split.shift() || ''; const value = split.shift() || ''
data[key] = value; data[key] = value
} }
let queryId, partNum; let queryId, partNum
const partFinal = ('final' in data); const partFinal = ('final' in data)
if (data.queryid) { if (data.queryid) {
const split = data.queryid.split('.'); const split = data.queryid.split('.')
if (split.length >= 2) { if (split.length >= 2) {
partNum = Number(split[1]); partNum = Number(split[1])
} }
queryId = split[0]; queryId = split[0]
} }
delete data.final; delete data.final
delete data.queryid; delete data.queryid
this.logger.debug("Received part num=" + partNum + " queryId=" + queryId + " final=" + partFinal); this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)
if (queryId) { if (queryId) {
if (receivedQueryId && receivedQueryId !== queryId) { if (receivedQueryId && receivedQueryId !== queryId) {
this.logger.debug("Rejected packet (Wrong query ID)"); this.logger.debug('Rejected packet (Wrong query ID)')
return; return
} else if (!receivedQueryId) { } else if (!receivedQueryId) {
receivedQueryId = queryId; receivedQueryId = queryId
} }
} }
if (!partNum) { if (!partNum) {
partNum = parts.size; partNum = parts.size
this.logger.debug("No part number received (assigned #" + partNum + ")"); this.logger.debug('No part number received (assigned #' + partNum + ')')
} }
if (parts.has(partNum)) { if (parts.has(partNum)) {
this.logger.debug("Rejected packet (Duplicate part)"); this.logger.debug('Rejected packet (Duplicate part)')
return; return
} }
parts.add(partNum); parts.add(partNum)
if (partFinal) { if (partFinal) {
maxPartNum = partNum; maxPartNum = partNum
} }
this.logger.debug("Received part #" + partNum + " of " + (maxPartNum ? maxPartNum : "?")); this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
for(const i of Object.keys(data)) { for (const i of Object.keys(data)) {
output[i] = data[i]; output[i] = data[i]
} }
if (maxPartNum && parts.size === maxPartNum) { if (maxPartNum && parts.size === maxPartNum) {
this.logger.debug("Received all parts"); this.logger.debug('Received all parts')
this.logger.debug(output); this.logger.debug(output)
return output; return output
} }
}); })
} }
} }

View file

@ -1,44 +1,44 @@
import Core from './core.js'; import Core from './core.js'
export default class gamespy2 extends Core { export default class gamespy2 extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
// Parse info // Parse info
{ {
const body = await this.sendPacket([0xff, 0, 0]); const body = await this.sendPacket([0xff, 0, 0])
const reader = this.reader(body); const reader = this.reader(body)
while (!reader.done()) { while (!reader.done()) {
const key = reader.string(); const key = reader.string()
const value = reader.string(); const value = reader.string()
if (!key) break; if (!key) break
state.raw[key] = value; state.raw[key] = value
} }
if ('hostname' in state.raw) state.name = state.raw.hostname; if ('hostname' in state.raw) state.name = state.raw.hostname
if ('mapname' in state.raw) state.map = state.raw.mapname; if ('mapname' in state.raw) state.map = state.raw.mapname
if (this.trueTest(state.raw.password)) state.password = true; if (this.trueTest(state.raw.password)) state.password = true
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport); if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
} }
// Parse players // Parse players
{ {
const body = await this.sendPacket([0, 0xff, 0]); const body = await this.sendPacket([0, 0xff, 0])
const reader = this.reader(body); const reader = this.reader(body)
for (const rawPlayer of this.readFieldData(reader)) { for (const rawPlayer of this.readFieldData(reader)) {
state.players.push(rawPlayer); state.players.push(rawPlayer)
} }
} }
// Parse teams // Parse teams
{ {
const body = await this.sendPacket([0, 0, 0xff]); const body = await this.sendPacket([0, 0, 0xff])
const reader = this.reader(body); const reader = this.reader(body)
state.raw.teams = this.readFieldData(reader); state.raw.teams = this.readFieldData(reader)
} }
// Special case for america's army 1 and 2 // Special case for america's army 1 and 2
@ -46,95 +46,96 @@ export default class gamespy2 extends Core {
if (state.raw.gamename === 'armygame') { if (state.raw.gamename === 'armygame') {
const stripColor = (str) => { const stripColor = (str) => {
// uses unreal 2 color codes // uses unreal 2 color codes
return str.replace(/\x1b...|[\x00-\x1a]/g,''); return str.replace(/\x1b...|[\x00-\x1a]/g, '')
}; }
state.name = stripColor(state.name); state.name = stripColor(state.name)
state.map = stripColor(state.map); state.map = stripColor(state.map)
for(const key of Object.keys(state.raw)) { for (const key of Object.keys(state.raw)) {
if(typeof state.raw[key] === 'string') { if (typeof state.raw[key] === 'string') {
state.raw[key] = stripColor(state.raw[key]); state.raw[key] = stripColor(state.raw[key])
} }
} }
for(const player of state.players) { for (const player of state.players) {
if(!('name' in player)) continue; if (!('name' in player)) continue
player.name = stripColor(player.name); player.name = stripColor(player.name)
} }
} }
} }
async sendPacket(type) { async sendPacket (type) {
const request = Buffer.concat([ const request = Buffer.concat([
Buffer.from([0xfe,0xfd,0x00]), // gamespy2 Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2
Buffer.from([0x00,0x00,0x00,0x01]), // ping ID Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID
Buffer.from(type) Buffer.from(type)
]); ])
return await this.udpSend(request, buffer => { return await this.udpSend(request, buffer => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const header = reader.uint(1); const header = reader.uint(1)
if (header !== 0) return; if (header !== 0) return
const pingId = reader.uint(4); const pingId = reader.uint(4)
if (pingId !== 1) return; if (pingId !== 1) return
return reader.rest(); return reader.rest()
}); })
} }
readFieldData(reader) { readFieldData (reader) {
const zero = reader.uint(1); // always 0 reader.uint(1) // always 0
const count = reader.uint(1); // number of rows in this data const count = reader.uint(1) // number of rows in this data
// some games omit the count byte entirely if it's 0 or at random (like americas army) // some games omit the count byte entirely if it's 0 or at random (like americas army)
// Luckily, count should always be <64, and ascii characters will typically be >64, // Luckily, count should always be <64, and ascii characters will typically be >64,
// so we can detect this. // so we can detect this.
if (count > 64) { if (count > 64) {
reader.skip(-1); reader.skip(-1)
this.logger.debug("Detected missing count byte, rewinding by 1"); this.logger.debug('Detected missing count byte, rewinding by 1')
} else { } else {
this.logger.debug("Detected row count: " + count); this.logger.debug('Detected row count: ' + count)
} }
this.logger.debug(() => "Reading fields, starting at: "+reader.rest()); this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest())
const fields = []; const fields = []
while(!reader.done()) { while (!reader.done()) {
let field = reader.string(); const field = reader.string()
if(!field) break; if (!field) break
fields.push(field); fields.push(field)
this.logger.debug("field:"+field); this.logger.debug('field:' + field)
} }
if (!fields.length) return []; if (!fields.length) return []
const units = []; const units = []
outer: while(!reader.done()) { while (!reader.done()) {
const unit = {}; const unit = {}
for(let iField = 0; iField < fields.length; iField++) { for (let iField = 0; iField < fields.length; iField++) {
let key = fields[iField]; let key = fields[iField]
let value = reader.string(); let value = reader.string()
if(!value && iField === 0) break outer; if (!value && iField === 0) return units
this.logger.debug("value:"+value);
if(key === 'player_') key = 'name';
else if(key === 'score_') key = 'score';
else if(key === 'deaths_') key = 'deaths';
else if(key === 'ping_') key = 'ping';
else if(key === 'team_') key = 'team';
else if(key === 'kills_') key = 'kills';
else if(key === 'team_t') key = 'name';
else if(key === 'tickets_t') key = 'tickets';
if( this.logger.debug('value:' + value)
key === 'score' || key === 'deaths' if (key === 'player_') key = 'name'
|| key === 'ping' || key === 'team' else if (key === 'score_') key = 'score'
|| key === 'kills' || key === 'tickets' else if (key === 'deaths_') key = 'deaths'
else if (key === 'ping_') key = 'ping'
else if (key === 'team_') key = 'team'
else if (key === 'kills_') key = 'kills'
else if (key === 'team_t') key = 'name'
else if (key === 'tickets_t') key = 'tickets'
if (
key === 'score' || key === 'deaths' ||
key === 'ping' || key === 'team' ||
key === 'kills' || key === 'tickets'
) { ) {
if(value === '') continue; if (value === '') continue
value = parseInt(value); value = parseInt(value)
} }
unit[key] = value; unit[key] = value
} }
units.push(unit); units.push(unit)
} }
return units; return units
} }
} }

View file

@ -1,106 +1,104 @@
import Core from './core.js'; import Core from './core.js'
export default class gamespy3 extends Core { export default class gamespy3 extends Core {
constructor() { constructor () {
super(); super()
this.sessionId = 1; this.sessionId = 1
this.encoding = 'latin1'; this.encoding = 'latin1'
this.byteorder = 'be'; this.byteorder = 'be'
this.useOnlySingleSplit = false; this.useOnlySingleSplit = false
this.isJc2mp = false; this.isJc2mp = false
} }
async run(state) { async run (state) {
const buffer = await this.sendPacket(9, false, false, false); const buffer = await this.sendPacket(9, false, false, false)
const reader = this.reader(buffer); const reader = this.reader(buffer)
let challenge = parseInt(reader.string()); let challenge = parseInt(reader.string())
this.logger.debug("Received challenge key: " + challenge); this.logger.debug('Received challenge key: ' + challenge)
if (challenge === 0) { if (challenge === 0) {
// Some servers send us a 0 if they don't want a challenge key used // Some servers send us a 0 if they don't want a challenge key used
// BF2 does this. // BF2 does this.
challenge = null; challenge = null
} }
let requestPayload; let requestPayload
if(this.isJc2mp) { if (this.isJc2mp) {
// they completely alter the protocol. because why not. // they completely alter the protocol. because why not.
requestPayload = Buffer.from([0xff,0xff,0xff,0x02]); requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
} else { } else {
requestPayload = Buffer.from([0xff,0xff,0xff,0x01]); requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
} }
/** @type Buffer[] */ /** @type Buffer[] */
const packets = await this.sendPacket(0,challenge,requestPayload,true); const packets = await this.sendPacket(0, challenge, requestPayload, true)
// iterate over the received packets // iterate over the received packets
// the first packet will start off with k/v pairs, followed with data fields // the first packet will start off with k/v pairs, followed with data fields
// the following packets will only have data fields // the following packets will only have data fields
state.raw.playerTeamInfo = {}; state.raw.playerTeamInfo = {}
for(let iPacket = 0; iPacket < packets.length; iPacket++) { for (let iPacket = 0; iPacket < packets.length; iPacket++) {
const packet = packets[iPacket]; const packet = packets[iPacket]
const reader = this.reader(packet); const reader = this.reader(packet)
this.logger.debug("Parsing packet #" + iPacket); this.logger.debug('Parsing packet #' + iPacket)
this.logger.debug(packet); this.logger.debug(packet)
// Parse raw server key/values // Parse raw server key/values
if(iPacket === 0) { if (iPacket === 0) {
while(!reader.done()) { while (!reader.done()) {
const key = reader.string(); const key = reader.string()
if(!key) break; if (!key) break
let value = reader.string(); let value = reader.string()
while(value.match(/^p[0-9]+$/)) { while (value.match(/^p[0-9]+$/)) {
// fix a weird ut3 bug where some keys don't have values // fix a weird ut3 bug where some keys don't have values
value = reader.string(); value = reader.string()
} }
state.raw[key] = value; state.raw[key] = value
this.logger.debug(key + " = " + value); this.logger.debug(key + ' = ' + value)
} }
} }
// Parse player, team, item array state // Parse player, team, item array state
if(this.isJc2mp) { if (this.isJc2mp) {
state.raw.numPlayers2 = reader.uint(2); state.raw.numPlayers2 = reader.uint(2)
while(!reader.done()) { while (!reader.done()) {
const player = {}; const player = {}
player.name = reader.string(); player.name = reader.string()
player.steamid = reader.string(); player.steamid = reader.string()
player.ping = reader.uint(2); player.ping = reader.uint(2)
state.players.push(player); state.players.push(player)
} }
} else { } else {
let firstMode = true; while (!reader.done()) {
while(!reader.done()) { if (reader.uint(1) <= 2) continue
if (reader.uint(1) <= 2) continue; reader.skip(-1)
reader.skip(-1); const fieldId = reader.string()
let fieldId = reader.string(); if (!fieldId) continue
if(!fieldId) continue; const fieldIdSplit = fieldId.split('_')
const fieldIdSplit = fieldId.split('_'); const fieldName = fieldIdSplit[0]
const fieldName = fieldIdSplit[0]; const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_';
if(!(itemType in state.raw.playerTeamInfo)) { if (!(itemType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[itemType] = []; state.raw.playerTeamInfo[itemType] = []
} }
const items = state.raw.playerTeamInfo[itemType]; const items = state.raw.playerTeamInfo[itemType]
let offset = reader.uint(1); let offset = reader.uint(1)
firstMode = false;
this.logger.debug(() => "Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset); this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
while(!reader.done()) { while (!reader.done()) {
const item = reader.string(); const item = reader.string()
if(!item) break; if (!item) break
while(items.length <= offset) { items.push({}); } while (items.length <= offset) { items.push({}) }
items[offset][fieldName] = item; items[offset][fieldName] = item
this.logger.debug("* " + item); this.logger.debug('* ' + item)
offset++; offset++
} }
} }
} }
@ -108,89 +106,89 @@ export default class gamespy3 extends Core {
// Turn all that raw state into something useful // Turn all that raw state into something useful
if ('hostname' in state.raw) state.name = state.raw.hostname; if ('hostname' in state.raw) state.name = state.raw.hostname
else if('servername' in state.raw) state.name = state.raw.servername; else if ('servername' in state.raw) state.name = state.raw.servername
if ('mapname' in state.raw) state.map = state.raw.mapname; if ('mapname' in state.raw) state.map = state.raw.mapname
if (state.raw.password === '1') state.password = true; if (state.raw.password === '1') state.password = true
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport); if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
if('' in state.raw.playerTeamInfo) { if ('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) { for (const playerInfo of state.raw.playerTeamInfo['']) {
const player = {}; const player = {}
for(const from of Object.keys(playerInfo)) { for (const from of Object.keys(playerInfo)) {
let key = from; let key = from
let value = playerInfo[from]; let value = playerInfo[from]
if(key === 'player') key = 'name'; if (key === 'player') key = 'name'
if(key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value); if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value)
player[key] = value; player[key] = value
} }
state.players.push(player); state.players.push(player)
} }
} }
} }
async sendPacket(type,challenge,payload,assemble) { async sendPacket (type, challenge, payload, assemble) {
const challengeLength = challenge === null ? 0 : 4; const challengeLength = challenge === null ? 0 : 4
const payloadLength = payload ? payload.length : 0; const payloadLength = payload ? payload.length : 0
const b = Buffer.alloc(7 + challengeLength + payloadLength); const b = Buffer.alloc(7 + challengeLength + payloadLength)
b.writeUInt8(0xFE, 0); b.writeUInt8(0xFE, 0)
b.writeUInt8(0xFD, 1); b.writeUInt8(0xFD, 1)
b.writeUInt8(type, 2); b.writeUInt8(type, 2)
b.writeUInt32BE(this.sessionId, 3); b.writeUInt32BE(this.sessionId, 3)
if(challengeLength) b.writeInt32BE(challenge, 7); if (challengeLength) b.writeInt32BE(challenge, 7)
if(payloadLength) payload.copy(b, 7+challengeLength); if (payloadLength) payload.copy(b, 7 + challengeLength)
let numPackets = 0; let numPackets = 0
const packets = {}; const packets = {}
return await this.udpSend(b,(buffer) => { return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const iType = reader.uint(1); const iType = reader.uint(1)
if(iType !== type) { if (iType !== type) {
this.logger.debug('Skipping packet, type mismatch'); this.logger.debug('Skipping packet, type mismatch')
return; return
} }
const iSessionId = reader.uint(4); const iSessionId = reader.uint(4)
if(iSessionId !== this.sessionId) { if (iSessionId !== this.sessionId) {
this.logger.debug('Skipping packet, session id mismatch'); this.logger.debug('Skipping packet, session id mismatch')
return; return
} }
if(!assemble) { if (!assemble) {
return reader.rest(); return reader.rest()
} }
if(this.useOnlySingleSplit) { if (this.useOnlySingleSplit) {
// has split headers, but they are worthless and only one packet is used // has split headers, but they are worthless and only one packet is used
reader.skip(11); reader.skip(11)
return [reader.rest()]; return [reader.rest()]
} }
reader.skip(9); // filler data -- usually set to 'splitnum\0' reader.skip(9) // filler data -- usually set to 'splitnum\0'
let id = reader.uint(1); let id = reader.uint(1)
const last = (id & 0x80); const last = (id & 0x80)
id = id & 0x7f; id = id & 0x7f
if(last) numPackets = id+1; if (last) numPackets = id + 1
reader.skip(1); // "another 'packet number' byte, but isn't understood." reader.skip(1) // "another 'packet number' byte, but isn't understood."
packets[id] = reader.rest(); packets[id] = reader.rest()
if(this.debug) { if (this.debug) {
this.logger.debug("Received packet #"+id + (last ? " (last)" : "")); this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
} }
if(!numPackets || Object.keys(packets).length !== numPackets) return; if (!numPackets || Object.keys(packets).length !== numPackets) return
// assemble the parts // assemble the parts
const list = []; const list = []
for(let i = 0; i < numPackets; i++) { for (let i = 0; i < numPackets; i++) {
if(!(i in packets)) { if (!(i in packets)) {
throw new Error('Missing packet #'+i); throw new Error('Missing packet #' + i)
} }
list.push(packets[i]); list.push(packets[i])
} }
return list; return list
}); })
} }
} }

View file

@ -1,46 +1,46 @@
import Core from './core.js'; import Core from './core.js'
export default class geneshift extends Core { export default class geneshift extends Core {
async run(state) { async run (state) {
await this.tcpPing(); await this.tcpPing()
const body = await this.request({ const body = await this.request({
url: 'http://geneshift.net/game/receiveLobby.php' url: 'http://geneshift.net/game/receiveLobby.php'
}); })
const split = body.split('<br/>'); const split = body.split('<br/>')
let found = null; let found = null
for(const line of split) { for (const line of split) {
const fields = line.split('::'); const fields = line.split('::')
const ip = fields[2]; const ip = fields[2]
const port = fields[3]; const port = fields[3]
if(ip === this.options.address && parseInt(port) === this.options.port) { if (ip === this.options.address && parseInt(port) === this.options.port) {
found = fields; found = fields
break; break
} }
} }
if(found === null) { if (found === null) {
throw new Error('Server not found in list'); throw new Error('Server not found in list')
} }
state.raw.countrycode = found[0]; state.raw.countrycode = found[0]
state.raw.country = found[1]; state.raw.country = found[1]
state.name = found[4]; state.name = found[4]
state.map = found[5]; state.map = found[5]
state.players.setNum(parseInt(found[6])); state.players.setNum(parseInt(found[6]))
state.maxplayers = parseInt(found[7]); state.maxplayers = parseInt(found[7])
// fields[8] is unknown? // fields[8] is unknown?
state.raw.rules = found[9]; state.raw.rules = found[9]
state.raw.gamemode = parseInt(found[10]); state.raw.gamemode = parseInt(found[10])
state.raw.gangsters = parseInt(found[11]); state.raw.gangsters = parseInt(found[11])
state.raw.cashrate = parseInt(found[12]); state.raw.cashrate = parseInt(found[12])
state.raw.missions = !!parseInt(found[13]); state.raw.missions = !!parseInt(found[13])
state.raw.vehicles = !!parseInt(found[14]); state.raw.vehicles = !!parseInt(found[14])
state.raw.customweapons = !!parseInt(found[15]); state.raw.customweapons = !!parseInt(found[15])
state.raw.friendlyfire = !!parseInt(found[16]); state.raw.friendlyfire = !!parseInt(found[16])
state.raw.mercs = !!parseInt(found[17]); state.raw.mercs = !!parseInt(found[17])
// fields[18] is unknown? listen server? // fields[18] is unknown? listen server?
state.raw.version = found[19]; state.raw.version = found[19]
} }
} }

View file

@ -1,8 +1,8 @@
import valve from './valve.js'; import valve from './valve.js'
export default class goldsrc extends valve { export default class goldsrc extends valve {
constructor() { constructor () {
super(); super()
this.goldsrcInfo = true; this.goldsrcInfo = true
} }
} }

View file

@ -1,13 +1,14 @@
import quake1 from './quake1.js'; import quake1 from './quake1.js'
export default class hexen2 extends quake1 { export default class hexen2 extends quake1 {
constructor() { constructor () {
super(); super()
this.sendHeader = '\xFFstatus\x0a'; this.sendHeader = '\xFFstatus\x0a'
this.responseHeader = '\xffn'; this.responseHeader = '\xffn'
} }
async run(state) {
await super.run(state); async run (state) {
state.gamePort = this.options.port - 50; await super.run(state)
state.gamePort = this.options.port - 50
} }
} }

View file

@ -1,53 +1,55 @@
import armagetron from "./armagetron.js"; import armagetron from './armagetron.js'
import ase from "./ase.js"; import ase from './ase.js'
import assettocorsa from "./assettocorsa.js"; import assettocorsa from './assettocorsa.js'
import battlefield from "./battlefield.js"; import battlefield from './battlefield.js'
import buildandshoot from "./buildandshoot.js"; import buildandshoot from './buildandshoot.js'
import cs2d from "./cs2d.js"; import cs2d from './cs2d.js'
import discord from "./discord.js"; import discord from './discord.js'
import doom3 from "./doom3.js"; import doom3 from './doom3.js'
import eco from "./eco.js"; import eco from './eco.js'
import ffow from "./ffow.js"; import ffow from './ffow.js'
import fivem from "./fivem.js"; import fivem from './fivem.js'
import gamespy1 from "./gamespy1.js"; import gamespy1 from './gamespy1.js'
import gamespy2 from "./gamespy2.js"; import gamespy2 from './gamespy2.js'
import gamespy3 from "./gamespy3.js"; import gamespy3 from './gamespy3.js'
import geneshift from "./geneshift.js"; import geneshift from './geneshift.js'
import goldsrc from "./goldsrc.js"; import goldsrc from './goldsrc.js'
import hexen2 from "./hexen2.js"; import hexen2 from './hexen2.js'
import jc2mp from "./jc2mp.js"; import jc2mp from './jc2mp.js'
import kspdmp from "./kspdmp.js"; import kspdmp from './kspdmp.js'
import mafia2mp from "./mafia2mp.js"; import mafia2mp from './mafia2mp.js'
import mafia2online from "./mafia2online.js"; import mafia2online from './mafia2online.js'
import minecraft from "./minecraft.js"; import minecraft from './minecraft.js'
import minecraftbedrock from "./minecraftbedrock.js"; import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from "./minecraftvanilla.js"; import minecraftvanilla from './minecraftvanilla.js'
import mumble from "./mumble.js"; import mumble from './mumble.js'
import mumbleping from "./mumbleping.js"; import mumbleping from './mumbleping.js'
import nadeo from "./nadeo.js"; import nadeo from './nadeo.js'
import openttd from "./openttd.js"; import openttd from './openttd.js'
import quake1 from "./quake1.js"; import quake1 from './quake1.js'
import quake2 from "./quake2.js"; import quake2 from './quake2.js'
import quake3 from "./quake3.js"; import quake3 from './quake3.js'
import rfactor from "./rfactor.js"; import rfactor from './rfactor.js'
import samp from "./samp.js"; import samp from './samp.js'
import savage2 from "./savage2.js"; import savage2 from './savage2.js'
import starmade from "./starmade.js"; import starmade from './starmade.js'
import starsiege from "./starsiege.js"; import starsiege from './starsiege.js'
import teamspeak2 from "./teamspeak2.js"; import teamspeak2 from './teamspeak2.js'
import teamspeak3 from "./teamspeak3.js"; import teamspeak3 from './teamspeak3.js'
import terraria from "./terraria.js"; import terraria from './terraria.js'
import tribes1 from "./tribes1.js"; import tribes1 from './tribes1.js'
import tribes1master from "./tribes1master.js"; import tribes1master from './tribes1master.js'
import unreal2 from "./unreal2.js"; import unreal2 from './unreal2.js'
import ut3 from "./ut3.js"; import ut3 from './ut3.js'
import valve from "./valve.js"; import valve from './valve.js'
import vcmp from "./vcmp.js"; import vcmp from './vcmp.js'
import ventrilo from "./ventrilo.js"; import ventrilo from './ventrilo.js'
import warsow from "./warsow.js"; import warsow from './warsow.js'
export { armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1, export {
armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1,
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp, minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve, savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
vcmp, ventrilo, warsow } vcmp, ventrilo, warsow
}

View file

@ -1,18 +1,19 @@
import gamespy3 from './gamespy3.js'; import gamespy3 from './gamespy3.js'
// supposedly, gamespy3 is the "official" query protocol for jcmp, // supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires useOnlySingleSplit), and may not include some player names // but it's broken (requires useOnlySingleSplit), and may not include some player names
export default class jc2mp extends gamespy3 { export default class jc2mp extends gamespy3 {
constructor() { constructor () {
super(); super()
this.useOnlySingleSplit = true; this.useOnlySingleSplit = true
this.isJc2mp = true; this.isJc2mp = true
this.encoding = 'utf8'; this.encoding = 'utf8'
} }
async run(state) {
await super.run(state); async run (state) {
if(!state.players.length && parseInt(state.raw.numplayers)) { await super.run(state)
state.players.setNum(parseInt(state.raw.numplayers)); if (!state.players.length && parseInt(state.raw.numplayers)) {
state.players.setNum(parseInt(state.raw.numplayers))
} }
} }
} }

View file

@ -1,26 +1,26 @@
import Core from './core.js'; import Core from './core.js'
export default class kspdmp extends Core { export default class kspdmp extends Core {
async run(state) { async run (state) {
const json = await this.request({ const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port, url: 'http://' + this.options.address + ':' + this.options.port,
responseType: 'json' responseType: 'json'
}); })
for (const one of json.players) { for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team}); state.players.push({ name: one.nickname, team: one.team })
} }
for (const key of Object.keys(json)) { for (const key of Object.keys(json)) {
state.raw[key] = json[key]; state.raw[key] = json[key]
} }
state.name = json.server_name; state.name = json.server_name
state.maxplayers = json.max_players; state.maxplayers = json.max_players
state.gamePort = json.port; state.gamePort = json.port
if (json.players) { if (json.players) {
const split = json.players.split(', '); const split = json.players.split(', ')
for (const name of split) { for (const name of split) {
state.players.push({name:name}); state.players.push({ name })
} }
} }
} }

View file

@ -1,41 +1,41 @@
import Core from './core.js'; import Core from './core.js'
export default class mafia2mp extends Core { export default class mafia2mp extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.header = 'M2MP'; this.header = 'M2MP'
this.isMafia2Online = false; this.isMafia2Online = false
} }
async run(state) { async run (state) {
const body = await this.udpSend(this.header,(buffer) => { const body = await this.udpSend(this.header, (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const header = reader.string(this.header.length); const header = reader.string(this.header.length)
if (header !== this.header) return; if (header !== this.header) return
return reader.rest(); return reader.rest()
}); })
const reader = this.reader(body); const reader = this.reader(body)
state.name = this.readString(reader); state.name = this.readString(reader)
state.raw.numplayers = this.readString(reader); state.raw.numplayers = this.readString(reader)
state.maxplayers = parseInt(this.readString(reader)); state.maxplayers = parseInt(this.readString(reader))
state.raw.gamemode = this.readString(reader); state.raw.gamemode = this.readString(reader)
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
state.gamePort = this.options.port - 1; state.gamePort = this.options.port - 1
while(!reader.done()) { while (!reader.done()) {
const player = {}; const player = {}
player.name = this.readString(reader); player.name = this.readString(reader)
if(!player.name) break; if (!player.name) break
if (this.isMafia2Online) { if (this.isMafia2Online) {
player.ping = parseInt(this.readString(reader)); player.ping = parseInt(this.readString(reader))
} }
state.players.push(player); state.players.push(player)
} }
} }
readString(reader) { readString (reader) {
return reader.pascalString(1,-1); return reader.pascalString(1, -1)
} }
} }

View file

@ -1,9 +1,9 @@
import mafia2mp from './mafia2mp.js'; import mafia2mp from './mafia2mp.js'
export default class mafia2online extends mafia2mp { export default class mafia2online extends mafia2mp {
constructor() { constructor () {
super(); super()
this.header = 'M2Online'; this.header = 'M2Online'
this.isMafia2Online = true; this.isMafia2Online = true
} }
} }

View file

@ -1,8 +1,7 @@
import Core from './core.js'; import Core from './core.js'
import minecraftbedrock from "./minecraftbedrock.js"; import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from "./minecraftvanilla.js"; import minecraftvanilla from './minecraftvanilla.js'
import Gamespy3 from "./gamespy3.js"; import Gamespy3 from './gamespy3.js'
import {Results} from "../lib/Results.js";
/* /*
Vanilla servers respond to minecraftvanilla only Vanilla servers respond to minecraftvanilla only
@ -13,87 +12,88 @@ Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
*/ */
export default class minecraft extends Core { export default class minecraft extends Core {
constructor() { constructor () {
super(); super()
this.srvRecord = "_minecraft._tcp"; this.srvRecord = '_minecraft._tcp'
} }
async run(state) {
async run (state) {
/** @type {Promise<Results>[]} */ /** @type {Promise<Results>[]} */
const promises = []; const promises = []
const vanillaResolver = new minecraftvanilla(); const vanillaResolver = new minecraftvanilla()
vanillaResolver.options = this.options; vanillaResolver.options = this.options
vanillaResolver.udpSocket = this.udpSocket; vanillaResolver.udpSocket = this.udpSocket
promises.push((async () => { promises.push((async () => {
try { return await vanillaResolver.runOnceSafe(); } catch(e) {} try { return await vanillaResolver.runOnceSafe() } catch (e) {}
})()); })())
const gamespyResolver = new Gamespy3(); const gamespyResolver = new Gamespy3()
gamespyResolver.options = { gamespyResolver.options = {
...this.options, ...this.options,
encoding: 'utf8', encoding: 'utf8'
}; }
gamespyResolver.udpSocket = this.udpSocket; gamespyResolver.udpSocket = this.udpSocket
promises.push((async () => { promises.push((async () => {
try { return await gamespyResolver.runOnceSafe(); } catch(e) {} try { return await gamespyResolver.runOnceSafe() } catch (e) {}
})()); })())
const bedrockResolver = new minecraftbedrock(); const bedrockResolver = new minecraftbedrock()
bedrockResolver.options = this.options; bedrockResolver.options = this.options
bedrockResolver.udpSocket = this.udpSocket; bedrockResolver.udpSocket = this.udpSocket
promises.push((async () => { promises.push((async () => {
try { return await bedrockResolver.runOnceSafe(); } catch(e) {} try { return await bedrockResolver.runOnceSafe() } catch (e) {}
})()); })())
const [ vanillaState, gamespyState, bedrockState ] = await Promise.all(promises); const [vanillaState, gamespyState, bedrockState] = await Promise.all(promises)
state.raw.vanilla = vanillaState; state.raw.vanilla = vanillaState
state.raw.gamespy = gamespyState; state.raw.gamespy = gamespyState
state.raw.bedrock = bedrockState; state.raw.bedrock = bedrockState
if (!vanillaState && !gamespyState && !bedrockState) { if (!vanillaState && !gamespyState && !bedrockState) {
throw new Error('No protocols succeeded'); throw new Error('No protocols succeeded')
} }
// Ordered from least worth to most worth (player names / etc) // Ordered from least worth to most worth (player names / etc)
if (bedrockState) { if (bedrockState) {
if (bedrockState.players.length) state.players = bedrockState.players; if (bedrockState.players.length) state.players = bedrockState.players
} }
if (vanillaState) { if (vanillaState) {
try { try {
let name = ''; let name = ''
const description = vanillaState.raw.description; const description = vanillaState.raw.description
if (typeof description === 'string') { if (typeof description === 'string') {
name = description; name = description
} }
if (!name && typeof description === 'object' && description.text) { if (!name && typeof description === 'object' && description.text) {
name = description.text; name = description.text
} }
if (!name && typeof description === 'object' && description.extra) { if (!name && typeof description === 'object' && description.extra) {
name = description.extra.map(part => part.text).join(''); name = description.extra.map(part => part.text).join('')
} }
state.name = name; state.name = name
} catch(e) {} } catch (e) {}
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers; if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
if (vanillaState.players.length) state.players = vanillaState.players; if (vanillaState.players.length) state.players = vanillaState.players
if (vanillaState.ping) this.registerRtt(vanillaState.ping); if (vanillaState.ping) this.registerRtt(vanillaState.ping)
} }
if (gamespyState) { if (gamespyState) {
if (gamespyState.name) state.name = gamespyState.name; if (gamespyState.name) state.name = gamespyState.name
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers; if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
if (gamespyState.players.length) state.players = gamespyState.players; if (gamespyState.players.length) state.players = gamespyState.players
else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers)); else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers))
if (gamespyState.ping) this.registerRtt(gamespyState.ping); if (gamespyState.ping) this.registerRtt(gamespyState.ping)
} }
if (bedrockState) { if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name; if (bedrockState.name) state.name = bedrockState.name
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers; if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
if (bedrockState.map) state.map = bedrockState.map; if (bedrockState.map) state.map = bedrockState.map
if (bedrockState.ping) this.registerRtt(bedrockState.ping); if (bedrockState.ping) this.registerRtt(bedrockState.ping)
} }
// remove dupe spaces from name // remove dupe spaces from name
state.name = state.name.replace(/\s+/g, ' '); state.name = state.name.replace(/\s+/g, ' ')
// remove color codes from name // remove color codes from name
state.name = state.name.replace(/\u00A7./g, ''); state.name = state.name.replace(/\u00A7./g, '')
} }
} }

View file

@ -1,72 +1,72 @@
import Core from './core.js'; import Core from './core.js'
export default class minecraftbedrock extends Core { export default class minecraftbedrock extends Core {
constructor() { constructor () {
super(); super()
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
const bufs = [ const bufs = [
Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
Buffer.from('0000000000000000', 'hex') // Cliend GUID Buffer.from('0000000000000000', 'hex') // Cliend GUID
]; ]
return await this.udpSend(Buffer.concat(bufs), buffer => { return await this.udpSend(Buffer.concat(bufs), buffer => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const messageId = reader.uint(1); const messageId = reader.uint(1)
if (messageId !== 0x1c) { if (messageId !== 0x1c) {
this.logger.debug('Skipping packet, invalid message id'); this.logger.debug('Skipping packet, invalid message id')
return; return
} }
const nonce = reader.part(8).toString('hex'); // should match the nonce we sent const nonce = reader.part(8).toString('hex') // should match the nonce we sent
this.logger.debug('Nonce: ' + nonce); this.logger.debug('Nonce: ' + nonce)
if (nonce !== '1122334455667788') { if (nonce !== '1122334455667788') {
this.logger.debug('Skipping packet, invalid nonce'); this.logger.debug('Skipping packet, invalid nonce')
return; return
} }
// These 8 bytes are identical to the serverId string we receive in decimal below // These 8 bytes are identical to the serverId string we receive in decimal below
reader.skip(8); reader.skip(8)
const magic = reader.part(16).toString('hex'); const magic = reader.part(16).toString('hex')
this.logger.debug('Magic value: ' + magic); this.logger.debug('Magic value: ' + magic)
if (magic !== '00ffff00fefefefefdfdfdfd12345678') { if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
this.logger.debug('Skipping packet, invalid magic'); this.logger.debug('Skipping packet, invalid magic')
return; return
} }
const statusLen = reader.uint(2); const statusLen = reader.uint(2)
if (reader.remaining() !== statusLen) { if (reader.remaining() !== statusLen) {
throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen); throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen)
} }
const statusStr = reader.rest().toString('utf8'); const statusStr = reader.rest().toString('utf8')
this.logger.debug('Raw status str: ' + statusStr); this.logger.debug('Raw status str: ' + statusStr)
const split = statusStr.split(';'); const split = statusStr.split(';')
if (split.length < 6) { if (split.length < 6) {
throw new Error('Missing enough chunks in status str'); throw new Error('Missing enough chunks in status str')
} }
state.raw.edition = split.shift(); state.raw.edition = split.shift()
state.name = split.shift(); state.name = split.shift()
state.raw.protocolVersion = split.shift(); state.raw.protocolVersion = split.shift()
state.raw.mcVersion = split.shift(); state.raw.mcVersion = split.shift()
state.players.setNum(parseInt(split.shift())); state.players.setNum(parseInt(split.shift()))
state.maxplayers = parseInt(split.shift()); state.maxplayers = parseInt(split.shift())
if (split.length) state.raw.serverId = split.shift(); if (split.length) state.raw.serverId = split.shift()
if (split.length) state.map = split.shift(); if (split.length) state.map = split.shift()
if (split.length) state.raw.gameMode = split.shift(); if (split.length) state.raw.gameMode = split.shift()
if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift()); if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift())
if (split.length) state.raw.ipv4Port = split.shift(); if (split.length) state.raw.ipv4Port = split.shift()
if (split.length) state.raw.ipv6Port = split.shift(); if (split.length) state.raw.ipv6Port = split.shift()
return true; return true
}); })
} }
} }

View file

@ -1,12 +1,12 @@
import Core from './core.js'; import Core from './core.js'
import Varint from "varint"; import Varint from 'varint'
export default class minecraftvanilla extends Core { export default class minecraftvanilla extends Core {
async run(state) { async run (state) {
const portBuf = Buffer.alloc(2); const portBuf = Buffer.alloc(2)
portBuf.writeUInt16BE(this.options.port,0); portBuf.writeUInt16BE(this.options.port, 0)
const addressBuf = Buffer.from(this.options.host,'utf8'); const addressBuf = Buffer.from(this.options.host, 'utf8')
const bufs = [ const bufs = [
this.varIntBuffer(47), this.varIntBuffer(47),
@ -14,46 +14,46 @@ export default class minecraftvanilla extends Core {
addressBuf, addressBuf,
portBuf, portBuf,
this.varIntBuffer(1) this.varIntBuffer(1)
]; ]
const outBuffer = Buffer.concat([ const outBuffer = Buffer.concat([
this.buildPacket(0,Buffer.concat(bufs)), this.buildPacket(0, Buffer.concat(bufs)),
this.buildPacket(0) this.buildPacket(0)
]); ])
const data = await this.withTcp(async socket => { const data = await this.withTcp(async socket => {
return await this.tcpSend(socket, outBuffer, data => { return await this.tcpSend(socket, outBuffer, data => {
if(data.length < 10) return; if (data.length < 10) return
const reader = this.reader(data); const reader = this.reader(data)
const length = reader.varint(); const length = reader.varint()
if(data.length < length) return; if (data.length < length) return
return reader.rest(); return reader.rest()
}); })
}); })
const reader = this.reader(data); const reader = this.reader(data)
const packetId = reader.varint(); const packetId = reader.varint()
this.logger.debug("Packet ID: "+packetId); this.logger.debug('Packet ID: ' + packetId)
const strLen = reader.varint(); const strLen = reader.varint()
this.logger.debug("String Length: "+strLen); this.logger.debug('String Length: ' + strLen)
const str = reader.rest().toString('utf8'); const str = reader.rest().toString('utf8')
this.logger.debug(str); this.logger.debug(str)
const json = JSON.parse(str); const json = JSON.parse(str)
delete json.favicon; delete json.favicon
state.raw = json; state.raw = json
state.maxplayers = json.players.max; state.maxplayers = json.players.max
if(json.players.sample) { if (json.players.sample) {
for(const player of json.players.sample) { for (const player of json.players.sample) {
state.players.push({ state.players.push({
id: player.id, id: player.id,
name: player.name name: player.name
}); })
} }
} }
@ -61,20 +61,21 @@ export default class minecraftvanilla extends Core {
// Insert a dummy player object for every online player that is not listed in players.sample. // Insert a dummy player object for every online player that is not listed in players.sample.
// Limit player amount to 10.000 players for performance reasons. // Limit player amount to 10.000 players for performance reasons.
for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) { for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) {
state.players.push({}); state.players.push({})
} }
} }
varIntBuffer(num) { varIntBuffer (num) {
return Buffer.from(Varint.encode(num)); return Buffer.from(Varint.encode(num))
} }
buildPacket(id,data) {
if(!data) data = Buffer.from([]); buildPacket (id, data) {
const idBuffer = this.varIntBuffer(id); if (!data) data = Buffer.from([])
const idBuffer = this.varIntBuffer(id)
return Buffer.concat([ return Buffer.concat([
this.varIntBuffer(data.length+idBuffer.length), this.varIntBuffer(data.length + idBuffer.length),
idBuffer, idBuffer,
data data
]); ])
} }
} }

View file

@ -1,39 +1,39 @@
import Core from './core.js'; import Core from './core.js'
export default class mumble extends Core { export default class mumble extends Core {
async run(state) { async run (state) {
const json = await this.withTcp(async socket => { const json = await this.withTcp(async socket => {
return await this.tcpSend(socket, 'json', (buffer) => { return await this.tcpSend(socket, 'json', (buffer) => {
if (buffer.length < 10) return; if (buffer.length < 10) return
const str = buffer.toString(); const str = buffer.toString()
let json; let json
try { try {
json = JSON.parse(str); json = JSON.parse(str)
} catch (e) { } catch (e) {
// probably not all here yet // probably not all here yet
return; return
} }
return json; return json
}); })
}); })
state.raw = json; state.raw = json
state.name = json.name; state.name = json.name
state.gamePort = json.x_gtmurmur_connectport || 64738; state.gamePort = json.x_gtmurmur_connectport || 64738
let channelStack = [state.raw.root]; let channelStack = [state.raw.root]
while(channelStack.length) { while (channelStack.length) {
const channel = channelStack.shift(); const channel = channelStack.shift()
channel.description = this.cleanComment(channel.description); channel.description = this.cleanComment(channel.description)
channelStack = channelStack.concat(channel.channels); channelStack = channelStack.concat(channel.channels)
for(const user of channel.users) { for (const user of channel.users) {
user.comment = this.cleanComment(user.comment); user.comment = this.cleanComment(user.comment)
state.players.push(user); state.players.push(user)
} }
} }
} }
cleanComment(str) { cleanComment (str) {
return str.replace(/<.*>/g,''); return str.replace(/<.*>/g, '')
} }
} }

View file

@ -1,24 +1,24 @@
import Core from './core.js'; import Core from './core.js'
export default class mumbleping extends Core { export default class mumbleping extends Core {
constructor() { constructor () {
super(); super()
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => { const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
if (buffer.length >= 24) return buffer; if (buffer.length >= 24) return buffer
}); })
const reader = this.reader(data); const reader = this.reader(data)
reader.skip(1); reader.skip(1)
state.raw.versionMajor = reader.uint(1); state.raw.versionMajor = reader.uint(1)
state.raw.versionMinor = reader.uint(1); state.raw.versionMinor = reader.uint(1)
state.raw.versionPatch = reader.uint(1); state.raw.versionPatch = reader.uint(1)
reader.skip(8); reader.skip(8)
state.players.setNum(reader.uint(4)); state.players.setNum(reader.uint(4))
state.maxplayers = reader.uint(4); state.maxplayers = reader.uint(4)
state.raw.allowedbandwidth = reader.uint(4); state.raw.allowedbandwidth = reader.uint(4)
} }
} }

View file

@ -1,85 +1,85 @@
import Core from './core.js'; import Core from './core.js'
import Promises from "../lib/Promises.js"; import Promises from '../lib/Promises.js'
import * as gbxremote from 'gbxremote'; import * as gbxremote from 'gbxremote'
export default class nadeo extends Core { export default class nadeo extends Core {
async run(state) { async run (state) {
await this.withClient(async client => { await this.withClient(async client => {
const start = Date.now(); const start = Date.now()
await this.query(client, 'Authenticate', this.options.login, this.options.password); await this.query(client, 'Authenticate', this.options.login, this.options.password)
this.registerRtt(Date.now()-start); this.registerRtt(Date.now() - start)
//const data = this.methodCall(client, 'GetStatus'); // const data = this.methodCall(client, 'GetStatus');
{ {
const results = await this.query(client, 'GetServerOptions'); const results = await this.query(client, 'GetServerOptions')
state.name = this.stripColors(results.Name); state.name = this.stripColors(results.Name)
state.password = (results.Password !== 'No password'); state.password = (results.Password !== 'No password')
state.maxplayers = results.CurrentMaxPlayers; state.maxplayers = results.CurrentMaxPlayers
state.raw.maxspectators = results.CurrentMaxSpectators; state.raw.maxspectators = results.CurrentMaxSpectators
} }
{ {
const results = await this.query(client, 'GetCurrentMapInfo'); const results = await this.query(client, 'GetCurrentMapInfo')
state.map = this.stripColors(results.Name); state.map = this.stripColors(results.Name)
state.raw.mapUid = results.UId; state.raw.mapUid = results.UId
} }
{ {
const results = await this.query(client, 'GetCurrentGameInfo'); const results = await this.query(client, 'GetCurrentGameInfo')
let gamemode = ''; let gamemode = ''
const igm = results.GameMode; const igm = results.GameMode
if(igm === 0) gamemode="Rounds"; if (igm === 0) gamemode = 'Rounds'
if(igm === 1) gamemode="Time Attack"; if (igm === 1) gamemode = 'Time Attack'
if(igm === 2) gamemode="Team"; if (igm === 2) gamemode = 'Team'
if(igm === 3) gamemode="Laps"; if (igm === 3) gamemode = 'Laps'
if(igm === 4) gamemode="Stunts"; if (igm === 4) gamemode = 'Stunts'
if(igm === 5) gamemode="Cup"; if (igm === 5) gamemode = 'Cup'
state.raw.gametype = gamemode; state.raw.gametype = gamemode
state.raw.mapcount = results.NbChallenge; state.raw.mapcount = results.NbChallenge
} }
{ {
const results = await this.query(client, 'GetNextMapInfo'); const results = await this.query(client, 'GetNextMapInfo')
state.raw.nextmapName = this.stripColors(results.Name); state.raw.nextmapName = this.stripColors(results.Name)
state.raw.nextmapUid = results.UId; state.raw.nextmapUid = results.UId
} }
if (this.options.port === 5000) { if (this.options.port === 5000) {
state.gamePort = 2350; state.gamePort = 2350
} }
state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0); state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0)
for (const player of state.raw.players) { for (const player of state.raw.players) {
state.players.push({ state.players.push({
name:this.stripColors(player.Name || player.NickName) name: this.stripColors(player.Name || player.NickName)
}); })
} }
}); })
} }
async withClient(fn) { async withClient (fn) {
const socket = new gbxremote.Client(this.options.port, this.options.host); const socket = new gbxremote.Client(this.options.port, this.options.host)
try { try {
const connectPromise = socket.connect(); const connectPromise = socket.connect()
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening'); const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening')
await Promise.race([connectPromise, timeoutPromise, this.abortedPromise]); await Promise.race([connectPromise, timeoutPromise, this.abortedPromise])
return await fn(socket); return await fn(socket)
} finally { } finally {
socket.terminate(); socket.terminate()
} }
} }
async query(client, ...cmdset) { async query (client, ...cmdset) {
const cmd = cmdset[0]; const cmd = cmdset[0]
const params = cmdset.slice(1); const params = cmdset.slice(1)
const sentPromise = client.query(cmd, params); const sentPromise = client.query(cmd, params)
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call'); const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call')
return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise]); return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise])
} }
stripColors(str) { stripColors (str) {
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,''); return str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '')
} }
} }

View file

@ -1,127 +1,127 @@
import Core from './core.js'; import Core from './core.js'
export default class openttd extends Core { export default class openttd extends Core {
async run(state) { async run (state) {
{ {
const [reader, version] = await this.query(0, 1, 1, 4); const [reader, version] = await this.query(0, 1, 1, 4)
if (version >= 4) { if (version >= 4) {
const numGrf = reader.uint(1); const numGrf = reader.uint(1)
state.raw.grfs = []; state.raw.grfs = []
for (let i = 0; i < numGrf; i++) { for (let i = 0; i < numGrf; i++) {
const grf = {}; const grf = {}
grf.id = reader.part(4).toString('hex'); grf.id = reader.part(4).toString('hex')
grf.md5 = reader.part(16).toString('hex'); grf.md5 = reader.part(16).toString('hex')
state.raw.grfs.push(grf); state.raw.grfs.push(grf)
} }
} }
if (version >= 3) { if (version >= 3) {
state.raw.date_current = this.readDate(reader); state.raw.date_current = this.readDate(reader)
state.raw.date_start = this.readDate(reader); state.raw.date_start = this.readDate(reader)
} }
if (version >= 2) { if (version >= 2) {
state.raw.maxcompanies = reader.uint(1); state.raw.maxcompanies = reader.uint(1)
state.raw.numcompanies = reader.uint(1); state.raw.numcompanies = reader.uint(1)
state.raw.maxspectators = reader.uint(1); state.raw.maxspectators = reader.uint(1)
} }
state.name = reader.string(); state.name = reader.string()
state.raw.version = reader.string(); state.raw.version = reader.string()
state.raw.language = this.decode( state.raw.language = this.decode(
reader.uint(1), reader.uint(1),
['any', 'en', 'de', 'fr'] ['any', 'en', 'de', 'fr']
); )
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
state.maxplayers = reader.uint(1); state.maxplayers = reader.uint(1)
state.players.setNum(reader.uint(1)); state.players.setNum(reader.uint(1))
state.raw.numspectators = reader.uint(1); state.raw.numspectators = reader.uint(1)
state.map = reader.string(); state.map = reader.string()
state.raw.map_width = reader.uint(2); state.raw.map_width = reader.uint(2)
state.raw.map_height = reader.uint(2); state.raw.map_height = reader.uint(2)
state.raw.landscape = this.decode( state.raw.landscape = this.decode(
reader.uint(1), reader.uint(1),
['temperate', 'arctic', 'desert', 'toyland'] ['temperate', 'arctic', 'desert', 'toyland']
); )
state.raw.dedicated = !!reader.uint(1); state.raw.dedicated = !!reader.uint(1)
} }
{ {
const [reader,version] = await this.query(2,3,-1,-1); const [reader, version] = await this.query(2, 3, -1, -1)
// we don't know how to deal with companies outside version 6 // we don't know how to deal with companies outside version 6
if(version === 6) { if (version === 6) {
state.raw.companies = []; state.raw.companies = []
const numCompanies = reader.uint(1); const numCompanies = reader.uint(1)
for (let iCompany = 0; iCompany < numCompanies; iCompany++) { for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
const company = {}; const company = {}
company.id = reader.uint(1); company.id = reader.uint(1)
company.name = reader.string(); company.name = reader.string()
company.year_start = reader.uint(4); company.year_start = reader.uint(4)
company.value = reader.uint(8).toString(); company.value = reader.uint(8).toString()
company.money = reader.uint(8).toString(); company.money = reader.uint(8).toString()
company.income = reader.uint(8).toString(); company.income = reader.uint(8).toString()
company.performance = reader.uint(2); company.performance = reader.uint(2)
company.password = !!reader.uint(1); company.password = !!reader.uint(1)
const vehicle_types = ['train', 'truck', 'bus', 'aircraft', 'ship']; const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship']
const station_types = ['station', 'truckbay', 'busstation', 'airport', 'dock']; const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock']
company.vehicles = {}; company.vehicles = {}
for (const type of vehicle_types) { for (const type of vehicleTypes) {
company.vehicles[type] = reader.uint(2); company.vehicles[type] = reader.uint(2)
} }
company.stations = {}; company.stations = {}
for (const type of station_types) { for (const type of stationTypes) {
company.stations[type] = reader.uint(2); company.stations[type] = reader.uint(2)
} }
company.clients = reader.string(); company.clients = reader.string()
state.raw.companies.push(company); state.raw.companies.push(company)
} }
} }
} }
} }
async query(type,expected,minver,maxver) { async query (type, expected, minver, maxver) {
const b = Buffer.from([0x03,0x00,type]); const b = Buffer.from([0x03, 0x00, type])
return await this.udpSend(b,(buffer) => { return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const packetLen = reader.uint(2); const packetLen = reader.uint(2)
if(packetLen !== buffer.length) { if (packetLen !== buffer.length) {
this.logger.debug('Invalid reported packet length: '+packetLen+' '+buffer.length); this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length)
return; return
} }
const packetType = reader.uint(1); const packetType = reader.uint(1)
if(packetType !== expected) { if (packetType !== expected) {
this.logger.debug('Unexpected response packet type: '+packetType); this.logger.debug('Unexpected response packet type: ' + packetType)
return; return
} }
const protocolVersion = reader.uint(1); const protocolVersion = reader.uint(1)
if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) { if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
throw new Error('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver); throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver)
} }
return [reader,protocolVersion]; return [reader, protocolVersion]
}); })
} }
readDate(reader) { readDate (reader) {
const daysSinceZero = reader.uint(4); const daysSinceZero = reader.uint(4)
const temp = new Date(0,0,1); const temp = new Date(0, 0, 1)
temp.setFullYear(0); temp.setFullYear(0)
temp.setDate(daysSinceZero+1); temp.setDate(daysSinceZero + 1)
return temp.toISOString().split('T')[0]; return temp.toISOString().split('T')[0]
} }
decode(num,arr) { decode (num, arr) {
if(num < 0 || num >= arr.length) { if (num < 0 || num >= arr.length) {
return num; return num
} }
return arr[num]; return arr[num]
} }
} }

View file

@ -1,9 +1,9 @@
import quake2 from './quake2.js'; import quake2 from './quake2.js'
export default class quake1 extends quake2 { export default class quake1 extends quake2 {
constructor() { constructor () {
super(); super()
this.responseHeader = 'n'; this.responseHeader = 'n'
this.isQuake1 = true; this.isQuake1 = true
} }
} }

View file

@ -1,86 +1,86 @@
import Core from './core.js'; import Core from './core.js'
export default class quake2 extends Core { export default class quake2 extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.delimiter = '\n'; this.delimiter = '\n'
this.sendHeader = 'status'; this.sendHeader = 'status'
this.responseHeader = 'print'; this.responseHeader = 'print'
this.isQuake1 = false; this.isQuake1 = false
} }
async run(state) { async run (state) {
const body = await this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', packet => { const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => {
const reader = this.reader(packet); const reader = this.reader(packet)
const header = reader.string({length: 4, encoding: 'latin1'}); const header = reader.string({ length: 4, encoding: 'latin1' })
if (header !== '\xff\xff\xff\xff') return; if (header !== '\xff\xff\xff\xff') return
let type; let type
if (this.isQuake1) { if (this.isQuake1) {
type = reader.string(this.responseHeader.length); type = reader.string(this.responseHeader.length)
} else { } else {
type = reader.string({encoding: 'latin1'}); type = reader.string({ encoding: 'latin1' })
} }
if (type !== this.responseHeader) return; if (type !== this.responseHeader) return
return reader.rest(); return reader.rest()
}); })
const reader = this.reader(body); const reader = this.reader(body)
const info = reader.string().split('\\'); const info = reader.string().split('\\')
if(info[0] === '') info.shift(); if (info[0] === '') info.shift()
while(true) { while (true) {
const key = info.shift(); const key = info.shift()
const value = info.shift(); const value = info.shift()
if(typeof value === 'undefined') break; if (typeof value === 'undefined') break
state.raw[key] = value; state.raw[key] = value
} }
while(!reader.done()) { while (!reader.done()) {
const line = reader.string(); const line = reader.string()
if(!line || line.charAt(0) === '\0') break; if (!line || line.charAt(0) === '\0') break
const args = []; const args = []
const split = line.split('"'); const split = line.split('"')
split.forEach((part,i) => { split.forEach((part, i) => {
const inQuote = (i%2 === 1); const inQuote = (i % 2 === 1)
if(inQuote) { if (inQuote) {
args.push(part); args.push(part)
} else { } else {
const splitSpace = part.split(' '); const splitSpace = part.split(' ')
for (const subpart of splitSpace) { for (const subpart of splitSpace) {
if(subpart) args.push(subpart); if (subpart) args.push(subpart)
} }
} }
}); })
const player = {}; const player = {}
if(this.isQuake1) { if (this.isQuake1) {
player.id = parseInt(args.shift()); player.id = parseInt(args.shift())
player.score = parseInt(args.shift()); player.score = parseInt(args.shift())
player.time = parseInt(args.shift()); player.time = parseInt(args.shift())
player.ping = parseInt(args.shift()); player.ping = parseInt(args.shift())
player.name = args.shift(); player.name = args.shift()
player.skin = args.shift(); player.skin = args.shift()
player.color1 = parseInt(args.shift()); player.color1 = parseInt(args.shift())
player.color2 = parseInt(args.shift()); player.color2 = parseInt(args.shift())
} else { } else {
player.frags = parseInt(args.shift()); player.frags = parseInt(args.shift())
player.ping = parseInt(args.shift()); player.ping = parseInt(args.shift())
player.name = args.shift() || ''; player.name = args.shift() || ''
if (!player.name) delete player.name; if (!player.name) delete player.name
player.address = args.shift() || ''; player.address = args.shift() || ''
if (!player.address) delete player.address; if (!player.address) delete player.address
} }
(player.ping ? state.players : state.bots).push(player); (player.ping ? state.players : state.bots).push(player)
} }
if('g_needpass' in state.raw) state.password = state.raw.g_needpass; if ('g_needpass' in state.raw) state.password = state.raw.g_needpass
if('mapname' in state.raw) state.map = state.raw.mapname; if ('mapname' in state.raw) state.map = state.raw.mapname
if('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients; if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients
if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients; if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients
if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname; if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname
if('hostname' in state.raw) state.name = state.raw.hostname; if ('hostname' in state.raw) state.name = state.raw.hostname
} }
} }

View file

@ -1,22 +1,24 @@
import quake2 from './quake2.js'; import quake2 from './quake2.js'
export default class quake3 extends quake2 { export default class quake3 extends quake2 {
constructor() { constructor () {
super(); super()
this.sendHeader = 'getstatus'; this.sendHeader = 'getstatus'
this.responseHeader = 'statusResponse'; this.responseHeader = 'statusResponse'
} }
async run(state) {
await super.run(state); async run (state) {
state.name = this.stripColors(state.name); await super.run(state)
for(const key of Object.keys(state.raw)) { state.name = this.stripColors(state.name)
state.raw[key] = this.stripColors(state.raw[key]); for (const key of Object.keys(state.raw)) {
state.raw[key] = this.stripColors(state.raw[key])
} }
for(const player of state.players) { for (const player of state.players) {
player.name = this.stripColors(player.name); player.name = this.stripColors(player.name)
} }
} }
stripColors(str) {
return str.replace(/\^(X.{6}|.)/g,''); stripColors (str) {
return str.replace(/\^(X.{6}|.)/g, '')
} }
} }

View file

@ -1,49 +1,44 @@
import Core from './core.js'; import Core from './core.js'
export default class rfactor extends Core { export default class rfactor extends Core {
constructor() { async run (state) {
super(); const buffer = await this.udpSend('rF_S', b => b)
//this.byteorder = 'be'; const reader = this.reader(buffer)
}
async run(state) { state.raw.gamename = this.readString(reader, 8)
const buffer = await this.udpSend('rF_S',b => b); state.raw.fullUpdate = reader.uint(1)
const reader = this.reader(buffer); state.raw.region = reader.uint(2)
state.raw.ip = reader.part(4)
state.raw.size = reader.uint(2)
state.raw.version = reader.uint(2)
state.raw.versionRaceCast = reader.uint(2)
state.gamePort = reader.uint(2)
state.raw.queryPort = reader.uint(2)
state.raw.game = this.readString(reader, 20)
state.name = this.readString(reader, 28)
state.map = this.readString(reader, 32)
state.raw.motd = this.readString(reader, 96)
state.raw.packedAids = reader.uint(2)
state.raw.ping = reader.uint(2)
state.raw.packedFlags = reader.uint(1)
state.raw.rate = reader.uint(1)
state.players.setNum(reader.uint(1))
state.maxplayers = reader.uint(1)
state.raw.bots = reader.uint(1)
state.raw.packedSpecial = reader.uint(1)
state.raw.damage = reader.uint(1)
state.raw.packedRules = reader.uint(2)
state.raw.credits1 = reader.uint(1)
state.raw.credits2 = reader.uint(2)
this.logger.debug(reader.offset())
state.raw.time = reader.uint(2)
state.raw.laps = reader.uint(2) / 16
reader.skip(3)
state.raw.vehicles = reader.string()
state.raw.gamename = this.readString(reader, 8); state.password = !!(state.raw.packedSpecial & 2)
state.raw.fullUpdate = reader.uint(1); state.raw.raceCast = !!(state.raw.packedSpecial & 4)
state.raw.region = reader.uint(2); state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
state.raw.ip = reader.part(4);
state.raw.size = reader.uint(2);
state.raw.version = reader.uint(2);
state.raw.versionRaceCast = reader.uint(2);
state.gamePort = reader.uint(2);
state.raw.queryPort = reader.uint(2);
state.raw.game = this.readString(reader, 20);
state.name = this.readString(reader, 28);
state.map = this.readString(reader, 32);
state.raw.motd = this.readString(reader, 96);
state.raw.packedAids = reader.uint(2);
state.raw.ping = reader.uint(2);
state.raw.packedFlags = reader.uint(1);
state.raw.rate = reader.uint(1);
state.players.setNum(reader.uint(1));
state.maxplayers = reader.uint(1);
state.raw.bots = reader.uint(1);
state.raw.packedSpecial = reader.uint(1);
state.raw.damage = reader.uint(1);
state.raw.packedRules = reader.uint(2);
state.raw.credits1 = reader.uint(1);
state.raw.credits2 = reader.uint(2);
this.logger.debug(reader.offset());
state.raw.time = reader.uint(2);
state.raw.laps = reader.uint(2) / 16;
reader.skip(3);
state.raw.vehicles = reader.string();
state.password = !!(state.raw.packedSpecial & 2);
state.raw.raceCast = !!(state.raw.packedSpecial & 4);
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16);
const aids = [ const aids = [
'TractionControl', 'TractionControl',
@ -57,18 +52,18 @@ export default class rfactor extends Core {
'BrakingHelp', 'BrakingHelp',
'SpinRecovery', 'SpinRecovery',
'AutoPitstop' 'AutoPitstop'
]; ]
state.raw.aids = []; state.raw.aids = []
for (let offset = 0; offset < aids.length; offset++) { for (let offset = 0; offset < aids.length; offset++) {
if (state.packedAids && (1 << offset)) { if (state.packedAids && (1 << offset)) {
state.raw.aids.push(aids[offset]); state.raw.aids.push(aids[offset])
} }
} }
} }
// Consumes bytesToConsume, but only returns string up to the first null // Consumes bytesToConsume, but only returns string up to the first null
readString(reader, bytesToConsume) { readString (reader, bytesToConsume) {
const consumed = reader.part(bytesToConsume); const consumed = reader.part(bytesToConsume)
return this.reader(consumed).string(); return this.reader(consumed).string()
} }
} }

View file

@ -1,107 +1,108 @@
import Core from './core.js'; import Core from './core.js'
export default class samp extends Core { export default class samp extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'win1252'; this.encoding = 'win1252'
this.magicHeader = 'SAMP'; this.magicHeader = 'SAMP'
this.responseMagicHeader = null; this.responseMagicHeader = null
this.isVcmp = false; this.isVcmp = false
} }
async run(state) { async run (state) {
// read info // read info
{ {
const reader = await this.sendPacket('i'); const reader = await this.sendPacket('i')
if (this.isVcmp) { if (this.isVcmp) {
const consumed = reader.part(12); const consumed = reader.part(12)
state.raw.version = this.reader(consumed).string(); state.raw.version = this.reader(consumed).string()
} }
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
state.raw.numplayers = reader.uint(2); state.raw.numplayers = reader.uint(2)
state.maxplayers = reader.uint(2); state.maxplayers = reader.uint(2)
state.name = reader.pascalString(4); state.name = reader.pascalString(4)
state.raw.gamemode = reader.pascalString(4); state.raw.gamemode = reader.pascalString(4)
state.raw.map = reader.pascalString(4); state.raw.map = reader.pascalString(4)
} }
// read rules // read rules
if (!this.isVcmp) { if (!this.isVcmp) {
const reader = await this.sendPacket('r'); const reader = await this.sendPacket('r')
const ruleCount = reader.uint(2); const ruleCount = reader.uint(2)
state.raw.rules = {}; state.raw.rules = {}
for(let i = 0; i < ruleCount; i++) { for (let i = 0; i < ruleCount; i++) {
const key = reader.pascalString(1); const key = reader.pascalString(1)
const value = reader.pascalString(1); const value = reader.pascalString(1)
state.raw.rules[key] = value; state.raw.rules[key] = value
} }
} }
// read players // read players
// don't even bother if > 100 players, because the server won't respond // don't even bother if > 100 players, because the server won't respond
let gotPlayerData = false; let gotPlayerData = false
if (state.raw.numplayers < 100) { if (state.raw.numplayers < 100) {
if (this.isVcmp) { if (this.isVcmp) {
const reader = await this.sendPacket('c', true); const reader = await this.sendPacket('c', true)
if (reader !== null) { if (reader !== null) {
gotPlayerData = true; gotPlayerData = true
const playerCount = reader.uint(2); const playerCount = reader.uint(2)
for(let i = 0; i < playerCount; i++) { for (let i = 0; i < playerCount; i++) {
const player = {}; const player = {}
player.name = reader.pascalString(1); player.name = reader.pascalString(1)
state.players.push(player); state.players.push(player)
} }
} }
} else { } else {
const reader = await this.sendPacket('d', true); const reader = await this.sendPacket('d', true)
if (reader !== null) { if (reader !== null) {
gotPlayerData = true; gotPlayerData = true
const playerCount = reader.uint(2); const playerCount = reader.uint(2)
for(let i = 0; i < playerCount; i++) { for (let i = 0; i < playerCount; i++) {
const player = {}; const player = {}
player.id = reader.uint(1); player.id = reader.uint(1)
player.name = reader.pascalString(1); player.name = reader.pascalString(1)
player.score = reader.int(4); player.score = reader.int(4)
player.ping = reader.uint(4); player.ping = reader.uint(4)
state.players.push(player); state.players.push(player)
} }
} }
} }
} }
if (!gotPlayerData) { if (!gotPlayerData) {
state.players.setNum(state.raw.numplayers); state.players.setNum(state.raw.numplayers)
} }
} }
async sendPacket(type,allowTimeout) {
const outBuffer = Buffer.alloc(11);
outBuffer.write(this.magicHeader,0, 4);
const ipSplit = this.options.address.split('.');
outBuffer.writeUInt8(parseInt(ipSplit[0]),4);
outBuffer.writeUInt8(parseInt(ipSplit[1]),5);
outBuffer.writeUInt8(parseInt(ipSplit[2]),6);
outBuffer.writeUInt8(parseInt(ipSplit[3]),7);
outBuffer.writeUInt16LE(this.options.port,8);
outBuffer.writeUInt8(type.charCodeAt(0),10);
const checkBuffer = Buffer.from(outBuffer); async sendPacket (type, allowTimeout) {
const outBuffer = Buffer.alloc(11)
outBuffer.write(this.magicHeader, 0, 4)
const ipSplit = this.options.address.split('.')
outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
outBuffer.writeUInt16LE(this.options.port, 8)
outBuffer.writeUInt8(type.charCodeAt(0), 10)
const checkBuffer = Buffer.from(outBuffer)
if (this.responseMagicHeader) { if (this.responseMagicHeader) {
checkBuffer.write(this.responseMagicHeader, 0, 4); checkBuffer.write(this.responseMagicHeader, 0, 4)
} }
return await this.udpSend( return await this.udpSend(
outBuffer, outBuffer,
(buffer) => { (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
for(let i = 0; i < checkBuffer.length; i++) { for (let i = 0; i < checkBuffer.length; i++) {
if(checkBuffer.readUInt8(i) !== reader.uint(1)) return; if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
} }
return reader; return reader
}, },
() => { () => {
if(allowTimeout) { if (allowTimeout) {
return null; return null
} }
} }
); )
} }
} }

View file

@ -1,29 +1,25 @@
import Core from './core.js'; import Core from './core.js'
export default class savage2 extends Core { export default class savage2 extends Core {
constructor() { async run (state) {
super(); const buffer = await this.udpSend('\x01', b => b)
const reader = this.reader(buffer)
reader.skip(12)
state.name = this.stripColorCodes(reader.string())
state.players.setNum(reader.uint(1))
state.maxplayers = reader.uint(1)
state.raw.time = reader.string()
state.map = reader.string()
state.raw.nextmap = reader.string()
state.raw.location = reader.string()
state.raw.minplayers = reader.uint(1)
state.raw.gametype = reader.string()
state.raw.version = reader.string()
state.raw.minlevel = reader.uint(1)
} }
async run(state) { stripColorCodes (str) {
const buffer = await this.udpSend('\x01',b => b); return str.replace(/\^./g, '')
const reader = this.reader(buffer);
reader.skip(12);
state.name = this.stripColorCodes(reader.string());
state.players.setNum(reader.uint(1));
state.maxplayers = reader.uint(1);
state.raw.time = reader.string();
state.map = reader.string();
state.raw.nextmap = reader.string();
state.raw.location = reader.string();
state.raw.minplayers = reader.uint(1);
state.raw.gametype = reader.string();
state.raw.version = reader.string();
state.raw.minlevel = reader.uint(1);
}
stripColorCodes(str) {
return str.replace(/\^./g,'');
} }
} }

View file

@ -1,67 +1,67 @@
import Core from './core.js'; import Core from './core.js'
export default class starmade extends Core { export default class starmade extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
const b = Buffer.from([0x00,0x00,0x00,0x09,0x2a,0xff,0xff,0x01,0x6f,0x00,0x00,0x00,0x00]); const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00])
const payload = await this.withTcp(async socket => { const payload = await this.withTcp(async socket => {
return await this.tcpSend(socket, b, buffer => { return await this.tcpSend(socket, b, buffer => {
if (buffer.length < 12) return; if (buffer.length < 12) return
const reader = this.reader(buffer); const reader = this.reader(buffer)
const packetLength = reader.uint(4); const packetLength = reader.uint(4)
this.logger.debug("Received packet length: " + packetLength); this.logger.debug('Received packet length: ' + packetLength)
const timestamp = reader.uint(8).toString(); const timestamp = reader.uint(8).toString()
this.logger.debug("Received timestamp: " + timestamp); this.logger.debug('Received timestamp: ' + timestamp)
if (reader.remaining() < packetLength || reader.remaining() < 5) return; if (reader.remaining() < packetLength || reader.remaining() < 5) return
const checkId = reader.uint(1); const checkId = reader.uint(1)
const packetId = reader.uint(2); const packetId = reader.uint(2)
const commandId = reader.uint(1); const commandId = reader.uint(1)
const type = reader.uint(1); const type = reader.uint(1)
this.logger.debug("checkId=" + checkId + " packetId=" + packetId + " commandId=" + commandId + " type=" + type); this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type)
if (checkId !== 0x2a) return; if (checkId !== 0x2a) return
return reader.rest(); return reader.rest()
}); })
}); })
const reader = this.reader(payload); const reader = this.reader(payload)
const data = []; const data = []
state.raw.data = data; state.raw.data = data
while(!reader.done()) { while (!reader.done()) {
const mark = reader.uint(1); const mark = reader.uint(1)
if(mark === 1) { if (mark === 1) {
// signed int // signed int
data.push(reader.int(4)); data.push(reader.int(4))
} else if(mark === 3) { } else if (mark === 3) {
// float // float
data.push(reader.float()); data.push(reader.float())
} else if(mark === 4) { } else if (mark === 4) {
// string // string
data.push(reader.pascalString(2)); data.push(reader.pascalString(2))
} else if(mark === 6) { } else if (mark === 6) {
// byte // byte
data.push(reader.uint(1)); data.push(reader.uint(1))
} }
} }
this.logger.debug("Received raw data array", data); this.logger.debug('Received raw data array', data)
if(typeof data[0] === 'number') state.raw.infoVersion = data[0]; if (typeof data[0] === 'number') state.raw.infoVersion = data[0]
if(typeof data[1] === 'number') state.raw.version = data[1]; if (typeof data[1] === 'number') state.raw.version = data[1]
if(typeof data[2] === 'string') state.name = data[2]; if (typeof data[2] === 'string') state.name = data[2]
if(typeof data[3] === 'string') state.raw.description = data[3]; if (typeof data[3] === 'string') state.raw.description = data[3]
if(typeof data[4] === 'number') state.raw.startTime = data[4]; if (typeof data[4] === 'number') state.raw.startTime = data[4]
if(typeof data[5] === 'number') state.players.setNum(data[5]); if (typeof data[5] === 'number') state.players.setNum(data[5])
if(typeof data[6] === 'number') state.maxplayers = data[6]; if (typeof data[6] === 'number') state.maxplayers = data[6]
} }
} }

View file

@ -1,10 +1,10 @@
import tribes1 from "./tribes1.js"; import tribes1 from './tribes1.js'
export default class starsiege extends tribes1 { export default class starsiege extends tribes1 {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.requestByte = 0x72; this.requestByte = 0x72
this.responseByte = 0x73; this.responseByte = 0x73
} }
} }

View file

@ -1,70 +1,70 @@
import Core from './core.js'; import Core from './core.js'
export default class teamspeak2 extends Core { export default class teamspeak2 extends Core {
async run(state) { async run (state) {
const queryPort = this.options.teamspeakQueryPort || 51234; const queryPort = this.options.teamspeakQueryPort || 51234
await this.withTcp(async socket => { await this.withTcp(async socket => {
{ {
const data = await this.sendCommand(socket, 'sel '+this.options.port); const data = await this.sendCommand(socket, 'sel ' + this.options.port)
if(data !== '[TS]') throw new Error('Invalid header'); if (data !== '[TS]') throw new Error('Invalid header')
} }
{ {
const data = await this.sendCommand(socket, 'si'); const data = await this.sendCommand(socket, 'si')
for (const line of data.split('\r\n')) { for (const line of data.split('\r\n')) {
const equals = line.indexOf('='); const equals = line.indexOf('=')
const key = equals === -1 ? line : line.substring(0,equals); const key = equals === -1 ? line : line.substring(0, equals)
const value = equals === -1 ? '' : line.substring(equals+1); const value = equals === -1 ? '' : line.substring(equals + 1)
state.raw[key] = value; state.raw[key] = value
} }
} }
{ {
const data = await this.sendCommand(socket, 'pl'); const data = await this.sendCommand(socket, 'pl')
const split = data.split('\r\n'); const split = data.split('\r\n')
const fields = split.shift().split('\t'); const fields = split.shift().split('\t')
for (const line of split) { for (const line of split) {
const split2 = line.split('\t'); const split2 = line.split('\t')
const player = {}; const player = {}
split2.forEach((value,i) => { split2.forEach((value, i) => {
let key = fields[i]; let key = fields[i]
if(!key) return; if (!key) return
if(key === 'nick') key = 'name'; if (key === 'nick') key = 'name'
const m = value.match(/^"(.*)"$/); const m = value.match(/^"(.*)"$/)
if(m) value = m[1]; if (m) value = m[1]
player[key] = value; player[key] = value
}); })
state.players.push(player); state.players.push(player)
} }
} }
{ {
const data = await this.sendCommand(socket, 'cl'); const data = await this.sendCommand(socket, 'cl')
const split = data.split('\r\n'); const split = data.split('\r\n')
const fields = split.shift().split('\t'); const fields = split.shift().split('\t')
state.raw.channels = []; state.raw.channels = []
for (const line of split) { for (const line of split) {
const split2 = line.split('\t'); const split2 = line.split('\t')
const channel = {}; const channel = {}
split2.forEach((value,i) => { split2.forEach((value, i) => {
const key = fields[i]; const key = fields[i]
if(!key) return; if (!key) return
const m = value.match(/^"(.*)"$/); const m = value.match(/^"(.*)"$/)
if(m) value = m[1]; if (m) value = m[1]
channel[key] = value; channel[key] = value
}); })
state.raw.channels.push(channel); state.raw.channels.push(channel)
} }
} }
}, queryPort); }, queryPort)
} }
async sendCommand(socket,cmd) { async sendCommand (socket, cmd) {
return await this.tcpSend(socket, cmd+'\x0A', buffer => { return await this.tcpSend(socket, cmd + '\x0A', buffer => {
if(buffer.length < 6) return; if (buffer.length < 6) return
if(buffer.slice(-6).toString() !== '\r\nOK\r\n') return; if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return
return buffer.slice(0,-6).toString(); return buffer.slice(0, -6).toString()
}); })
} }
} }

View file

@ -1,66 +1,68 @@
import Core from './core.js'; import Core from './core.js'
export default class teamspeak3 extends Core { export default class teamspeak3 extends Core {
async run(state) { async run (state) {
const queryPort = this.options.teamspeakQueryPort || 10011; const queryPort = this.options.teamspeakQueryPort || 10011
await this.withTcp(async socket => { await this.withTcp(async socket => {
{ {
const data = await this.sendCommand(socket, 'use port='+this.options.port, true); const data = await this.sendCommand(socket, 'use port=' + this.options.port, true)
const split = data.split('\n\r'); const split = data.split('\n\r')
if(split[0] !== 'TS3') throw new Error('Invalid header'); if (split[0] !== 'TS3') throw new Error('Invalid header')
} }
{ {
const data = await this.sendCommand(socket, 'serverinfo'); const data = await this.sendCommand(socket, 'serverinfo')
state.raw = data[0]; state.raw = data[0]
if('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name; if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name
if('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients; if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients
} }
{ {
const list = await this.sendCommand(socket, 'clientlist'); const list = await this.sendCommand(socket, 'clientlist')
for (const client of list) { for (const client of list) {
client.name = client.client_nickname; client.name = client.client_nickname
delete client.client_nickname; delete client.client_nickname
if(client.client_type === '0') { if (client.client_type === '0') {
state.players.push(client); state.players.push(client)
} }
} }
} }
{ {
const data = await this.sendCommand(socket, 'channellist -topic'); const data = await this.sendCommand(socket, 'channellist -topic')
state.raw.channels = data; state.raw.channels = data
} }
}, queryPort); }, queryPort)
} }
async sendCommand(socket,cmd,raw) { async sendCommand (socket, cmd, raw) {
const body = await this.tcpSend(socket, cmd+'\x0A', (buffer) => { const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => {
if (buffer.length < 21) return; if (buffer.length < 21) return
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return; if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return
return buffer.slice(0, -21).toString(); return buffer.slice(0, -21).toString()
}); })
if(raw) { if (raw) {
return body; return body
} else { } else {
const segments = body.split('|'); const segments = body.split('|')
const out = []; const out = []
for (const line of segments) { for (const line of segments) {
const split = line.split(' '); const split = line.split(' ')
const unit = {}; const unit = {}
for (const field of split) { for (const field of split) {
const equals = field.indexOf('='); const equals = field.indexOf('=')
const key = equals === -1 ? field : field.substring(0, equals); const key = equals === -1 ? field : field.substring(0, equals)
const value = equals === -1 ? '' : field.substring(equals + 1) const value = equals === -1
.replace(/\\s/g,' ').replace(/\\\//g,'/'); ? ''
unit[key] = value; : field.substring(equals + 1)
.replace(/\\s/g, ' ').replace(/\\\//g, '/')
unit[key] = value
} }
out.push(unit); out.push(unit)
} }
return out; return out
} }
} }
} }

View file

@ -1,24 +1,24 @@
import Core from './core.js'; import Core from './core.js'
export default class terraria extends Core { export default class terraria extends Core {
async run(state) { async run (state) {
const json = await this.request({ const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status', url: 'http://' + this.options.address + ':' + this.options.port + '/v2/server/status',
searchParams: { searchParams: {
players: 'true', players: 'true',
token: this.options.token token: this.options.token
}, },
responseType: 'json' responseType: 'json'
}); })
if(json.status !== '200') throw new Error('Invalid status'); if (json.status !== '200') throw new Error('Invalid status')
for (const one of json.players) { for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team}); state.players.push({ name: one.nickname, team: one.team })
} }
state.name = json.name; state.name = json.name
state.gamePort = json.port; state.gamePort = json.port
state.raw.numplayers = json.playercount; state.raw.numplayers = json.playercount
} }
} }

View file

@ -1,150 +1,153 @@
import Core from './core.js'; import Core from './core.js'
export default class tribes1 extends Core { export default class tribes1 extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.requestByte = 0x62; this.requestByte = 0x62
this.responseByte = 0x63; this.responseByte = 0x63
this.challenge = 0x01; this.challenge = 0x01
} }
async run(state) { async run (state) {
const query = Buffer.alloc(3); const query = Buffer.alloc(3)
query.writeUInt8(this.requestByte, 0); query.writeUInt8(this.requestByte, 0)
query.writeUInt16LE(this.challenge, 1); query.writeUInt16LE(this.challenge, 1)
const reader = await this.udpSend(query,(buffer) => { const reader = await this.udpSend(query, (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const responseByte = reader.uint(1); const responseByte = reader.uint(1)
if (responseByte !== this.responseByte) { if (responseByte !== this.responseByte) {
this.logger.debug('Unexpected response byte'); this.logger.debug('Unexpected response byte')
return; return
} }
const challenge = reader.uint(2); const challenge = reader.uint(2)
if (challenge !== this.challenge) { if (challenge !== this.challenge) {
this.logger.debug('Unexpected challenge'); this.logger.debug('Unexpected challenge')
return; return
} }
const requestByte = reader.uint(1); const requestByte = reader.uint(1)
if (requestByte !== this.requestByte) { if (requestByte !== this.requestByte) {
this.logger.debug('Unexpected request byte'); this.logger.debug('Unexpected request byte')
return; return
} }
return reader; return reader
}); })
state.raw.gametype = this.readString(reader); state.raw.gametype = this.readString(reader)
const isStarsiege2009 = state.raw.gametype === 'Starsiege'; const isStarsiege2009 = state.raw.gametype === 'Starsiege'
state.raw.version = this.readString(reader); state.raw.version = this.readString(reader)
state.name = this.readString(reader); state.name = this.readString(reader)
if (isStarsiege2009) { if (isStarsiege2009) {
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
state.raw.dedicated = !!reader.uint(1); state.raw.dedicated = !!reader.uint(1)
state.raw.dropInProgress = !!reader.uint(1); state.raw.dropInProgress = !!reader.uint(1)
state.raw.gameInProgress = !!reader.uint(1); state.raw.gameInProgress = !!reader.uint(1)
state.raw.playerCount = reader.uint(4); state.raw.playerCount = reader.uint(4)
state.maxplayers = reader.uint(4); state.maxplayers = reader.uint(4)
state.raw.teamPlay = reader.uint(1); state.raw.teamPlay = reader.uint(1)
state.map = this.readString(reader); state.map = this.readString(reader)
state.raw.cpuSpeed = reader.uint(2); state.raw.cpuSpeed = reader.uint(2)
state.raw.factoryVeh = reader.uint(1); state.raw.factoryVeh = reader.uint(1)
state.raw.allowTecmix = reader.uint(1); state.raw.allowTecmix = reader.uint(1)
state.raw.spawnLimit = reader.uint(4); state.raw.spawnLimit = reader.uint(4)
state.raw.fragLimit = reader.uint(4); state.raw.fragLimit = reader.uint(4)
state.raw.timeLimit = reader.uint(4); state.raw.timeLimit = reader.uint(4)
state.raw.techLimit = reader.uint(4); state.raw.techLimit = reader.uint(4)
state.raw.combatLimit = reader.uint(4); state.raw.combatLimit = reader.uint(4)
state.raw.massLimit = reader.uint(4); state.raw.massLimit = reader.uint(4)
state.raw.playersSent = reader.uint(4); state.raw.playersSent = reader.uint(4)
const teams = {1:'yellow', 2:'blue', 4:'red', 8:'purple'}; const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' }
while (!reader.done()) { while (!reader.done()) {
const player = {}; const player = {}
player.name = this.readString(reader); player.name = this.readString(reader)
const teamId = reader.uint(1); const teamId = reader.uint(1)
const team = teams[teamId]; const team = teams[teamId]
if (team) player.team = teams[teamId]; if (team) player.team = teams[teamId]
} }
return; return
} }
state.raw.dedicated = !!reader.uint(1); state.raw.dedicated = !!reader.uint(1)
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
state.raw.playerCount = reader.uint(1); state.raw.playerCount = reader.uint(1)
state.maxplayers = reader.uint(1); state.maxplayers = reader.uint(1)
state.raw.cpuSpeed = reader.uint(2); state.raw.cpuSpeed = reader.uint(2)
state.raw.mod = this.readString(reader); state.raw.mod = this.readString(reader)
state.raw.type = this.readString(reader); state.raw.type = this.readString(reader)
state.map = this.readString(reader); state.map = this.readString(reader)
state.raw.motd = this.readString(reader); state.raw.motd = this.readString(reader)
state.raw.teamCount = reader.uint(1); state.raw.teamCount = reader.uint(1)
const teamFields = this.readFieldList(reader); const teamFields = this.readFieldList(reader)
const playerFields = this.readFieldList(reader); const playerFields = this.readFieldList(reader)
state.raw.teams = []; state.raw.teams = []
for(let i = 0; i < state.raw.teamCount; i++) { for (let i = 0; i < state.raw.teamCount; i++) {
const teamName = this.readString(reader); const teamName = this.readString(reader)
const teamValues = this.readValues(reader); const teamValues = this.readValues(reader)
const teamInfo = {}; const teamInfo = {}
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) { for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
let key = teamFields[i]; let key = teamFields[i]
let value = teamValues[i]; let value = teamValues[i]
if (key === 'ultra_base') key = 'name'; if (key === 'ultra_base') key = 'name'
if (value === '%t') value = teamName; if (value === '%t') value = teamName
if (['score','players'].includes(key)) value = parseInt(value); if (['score', 'players'].includes(key)) value = parseInt(value)
teamInfo[key] = value; teamInfo[key] = value
} }
state.raw.teams.push(teamInfo); state.raw.teams.push(teamInfo)
} }
for(let i = 0; i < state.raw.playerCount; i++) { for (let i = 0; i < state.raw.playerCount; i++) {
const ping = reader.uint(1) * 4; const ping = reader.uint(1) * 4
const packetLoss = reader.uint(1); const packetLoss = reader.uint(1)
const teamNum = reader.uint(1); const teamNum = reader.uint(1)
const name = this.readString(reader); const name = this.readString(reader)
const playerValues = this.readValues(reader); const playerValues = this.readValues(reader)
const playerInfo = {}; const playerInfo = {}
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) { for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
let key = playerFields[i]; const key = playerFields[i]
let value = playerValues[i]; let value = playerValues[i]
if (value === '%p') value = ping; if (value === '%p') value = ping
if (value === '%l') value = packetLoss; if (value === '%l') value = packetLoss
if (value === '%t') value = teamNum; if (value === '%t') value = teamNum
if (value === '%n') value = name; if (value === '%n') value = name
if (['score','ping','pl','kills','lvl'].includes(key)) value = parseInt(value); if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value)
if (key === 'team') { if (key === 'team') {
const teamId = parseInt(value); const teamId = parseInt(value)
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) { if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
value = state.raw.teams[teamId].name; value = state.raw.teams[teamId].name
} else { } else {
continue; continue
} }
} }
playerInfo[key] = value; playerInfo[key] = value
} }
state.players.push(playerInfo); state.players.push(playerInfo)
} }
} }
readFieldList(reader) {
const str = this.readString(reader); readFieldList (reader) {
if (!str) return []; const str = this.readString(reader)
return ('?'+str) if (!str) return []
return ('?' + str)
.split('\t') .split('\t')
.map((a) => a.substring(1).trim().toLowerCase()) .map((a) => a.substring(1).trim().toLowerCase())
.map((a) => a === 'team name' ? 'name' : a) .map((a) => a === 'team name' ? 'name' : a)
.map((a) => a === 'player name' ? 'name' : a); .map((a) => a === 'player name' ? 'name' : a)
} }
readValues(reader) {
const str = this.readString(reader); readValues (reader) {
if (!str) return []; const str = this.readString(reader)
if (!str) return []
return str return str
.split('\t') .split('\t')
.map((a) => a.trim()); .map((a) => a.trim())
} }
readString(reader) {
return reader.pascalString(1); readString (reader) {
return reader.pascalString(1)
} }
} }

View file

@ -1,79 +1,80 @@
import Core from './core.js'; import Core from './core.js'
/** Unsupported -- use at your own risk!! */ /** Unsupported -- use at your own risk!! */
export default class tribes1master extends Core { export default class tribes1master extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
} }
async run(state) { async run (state) {
const queryBuffer = Buffer.from([ const queryBuffer = Buffer.from([
0x10, // standard header 0x10, // standard header
0x03, // dump servers 0x03, // dump servers
0xff, // ask for all packets 0xff, // ask for all packets
0x00, // junk 0x00, // junk
0x01, 0x02, // challenge 0x01, 0x02 // challenge
]); ])
let parts = new Map(); const parts = new Map()
let total = 0; let total = 0
const full = await this.udpSend(queryBuffer,(buffer) => { const full = await this.udpSend(queryBuffer, (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const header = reader.uint(2); const header = reader.uint(2)
if (header !== 0x0610) { if (header !== 0x0610) {
this.logger.debug('Header response does not match: ' + header.toString(16)); this.logger.debug('Header response does not match: ' + header.toString(16))
return; return
} }
const num = reader.uint(1); const num = reader.uint(1)
const t = reader.uint(1); const t = reader.uint(1)
if (t <= 0 || (total > 0 && t !== total)) { if (t <= 0 || (total > 0 && t !== total)) {
throw new Error('Conflicting packet total: ' + t); throw new Error('Conflicting packet total: ' + t)
} }
total = t; total = t
if (num < 1 || num > total) { if (num < 1 || num > total) {
this.logger.debug('Invalid packet number: ' + num + ' ' + total); this.logger.debug('Invalid packet number: ' + num + ' ' + total)
return; return
} }
if (parts.has(num)) { if (parts.has(num)) {
this.logger.debug('Duplicate part: ' + num); this.logger.debug('Duplicate part: ' + num)
return; return
} }
reader.skip(2); // challenge (0x0201) reader.skip(2) // challenge (0x0201)
reader.skip(2); // always 0x6600 reader.skip(2) // always 0x6600
parts.set(num, reader.rest()); parts.set(num, reader.rest())
if (parts.size === total) { if (parts.size === total) {
const ordered = []; const ordered = []
for (let i = 1; i <= total; i++) ordered.push(parts.get(i)); for (let i = 1; i <= total; i++) ordered.push(parts.get(i))
return Buffer.concat(ordered); return Buffer.concat(ordered)
} }
}); })
const fullReader = this.reader(full); const fullReader = this.reader(full)
state.raw.name = this.readString(fullReader); state.raw.name = this.readString(fullReader)
state.raw.motd = this.readString(fullReader); state.raw.motd = this.readString(fullReader)
state.raw.servers = []; state.raw.servers = []
while (!fullReader.done()) { while (!fullReader.done()) {
fullReader.skip(1); // junk ? fullReader.skip(1) // junk ?
const count = fullReader.uint(1); const count = fullReader.uint(1)
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const six = fullReader.uint(1); const six = fullReader.uint(1)
if (six !== 6) { if (six !== 6) {
throw new Error('Expecting 6'); throw new Error('Expecting 6')
} }
const ip = fullReader.uint(4); const ip = fullReader.uint(4)
const port = fullReader.uint(2); const port = fullReader.uint(2)
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24); const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24)
state.raw.servers.push(ipStr+":"+port); state.raw.servers.push(ipStr + ':' + port)
} }
} }
} }
readString(reader) {
return reader.pascalString(1); readString (reader) {
return reader.pascalString(1)
} }
} }

View file

@ -1,150 +1,150 @@
import Core from './core.js'; import Core from './core.js'
export default class unreal2 extends Core { export default class unreal2 extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
} }
async run(state) {
let extraInfoReader; async run (state) {
let extraInfoReader
{ {
const b = await this.sendPacket(0, true); const b = await this.sendPacket(0, true)
const reader = this.reader(b); const reader = this.reader(b)
state.raw.serverid = reader.uint(4); state.raw.serverid = reader.uint(4)
state.raw.ip = this.readUnrealString(reader); state.raw.ip = this.readUnrealString(reader)
state.gamePort = reader.uint(4); state.gamePort = reader.uint(4)
state.raw.queryport = reader.uint(4); state.raw.queryport = reader.uint(4)
state.name = this.readUnrealString(reader, true); state.name = this.readUnrealString(reader, true)
state.map = this.readUnrealString(reader, true); state.map = this.readUnrealString(reader, true)
state.raw.gametype = this.readUnrealString(reader, true); state.raw.gametype = this.readUnrealString(reader, true)
state.raw.numplayers = reader.uint(4); state.raw.numplayers = reader.uint(4)
state.maxplayers = reader.uint(4); state.maxplayers = reader.uint(4)
this.logger.debug(log => { this.logger.debug(log => {
log("UNREAL2 EXTRA INFO", reader.buffer.slice(reader.i)); log('UNREAL2 EXTRA INFO', reader.buffer.slice(reader.i))
}); })
extraInfoReader = reader; extraInfoReader = reader
} }
{ {
const b = await this.sendPacket(1,true); const b = await this.sendPacket(1, true)
const reader = this.reader(b); const reader = this.reader(b)
state.raw.mutators = []; state.raw.mutators = []
state.raw.rules = {}; state.raw.rules = {}
while(!reader.done()) { while (!reader.done()) {
const key = this.readUnrealString(reader,true); const key = this.readUnrealString(reader, true)
const value = this.readUnrealString(reader,true); const value = this.readUnrealString(reader, true)
this.logger.debug(key+'='+value); this.logger.debug(key + '=' + value)
if(key === 'Mutator' || key === 'mutator') { if (key === 'Mutator' || key === 'mutator') {
state.raw.mutators.push(value); state.raw.mutators.push(value)
} else if (key || value) { } else if (key || value) {
if (state.raw.rules.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(state.raw.rules, key)) {
state.raw.rules[key] += ',' + value; state.raw.rules[key] += ',' + value
} else { } else {
state.raw.rules[key] = value; state.raw.rules[key] = value
} }
} }
} }
if('GamePassword' in state.raw.rules) if ('GamePassword' in state.raw.rules) { state.password = state.raw.rules.GamePassword !== 'True' }
state.password = state.raw.rules.GamePassword !== 'True';
} }
if (state.raw.mutators.includes('KillingFloorMut') if (state.raw.mutators.includes('KillingFloorMut') ||
|| state.raw.rules['Num trader weapons'] state.raw.rules['Num trader weapons'] ||
|| state.raw.rules['Server Version'] === '1065' state.raw.rules['Server Version'] === '1065'
) { ) {
// Killing Floor // Killing Floor
state.raw.wavecurrent = extraInfoReader.uint(4); state.raw.wavecurrent = extraInfoReader.uint(4)
state.raw.wavetotal = extraInfoReader.uint(4); state.raw.wavetotal = extraInfoReader.uint(4)
state.raw.ping = extraInfoReader.uint(4); state.raw.ping = extraInfoReader.uint(4)
state.raw.flags = extraInfoReader.uint(4); state.raw.flags = extraInfoReader.uint(4)
state.raw.skillLevel = this.readUnrealString(extraInfoReader, true); state.raw.skillLevel = this.readUnrealString(extraInfoReader, true)
} else { } else {
state.raw.ping = extraInfoReader.uint(4); state.raw.ping = extraInfoReader.uint(4)
// These fields were added in later revisions of unreal engine // These fields were added in later revisions of unreal engine
if (extraInfoReader.remaining() >= 8) { if (extraInfoReader.remaining() >= 8) {
state.raw.flags = extraInfoReader.uint(4); state.raw.flags = extraInfoReader.uint(4)
state.raw.skill = this.readUnrealString(extraInfoReader, true); state.raw.skill = this.readUnrealString(extraInfoReader, true)
} }
} }
{ {
const b = await this.sendPacket(2,false); const b = await this.sendPacket(2, false)
const reader = this.reader(b); const reader = this.reader(b)
state.raw.scoreboard = {}; state.raw.scoreboard = {}
while(!reader.done()) { while (!reader.done()) {
const player = {}; const player = {}
player.id = reader.uint(4); player.id = reader.uint(4)
player.name = this.readUnrealString(reader,true); player.name = this.readUnrealString(reader, true)
player.ping = reader.uint(4); player.ping = reader.uint(4)
player.score = reader.int(4); player.score = reader.int(4)
player.statsId = reader.uint(4); player.statsId = reader.uint(4)
this.logger.debug(player); this.logger.debug(player)
if (!player.id) { if (!player.id) {
state.raw.scoreboard[player.name] = player.score; state.raw.scoreboard[player.name] = player.score
} else if (!player.ping) { } else if (!player.ping) {
state.bots.push(player); state.bots.push(player)
} else { } else {
state.players.push(player); state.players.push(player)
} }
} }
} }
} }
readUnrealString(reader, stripColor) { readUnrealString (reader, stripColor) {
let length = reader.uint(1), ucs2 = false; let length = reader.uint(1); let ucs2 = false
if(length >= 0x80) { if (length >= 0x80) {
// This is flagged as a UCS-2 String // This is flagged as a UCS-2 String
length = (length&0x7f)*2; length = (length & 0x7f) * 2
ucs2 = true; ucs2 = true
// For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here, // For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here,
// not included in the length. Skip it if present (hopefully this never happens legitimately) // not included in the length. Skip it if present (hopefully this never happens legitimately)
const peek = reader.uint(1); const peek = reader.uint(1)
if (peek !== 1) reader.skip(-1); if (peek !== 1) reader.skip(-1)
this.logger.debug(log => { this.logger.debug(log => {
log("UCS2 STRING"); log('UCS2 STRING')
log("UCS2 Length: " + length); log('UCS2 Length: ' + length)
log(reader.buffer.slice(reader.i,reader.i+length)); log(reader.buffer.slice(reader.i, reader.i + length))
}); })
} }
let out = ''; let out = ''
if (ucs2) { if (ucs2) {
out = reader.string({encoding:'ucs2',length:length}); out = reader.string({ encoding: 'ucs2', length })
this.logger.debug("UCS2 String decoded: " + out); this.logger.debug('UCS2 String decoded: ' + out)
} else if (length > 0) { } else if (length > 0) {
out = reader.string(); out = reader.string()
} }
// Sometimes the string has a null at the end (included with the length) // Sometimes the string has a null at the end (included with the length)
// Strip it if present // Strip it if present
if(out.charCodeAt(out.length-1) === 0) { if (out.charCodeAt(out.length - 1) === 0) {
out = out.substring(0, out.length - 1); out = out.substring(0, out.length - 1)
} }
if(stripColor) { if (stripColor) {
out = out.replace(/\x1b...|[\x00-\x1a]/gus,''); out = out.replace(/\x1b...|[\x00-\x1a]/gus, '')
} }
return out; return out
} }
async sendPacket(type,required) { async sendPacket (type, required) {
const outbuffer = Buffer.from([0x79,0,0,0,type]); const outbuffer = Buffer.from([0x79, 0, 0, 0, type])
const packets = []; const packets = []
return await this.udpSend(outbuffer,(buffer) => { return await this.udpSend(outbuffer, (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const header = reader.uint(4); reader.uint(4) // header
const iType = reader.uint(1); const iType = reader.uint(1)
if(iType !== type) return; if (iType !== type) return
packets.push(reader.rest()); packets.push(reader.rest())
}, () => { }, () => {
if(!packets.length && required) return; if (!packets.length && required) return
return Buffer.concat(packets); return Buffer.concat(packets)
}); })
} }
} }

View file

@ -1,45 +1,45 @@
import gamespy3 from './gamespy3.js'; import gamespy3 from './gamespy3.js'
export default class ut3 extends gamespy3 { export default class ut3 extends gamespy3 {
async run(state) { async run (state) {
await super.run(state); await super.run(state)
this.translate(state.raw,{ this.translate(state.raw, {
'mapname': false, mapname: false,
'p1073741825': 'map', p1073741825: 'map',
'p1073741826': 'gametype', p1073741826: 'gametype',
'p1073741827': 'servername', p1073741827: 'servername',
'p1073741828': 'custom_mutators', p1073741828: 'custom_mutators',
'gamemode': 'joininprogress', gamemode: 'joininprogress',
's32779': 'gamemode', s32779: 'gamemode',
's0': 'bot_skill', s0: 'bot_skill',
's6': 'pure_server', s6: 'pure_server',
's7': 'password', s7: 'password',
's8': 'vs_bots', s8: 'vs_bots',
's10': 'force_respawn', s10: 'force_respawn',
'p268435704': 'frag_limit', p268435704: 'frag_limit',
'p268435705': 'time_limit', p268435705: 'time_limit',
'p268435703': 'numbots', p268435703: 'numbots',
'p268435717': 'stock_mutators', p268435717: 'stock_mutators',
'p1073741829': 'stock_mutators', p1073741829: 'stock_mutators',
's1': false, s1: false,
's9': false, s9: false,
's11': false, s11: false,
's12': false, s12: false,
's13': false, s13: false,
's14': false, s14: false,
'p268435706': false, p268435706: false,
'p268435968': false, p268435968: false,
'p268435969': false p268435969: false
}); })
const split = (a) => { const split = (a) => {
let s = a.split('\x1c'); let s = a.split('\x1c')
s = s.filter((e) => { return e }); s = s.filter((e) => { return e })
return s; return s
}; }
if('custom_mutators' in state.raw) state.raw['custom_mutators'] = split(state.raw['custom_mutators']); if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
if('stock_mutators' in state.raw) state.raw['stock_mutators'] = split(state.raw['stock_mutators']); if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
if('map' in state.raw) state.map = state.raw.map; if ('map' in state.raw) state.map = state.raw.map
} }
} }

View file

@ -1,5 +1,5 @@
import Bzip2 from 'seek-bzip'; import Bzip2 from 'seek-bzip'
import Core from './core.js'; import Core from './core.js'
const AppId = { const AppId = {
Squad: 393380, Squad: 393380,
@ -12,123 +12,123 @@ const AppId = {
EternalSilence: 17550, EternalSilence: 17550,
Insurgency_MIC: 17700, Insurgency_MIC: 17700,
Source_SDK_Base_2006: 215 Source_SDK_Base_2006: 215
}; }
export default class valve extends Core { export default class valve extends Core {
constructor() { constructor () {
super(); super()
// legacy goldsrc info response -- basically not used by ANYTHING now, // legacy goldsrc info response -- basically not used by ANYTHING now,
// as most (all?) goldsrc servers respond with the source info reponse // as most (all?) goldsrc servers respond with the source info reponse
// delete in a few years if nothing ends up using it anymore // delete in a few years if nothing ends up using it anymore
this.goldsrcInfo = false; this.goldsrcInfo = false
// unfortunately, the split format from goldsrc is still around, but we // unfortunately, the split format from goldsrc is still around, but we
// can detect that during the query // can detect that during the query
this.goldsrcSplits = false; this.goldsrcSplits = false
// some mods require a challenge, but don't provide them in the new format // some mods require a challenge, but don't provide them in the new format
// at all, use the old dedicated challenge query if needed // at all, use the old dedicated challenge query if needed
this.legacyChallenge = false; this.legacyChallenge = false
// 2006 engines don't pass packet switching size in split packet header // 2006 engines don't pass packet switching size in split packet header
// while all others do, this need is detected automatically // while all others do, this need is detected automatically
this._skipSizeInSplitHeader = false; this._skipSizeInSplitHeader = false
this._challenge = ''; this._challenge = ''
} }
async run(state) { async run (state) {
if (!this.options.port) this.options.port = 27015; if (!this.options.port) this.options.port = 27015
await this.queryInfo(state); await this.queryInfo(state)
await this.queryChallenge(); await this.queryChallenge()
await this.queryPlayers(state); await this.queryPlayers(state)
await this.queryRules(state); await this.queryRules(state)
await this.cleanup(state); await this.cleanup(state)
} }
async queryInfo(/** Results */ state) { async queryInfo (/** Results */ state) {
this.logger.debug("Requesting info ..."); this.logger.debug('Requesting info ...')
const b = await this.sendPacket( const b = await this.sendPacket(
this.goldsrcInfo ? undefined : 0x54, this.goldsrcInfo ? undefined : 0x54,
this.goldsrcInfo ? 'details' : 'Source Engine Query\0', this.goldsrcInfo ? 'details' : 'Source Engine Query\0',
this.goldsrcInfo ? 0x6D : 0x49, this.goldsrcInfo ? 0x6D : 0x49,
false false
); )
const reader = this.reader(b); const reader = this.reader(b)
if(this.goldsrcInfo) state.raw.address = reader.string(); if (this.goldsrcInfo) state.raw.address = reader.string()
else state.raw.protocol = reader.uint(1); else state.raw.protocol = reader.uint(1)
state.name = reader.string(); state.name = reader.string()
state.map = reader.string(); state.map = reader.string()
state.raw.folder = reader.string(); state.raw.folder = reader.string()
state.raw.game = reader.string(); state.raw.game = reader.string()
if(!this.goldsrcInfo) state.raw.appId = reader.uint(2); if (!this.goldsrcInfo) state.raw.appId = reader.uint(2)
state.raw.numplayers = reader.uint(1); state.raw.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1); state.maxplayers = reader.uint(1)
if(this.goldsrcInfo) state.raw.protocol = reader.uint(1); if (this.goldsrcInfo) state.raw.protocol = reader.uint(1)
else state.raw.numbots = reader.uint(1); else state.raw.numbots = reader.uint(1)
state.raw.listentype = String.fromCharCode(reader.uint(1)); state.raw.listentype = String.fromCharCode(reader.uint(1))
state.raw.environment = String.fromCharCode(reader.uint(1)); state.raw.environment = String.fromCharCode(reader.uint(1))
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
if(this.goldsrcInfo) { if (this.goldsrcInfo) {
state.raw.ismod = reader.uint(1); state.raw.ismod = reader.uint(1)
if(state.raw.ismod) { if (state.raw.ismod) {
state.raw.modlink = reader.string(); state.raw.modlink = reader.string()
state.raw.moddownload = reader.string(); state.raw.moddownload = reader.string()
reader.skip(1); reader.skip(1)
state.raw.modversion = reader.uint(4); state.raw.modversion = reader.uint(4)
state.raw.modsize = reader.uint(4); state.raw.modsize = reader.uint(4)
state.raw.modtype = reader.uint(1); state.raw.modtype = reader.uint(1)
state.raw.moddll = reader.uint(1); state.raw.moddll = reader.uint(1)
} }
} else { } else {
state.raw.secure = reader.uint(1); state.raw.secure = reader.uint(1)
if(state.raw.appId === AppId.Ship) { if (state.raw.appId === AppId.Ship) {
state.raw.shipmode = reader.uint(1); state.raw.shipmode = reader.uint(1)
state.raw.shipwitnesses = reader.uint(1); state.raw.shipwitnesses = reader.uint(1)
state.raw.shipduration = reader.uint(1); state.raw.shipduration = reader.uint(1)
} }
state.raw.version = reader.string(); state.raw.version = reader.string()
const extraFlag = reader.uint(1); const extraFlag = reader.uint(1)
if(extraFlag & 0x80) state.gamePort = reader.uint(2); if (extraFlag & 0x80) state.gamePort = reader.uint(2)
if(extraFlag & 0x10) state.raw.steamid = reader.uint(8).toString(); if (extraFlag & 0x10) state.raw.steamid = reader.uint(8).toString()
if(extraFlag & 0x40) { if (extraFlag & 0x40) {
state.raw.sourcetvport = reader.uint(2); state.raw.sourcetvport = reader.uint(2)
state.raw.sourcetvname = reader.string(); state.raw.sourcetvname = reader.string()
} }
if(extraFlag & 0x20) state.raw.tags = reader.string().split(','); if (extraFlag & 0x20) state.raw.tags = reader.string().split(',')
if(extraFlag & 0x01) { if (extraFlag & 0x01) {
const gameId = reader.uint(8); const gameId = reader.uint(8)
const betterAppId = gameId.getLowBitsUnsigned() & 0xffffff; const betterAppId = gameId.getLowBitsUnsigned() & 0xffffff
if (betterAppId) { if (betterAppId) {
state.raw.appId = betterAppId; state.raw.appId = betterAppId
} }
} }
} }
const appId = state.raw.appId; const appId = state.raw.appId
// from https://developer.valvesoftware.com/wiki/Server_queries // from https://developer.valvesoftware.com/wiki/Server_queries
if( if (
state.raw.protocol === 7 && ( state.raw.protocol === 7 && (
state.raw.appId === AppId.Source_SDK_Base_2006 state.raw.appId === AppId.Source_SDK_Base_2006 ||
|| state.raw.appId === AppId.EternalSilence state.raw.appId === AppId.EternalSilence ||
|| state.raw.appId === AppId.Insurgency_MIC state.raw.appId === AppId.Insurgency_MIC ||
|| state.raw.appId === AppId.CS_Source state.raw.appId === AppId.CS_Source
) )
) { ) {
this._skipSizeInSplitHeader = true; this._skipSizeInSplitHeader = true
} }
this.logger.debug("INFO: ", state.raw); this.logger.debug('INFO: ', state.raw)
if(state.raw.protocol === 48) { if (state.raw.protocol === 48) {
this.logger.debug("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT"); this.logger.debug('GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT')
this.goldsrcSplits = true; this.goldsrcSplits = true
} }
// DayZ embeds some of the server information inside the tags attribute // DayZ embeds some of the server information inside the tags attribute
@ -138,30 +138,30 @@ export default class valve extends Core {
state.raw.firstPerson = false state.raw.firstPerson = false
for (const tag of state.raw.tags) { for (const tag of state.raw.tags) {
if (tag.startsWith('lqs')) { if (tag.startsWith('lqs')) {
const value = parseInt(tag.replace('lqs', '')); const value = parseInt(tag.replace('lqs', ''))
if (!isNaN(value)) { if (!isNaN(value)) {
state.raw.queue = value; state.raw.queue = value
} }
} }
if (tag.includes('no3rd')) { if (tag.includes('no3rd')) {
state.raw.firstPerson = true; state.raw.firstPerson = true
} }
if (tag.includes('isDLC')) { if (tag.includes('isDLC')) {
state.raw.dlcEnabled = true; state.raw.dlcEnabled = true
} }
if (tag.includes(':')) { if (tag.includes(':')) {
state.raw.time = tag; state.raw.time = tag
} }
if (tag.startsWith('etm')) { if (tag.startsWith('etm')) {
const value = parseInt(tag.replace('etm', '')); const value = parseInt(tag.replace('etm', ''))
if (!isNaN(value)) { if (!isNaN(value)) {
state.raw.dayAcceleration = value; state.raw.dayAcceleration = value
} }
} }
if (tag.startsWith('entm')) { if (tag.startsWith('entm')) {
const value = parseInt(tag.replace('entm', '')); const value = parseInt(tag.replace('entm', ''))
if (!isNaN(value)) { if (!isNaN(value)) {
state.raw.nightAcceleration = value; state.raw.nightAcceleration = value
} }
} }
} }
@ -172,9 +172,9 @@ export default class valve extends Core {
if (state.raw.tags) { if (state.raw.tags) {
for (const tag of state.raw.tags) { for (const tag of state.raw.tags) {
if (tag.startsWith('mp')) { if (tag.startsWith('mp')) {
const value = parseInt(tag.replace('mp', '')); const value = parseInt(tag.replace('mp', ''))
if (!isNaN(value)) { if (!isNaN(value)) {
state.maxplayers = value; state.maxplayers = value
} }
} }
} }
@ -182,254 +182,258 @@ export default class valve extends Core {
} }
} }
async queryChallenge() { async queryChallenge () {
if(this.legacyChallenge) { if (this.legacyChallenge) {
// sendPacket will catch the response packet and // sendPacket will catch the response packet and
// save the challenge for us // save the challenge for us
this.logger.debug("Requesting legacy challenge key ..."); this.logger.debug('Requesting legacy challenge key ...')
await this.sendPacket( await this.sendPacket(
0x57, 0x57,
null, null,
0x41, 0x41,
false false
); )
} }
} }
async queryPlayers(/** Results */ state) { async queryPlayers (/** Results */ state) {
state.raw.players = []; state.raw.players = []
this.logger.debug("Requesting player list ..."); this.logger.debug('Requesting player list ...')
const b = await this.sendPacket( const b = await this.sendPacket(
this.goldsrcInfo ? undefined : 0x55, this.goldsrcInfo ? undefined : 0x55,
this.goldsrcInfo ? 'players' : null, this.goldsrcInfo ? 'players' : null,
0x44, 0x44,
true true
); )
if (b === null) { if (b === null) {
// Player query timed out // Player query timed out
// CSGO doesn't respond to player query if host_players_show is not 2 // CSGO doesn't respond to player query if host_players_show is not 2
// Conan Exiles never responds to player query // Conan Exiles never responds to player query
// Just skip it, and we'll fill with dummy objects in cleanup() // Just skip it, and we'll fill with dummy objects in cleanup()
return; return
} }
const reader = this.reader(b); const reader = this.reader(b)
const num = reader.uint(1); const num = reader.uint(1)
for(let i = 0; i < num; i++) { for (let i = 0; i < num; i++) {
reader.skip(1); reader.skip(1)
const name = reader.string(); const name = reader.string()
const score = reader.int(4); const score = reader.int(4)
const time = reader.float(); const time = reader.float()
this.logger.debug("Found player: "+name+" "+score+" "+time); this.logger.debug('Found player: ' + name + ' ' + score + ' ' + time)
// connecting players don't count as players. // connecting players don't count as players.
if(!name) continue; if (!name) continue
// CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2 // CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2
if (state.raw.appId === AppId.CSGO && name === 'Max Players') continue; if (state.raw.appId === AppId.CSGO && name === 'Max Players') continue
state.raw.players.push({ state.raw.players.push({
name:name, score:score, time:time name, score, time
}); })
} }
} }
async queryRules(/** Results */ state) { async queryRules (/** Results */ state) {
const appId = state.raw.appId; const appId = state.raw.appId
if (appId === AppId.Squad if (appId === AppId.Squad ||
|| appId === AppId.Bat1944 appId === AppId.Bat1944 ||
|| this.options.requestRules) { this.options.requestRules) {
// let's get 'em // let's get 'em
} else { } else {
return; return
} }
const rules = {}; const rules = {}
state.raw.rules = rules; state.raw.rules = rules
const dayZPayload = []; const dayZPayload = []
this.logger.debug("Requesting rules ..."); this.logger.debug('Requesting rules ...')
if (this.goldsrcInfo) { if (this.goldsrcInfo) {
const b = await this.udpSend('\xff\xff\xff\xffrules', b=>b, ()=>null); const b = await this.udpSend('\xff\xff\xff\xffrules', b => b, () => null)
if (b === null) return; // timed out - the server probably has rules disabled if (b === null) return // timed out - the server probably has rules disabled
const reader = this.reader(b); const reader = this.reader(b)
while (!reader.done()) { while (!reader.done()) {
const key = reader.string(); const key = reader.string()
const value = reader.string(); const value = reader.string()
rules[key] = value; rules[key] = value
} }
} else { } else {
const b = await this.sendPacket(0x56,null,0x45,true); const b = await this.sendPacket(0x56, null, 0x45, true)
if (b === null) return; // timed out - the server probably has rules disabled if (b === null) return // timed out - the server probably has rules disabled
let dayZPayloadEnded = false; let dayZPayloadEnded = false
const reader = this.reader(b); const reader = this.reader(b)
const num = reader.uint(2); const num = reader.uint(2)
for (let i = 0; i < num; i++) { for (let i = 0; i < num; i++) {
if (appId === AppId.DayZ && !dayZPayloadEnded) { if (appId === AppId.DayZ && !dayZPayloadEnded) {
const one = reader.uint(1); const one = reader.uint(1)
const two = reader.uint(1); const two = reader.uint(1)
const three = reader.uint(1); const three = reader.uint(1)
if (one !== 0 && two !== 0 && three === 0) { if (one !== 0 && two !== 0 && three === 0) {
while (true) { while (true) {
const byte = reader.uint(1); const byte = reader.uint(1)
if (byte === 0) break; if (byte === 0) break
dayZPayload.push(byte); dayZPayload.push(byte)
} }
continue; continue
} else { } else {
reader.skip(-3); reader.skip(-3)
dayZPayloadEnded = true; dayZPayloadEnded = true
} }
} }
const key = reader.string(); const key = reader.string()
const value = reader.string(); const value = reader.string()
rules[key] = value; rules[key] = value
} }
} }
// Battalion 1944 puts its info into rules fields for some reason // Battalion 1944 puts its info into rules fields for some reason
if (appId === AppId.Bat1944) { if (appId === AppId.Bat1944) {
if ('bat_name_s' in rules) { if ('bat_name_s' in rules) {
state.name = rules.bat_name_s; state.name = rules.bat_name_s
delete rules.bat_name_s; delete rules.bat_name_s
if ('bat_player_count_s' in rules) { if ('bat_player_count_s' in rules) {
state.raw.numplayers = parseInt(rules.bat_player_count_s); state.raw.numplayers = parseInt(rules.bat_player_count_s)
delete rules.bat_player_count_s; delete rules.bat_player_count_s
} }
if ('bat_max_players_i' in rules) { if ('bat_max_players_i' in rules) {
state.maxplayers = parseInt(rules.bat_max_players_i); state.maxplayers = parseInt(rules.bat_max_players_i)
delete rules.bat_max_players_i; delete rules.bat_max_players_i
} }
if ('bat_has_password_s' in rules) { if ('bat_has_password_s' in rules) {
state.password = rules.bat_has_password_s === 'Y'; state.password = rules.bat_has_password_s === 'Y'
delete rules.bat_has_password_s; delete rules.bat_has_password_s
} }
// apparently map is already right, and this var is often wrong // apparently map is already right, and this var is often wrong
delete rules.bat_map_s; delete rules.bat_map_s
} }
} }
// Squad keeps its password in a separate field // Squad keeps its password in a separate field
if (appId === AppId.Squad) { if (appId === AppId.Squad) {
if (rules.Password_b === "true") { if (rules.Password_b === 'true') {
state.password = true; state.password = true
} }
} }
if (appId === AppId.DayZ) { if (appId === AppId.DayZ) {
state.raw.dayzMods = this.readDayzMods(Buffer.from(dayZPayload)); state.raw.dayzMods = this.readDayzMods(Buffer.from(dayZPayload))
} }
} }
readDayzMods(/** Buffer */ buffer) { readDayzMods (/** Buffer */ buffer) {
if (!buffer.length) { if (!buffer.length) {
return {}; return {}
} }
this.logger.debug("DAYZ BUFFER"); this.logger.debug('DAYZ BUFFER')
this.logger.debug(buffer); this.logger.debug(buffer)
const reader = this.reader(buffer); const reader = this.reader(buffer)
const version = this.readDayzByte(reader); const version = this.readDayzByte(reader)
const overflow = this.readDayzByte(reader); const overflow = this.readDayzByte(reader)
const dlc1 = this.readDayzByte(reader); const dlc1 = this.readDayzByte(reader)
const dlc2 = this.readDayzByte(reader); const dlc2 = this.readDayzByte(reader)
this.logger.debug("version " + version); this.logger.debug('version ' + version)
this.logger.debug("overflow " + overflow); this.logger.debug('overflow ' + overflow)
this.logger.debug("dlc1 " + dlc1); this.logger.debug('dlc1 ' + dlc1)
this.logger.debug("dlc2 " + dlc2); this.logger.debug('dlc2 ' + dlc2)
if (dlc1) { if (dlc1) {
const unknown = this.readDayzUint(reader, 4); // ? const unknown = this.readDayzUint(reader, 4) // ?
this.logger.debug("unknown " + unknown); this.logger.debug('unknown ' + unknown)
} }
if (dlc2) { if (dlc2) {
const unknown = this.readDayzUint(reader, 4); // ? const unknown = this.readDayzUint(reader, 4) // ?
this.logger.debug("unknown " + unknown); this.logger.debug('unknown ' + unknown)
} }
const mods = []; const mods = []
mods.push(...this.readDayzModsSection(reader, true)); mods.push(...this.readDayzModsSection(reader, true))
mods.push(...this.readDayzModsSection(reader, false)); mods.push(...this.readDayzModsSection(reader, false))
this.logger.debug("dayz buffer rest:", reader.rest()); this.logger.debug('dayz buffer rest:', reader.rest())
return mods; return mods
} }
readDayzModsSection(/** Reader */ reader, withHeader) {
const out = []; readDayzModsSection (/** Reader */ reader, withHeader) {
const count = this.readDayzByte(reader); const out = []
this.logger.debug("dayz mod section withHeader:" + withHeader + " count:" + count); const count = this.readDayzByte(reader)
for(let i = 0; i < count; i++) { this.logger.debug('dayz mod section withHeader:' + withHeader + ' count:' + count)
if (reader.done()) break; for (let i = 0; i < count; i++) {
const mod = {}; if (reader.done()) break
const mod = {}
if (withHeader) { if (withHeader) {
mod.unknown = this.readDayzUint(reader, 4); // ? mod.unknown = this.readDayzUint(reader, 4) // ?
// For some reason this is 4 on all of them, but doesn't exist on the last one? but only sometimes? // For some reason this is 4 on all of them, but doesn't exist on the last one? but only sometimes?
const offset = reader.offset(); const offset = reader.offset()
const flag = this.readDayzByte(reader); const flag = this.readDayzByte(reader)
if (flag !== 4) reader.setOffset(offset); if (flag !== 4) reader.setOffset(offset)
mod.workshopId = this.readDayzUint(reader, 4); mod.workshopId = this.readDayzUint(reader, 4)
} }
mod.title = this.readDayzString(reader); mod.title = this.readDayzString(reader)
this.logger.debug(mod); this.logger.debug(mod)
out.push(mod); out.push(mod)
} }
return out; return out
} }
readDayzUint(reader, bytes) {
const out = []; readDayzUint (reader, bytes) {
const out = []
for (let i = 0; i < bytes; i++) { for (let i = 0; i < bytes; i++) {
out.push(this.readDayzByte(reader)); out.push(this.readDayzByte(reader))
} }
const buf = Buffer.from(out); const buf = Buffer.from(out)
const r2 = this.reader(buf); const r2 = this.reader(buf)
return r2.uint(bytes); return r2.uint(bytes)
}
readDayzByte(reader) {
const byte = reader.uint(1);
if (byte === 1) {
const byte2 = reader.uint(1);
if (byte2 === 1) return 1;
if (byte2 === 2) return 0;
if (byte2 === 3) return 0xff;
return 0; // ?
}
return byte;
}
readDayzString(reader) {
const length = this.readDayzByte(reader);
const out = [];
for (let i = 0; i < length; i++) {
out.push(this.readDayzByte(reader));
}
return Buffer.from(out).toString('utf8');
} }
async cleanup(/** Results */ state) { readDayzByte (reader) {
const byte = reader.uint(1)
if (byte === 1) {
const byte2 = reader.uint(1)
if (byte2 === 1) return 1
if (byte2 === 2) return 0
if (byte2 === 3) return 0xff
return 0 // ?
}
return byte
}
readDayzString (reader) {
const length = this.readDayzByte(reader)
const out = []
for (let i = 0; i < length; i++) {
out.push(this.readDayzByte(reader))
}
return Buffer.from(out).toString('utf8')
}
async cleanup (/** Results */ state) {
// Organize players / hidden players into player / bot arrays // Organize players / hidden players into player / bot arrays
const botProbability = (p) => { const botProbability = (p) => {
if (p.time === -1) return Number.MAX_VALUE; if (p.time === -1) return Number.MAX_VALUE
return p.time; return p.time
};
const sortedPlayers = state.raw.players.sort((a,b) => {
return botProbability(a) - botProbability(b);
});
delete state.raw.players;
const numBots = state.raw.numbots || 0;
const numPlayers = state.raw.numplayers - numBots;
while(state.bots.length < numBots) {
if (sortedPlayers.length) state.bots.push(sortedPlayers.pop());
else state.bots.push({});
} }
while(state.players.length < numPlayers || sortedPlayers.length) { const sortedPlayers = state.raw.players.sort((a, b) => {
if (sortedPlayers.length) state.players.push(sortedPlayers.pop()); return botProbability(a) - botProbability(b)
else state.players.push({}); })
delete state.raw.players
const numBots = state.raw.numbots || 0
const numPlayers = state.raw.numplayers - numBots
while (state.bots.length < numBots) {
if (sortedPlayers.length) state.bots.push(sortedPlayers.pop())
else state.bots.push({})
}
while (state.players.length < numPlayers || sortedPlayers.length) {
if (sortedPlayers.length) state.players.push(sortedPlayers.pop())
else state.players.push({})
} }
} }
@ -442,43 +446,43 @@ export default class valve extends Core {
* @param {boolean=} allowTimeout * @param {boolean=} allowTimeout
* @returns Buffer|null * @returns Buffer|null
**/ **/
async sendPacket( async sendPacket (
type, type,
payload, payload,
expect, expect,
allowTimeout allowTimeout
) { ) {
for (let keyRetry = 0; keyRetry < 3; keyRetry++) { for (let keyRetry = 0; keyRetry < 3; keyRetry++) {
let receivedNewChallengeKey = false; let receivedNewChallengeKey = false
const response = await this.sendPacketRaw( const response = await this.sendPacketRaw(
type, payload, type, payload,
(payload) => { (payload) => {
const reader = this.reader(payload); const reader = this.reader(payload)
const type = reader.uint(1); const type = reader.uint(1)
this.logger.debug(() => "Received 0x" + type.toString(16) + " expected 0x" + expect.toString(16)); this.logger.debug(() => 'Received 0x' + type.toString(16) + ' expected 0x' + expect.toString(16))
if (type === 0x41) { if (type === 0x41) {
const key = reader.uint(4); const key = reader.uint(4)
if (this._challenge !== key) { if (this._challenge !== key) {
this.logger.debug('Received new challenge key: 0x' + key.toString(16)); this.logger.debug('Received new challenge key: 0x' + key.toString(16))
this._challenge = key; this._challenge = key
receivedNewChallengeKey = true; receivedNewChallengeKey = true
} }
} }
if (type === expect) { if (type === expect) {
return reader.rest(); return reader.rest()
} else if (receivedNewChallengeKey) { } else if (receivedNewChallengeKey) {
return null; return null
} }
}, },
() => { () => {
if (allowTimeout) return null; if (allowTimeout) return null
} }
); )
if (!receivedNewChallengeKey) { if (!receivedNewChallengeKey) {
return response; return response
} }
} }
throw new Error('Received too many challenge key responses'); throw new Error('Received too many challenge key responses')
} }
/** /**
@ -489,118 +493,118 @@ export default class valve extends Core {
* @param {function(Buffer)} onResponse * @param {function(Buffer)} onResponse
* @param {function()} onTimeout * @param {function()} onTimeout
**/ **/
async sendPacketRaw( async sendPacketRaw (
type, type,
payload, payload,
onResponse, onResponse,
onTimeout onTimeout
) { ) {
const challengeAtBeginning = type === 0x55 || type === 0x56; const challengeAtBeginning = type === 0x55 || type === 0x56
const challengeAtEnd = type === 0x54 && !!this._challenge; const challengeAtEnd = type === 0x54 && !!this._challenge
if (typeof payload === 'string') payload = Buffer.from(payload, 'binary'); if (typeof payload === 'string') payload = Buffer.from(payload, 'binary')
const b = Buffer.alloc(4 const b = Buffer.alloc(4 +
+ (type !== undefined ? 1 : 0) (type !== undefined ? 1 : 0) +
+ (challengeAtBeginning ? 4 : 0) (challengeAtBeginning ? 4 : 0) +
+ (challengeAtEnd ? 4 : 0) (challengeAtEnd ? 4 : 0) +
+ (payload ? payload.length : 0) (payload ? payload.length : 0)
); )
let offset = 0; let offset = 0
let challenge = this._challenge; let challenge = this._challenge
if (!challenge) challenge = 0xffffffff; if (!challenge) challenge = 0xffffffff
b.writeInt32LE(-1, offset); b.writeInt32LE(-1, offset)
offset += 4; offset += 4
if (type !== undefined) { if (type !== undefined) {
b.writeUInt8(type, offset); b.writeUInt8(type, offset)
offset += 1; offset += 1
} }
if (challengeAtBeginning) { if (challengeAtBeginning) {
if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset); if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset)
else b.writeUInt32BE(challenge, offset); else b.writeUInt32BE(challenge, offset)
offset += 4; offset += 4
} }
if (payload) { if (payload) {
payload.copy(b, offset); payload.copy(b, offset)
offset += payload.length; offset += payload.length
} }
if (challengeAtEnd) { if (challengeAtEnd) {
if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset); if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset)
else b.writeUInt32BE(challenge, offset); else b.writeUInt32BE(challenge, offset)
offset += 4; offset += 4
} }
const packetStorage = {}; const packetStorage = {}
return await this.udpSend( return await this.udpSend(
b, b,
(buffer) => { (buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer)
const header = reader.int(4); const header = reader.int(4)
if(header === -1) { if (header === -1) {
// full package // full package
this.logger.debug("Received full packet"); this.logger.debug('Received full packet')
return onResponse(reader.rest()); return onResponse(reader.rest())
} }
if(header === -2) { if (header === -2) {
// partial package // partial package
const uid = reader.uint(4); const uid = reader.uint(4)
if(!(uid in packetStorage)) packetStorage[uid] = {}; if (!(uid in packetStorage)) packetStorage[uid] = {}
const packets = packetStorage[uid]; const packets = packetStorage[uid]
let bzip = false; let bzip = false
if(!this.goldsrcSplits && uid & 0x80000000) bzip = true; if (!this.goldsrcSplits && uid & 0x80000000) bzip = true
let packetNum,payload,numPackets; let packetNum, payload, numPackets
if(this.goldsrcSplits) { if (this.goldsrcSplits) {
packetNum = reader.uint(1); packetNum = reader.uint(1)
numPackets = packetNum & 0x0f; numPackets = packetNum & 0x0f
packetNum = (packetNum & 0xf0) >> 4; packetNum = (packetNum & 0xf0) >> 4
payload = reader.rest(); payload = reader.rest()
} else { } else {
numPackets = reader.uint(1); numPackets = reader.uint(1)
packetNum = reader.uint(1); packetNum = reader.uint(1)
if(!this._skipSizeInSplitHeader) reader.skip(2); if (!this._skipSizeInSplitHeader) reader.skip(2)
if(packetNum === 0 && bzip) reader.skip(8); if (packetNum === 0 && bzip) reader.skip(8)
payload = reader.rest(); payload = reader.rest()
} }
packets[packetNum] = payload; packets[packetNum] = payload
this.logger.debug(() => "Received partial packet uid: 0x"+uid.toString(16)+" num: "+packetNum); this.logger.debug(() => 'Received partial packet uid: 0x' + uid.toString(16) + ' num: ' + packetNum)
this.logger.debug(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID"); this.logger.debug(() => 'Received ' + Object.keys(packets).length + '/' + numPackets + ' packets for this UID')
if(Object.keys(packets).length !== numPackets) return; if (Object.keys(packets).length !== numPackets) return
// assemble the parts // assemble the parts
const list = []; const list = []
for(let i = 0; i < numPackets; i++) { for (let i = 0; i < numPackets; i++) {
if(!(i in packets)) { if (!(i in packets)) {
throw new Error('Missing packet #'+i); throw new Error('Missing packet #' + i)
} }
list.push(packets[i]); list.push(packets[i])
} }
let assembled = Buffer.concat(list); let assembled = Buffer.concat(list)
if(bzip) { if (bzip) {
this.logger.debug("BZIP DETECTED - Extracing packet..."); this.logger.debug('BZIP DETECTED - Extracing packet...')
try { try {
assembled = Bzip2.decode(assembled); assembled = Bzip2.decode(assembled)
} catch(e) { } catch (e) {
throw new Error('Invalid bzip packet'); throw new Error('Invalid bzip packet')
} }
} }
const assembledReader = this.reader(assembled); const assembledReader = this.reader(assembled)
assembledReader.skip(4); // header assembledReader.skip(4) // header
return onResponse(assembledReader.rest()); return onResponse(assembledReader.rest())
} }
}, },
onTimeout onTimeout
); )
} }
} }

View file

@ -1,10 +1,10 @@
import samp from './samp.js'; import samp from './samp.js'
export default class vcmp extends samp { export default class vcmp extends samp {
constructor() { constructor () {
super(); super()
this.magicHeader = 'VCMP'; this.magicHeader = 'VCMP'
this.responseMagicHeader = 'MP04'; this.responseMagicHeader = 'MP04'
this.isVcmp = true; this.isVcmp = true
} }
} }

View file

@ -1,154 +1,155 @@
import Core from './core.js'; import Core from './core.js'
export default class ventrilo extends Core { export default class ventrilo extends Core {
constructor() { constructor () {
super(); super()
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
const data = await this.sendCommand(2,''); const data = await this.sendCommand(2, '')
state.raw = splitFields(data.toString()); state.raw = splitFields(data.toString())
for (const client of state.raw.CLIENTS) { for (const client of state.raw.CLIENTS) {
client.name = client.NAME; client.name = client.NAME
delete client.NAME; delete client.NAME
client.ping = parseInt(client.PING); client.ping = parseInt(client.PING)
delete client.PING; delete client.PING
state.players.push(client); state.players.push(client)
} }
delete state.raw.CLIENTS; delete state.raw.CLIENTS
if('NAME' in state.raw) state.name = state.raw.NAME; if ('NAME' in state.raw) state.name = state.raw.NAME
if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS; if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
if(this.trueTest(state.raw.AUTH)) state.password = true; if (this.trueTest(state.raw.AUTH)) state.password = true
} }
async sendCommand(cmd,password) {
const body = Buffer.alloc(16);
body.write(password,0,15,'utf8');
const encrypted = encrypt(cmd,body);
const packets = {}; async sendCommand (cmd, password) {
const body = Buffer.alloc(16)
body.write(password, 0, 15, 'utf8')
const encrypted = encrypt(cmd, body)
const packets = {}
return await this.udpSend(encrypted, (buffer) => { return await this.udpSend(encrypted, (buffer) => {
if(buffer.length < 20) return; if (buffer.length < 20) return
const data = decrypt(buffer); const data = decrypt(buffer)
if(data.zero !== 0) return; if (data.zero !== 0) return
packets[data.packetNum] = data.body; packets[data.packetNum] = data.body
if(Object.keys(packets).length !== data.packetTotal) return; if (Object.keys(packets).length !== data.packetTotal) return
const out = []; const out = []
for(let i = 0; i < data.packetTotal; i++) { for (let i = 0; i < data.packetTotal; i++) {
if(!(i in packets)) throw new Error('Missing packet #'+i); if (!(i in packets)) throw new Error('Missing packet #' + i)
out.push(packets[i]); out.push(packets[i])
} }
return Buffer.concat(out); return Buffer.concat(out)
}); })
} }
} }
function splitFields(str,subMode) { function splitFields (str, subMode) {
let splitter,delim; let splitter, delim
if(subMode) { if (subMode) {
splitter = '='; splitter = '='
delim = ','; delim = ','
} else { } else {
splitter = ': '; splitter = ': '
delim = '\n'; delim = '\n'
} }
const split = str.split(delim); const split = str.split(delim)
const out = {}; const out = {}
if(!subMode) { if (!subMode) {
out.CHANNELS = []; out.CHANNELS = []
out.CLIENTS = []; out.CLIENTS = []
} }
for (const one of split) { for (const one of split) {
const equal = one.indexOf(splitter); const equal = one.indexOf(splitter)
const key = equal === -1 ? one : one.substring(0,equal); const key = equal === -1 ? one : one.substring(0, equal)
if(!key || key === '\0') continue; if (!key || key === '\0') continue
const value = equal === -1 ? '' : one.substring(equal+splitter.length); const value = equal === -1 ? '' : one.substring(equal + splitter.length)
if(!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value,true)); if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
else if(!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value,true)); else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
else out[key] = value; else out[key] = value
} }
return out; return out
} }
function randInt(min,max) { function randInt (min, max) {
return Math.floor(Math.random()*(max-min+1)+min) return Math.floor(Math.random() * (max - min + 1) + min)
} }
function crc(body) { function crc (body) {
let crc = 0; let crc = 0
for(let i = 0; i < body.length; i++) { for (let i = 0; i < body.length; i++) {
crc = crc_table[crc>>8] ^ body.readUInt8(i) ^ (crc<<8); crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
crc &= 0xffff; crc &= 0xffff
} }
return crc; return crc
} }
function encrypt(cmd,body) { function encrypt (cmd, body) {
const headerKeyStart = randInt(0,0xff); const headerKeyStart = randInt(0, 0xff)
const headerKeyAdd = randInt(1,0xff); const headerKeyAdd = randInt(1, 0xff)
const bodyKeyStart = randInt(0,0xff); const bodyKeyStart = randInt(0, 0xff)
const bodyKeyAdd = randInt(1,0xff); const bodyKeyAdd = randInt(1, 0xff)
const header = Buffer.alloc(20); const header = Buffer.alloc(20)
header.writeUInt8(headerKeyStart,0); header.writeUInt8(headerKeyStart, 0)
header.writeUInt8(headerKeyAdd,1); header.writeUInt8(headerKeyAdd, 1)
header.writeUInt16BE(cmd,4); header.writeUInt16BE(cmd, 4)
header.writeUInt16BE(body.length,8); header.writeUInt16BE(body.length, 8)
header.writeUInt16BE(body.length,10); header.writeUInt16BE(body.length, 10)
header.writeUInt16BE(1,12); header.writeUInt16BE(1, 12)
header.writeUInt16BE(0,14); header.writeUInt16BE(0, 14)
header.writeUInt8(bodyKeyStart,16); header.writeUInt8(bodyKeyStart, 16)
header.writeUInt8(bodyKeyAdd,17); header.writeUInt8(bodyKeyAdd, 17)
header.writeUInt16BE(crc(body),18); header.writeUInt16BE(crc(body), 18)
let offset = headerKeyStart; let offset = headerKeyStart
for(let i = 2; i < header.length; i++) { for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i); let val = header.readUInt8(i)
val += code_head.charCodeAt(offset) + ((i-2) % 5); val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff; val = val & 0xff
header.writeUInt8(val,i); header.writeUInt8(val, i)
offset = (offset+headerKeyAdd) & 0xff; offset = (offset + headerKeyAdd) & 0xff
} }
offset = bodyKeyStart; offset = bodyKeyStart
for(let i = 0; i < body.length; i++) { for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i); let val = body.readUInt8(i)
val += code_body.charCodeAt(offset) + (i % 72); val += codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff; val = val & 0xff
body.writeUInt8(val,i); body.writeUInt8(val, i)
offset = (offset+bodyKeyAdd) & 0xff; offset = (offset + bodyKeyAdd) & 0xff
} }
return Buffer.concat([header,body]); return Buffer.concat([header, body])
} }
function decrypt(data) { function decrypt (data) {
const header = data.slice(0,20); const header = data.slice(0, 20)
const body = data.slice(20); const body = data.slice(20)
const headerKeyStart = header.readUInt8(0); const headerKeyStart = header.readUInt8(0)
const headerKeyAdd = header.readUInt8(1); const headerKeyAdd = header.readUInt8(1)
let offset = headerKeyStart; let offset = headerKeyStart
for(let i = 2; i < header.length; i++) { for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i); let val = header.readUInt8(i)
val -= code_head.charCodeAt(offset) + ((i-2) % 5); val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff; val = val & 0xff
header.writeUInt8(val,i); header.writeUInt8(val, i)
offset = (offset+headerKeyAdd) & 0xff; offset = (offset + headerKeyAdd) & 0xff
} }
const bodyKeyStart = header.readUInt8(16); const bodyKeyStart = header.readUInt8(16)
const bodyKeyAdd = header.readUInt8(17); const bodyKeyAdd = header.readUInt8(17)
offset = bodyKeyStart; offset = bodyKeyStart
for(let i = 0; i < body.length; i++) { for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i); let val = body.readUInt8(i)
val -= code_body.charCodeAt(offset) + (i % 72); val -= codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff; val = val & 0xff
body.writeUInt8(val,i); body.writeUInt8(val, i)
offset = (offset+bodyKeyAdd) & 0xff; offset = (offset + bodyKeyAdd) & 0xff
} }
// header format: // header format:
@ -159,47 +160,47 @@ function decrypt(data) {
cmd: header.readUInt16BE(4), cmd: header.readUInt16BE(4),
packetTotal: header.readUInt16BE(12), packetTotal: header.readUInt16BE(12),
packetNum: header.readUInt16BE(14), packetNum: header.readUInt16BE(14),
body: body body
}; }
} }
const code_head = const codeHead =
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e'+ '\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' +
'\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea'+ '\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' +
'\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24'+ '\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' +
'\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb'+ '\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' +
'\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c'+ '\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' +
'\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53'+ '\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' +
'\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae'+ '\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' +
'\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09'+ '\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' +
'\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62'+ '\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' +
'\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5'+ '\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' +
'\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f'+ '\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' +
'\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff'+ '\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' +
'\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f'+ '\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' +
'\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f'+ '\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' +
'\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a'+ '\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' +
'\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'; '\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
const code_body = const codeBody =
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23'+ '\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' +
'\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48'+ '\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' +
'\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e'+ '\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' +
'\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe'+ '\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' +
'\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42'+ '\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' +
'\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81'+ '\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' +
'\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36'+ '\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' +
'\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59'+ '\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' +
'\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24'+ '\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' +
'\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e'+ '\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' +
'\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2'+ '\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' +
'\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67'+ '\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' +
'\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08'+ '\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' +
'\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e'+ '\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' +
'\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90'+ '\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' +
'\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'; '\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
const crc_table = [ const crcTable = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
@ -232,4 +233,4 @@ const crc_table = [
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
]; ]

View file

@ -1,12 +1,12 @@
import quake3 from './quake3.js'; import quake3 from './quake3.js'
export default class warsow extends quake3 { export default class warsow extends quake3 {
async run(state) { async run (state) {
await super.run(state); await super.run(state)
if(state.players) { if (state.players) {
for(const player of state.players) { for (const player of state.players) {
player.team = player.address; player.team = player.address
delete player.address; delete player.address
} }
} }
} }