gamedigz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/GAMES_LIST.md +453 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -0
  4. package/bin/gamedig.js +79 -0
  5. package/lib/DnsResolver.js +76 -0
  6. package/lib/GlobalUdpSocket.js +69 -0
  7. package/lib/HexUtil.js +20 -0
  8. package/lib/Logger.js +45 -0
  9. package/lib/Promises.js +18 -0
  10. package/lib/ProtocolResolver.js +7 -0
  11. package/lib/QueryRunner.js +95 -0
  12. package/lib/Results.js +32 -0
  13. package/lib/game-resolver.js +17 -0
  14. package/lib/gamedig.js +23 -0
  15. package/lib/games.js +2747 -0
  16. package/lib/index.js +5 -0
  17. package/lib/reader.js +172 -0
  18. package/package.json +74 -0
  19. package/protocols/armagetron.js +65 -0
  20. package/protocols/asa.js +12 -0
  21. package/protocols/ase.js +45 -0
  22. package/protocols/assettocorsa.js +40 -0
  23. package/protocols/battlefield.js +162 -0
  24. package/protocols/beammp.js +32 -0
  25. package/protocols/beammpmaster.js +17 -0
  26. package/protocols/buildandshoot.js +55 -0
  27. package/protocols/core.js +349 -0
  28. package/protocols/cs2d.js +65 -0
  29. package/protocols/dayz.js +196 -0
  30. package/protocols/discord.js +29 -0
  31. package/protocols/doom3.js +148 -0
  32. package/protocols/eco.js +20 -0
  33. package/protocols/eldewrito.js +21 -0
  34. package/protocols/epic.js +95 -0
  35. package/protocols/ffow.js +38 -0
  36. package/protocols/fivem.js +33 -0
  37. package/protocols/gamespy1.js +181 -0
  38. package/protocols/gamespy2.js +144 -0
  39. package/protocols/gamespy3.js +197 -0
  40. package/protocols/geneshift.js +46 -0
  41. package/protocols/goldsrc.js +8 -0
  42. package/protocols/hexen2.js +14 -0
  43. package/protocols/index.js +61 -0
  44. package/protocols/jc2mp.js +16 -0
  45. package/protocols/kspdmp.js +28 -0
  46. package/protocols/mafia2mp.js +41 -0
  47. package/protocols/mafia2online.js +9 -0
  48. package/protocols/minecraft.js +102 -0
  49. package/protocols/minecraftbedrock.js +72 -0
  50. package/protocols/minecraftvanilla.js +87 -0
  51. package/protocols/mumble.js +39 -0
  52. package/protocols/mumbleping.js +24 -0
  53. package/protocols/nadeo.js +86 -0
  54. package/protocols/openttd.js +127 -0
  55. package/protocols/quake1.js +9 -0
  56. package/protocols/quake2.js +88 -0
  57. package/protocols/quake3.js +24 -0
  58. package/protocols/rfactor.js +69 -0
  59. package/protocols/samp.js +102 -0
  60. package/protocols/savage2.js +25 -0
  61. package/protocols/starmade.js +67 -0
  62. package/protocols/starsiege.js +10 -0
  63. package/protocols/teamspeak2.js +71 -0
  64. package/protocols/teamspeak3.js +69 -0
  65. package/protocols/terraria.js +24 -0
  66. package/protocols/tribes1.js +153 -0
  67. package/protocols/tribes1master.js +80 -0
  68. package/protocols/unreal2.js +150 -0
  69. package/protocols/ut3.js +45 -0
  70. package/protocols/valve.js +455 -0
  71. package/protocols/vcmp.js +10 -0
  72. package/protocols/ventrilo.js +237 -0
  73. package/protocols/warsow.js +13 -0
package/bin/gamedig.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as process from "node:process";
4
+
5
+ import Minimist from 'minimist'
6
+ import { GameDig } from './../lib/index.js'
7
+
8
+ const argv = Minimist(process.argv.slice(2), {
9
+ boolean: ['pretty', 'debug', 'givenPortOnly', 'requestRules', 'requestRulesRequired', 'requestPlayersRequired'],
10
+ string: ['guildId', 'listenUdpPort', 'ipFamily']
11
+ })
12
+
13
+ const debug = argv.debug
14
+ delete argv.debug
15
+ const pretty = !!argv.pretty || debug
16
+ delete argv.pretty
17
+ const givenPortOnly = argv.givenPortOnly
18
+ delete argv.givenPortOnly
19
+ const requestRulesRequired = argv.requestRulesRequired
20
+ delete argv.requestRulesRequired
21
+ const requestPlayersRequired = argv.requestPlayersRequired
22
+ delete argv.requestPlayersRequired
23
+
24
+ const options = {}
25
+ for (const key of Object.keys(argv)) {
26
+ const value = argv[key]
27
+
28
+ if (key === '_' || key.charAt(0) === '$') { continue }
29
+
30
+ options[key] = value
31
+ }
32
+
33
+ if (argv._.length >= 1) {
34
+ const target = argv._[0]
35
+ const split = target.split(':')
36
+ options.host = split[0]
37
+ if (split.length >= 2) {
38
+ options.port = split[1]
39
+ }
40
+ }
41
+ if (debug) {
42
+ options.debug = true
43
+ }
44
+ if (givenPortOnly) {
45
+ options.givenPortOnly = true
46
+ }
47
+ if (requestRulesRequired) {
48
+ options.requestRulesRequired = true
49
+ }
50
+ if (requestPlayersRequired) {
51
+ options.requestPlayersRequired = true
52
+ }
53
+
54
+ const printOnPretty = (object) => {
55
+ if (pretty) {
56
+ console.log(JSON.stringify(object, null, ' '))
57
+ } else {
58
+ console.log(JSON.stringify(object))
59
+ }
60
+ }
61
+
62
+ const gamedig = new GameDig(options)
63
+ gamedig.query(options)
64
+ .then(printOnPretty)
65
+ .catch((error) => {
66
+ if (debug) {
67
+ if (error instanceof Error) {
68
+ console.log(error.stack)
69
+ } else {
70
+ console.log(error)
71
+ }
72
+ } else {
73
+ if (error instanceof Error) {
74
+ error = error.message
75
+ }
76
+
77
+ printOnPretty({ error })
78
+ }
79
+ })
@@ -0,0 +1,76 @@
1
+ import dns from 'node:dns'
2
+ import { isIP } from 'node:net'
3
+ import punycode from 'punycode/punycode.js'
4
+
5
+ export default class DnsResolver {
6
+ /**
7
+ * @param {Logger} logger
8
+ */
9
+ constructor (logger) {
10
+ this.logger = logger
11
+ }
12
+
13
+ /**
14
+ * Resolve a host name to its IP, if the given host name is already
15
+ * an IP address no request is made.
16
+ *
17
+ * If a srvRecordPrefix is provided a SRV request will be made and the
18
+ * port returned will be included in the output.
19
+ * @param {string} host
20
+ * @param {number} ipFamily
21
+ * @param {string=} srvRecordPrefix
22
+ * @returns {Promise<{address:string, port:number=}>}
23
+ */
24
+ async resolve (host, ipFamily, srvRecordPrefix) {
25
+ this.logger.debug('DNS Lookup: ' + host)
26
+
27
+ // Check if host is IPv4 or IPv6
28
+ if (isIP(host) === 4 || isIP(host) === 6) {
29
+ this.logger.debug('Raw IP Address: ' + host)
30
+ return { address: host }
31
+ }
32
+
33
+ const asciiForm = punycode.toASCII(host)
34
+ if (asciiForm !== host) {
35
+ this.logger.debug('Encoded punycode: ' + host + ' -> ' + asciiForm)
36
+ host = asciiForm
37
+ }
38
+
39
+ if (srvRecordPrefix) {
40
+ this.logger.debug('SRV Resolve: ' + srvRecordPrefix + '.' + host)
41
+ let records
42
+ try {
43
+ records = await dns.promises.resolve(srvRecordPrefix + '.' + host, 'SRV')
44
+ if (records.length >= 1) {
45
+ this.logger.debug('Found SRV Records: ', records)
46
+ const record = records[0]
47
+ const srvPort = record.port
48
+ const srvHost = record.name
49
+ if (srvHost === host) {
50
+ throw new Error('Loop in DNS SRV records')
51
+ }
52
+ return {
53
+ port: srvPort,
54
+ ...await this.resolve(srvHost, ipFamily, srvRecordPrefix)
55
+ }
56
+ }
57
+ this.logger.debug('No SRV Record')
58
+ } catch (e) {
59
+ this.logger.debug(e)
60
+ }
61
+ }
62
+
63
+ this.logger.debug('Standard Resolve: ' + host)
64
+ const dnsResult = await dns.promises.lookup(host, ipFamily)
65
+ // For some reason, this sometimes returns a string address rather than an object.
66
+ // I haven't been able to reproduce, but it's been reported on the issue tracker.
67
+ let address
68
+ if (typeof dnsResult === 'string') {
69
+ address = dnsResult
70
+ } else {
71
+ address = dnsResult.address
72
+ }
73
+ this.logger.debug('Found address: ' + address)
74
+ return { address }
75
+ }
76
+ }
@@ -0,0 +1,69 @@
1
+ import { createSocket } from 'node:dgram'
2
+ import { debugDump } from './HexUtil.js'
3
+ import { promisify } from 'node:util'
4
+ import Logger from './Logger.js'
5
+
6
+ export default class GlobalUdpSocket {
7
+ constructor ({ port }) {
8
+ this.socket = null
9
+ this.callbacks = new Set()
10
+ this.debuggingCallbacks = new Set()
11
+ this.logger = new Logger()
12
+ this.port = port
13
+ }
14
+
15
+ async _getSocket () {
16
+ if (!this.socket) {
17
+ const udpSocket = createSocket({
18
+ type: 'udp4',
19
+ reuseAddr: true
20
+ })
21
+ udpSocket.unref()
22
+ udpSocket.on('message', (buffer, rinfo) => {
23
+ const fromAddress = rinfo.address
24
+ const fromPort = rinfo.port
25
+ this.logger.debug(log => {
26
+ log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')')
27
+ log(debugDump(buffer))
28
+ })
29
+ for (const callback of this.callbacks) {
30
+ callback(fromAddress, fromPort, buffer)
31
+ }
32
+ })
33
+ udpSocket.on('error', e => {
34
+ this.logger.debug('UDP ERROR:', e)
35
+ })
36
+ await promisify(udpSocket.bind).bind(udpSocket)(this.port)
37
+ this.port = udpSocket.address().port
38
+ this.socket = udpSocket
39
+ }
40
+ return this.socket
41
+ }
42
+
43
+ async send (buffer, address, port, debug) {
44
+ const socket = await this._getSocket()
45
+
46
+ if (debug) {
47
+ this.logger._print(log => {
48
+ log(address + ':' + port + ' UDP(' + this.port + ')-->')
49
+ log(debugDump(buffer))
50
+ })
51
+ }
52
+
53
+ await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address)
54
+ }
55
+
56
+ addCallback (callback, debug) {
57
+ this.callbacks.add(callback)
58
+ if (debug) {
59
+ this.debuggingCallbacks.add(callback)
60
+ this.logger.debugEnabled = true
61
+ }
62
+ }
63
+
64
+ removeCallback (callback) {
65
+ this.callbacks.delete(callback)
66
+ this.debuggingCallbacks.delete(callback)
67
+ this.logger.debugEnabled = this.debuggingCallbacks.size > 0
68
+ }
69
+ }
package/lib/HexUtil.js ADDED
@@ -0,0 +1,20 @@
1
+ /** @param {Buffer} buffer */
2
+ export const debugDump = (buffer) => {
3
+ let hexLine = ''
4
+ let chrLine = ''
5
+ let out = ''
6
+ out += 'Buffer length: ' + buffer.length + ' bytes\n'
7
+ for (let i = 0; i < buffer.length; i++) {
8
+ const sliced = buffer.slice(i, i + 1)
9
+ hexLine += sliced.toString('hex') + ' '
10
+ let chr = sliced.toString()
11
+ if (chr < ' ' || chr > '~') chr = ' '
12
+ chrLine += chr + ' '
13
+ if (hexLine.length > 60 || i === buffer.length - 1) {
14
+ out += hexLine + '\n'
15
+ out += chrLine + '\n'
16
+ hexLine = chrLine = ''
17
+ }
18
+ }
19
+ return out
20
+ }
package/lib/Logger.js ADDED
@@ -0,0 +1,45 @@
1
+ import { debugDump } from './HexUtil.js'
2
+ import { Buffer} from 'node:buffer'
3
+
4
+ export default class Logger {
5
+ constructor () {
6
+ this.debugEnabled = false
7
+ this.prefix = ''
8
+ }
9
+
10
+ debug (...args) {
11
+ if (!this.debugEnabled) return
12
+ this._print(...args)
13
+ }
14
+
15
+ _print (...args) {
16
+ try {
17
+ const strings = this._convertArgsToStrings(...args)
18
+ if (strings.length) {
19
+ if (this.prefix) {
20
+ strings.unshift(this.prefix)
21
+ }
22
+ console.log(...strings)
23
+ }
24
+ } catch (e) {
25
+ console.log('Error while logging: ' + e)
26
+ }
27
+ }
28
+
29
+ _convertArgsToStrings (...args) {
30
+ const out = []
31
+ for (const arg of args) {
32
+ if (arg instanceof Error) {
33
+ out.push(arg.stack)
34
+ } else if (arg instanceof Buffer) {
35
+ out.push(debugDump(arg))
36
+ } else if (typeof arg === 'function') {
37
+ const result = arg.call(undefined, (...args) => this._print(...args))
38
+ if (result !== undefined) out.push(...this._convertArgsToStrings(result))
39
+ } else {
40
+ out.push(arg)
41
+ }
42
+ }
43
+ return out
44
+ }
45
+ }
@@ -0,0 +1,18 @@
1
+ export default class Promises {
2
+ static createTimeout (timeoutMs, timeoutMsg) {
3
+ let cancel = null
4
+ const wrapped = new Promise((resolve, reject) => {
5
+ const timeout = setTimeout(
6
+ () => {
7
+ reject(new Error(timeoutMsg + ' - Timed out after ' + timeoutMs + 'ms'))
8
+ },
9
+ timeoutMs
10
+ )
11
+ cancel = () => {
12
+ clearTimeout(timeout)
13
+ }
14
+ })
15
+ wrapped.cancel = cancel
16
+ return wrapped
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ import * as protocols from '../protocols/index.js'
2
+
3
+ export const getProtocol = (protocolId) => {
4
+ if (!(protocolId in protocols)) { throw Error('Protocol definition file missing: ' + protocolId) }
5
+
6
+ return new protocols[protocolId]()
7
+ }
@@ -0,0 +1,95 @@
1
+ import { lookup } from './game-resolver.js'
2
+ import { getProtocol } from './ProtocolResolver.js'
3
+ import GlobalUdpSocket from './GlobalUdpSocket.js'
4
+
5
+ const defaultOptions = {
6
+ socketTimeout: 2000,
7
+ attemptTimeout: 10000,
8
+ maxAttempts: 1,
9
+ ipFamily: 0
10
+ }
11
+
12
+ export default class QueryRunner {
13
+ constructor (runnerOpts = {}) {
14
+ this.udpSocket = new GlobalUdpSocket({
15
+ port: runnerOpts.listenUdpPort
16
+ })
17
+ }
18
+
19
+ async run (userOptions) {
20
+ for (const key of Object.keys(userOptions)) {
21
+ const value = userOptions[key]
22
+ if (['port', 'ipFamily'].includes(key)) {
23
+ userOptions[key] = parseInt(value)
24
+ }
25
+ }
26
+
27
+ const {
28
+ port_query: gameQueryPort,
29
+ port_query_offset: gameQueryPortOffset,
30
+ ...gameOptions
31
+ } = lookup(userOptions.type)
32
+ const attempts = []
33
+
34
+ const optionsCollection = {
35
+ ...defaultOptions,
36
+ ...gameOptions,
37
+ ...userOptions
38
+ }
39
+
40
+ const addAttemptWithPort = port => {
41
+ attempts.push({
42
+ ...optionsCollection,
43
+ port
44
+ })
45
+ }
46
+
47
+ if (userOptions.port) {
48
+ if (!userOptions.givenPortOnly) {
49
+ if (gameQueryPortOffset) { addAttemptWithPort(userOptions.port + gameQueryPortOffset) }
50
+
51
+ if (userOptions.port === gameOptions.port && gameQueryPort) { addAttemptWithPort(gameQueryPort) }
52
+ }
53
+
54
+ attempts.push(optionsCollection)
55
+ } else if (gameQueryPort) {
56
+ addAttemptWithPort(gameQueryPort)
57
+ } else if (gameOptions.port) {
58
+ addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0))
59
+ } else {
60
+ // Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
61
+ attempts.push(optionsCollection)
62
+ }
63
+
64
+ const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts
65
+
66
+ let attemptNum = 0
67
+ const errors = []
68
+ for (const attempt of attempts) {
69
+ for (let retry = 0; retry < numRetries; retry++) {
70
+ attemptNum++
71
+
72
+ try {
73
+ return await this._attempt(attempt)
74
+ } catch (e) {
75
+ e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack
76
+ errors.push(e)
77
+ }
78
+ }
79
+ }
80
+
81
+ const err = new Error('Failed all ' + errors.length + ' attempts')
82
+ for (const e of errors) {
83
+ err.stack += '\n' + e.stack
84
+ }
85
+
86
+ throw err
87
+ }
88
+
89
+ async _attempt (options) {
90
+ const core = getProtocol(options.protocol)
91
+ core.options = options
92
+ core.udpSocket = this.udpSocket
93
+ return await core.runOnceSafe()
94
+ }
95
+ }
package/lib/Results.js ADDED
@@ -0,0 +1,32 @@
1
+ export class Player {
2
+ name = ''
3
+ raw = {}
4
+
5
+ constructor (data) {
6
+ if (typeof data === 'string') {
7
+ this.name = data
8
+ } else {
9
+ const { name, ...raw } = data
10
+ if (name) this.name = name
11
+ if (raw) this.raw = raw
12
+ }
13
+ }
14
+ }
15
+
16
+ export class Players extends Array {
17
+ push (data) {
18
+ super.push(new Player(data))
19
+ }
20
+ }
21
+
22
+ export class Results {
23
+ name = ''
24
+ map = ''
25
+ password = false
26
+
27
+ raw = {}
28
+
29
+ maxplayers = 0
30
+ players = new Players()
31
+ bots = new Players()
32
+ }
@@ -0,0 +1,17 @@
1
+ import { games } from './games.js'
2
+
3
+ export const lookup = (type) => {
4
+ if (!type) { throw Error('No game specified') }
5
+
6
+ if (type.startsWith('protocol-')) {
7
+ return {
8
+ protocol: type.substring(9)
9
+ }
10
+ }
11
+
12
+ const game = games[type]
13
+
14
+ if (!game) { throw Error('Invalid game: ' + type) }
15
+
16
+ return game.options
17
+ }
package/lib/gamedig.js ADDED
@@ -0,0 +1,23 @@
1
+ import QueryRunner from './QueryRunner.js'
2
+
3
+ let singleton = null
4
+
5
+ export class GameDig {
6
+ constructor (runnerOpts) {
7
+ this.queryRunner = new QueryRunner(runnerOpts)
8
+ }
9
+
10
+ async query (userOptions) {
11
+ return await this.queryRunner.run(userOptions)
12
+ }
13
+
14
+ static getInstance () {
15
+ if (!singleton) { singleton = new GameDig() }
16
+
17
+ return singleton
18
+ }
19
+
20
+ static async query (...args) {
21
+ return await GameDig.getInstance().query(...args)
22
+ }
23
+ }