diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07e6e47 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/lib/parser.js b/lib/parser.js new file mode 100644 index 0000000..7ef93a3 --- /dev/null +++ b/lib/parser.js @@ -0,0 +1,173 @@ +var net = require('net') + , EventEmitter = require('events').EventEmitter + , util = require('util') + , assert = require('assert') + , Iconv = require('iconv').Iconv + , packets = require('../packets.json') + , toUcs2 = new Iconv('UTF-8', 'utf16be') + , fromUcs2 = new Iconv('utf16be', 'UTF-8') + +module.exports = Parser; + +function Parser(options) { + EventEmitter.call(this); +} +util.inherits(Parser, EventEmitter); + +Parser.prototype.connect = function(port, host) { + var self = this; + self.client = net.connect(port, host, function() { + self.emit('connect'); + }); + var incomingBuffer = new Buffer(0); + self.client.on('data', function(data) { + incomingBuffer = Buffer.concat([incomingBuffer, data]); + var parsed; + while (true) { + parsed = parsePacket(incomingBuffer); + if (! parsed) break; + incomingBuffer = incomingBuffer.slice(parsed.size); + self.emit('packet', parsed.results); + } + }); + + self.client.on('error', function(err) { + self.emit('error', err); + }); + + self.client.on('end', function() { + self.emit('end'); + }); +}; + +Parser.prototype.writePacket = function(packetId, params) { + var buffer = createPacketBuffer(packetId, params); + this.client.write(buffer); +}; + +var writers = { + 'int': IntWriter, + 'byte': ByteWriter, + 'string': StringWriter, +}; + +var readers = { + 'string': readString, + 'byteArray': readByteArray, + 'short': readShort, +}; + +function readString (buffer, offset) { + var results = readShort(buffer, offset); + if (! results) return null; + + var strBegin = offset + results.size; + var strLen = results.value; + var strEnd = strBegin + strLen * 2; + if (strEnd > buffer.length) return null; + var str = fromUcs2.convert(buffer.slice(strBegin, strEnd)).toString(); + + return { + value: str, + size: strEnd - offset, + }; +} + +function readByteArray (buffer, offset) { + var results = readShort(buffer, offset); + if (! results) return null; + + var bytesBegin = offset + results.size; + var bytesSize = results.value; + var bytesEnd = bytesBegin + bytesSize; + if (bytesEnd > buffer.length) return null; + var bytes = buffer.slice(bytesBegin, bytesEnd); + + return { + value: bytes, + size: bytesEnd - offset, + }; +} + +function readShort(buffer, offset) { + if (offset + 2 > buffer.length) return null; + var value = buffer.readInt16BE(offset); + return { + value: value, + size: 2, + }; +} + +function StringWriter(value) { + this.value = value; + this.encoded = toUcs2.convert(value); + this.size = 2 + this.encoded.length; +} + +StringWriter.prototype.write = function(buffer, offset) { + buffer.writeInt16BE(this.value.length, offset); + this.encoded.copy(buffer, offset + 2); +} + +function ByteWriter(value) { + this.value = value; + this.size = 1; +} + +ByteWriter.prototype.write = function(buffer, offset) { + buffer.writeInt8(this.value, offset); +} + +function IntWriter(value) { + this.value = value; + this.size = 4; +} + +IntWriter.prototype.write = function(buffer, offset) { + buffer.writeInt32BE(this.value, offset); +} + +function createPacketBuffer(packetId, params) { + var size = 1; + var fields = [ new ByteWriter(packetId) ]; + var packet = packets[packetId]; + packet.forEach(function(fieldInfo) { + var value = params[fieldInfo.name]; + var field = new writers[fieldInfo.type](value); + size += field.size; + fields.push(field); + }); + var buffer = new Buffer(size); + var cursor = 0; + fields.forEach(function(field) { + field.write(buffer, cursor); + cursor += field.size; + }); + return buffer; +} + +function parsePacket(buffer) { + if (buffer.length < 1) return null; + var packetId = buffer.readUInt8(0); + var size = 1; + var results = { id: packetId }; + var packetInfo = packets[packetId]; + assert.ok(packetInfo, "Unrecognized packetId: " + packetId); + var i, fieldInfo, read, readResults; + for (i = 0; i < packetInfo.length; ++i) { + fieldInfo = packetInfo[i]; + read = readers[fieldInfo.type]; + readResults = read(buffer, size); + if (readResults) { + results[fieldInfo.name] = readResults.value; + size += readResults.size; + } else { + // buffer needs to be more full + return null; + } + } + return { + size: size, + results: results, + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..29ccd3e --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "minecraft-protocol", + "version": "0.0.0", + "description": "minecraft protocol", + "main": "index.js", + "scripts": { + "test": "mocha" + }, + "keywords": [ + "minecraft", + "protocol" + ], + "author": "Andrew Kelley", + "license": "BSD", +} diff --git a/packets.json b/packets.json new file mode 100644 index 0000000..4a19595 --- /dev/null +++ b/packets.json @@ -0,0 +1,34 @@ +{ + "2": [ + { + "name": "protocolVersion", + "type": "byte" + }, + { + "name": "userName", + "type": "string" + }, + { + "name": "serverHost", + "type": "string" + }, + { + "name": "serverPort", + "type": "int" + } + ], + "253": [ + { + "name": "serverId", + "type": "string" + }, + { + "name": "publicKey", + "type": "byteArray" + }, + { + "name": "verifyToken", + "type": "byteArray" + } + ] +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..d09debc --- /dev/null +++ b/test.js @@ -0,0 +1,45 @@ +var Parser = require('./lib/parser'); + +var parser = new Parser(); +parser.on('connect', function() { + console.info("connect"); + parser.writePacket(0x02, { + protocolVersion: 51, + userName: 'superjoe30', + serverHost: 'localhost', + serverPort: 25565, + }); +}); +parser.on('packet', function(packet) { + var handler = packetHandlers[packet.id]; + if (handler) { + handler(packet); + } else { + console.warn("No packet handler for", packet.id, "fields", packet); + } +}); +parser.on('error', function(err) { + console.error("error connecting", err.stack); +}); +parser.on('end', function() { + console.info("disconnect"); +}); +parser.connect(25565, 'localhost'); + +var packetHandlers = { + 0xFD: onEncryptionKeyRequest, +}; + +function onEncryptionKeyRequest(packet) { + var sharedSecret = randomBuffer(16); +} + +function randomBuffer(size) { + var buffer = new Buffer(size); + var i, number; + for (i = 0; i < size; ++i) { + number = Math.floor(Math.random() * 256); + buffer.writeUInt8(number, i); + } + return buffer; +}