Query game port on server create (#63)

* Query game port on server create

Seems to work okay with real game servers

* Only warn on wrong line endings instead of error
This commit is contained in:
Barnaby 2022-05-04 21:39:14 +01:00 committed by GitHub
parent 5c1249e9d8
commit 0bc5863459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 241 additions and 0 deletions

11
package-lock.json generated
View File

@ -8,6 +8,7 @@
"license": "MIT",
"dependencies": {
"bad-words": "^3.0.4",
"bit-buffer": "^0.2.5",
"dotenv": "^10.0.0",
"fastify": "^3.24.0",
"fastify-cors": "^6.0.2",
@ -371,6 +372,11 @@
"node": ">=8"
}
},
"node_modules/bit-buffer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/bit-buffer/-/bit-buffer-0.2.5.tgz",
"integrity": "sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA=="
},
"node_modules/block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
@ -4111,6 +4117,11 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"bit-buffer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/bit-buffer/-/bit-buffer-0.2.5.tgz",
"integrity": "sha512-x1yGnmXvFg6e3DiyRztElbcn1bsCTFSoM/ncAzY62uE0JdTl5xlKJd0ooqLYoPbhdsnpehSIQrdIvclcZJYwiA=="
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",

View File

@ -16,6 +16,7 @@
},
"dependencies": {
"bad-words": "^3.0.4",
"bit-buffer": "^0.2.5",
"dotenv": "^10.0.0",
"fastify": "^3.24.0",
"fastify-cors": "^6.0.2",

View File

@ -4,6 +4,7 @@ const { GameServer, GetGameServers, AddGameServer, RemoveGameServer, GetGhostSer
const asyncHttp = require( path.join( __dirname, "../shared/asynchttp.js" ) )
const pjson = require( path.join( __dirname, "../shared/pjson.js" ) )
const { minimumVersion } = require( path.join( __dirname, "../shared/version.js" ) )
const { QueryServerPort } = require( path.join( __dirname, "../shared/udp_query.js" ) )
const Filter = require( "bad-words" )
let filter = new Filter()
@ -35,6 +36,9 @@ async function TryVerifyServer( request )
if ( !authServerResponse || authServerResponse.toString() != VERIFY_STRING )
return 1
let gamePortDoesRespond = await QueryServerPort( request.ip, request.query.port )
if( !gamePortDoesRespond ) return 1
return 0
}

View File

@ -0,0 +1,192 @@
///// Credit to R5Reloaded for the following code /////
/// Netmessages ///
const BitBuffer = require( "bit-buffer" )
const R5Messages = {
a2s_getchallenge: function( uid )
{
let buf = Buffer.alloc( 1600 )
let stream = new BitBuffer.BitStream( buf )
stream.writeInt32( -1 )
stream.writeUint8( "H".charCodeAt( 0 ) )
stream.writeASCIIString( "connect" )
stream.writeUint32( Number( uid & 0xffffffffn ) )
stream.writeUint32( Number( uid >> 32n ) )
stream.writeUint8( 2 )
const ret = stream.buffer.slice( stream.buffer.byteOffset, stream.byteIndex ) // ???
return ret
}
}
/// Netmessages END ///
/// Crypto ///
const crypto = require( "crypto" )
class rSrcNetCrypto
{
constructor( key )
{
this.key = Buffer.from( key, "base64" )
this.aad = Buffer.from(
"\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10",
"ascii"
)
//this.iv = Buffer.from("aaaaaaaaaaaaaaaaaaaaaaaa", "hex")
}
encrypt( data, bytes = -1 )
{
this.iv = crypto.randomBytes( 12 )
let cipher = crypto.createCipheriv( "aes-128-gcm", this.key, this.iv )
cipher.setAAD( this.aad )
let encrypted = Buffer.concat( [
cipher.update( data.slice( 0, bytes > 0 ? bytes : data.length ) ),
cipher.final(),
] )
let tag = cipher.getAuthTag()
const ret = Buffer.concat( [this.iv, tag, encrypted] )
return ret
}
decrypt( packet_data )
{
let iv = packet_data.slice( 0, 12 )
let tag = packet_data.slice( 12, 16 )
let data = packet_data.slice( 12 + 16, packet_data.length )
let decipher = crypto.createDecipheriv( "aes-128-gcm", this.key, iv )
decipher.setAAD( this.aad )
decipher.setAuthTag( tag )
try
{
return Buffer.concat( [decipher.update( data ), decipher.final()] )
}
catch ( e )
{
return Buffer.from( "000000000000000000000000" )
}
}
}
/// Crypto END ///
/// Networking ///
const dgram = require( "dgram" )
const { EventEmitter } = require( "stream" )
class rSrcNetClient
{
constructor( owner )
{
this._owner = owner
}
onMessage( msg, rinfo )
{
if ( rinfo.address != this.ip || rinfo.port != this.port )
{
//console.log("Mismatch?", rinfo)
return
}
const decryptedData = this._owner.crypto.decrypt( msg )
this._owner.emit( "data", decryptedData )
}
send( data, bytes = -1 )
{
const enc = this._owner.crypto.encrypt( data, bytes )
this.socket.send( enc, this.port, this.ip )
}
connect( ip, port )
{
this.ip = ip
this.port = port
this.socket = dgram.createSocket( {
type: "udp4",
reuseAddr: false,
} )
const data = R5Messages.a2s_getchallenge( this._owner.connectionSettings.uid )
const enc = this._owner.crypto.encrypt( data )
this.socket.bind(
{
port: 0,
exclusive: true,
//exclusive: false,
},
() =>
{
this.bindPort = this.socket.address().port
this.socket.send( enc, this.port, this.ip )
}
)
this.socket.on( "message", this.onMessage.bind( this ) )
}
close()
{
this.socket.close()
}
}
/// Networking END ///
/// Main client class ///
class rSrcClient extends EventEmitter
{
constructor( constgs )
{
super()
this.connectionSettings = constgs
this.crypto = new rSrcNetCrypto( this.connectionSettings.encryptionKey )
this.net = new rSrcNetClient( this )
this.on( "data", this.onData.bind( this ) )
}
connect()
{
this.net.connect( this.connectionSettings.ip, this.connectionSettings.port )
}
onData( data )
{
if ( data.readInt32LE() != -1 )
return // not connectionless
if ( data.readUint8( 4 ) != 73 )
return // not challenge response
if ( data.readBigInt64LE( 9 ) != this.connectionSettings.uid )
return // challenge not for this uid
const challenge = data.readInt32LE( 5 )
this.emit( "challenge", challenge )
this.net.close()
}
close()
{
this.net.close()
}
}
/// Main client class END ///
module.exports = rSrcClient

33
shared/udp_query.js Normal file
View File

@ -0,0 +1,33 @@
const rSrcClient = require( "./a2s_challenge_query" )
const encryptionKey = "WDNWLmJYQ2ZlM0VoTid3Yg==" // standard r2 encryption key
function QueryServerPort( ip, port )
{
return new Promise( resolve =>
{
const client = new rSrcClient( {
encryptionKey,
ip,
port,
uid: 1000000001337n,
} )
let queryTimeout = setTimeout( () =>
{
resolve( false ) // return false on timeout reached
}, 1000 )
client.on( "challenge", async () =>
{
clearTimeout( queryTimeout )
resolve( true ) // return true on challenge response
} )
client.connect() // attempt to connect to the game server
} )
}
module.exports = {
QueryServerPort
}