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:
parent
5c1249e9d8
commit
0bc5863459
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue