diff --git a/package.json b/package.json index 6f9b891..b5647f9 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/clientTest.js b/samples/clientTest.js similarity index 95% rename from src/clientTest.js rename to samples/clientTest.js index 4e0e620..11b9b3a 100644 --- a/src/clientTest.js +++ b/samples/clientTest.js @@ -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) => { diff --git a/src/serverTest.js b/samples/serverTest.js similarity index 71% rename from src/serverTest.js rename to samples/serverTest.js index 975e00e..ac50b53 100644 --- a/src/serverTest.js +++ b/samples/serverTest.js @@ -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) } diff --git a/src/client.js b/src/client.js index 6ed2816..24d0b96 100644 --- a/src/client.js +++ b/src/client.js @@ -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 diff --git a/src/connection.js b/src/connection.js index 3795b0e..8cd8dad 100644 --- a/src/connection.js +++ b/src/connection.js @@ -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 diff --git a/src/datatypes/promises.js b/src/datatypes/promises.js new file mode 100644 index 0000000..c3763e1 --- /dev/null +++ b/src/datatypes/promises.js @@ -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) + ]) + } +} \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index b02489a..0000000 --- a/src/index.js +++ /dev/null @@ -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") -}; diff --git a/src/rak.js b/src/rak.js new file mode 100644 index 0000000..c9a60a6 --- /dev/null +++ b/src/rak.js @@ -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 +} \ No newline at end of file diff --git a/src/ConnWorker.js b/src/rakWorker.js similarity index 98% rename from src/ConnWorker.js rename to src/rakWorker.js index e43aea5..3dc837a 100644 --- a/src/ConnWorker.js +++ b/src/rakWorker.js @@ -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) diff --git a/src/server.js b/src/server.js index 0ad023a..2e3a924 100644 --- a/src/server.js +++ b/src/server.js @@ -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 } } diff --git a/src/texture b/src/texture deleted file mode 100644 index 83c6355..0000000 Binary files a/src/texture and /dev/null differ diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index 670d8a9..36f7c18 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -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) } }