use raknet-native with js fallback

This commit is contained in:
extremeheat 2021-03-08 02:36:25 -05:00
commit 86dcbc1f49
12 changed files with 262 additions and 138 deletions

View file

@ -27,6 +27,7 @@
"minecraft-folder-path": "^1.1.0",
"prismarine-nbt": "github:extremeheat/prismarine-nbt#le",
"protodef": "github:extremeheat/node-protodef#compiler",
"raknet-native": "^0.0.4",
"uuid-1345": "^0.99.7"
},
"devDependencies": {

View file

@ -1,12 +1,12 @@
process.env.DEBUG = 'minecraft-protocol raknet'
const { Client } = require('./client')
const { Client } = require('../src/client')
const fs = require('fs')
// console.log = () =>
async function test() {
const client = new Client({
hostname: '127.0.0.1',
port: 19130
port: 19132
})
client.once('resource_packs_info', (packet) => {

View file

@ -1,5 +1,5 @@
process.env.DEBUG = 'minecraft-protocol raknet'
const { Server } = require('./server')
const { Server } = require('../src/server')
const CreativeItems = require('../data/creativeitems.json')
const NBT = require('prismarine-nbt')
const fs = require('fs')
@ -50,35 +50,35 @@ server.on('connect', ({ client }) => {
client.queue('inventory_slot', {"inventory_id":120,"slot":i,"uniqueid":0,"item":{"network_id":0}})
}
client.queue('inventory_transaction', require('./packets/inventory_transaction.json'))
client.queue('player_list', require('./packets/player_list.json'))
client.queue('start_game', require('./packets/start_game.json'))
client.queue('inventory_transaction', require('../src/packets/inventory_transaction.json'))
client.queue('player_list', require('../src/packets/player_list.json'))
client.queue('start_game', require('../src/packets/start_game.json'))
client.queue('item_component', {"entries":[]})
client.queue('set_spawn_position', require('./packets/set_spawn_position.json'))
client.queue('set_spawn_position', require('../src/packets/set_spawn_position.json'))
client.queue('set_time', { time: 5433771 })
client.queue('set_difficulty', { difficulty: 1 })
client.queue('set_commands_enabled', { enabled: true })
client.queue('adventure_settings', require('./packets/adventure_settings.json'))
client.queue('adventure_settings', require('../src/packets/adventure_settings.json'))
client.queue('biome_definition_list', require('./packets/biome_definition_list.json'))
client.queue('available_entity_identifiers', require('./packets/available_entity_identifiers.json'))
client.queue('biome_definition_list', require('../src/packets/biome_definition_list.json'))
client.queue('available_entity_identifiers', require('../src/packets/available_entity_identifiers.json'))
client.queue('update_attributes', require('./packets/update_attributes.json'))
client.queue('creative_content', require('./packets/creative_content.json'))
client.queue('inventory_content', require('./packets/inventory_content.json'))
client.queue('update_attributes', require('../src/packets/update_attributes.json'))
client.queue('creative_content', require('../src/packets/creative_content.json'))
client.queue('inventory_content', require('../src/packets/inventory_content.json'))
client.queue('player_hotbar', {"selected_slot":3,"window_id":0,"select_slot":true})
client.queue('crafting_data', require('./packets/crafting_data.json'))
client.queue('available_commands', require('./packets/available_commands.json'))
client.queue('crafting_data', require('../src/packets/crafting_data.json'))
client.queue('available_commands', require('../src/packets/available_commands.json'))
client.queue('chunk_radius_update', {"chunk_radius":5})
client.queue('set_entity_data', require('./packets/set_entity_data.json'))
client.queue('set_entity_data', require('../src/packets/set_entity_data.json'))
client.queue('game_rules_changed', require('./packets/game_rules_changed.json'))
client.queue('game_rules_changed', require('../src/packets/game_rules_changed.json'))
client.queue('respawn', {"x":646.9405517578125,"y":65.62001037597656,"z":77.86255645751953,"state":0,"runtime_entity_id":0})
for (const file of fs.readdirSync('chunks')) {
const buffer = Buffer.from(fs.readFileSync('./chunks/' + file, 'utf8'), 'hex')
for (const file of fs.readdirSync('../src/chunks')) {
const buffer = Buffer.from(fs.readFileSync('../src/chunks/' + file, 'utf8'), 'hex')
// console.log('Sending chunk', chunk)
client.sendBuffer(buffer)
}

View file

@ -1,20 +1,14 @@
const RakClient = require('jsp-raknet/client')
const fs = require('fs')
const debug = require('debug')('minecraft-protocol')
const { Connection } = require('./connection')
const { createDeserializer, createSerializer } = require('./transforms/serializer')
const ConnWorker = require('./ConnWorker')
const { Encrypt } = require('./auth/encryption')
const auth = require('./client/auth')
const Options = require('./options')
const debug = require('debug')('minecraft-protocol')
const fs = require('fs')
const useWorkers = true
const { RakClient } = require('./Rak')
class Client extends Connection {
/**
*
* @param {{ version: number, hostname: string, port: number }} options
*/
/** @param {{ version: number, hostname: string, port: number }} options */
constructor(options) {
super()
this.options = { ...Options.defaultOptions, ...options }
@ -54,41 +48,11 @@ class Client extends Connection {
connect = async (sessionData) => {
const hostname = this.options.hostname || '127.0.0.1'
const port = this.options.port || 19132
if (useWorkers) {
this.worker = ConnWorker.connect(hostname, port)
this.worker.on('message', (evt) => {
switch (evt.type) {
case 'connected':
this.sendLogin()
break
case 'encapsulated':
this.onEncapsulated(...evt.args)
break
}
})
} else {
if (this.raknet) return
this.raknet = new RakClient('127.0.0.1', 19132)
await this.raknet.connect()
this.raknet.on('connecting', () => {
// console.log(`[client] connecting to ${hostname}/${port}`)
})
this.raknet.on('connected', (connection) => {
console.log(`[client] connected!`)
this.connection = connection
this.sendLogin()
})
this.raknet.on('encapsulated', this.onEncapsulated)
this.raknet.on('raw', (buffer, inetAddr) => {
console.log('Raw packet', buffer, inetAddr)
})
}
this.connection = new RakClient({ useWorkers: true, hostname, port })
this.connection.onConnected = () => this.sendLogin()
this.connection.onEncapsulated = this.onEncapsulated
this.connection.connect()
}
sendLogin() {
@ -100,7 +64,7 @@ class Client extends Connection {
]
const encodedChain = JSON.stringify({ chain })
const skinChain = JSON.stringify({})
// const skinChain = JSON.stringify({})
const bodyLength = this.clientUserChain.length + encodedChain.length + 8

View file

@ -2,7 +2,6 @@ const BinaryStream = require('@jsprismarine/jsbinaryutils').default
const BatchPacket = require('./datatypes/BatchPacket')
const cipher = require('./transforms/encryption')
const { EventEmitter } = require('events')
const EncapsulatedPacket = require('jsp-raknet/protocol/encapsulated_packet')
const Reliability = require('jsp-raknet/protocol/reliability')
const debug = require('debug')('minecraft-protocol')
@ -112,16 +111,17 @@ class Connection extends EventEmitter {
// TODO: Rename this to sendEncapsulated
sendMCPE(buffer, immediate) {
if (this.worker) {
this.outLog('-> buf', buffer)
this.worker.postMessage({ type: 'queueEncapsulated', packet: buffer, immediate })
} else {
const sendPacket = new EncapsulatedPacket()
sendPacket.reliability = Reliability.ReliableOrdered
sendPacket.buffer = buffer
this.connection.addEncapsulatedToQueue(sendPacket)
if (immediate) this.connection.sendQueue()
}
this.connection.sendReliable(buffer, immediate)
// if (this.worker) {
// this.outLog('-> buf', buffer)
// this.worker.postMessage({ type: 'queueEncapsulated', packet: buffer, immediate })
// } else {
// const sendPacket = new EncapsulatedPacket()
// sendPacket.reliability = Reliability.ReliableOrdered
// sendPacket.buffer = buffer
// this.connection.addEncapsulatedToQueue(sendPacket)
// if (immediate) this.connection.sendQueue()
// }
}
// These are callbacks called from encryption.js

12
src/datatypes/promises.js Normal file
View file

@ -0,0 +1,12 @@
module.exports = {
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
waitFor(cb, withTimeout) {
return Promise.race([
new Promise((res, rej) => cb(res)),
sleep(withTimeout)
])
}
}

View file

@ -1,7 +0,0 @@
module.exports = {
createSerializer: require("./transforms/serializer").createSerializer,
createDeserializer: require("./transforms/serializer").createDeserializer,
createProtocol: require('./transforms/serializer').createProtocol,
createServer: require("./createServer"),
createClient: require("./createClient")
};

179
src/rak.js Normal file
View file

@ -0,0 +1,179 @@
const { EventEmitter } = require('events')
const Listener = require('jsp-raknet/listener')
const EncapsulatedPacket = require('jsp-raknet/protocol/encapsulated_packet')
const RakClient = require('jsp-raknet/client')
const ConnWorker = require('./rakWorker')
try {
var { Client, Server, PacketPriority, PacketReliability, McPingMessage } = require('raknet-native')
} catch (e) {
console.debug('[raknet] native not found, using js', e)
}
class RakNativeClient extends EventEmitter {
constructor(options) {
super()
this.onConnected = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.raknet = new Client(options.hostname, options.port, 'minecraft')
this.raknet.on('encapsulated', thingy => {
console.log('Encap',thingy)
const { buffer, address, guid }=thingy
this.onEncapsulated(buffer, address)
})
this.raknet.on('connected', () => {
this.onConnected()
})
}
async ping() {
this.raknet.ping()
return waitFor((done) => {
this.raknet.on('pong', (ret) => {
if (ret.extra) {
done(ret.extra.toString())
}
})
}, 1000)
}
connect() {
this.raknet.connect()
}
sendReliable(buffer, immediate) {
const priority = immediate ? PacketPriority.IMMEDIATE_PRIORITY : PacketPriority.MEDIUM_PRIORITY
return this.raknet.send(buffer, priority, PacketReliability.RELIABLE_ORDERED, 0)
}
}
class RakNativeServer extends EventEmitter {
constructor(options = {}) {
super()
console.log('opts',options)
this.onOpenConnection = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
this.raknet = new Server(options.hostname, options.port, {
maxConnections: options.maxConnections || 3,
minecraft: { message: new McPingMessage().toString() }
})
this.raknet.on('openConnection', (client) => {
client.sendReliable = function(buffer, immediate) {
const priority = immediate ? PacketPriority.IMMEDIATE_PRIORITY : PacketPriority.MEDIUM_PRIORITY
return this.send(buffer, priority, PacketReliability.RELIABLE_ORDERED, 0)
}
this.onOpenConnection(client)
})
this.raknet.on('closeConnection', (client) => {
console.log('!!! Client CLOSED CONNECTION!')
this.onCloseConnection(client)
})
this.raknet.on('encapsulated', (thingy) => {
const { buffer, address, guid }=thingy
console.log('ENCAP',thingy)
this.onEncapsulated(buffer, address)
})
}
listen() {
this.raknet.listen()
}
}
class RakJsClient extends EventEmitter {
constructor(options = {}) {
super()
this.onConnected = () => {}
this.onEncapsulated = () => {}
if (options.useWorkers) {
this.connect = this.workerConnect
this.sendReliable = this.workerSendReliable
} else {
this.connect = this.plainConnect
this.sendReliable = this.plainSendReliable
}
}
workerConnect(hostname = this.options.hostname, port = this.options.port) {
this.worker = ConnWorker.connect(hostname, port)
this.worker.on('message', (evt) => {
switch (evt.type) {
case 'connected':
this.onConnected()
break
case 'encapsulated':
const [ecapsulated, address] = evt.args
this.onEncapsulated(ecapsulated.buffer, address.hash)
break
}
})
}
async plainConnect(hostname = this.options.hostname, port = this.options.port) {
this.raknet = new RakClient(hostname, port)
await this.raknet.connect()
this.raknet.on('connecting', () => {
console.log(`[client] connecting to ${hostname}/${port}`)
})
this.raknet.on('connected', this.onConnected)
this.raknet.on('encapsulated', (encapsulated, addr) => this.onEncapsulated(encapsulated.buffer, addr.hash))
}
workerSendReliable(buffer, immediate) {
this.worker.postMessage({ type: 'queueEncapsulated', packet: buffer, immediate })
}
plainSendReliable(buffer, immediate) {
const sendPacket = new EncapsulatedPacket()
sendPacket.reliability = Reliability.ReliableOrdered
sendPacket.buffer = buffer
this.connection.addEncapsulatedToQueue(sendPacket)
if (immediate) this.connection.sendQueue()
}
}
class RakJsServer extends EventEmitter {
constructor(options = {}) {
super()
this.options = options
this.onOpenConnection = () => {}
this.onCloseConnection = () => {}
this.onEncapsulated = () => {}
if (options.useWorkers) {
throw Error('nyi')
} else {
this.listen = this.plainListen
}
}
async plainListen() {
this.raknet = new Listener()
await this.raknet.listen(this.options.hostname, this.options.port)
this.raknet.on('openConnection', (conn) => {
conn.sendReliable = function(buffer, immediate) {
const sendPacket = new EncapsulatedPacket()
sendPacket.reliability = Reliability.ReliableOrdered
sendPacket.buffer = buffer
this.connection.addEncapsulatedToQueue(sendPacket)
if (immediate) this.raknet.sendQueue()
}
this.onOpenConnection(conn)
})
this.raknet.on('closeConnection', this.onCloseConnection)
this.raknet.on('encapsulated', this.onEncapsulated)
}
}
module.exports = {
RakClient: Client ? RakNativeClient : RakJsClient,
RakServer: Server ? RakNativeServer : RakJsServer
}

View file

@ -16,7 +16,6 @@ var raknet
function main() {
parentPort.on('message', (evt) => {
if (evt.type == 'connect') {
console.warn('-------- ', evt)
const { hostname, port } =evt
raknet = new RakClient(hostname, port)

View file

@ -1,8 +1,7 @@
const Listener = require('jsp-raknet/listener')
const { EventEmitter } = require('events')
const { createDeserializer, createSerializer } = require('./transforms/serializer')
const { Player } = require('./serverPlayer')
const { RakServer } = require('./rak')
const Options = require('./options')
const debug = require('debug')('minecraft-protocol')
@ -26,41 +25,35 @@ class Server extends EventEmitter {
}
onOpenConnection = (conn) => {
debug('new connection', conn)
this.inLog('new connection', conn)
const player = new Player(this, conn)
this.clients[hash(conn.address)] = player
this.clients[conn.address] = player
this.clientCount++
this.emit('connect', { client: player })
}
onCloseConnection = (inetAddr, reason) => {
debug('close connection', inetAddr, reason)
delete this.clients[hash(inetAddr)]
delete this.clients[inetAddr]
this.clientCount--
}
onEncapsulated = (encapsulated, inetAddr) => {
debug(inetAddr.address, 'Encapsulated', encapsulated)
const buffer = encapsulated.buffer
const client = this.clients[hash(inetAddr)]
onEncapsulated = (buffer, address) => {
debug(address, 'Encapsulated', buffer)
const client = this.clients[address]
if (!client) {
throw new Error(`packet from unknown inet addr: ${inetAddr.address}/${inetAddr.port}`)
throw new Error(`packet from unknown inet addr: ${address}`)
}
client.handle(buffer)
}
async create(serverIp = this.options.hostname, port = this.options.port) {
this.listener = new Listener(this)
this.raknet = await this.listener.listen(serverIp, port)
console.debug('Listening on', serverIp, port)
this.raknet.on('openConnection', this.onOpenConnection)
this.raknet.on('closeConnection', this.onCloseConnection)
this.raknet.on('encapsulated', this.onEncapsulated)
this.raknet.on('raw', (buffer, inetAddr) => {
debug('Raw packet', buffer, inetAddr)
})
async create(hostname = this.options.hostname, port = this.options.port) {
this.raknet = new RakServer({ hostname, port })
await this.raknet.listen()
console.debug('Listening on', hostname, port)
this.raknet.onOpenConnection = this.onOpenConnection
this.raknet.onCloseConnection = this.onCloseConnection
this.raknet.onEncapsulated = this.onEncapsulated
}
}

Binary file not shown.

View file

@ -72,43 +72,24 @@ function createEncryptor(client, iv) {
// A packet is encrypted via AES256(plaintext + SHA256(send_counter + plaintext + secret_key)[0:8]).
// The send counter is represented as a little-endian 64-bit long and incremented after each packet.
const addChecksum = new Transform({ // append checksum
transform(chunk, enc, cb) {
// console.log('Encryptor: checking checksum', chunk)
// Here we concat the payload + checksum before the encryption
const packet = Buffer.concat([chunk, computeCheckSum(chunk, client.sendCounter, client.secretKeyBytes)])
client.sendCounter++
this.push(packet)
cb()
}
})
function process(chunk) {
const buffer = Zlib.deflateRawSync(chunk, { level: 7 })
// client.outLog('🟡 Compressed', buffer, client.sendCounter)
const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)])
client.sendCounter++
// client.outLog('writing to cipher...', packet, client.secretKeyBytes, iv)
client.cipher.write(packet)
}
// https://stackoverflow.com/q/25971715/11173996
// TODO: Fix deflate stream - for some reason using .pipe() doesn't work using zlib.createDeflateRaw()
// so we define our own compressor transform
// const compressor = Zlib.createDeflateRaw({ level: 7, chunkSize: 1024 * 1024 * 2, flush: Zlib.Z_SYNC_FLUSH })
const compressor = new Transform({
transform(chunk, enc, cb) {
Zlib.deflateRaw(chunk, { level: 7 }, (err, res) => {
if (err) {
console.error(err)
throw new Error(`Failed to deflate stream`)
}
this.push(res)
cb()
})
}
})
// const stream = new PassThrough()
client.cipher.on('data', client.onEncryptedPacket)
const stream = new PassThrough()
stream
.pipe(compressor)
.pipe(addChecksum).pipe(client.cipher).on('data', client.onEncryptedPacket)
return (blob) => {
stream.write(blob)
client.outLog(client.options ? 'C':'S', '🟡 Encrypting', blob)
// stream.write(blob)
process(blob)
}
}
@ -119,7 +100,7 @@ function createDecryptor(client, iv) {
function verify(chunk) {
// console.log('Decryptor: checking checksum', client.receiveCounter, chunk)
// client.outLog('🔵 Inflating', chunk)
// First try to zlib decompress, then see how much bytes get read
const { buffer, engine } = Zlib.inflateRawSync(chunk, {
chunkSize: 1024 * 1024 * 2,
@ -158,6 +139,8 @@ function createDecryptor(client, iv) {
client.decipher.on('data', verify)
return (blob) => {
// client.inLog(client.options ? 'C':'S', ' 🔵 Decrypting', client.receiveCounter, blob)
// client.inLog('Using shared key', client.secretKeyBytes, iv)
client.decipher.write(blob)
}
}