diff --git a/README.md b/README.md index 90c2ac2..be74889 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Parse and serialize minecraft packets, plus authentication and encryption. objects. * Send a packet by supplying fields as a JavaScript object. * Supports authenticating and logging in. - - Supports encryption - - Supports online mode + - Supports encryption (client only) + - Supports online mode (client only) - Supports offline mode * Respond to keep-alive packets. * Test coverage @@ -45,6 +45,38 @@ client.on(0x03, function(packet) { }); ``` +### Hello World server example + +```js +var mc = require('minecraft-protocol'); +var server = mc.createServer({ + 'online-mode': true, // optional + encryption: true, // optional + host: '0.0.0.0', // optional + port: 25565, // optional +}); +server.on('login', function(client) { + client.write(0x01, { + entityId: 0, + levelType: 'default', + gameMode: 0, + dimension: 0, + difficulty: 2, + maxPlayers: server.maxPlayers + }); + client.write(0x0d, { + x: 0, + y: 1.62, + stance: 0, + z: 0, + yaw: 0, + pitch: 0, + onGround: true + }); + client.write(0x03, { message: 'Hello, ' + client.username }); +}); +``` + ## Testing * Ensure your system has the `java` executable in `PATH`. diff --git a/examples/server.js b/examples/server.js new file mode 100644 index 0000000..1d0fcf6 --- /dev/null +++ b/examples/server.js @@ -0,0 +1,76 @@ +var mc = require('../'); + +var yellow = '§e'; +var players = []; + +var options = { + 'online-mode': false, + motd: 'Vox Industries', + 'max-players': 127, + port: 25565, +}; + +var server = mc.createServer(options); + +server.on('login', function(client) { + var player = { + client: client, + username: client.username, + index: players.length + }; + players.push(player); + server.players = players.length; + + broadcast(yellow + player.username+' joined the game.'); + var addr = client.socket.remoteAddress + ':' + client.socket.remotePort; + console.log(player.username+' connected', '('+addr+')'); + + client.on('end', function() { + players.splice(player.index, 1); + server.players = players.length; + + broadcast(yellow + player.username+' left the game.', player); + console.log(player.username+' disconnected', '('+addr+')'); + }); + + // send init data so client will start rendering world + client.write(0x01, { + entityId: 0, + levelType: 'default', + gameMode: 1, + dimension: 0, + difficulty: 2, + maxPlayers: server.maxPlayers + }); + client.write(0x0d, { + x: 0, + y: 256, + stance: 255, + z: 0, + yaw: 0, + pitch: 0, + onGround: true + }); + + client.on(0x03, function(data) { + var message = '<'+player.username+'>' + ' ' + data.message; + broadcast(message); + console.log(message); + }); +}); + +server.on('error', function(error) { + console.log('Error:', error); +}); + +server.on('listening', function() { + console.log('Server listening on port', server.socket.address().port); +}); + +function broadcast(message, exclude) { + for(var i = 0; i < players.length; i++) { + if(players[i].username !== exclude && i !== exclude && players[i] !== exclude) { + players[i].client.write(0x03, { message: message }); + } + } +} diff --git a/examples/server_helloworld.js b/examples/server_helloworld.js new file mode 100644 index 0000000..90a0d50 --- /dev/null +++ b/examples/server_helloworld.js @@ -0,0 +1,45 @@ +var mc = require('../'); + +var options = { + 'online-mode': false, // optional +}; + +var server = mc.createServer(options); + +server.on('login', function(client) { + var addr = client.socket.remoteAddress; + console.log('Incoming connection', '('+addr+')'); + + client.on('end', function() { + console.log('Connection closed', '('+addr+')'); + }); + + // send init data so client will start rendering world + client.write(0x01, { + entityId: 0, + levelType: 'default', + gameMode: 0, + dimension: 0, + difficulty: 2, + maxPlayers: server.maxPlayers + }); + client.write(0x0d, { + x: 0, + y: 1.62, + stance: 0, + z: 0, + yaw: 0, + pitch: 0, + onGround: true + }); + + client.write(0x03, { message: 'Hello, world!' }); +}); + +server.on('error', function(error) { + console.log('Error:', error); +}); + +server.on('listening', function() { + console.log('Server listening on port', server.socket.address().port); +}); diff --git a/index.js b/index.js index b2989e3..e9a0dc0 100644 --- a/index.js +++ b/index.js @@ -1,19 +1,97 @@ -var net = require('net') - , EventEmitter = require('events').EventEmitter +var EventEmitter = require('events').EventEmitter , util = require('util') , assert = require('assert') - , Iconv = require('iconv').Iconv , ursa = require('ursa') , crypto = require('crypto') , superagent = require('superagent') , Batch = require('batch') - , packets = require('./packets') - , toUcs2 = new Iconv('UTF-8', 'utf16be') - , fromUcs2 = new Iconv('utf16be', 'UTF-8') - -require('buffer-more-ints'); + , protocol = require('./lib/protocol') + , Client = require('./lib/client') + , Server = require('./lib/server') exports.createClient = createClient; +exports.createServer = createServer; + +function createServer(options) { + var port = options.port != null ? + options.port : + options['server-port'] != null ? + options['server-port'] : + 25565 ; + var host = options.host || '0.0.0.0'; + var timeout = options.timeout || 10 * 1000; + var keepAliveInterval = options.keepAliveInterval || 4 * 1000; + var motd = options.motd || "A Minecraft server"; + var onlineMode = options['online-mode'] == null ? true : options['online-mode']; + assert.ok(! onlineMode, "online mode for servers is not yet supported"); + + var server = new Server(options); + server.on("connection", function(client) { + client.once(0xfe, onPing); + client.on(0x02, onHandshake); + client.on(0x00, onKeepAlive); + client.on('end', onEnd); + + var keepAlive = false; + var loggedIn = false; + var lastKeepAlive = null; + + var keepAliveTimer = setInterval(keepAliveLoop, keepAliveInterval); + + function keepAliveLoop() { + if (keepAlive) { + // check if the last keepAlive was too long ago (timeout) + if (lastKeepAlive) { + var elapsed = new Date() - lastKeepAlive; + if (elapsed > timeout) { + client.end(); + return; + } + } + client.write(0x00, { + keepAliveId: Math.floor(Math.random() * 2147483648) + }); + } + } + + function onEnd() { + clearInterval(keepAliveTimer); + } + + function onKeepAlive(packet) { + if (keepAlive) { + lastKeepAlive = new Date(); + } else { + lastKeepAlive = null; + } + } + + function onPing(packet) { + if (loggedIn) return; + client.write(0xff, { + reason: [ + '§1', + protocol.version, + protocol.minecraftVersion, + motd, + server.playerCount, + server.maxPlayers, + ].join('\u0000') + }); + } + + function onHandshake(packet) { + assert.ok(! onlineMode); + loggedIn = true; + keepAlive = true; + client.username = packet.username; + server.emit('login', client); + } + }); + server.listen(port, host); + return server; + +} function createClient(options) { // defaults @@ -23,16 +101,21 @@ function createClient(options) { assert.ok(options.username, "username is required"); var haveCredentials = options.email && options.password; - var client = new Client(); + var client = new Client({ + isServer: false + }); client.username = options.username; client.on('connect', function() { client.write(0x02, { - protocolVersion: packets.meta.protocolVersion, + protocolVersion: protocol.version, username: options.username, serverHost: host, serverPort: port, }); }); + client.on('packet', function(packet) { + console.log(packet.id, packet); + }); client.on(0x00, onKeepAlive); client.once(0xFC, onEncryptionKeyResponse); client.once(0xFD, onEncryptionKeyRequest); @@ -136,617 +219,7 @@ function createClient(options) { } } -function Client(options) { - EventEmitter.call(this); - this.socket = null; - this.encryptionEnabled = false; - this.cipher = null; - this.decipher = null; -} -util.inherits(Client, EventEmitter); - -Client.prototype.connect = function(port, host) { - var self = this; - self.socket = net.connect(port, host, function() { - self.emit('connect'); - }); - var incomingBuffer = new Buffer(0); - self.socket.on('data', function(data) { - if (self.encryptionEnabled) data = new Buffer(self.decipher.update(data), 'binary'); - incomingBuffer = Buffer.concat([incomingBuffer, data]); - var parsed, packet; - while (true) { - parsed = parsePacket(incomingBuffer); - if (! parsed) break; - packet = parsed.results; - hax(packet); // fuck you, notch - incomingBuffer = incomingBuffer.slice(parsed.size); - self.emit(packet.id, packet); - } - }); - - self.socket.on('error', function(err) { - self.emit('error', err); - }); - - self.socket.on('close', function() { - self.emit('end'); - }); -}; - -Client.prototype.end = function() { - this.socket.end(); -}; - -Client.prototype.write = function(packetId, params) { - var buffer = createPacketBuffer(packetId, params); - var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer; - this.socket.write(out); -}; - -var writers = { - 'int': IntWriter, - 'short': ShortWriter, - 'byte': ByteWriter, - 'ubyte': UByteWriter, - 'string': StringWriter, - 'byteArray16': ByteArray16Writer, - 'bool': BoolWriter, - 'double': DoubleWriter, - 'float': FloatWriter, - 'slot': SlotWriter, -}; - -var readers = { - 'string': readString, - 'ascii': readAscii, - 'byteArray16': readByteArray16, - 'byteArray32': readByteArray32, - 'short': readShort, - 'ushort': readUShort, - 'int': readInt, - 'byte': readByte, - 'ubyte': readUByte, - 'long': readLong, - 'slot': readSlot, - 'bool': readBool, - 'double': readDouble, - 'float': readFloat, - 'slotArray': readSlotArray, - 'mapChunkBulk': readMapChunkBulk, - 'entityMetadata': readEntityMetadata, - 'objectData': readObjectData, - 'intArray': readIntArray, - 'intVector': readIntVector, - 'byteVector': readByteVector, - 'byteVectorArray': readByteVectorArray, -}; - -function readIntArray(buffer, offset) { - var results = readByte(buffer, offset); - if (! results) return null; - var count = results.value; - var cursor = offset + results.size; - - var cursorEnd = cursor + 4 * count; - if (cursorEnd > buffer.length) return null; - var array = []; - for (var i = 0; i < count; ++i) { - array.push(buffer.readInt32BE(cursor)); - cursor += 4; - } - - return { - value: array, - size: cursorEnd - offset, - }; -} - -var entityMetadataReaders = { - 0: readByte, - 1: readShort, - 2: readInt, - 3: readFloat, - 4: readString, - 5: readSlot, - 6: readIntVector, -}; - -function readByteVectorArray(buffer, offset) { - var results = readInt(buffer, offset); - if (! results) return null; - var count = results.value; - var cursor = offset + results.size; - var cursorEnd = cursor + 3 * count; - if (cursorEnd > buffer.length) return null; - - var array = []; - for (var i = 0; i < count; ++i) { - array.push({ - x: buffer.readInt8(cursor), - y: buffer.readInt8(cursor + 1), - z: buffer.readInt8(cursor + 2), - }); - cursor += 3; - } - return { - value: array, - size: cursorEnd - offset, - }; -} - -function readByteVector(buffer, offset) { - if (offset + 3 > buffer.length) return null; - return { - value: { - x: buffer.readInt8(offset), - y: buffer.readInt8(offset + 1), - z: buffer.readInt8(offset + 2), - }, - size: 3, - }; -} - -function readIntVector(buffer, offset) { - if (offset + 12 > buffer.length) return null; - return { - value: { - x: buffer.readInt32BE(offset), - y: buffer.readInt32BE(offset + 4), - z: buffer.readInt32BE(offset + 8), - }, - size: 12, - }; -} - -function readEntityMetadata(buffer, offset) { - var cursor = offset; - var metadata = {}; - var item, key, type, results, reader; - while (true) { - if (cursor + 1 > buffer.length) return null; - item = buffer.readUInt8(cursor); - cursor += 1; - if (item === 0x7f) break; - key = item & 0x1f; - type = item >> 5; - reader = entityMetadataReaders[type]; - assert.ok(reader, "missing reader for entity metadata type " + type); - results = reader(buffer, cursor); - if (! results) return null; - metadata[key] = results.value; - cursor += results.size; - } - return { - value: metadata, - size: cursor - offset, - }; -} - -function readObjectData(buffer, offset) { - var cursor = offset + 4; - if (cursor > buffer.length) return null; - var intField = buffer.readInt32BE(offset); - - if (intField === 0) { - return { - value: { - intField: intField, - }, - size: cursor - offset, - }; - } - - if (cursor + 6 > buffer.length) return null; - var velocityX = buffer.readInt16BE(cursor); - cursor += 2; - var velocityY = buffer.readInt16BE(cursor); - cursor += 2; - var velocityZ = buffer.readInt16BE(cursor); - cursor += 2; - - return { - value: { - intField: intField, - velocityX: velocityX, - velocityY: velocityY, - velocityZ: velocityZ, - }, - size: cursor - offset, - }; -} - -function readMapChunkBulk (buffer, offset) { - var cursor = offset + 7; - if (cursor > buffer.length) return null; - var chunkCount = buffer.readInt16BE(offset); - var dataSize = buffer.readInt32BE(offset + 2); - var skyLightSent = !!buffer.readInt8(offset + 6); - - var cursorEnd = cursor + dataSize + 12 * chunkCount; - if (cursorEnd > buffer.length) return null; - - var compressedChunkDataEnd = cursor + dataSize; - var compressedChunkData = buffer.slice(cursor, compressedChunkDataEnd); - cursor = compressedChunkDataEnd; - - var meta = []; - var i, chunkX, chunkZ, primaryBitMap, addBitMap; - for (i = 0; i < chunkCount; ++i) { - chunkX = buffer.readInt32BE(cursor); - cursor += 4; - chunkZ = buffer.readInt32BE(cursor); - cursor += 4; - primaryBitMap = buffer.readUInt16BE(cursor); - cursor += 2; - addBitMap = buffer.readUInt16BE(cursor); - cursor += 2; - - meta.push({ - chunkX: chunkX, - chunkZ: chunkZ, - primaryBitMap: primaryBitMap, - addBitMap: addBitMap, - }); - } - - return { - value: { - skyLightSent: skyLightSent, - compressedChunkData: compressedChunkData, - meta: meta, - }, - size: cursorEnd - offset, - }; -} - -function readAscii (buffer, offset) { - var results = readShort(buffer, offset); - if (! results) return null; - - var strBegin = offset + results.size; - var strLen = results.value; - var strEnd = strBegin + strLen; - if (strEnd > buffer.length) return null; - var str = buffer.slice(strBegin, strEnd).toString('ascii'); - - return { - value: str, - size: strEnd - offset, - }; -} - -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('utf8'); - - return { - value: str, - size: strEnd - offset, - }; -} - -function readByteArray16 (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 readByteArray32(buffer, offset) { - var results = readInt(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 readSlotArray (buffer, offset) { - var results = readShort(buffer, offset); - if (! results) return null; - var count = results.value; - var cursor = offset + results.size; - - var slotArray = []; - for (var i = 0; i < count; ++i) { - results = readSlot(buffer, cursor); - if (! results) return null; - slotArray.push(results.value); - cursor += results.size; - } - - return { - value: slotArray, - size: cursor - offset, - }; -} - -function readShort(buffer, offset) { - if (offset + 2 > buffer.length) return null; - var value = buffer.readInt16BE(offset); - return { - value: value, - size: 2, - }; -} - -function readUShort(buffer, offset) { - if (offset + 2 > buffer.length) return null; - var value = buffer.readUInt16BE(offset); - return { - value: value, - size: 2, - }; -} - -function readInt(buffer, offset) { - if (offset + 4 > buffer.length) return null; - var value = buffer.readInt32BE(offset); - return { - value: value, - size: 4, - }; -} - -function readFloat(buffer, offset) { - if (offset + 4 > buffer.length) return null; - var value = buffer.readFloatBE(offset); - return { - value: value, - size: 4, - }; -} - -function readDouble(buffer, offset) { - if (offset + 8 > buffer.length) return null; - var value = buffer.readDoubleBE(offset); - return { - value: value, - size: 8, - }; -} - -function readLong(buffer, offset) { - if (offset + 8 > buffer.length) return null; - var value = buffer.readInt64BE(offset); - return { - value: value, - size: 8, - }; -} - -function readByte(buffer, offset) { - if (offset + 1 > buffer.length) return null; - var value = buffer.readInt8(offset); - return { - value: value, - size: 1, - }; -} - -function readUByte(buffer, offset) { - if (offset + 1 > buffer.length) return null; - var value = buffer.readUInt8(offset); - return { - value: value, - size: 1, - }; -} - -function readBool(buffer, offset) { - if (offset + 1 > buffer.length) return null; - var value = buffer.readInt8(offset); - return { - value: !!value, - size: 1, - }; -} - -function readSlot(buffer, offset) { - var results = readShort(buffer, offset); - if (! results) return null; - var blockId = results.value; - var cursor = offset + results.size; - - if (blockId === -1) { - return { - value: { id: blockId }, - size: cursor - offset, - }; - } - - var cursorEnd = cursor + 5; - if (cursorEnd > buffer.length) return null; - var itemCount = buffer.readInt8(cursor); - var itemDamage = buffer.readInt16BE(cursor + 1); - var nbtDataSize = buffer.readInt16BE(cursor + 3); - if (nbtDataSize === -1) nbtDataSize = 0; - var nbtDataEnd = cursorEnd + nbtDataSize; - var nbtData = buffer.slice(cursorEnd, nbtDataEnd); - - return { - value: { - id: blockId, - itemCount: itemCount, - itemDamage: itemDamage, - nbtData: nbtData, - }, - size: nbtDataEnd - offset, - }; -} - -function SlotWriter(value) { - this.value = value; - this.size = value.id === -1 ? 2 : 7 + this.value.nbtData.length; -} - -SlotWriter.prototype.write = function(buffer, offset) { - buffer.writeInt16BE(this.value.id, offset); - if (this.value.id === -1) return; - buffer.writeInt8(this.value.itemCount, offset + 2); - buffer.writeInt16BE(this.value.itemDamage, offset + 3); - var nbtDataSize = this.value.nbtData.length; - if (nbtDataSize === 0) nbtDataSize = -1; // I don't know wtf mojang smokes - buffer.writeInt16BE(nbtDataSize, offset + 5); - this.value.nbtData.copy(buffer, offset + 7); -}; - -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 ByteArray16Writer(value) { - assert.ok(Buffer.isBuffer(value), "non buffer passed to ByteArray16Writer"); - this.value = value; - this.size = 2 + value.length; -} - -ByteArray16Writer.prototype.write = function(buffer, offset) { - buffer.writeInt16BE(this.value.length, offset); - this.value.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 BoolWriter(value) { - this.value = value; - this.size = 1; -} - -BoolWriter.prototype.write = function(buffer, offset) { - buffer.writeInt8(this.value ? 1 : 0, offset); -} - -function UByteWriter(value) { - this.value = value; - this.size = 1; -} - -UByteWriter.prototype.write = function(buffer, offset) { - buffer.writeUInt8(this.value, offset); -}; - -function FloatWriter(value) { - this.value = value; - this.size = 4; -} - -FloatWriter.prototype.write = function(buffer, offset) { - buffer.writeFloatBE(this.value, offset); -} - -function DoubleWriter(value) { - this.value = value; - this.size = 8; -} - -DoubleWriter.prototype.write = function(buffer, offset) { - buffer.writeDoubleBE(this.value, offset); -} - -function ShortWriter(value) { - this.value = value; - this.size = 2; -} - -ShortWriter.prototype.write = function(buffer, offset) { - buffer.writeInt16BE(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 UByteWriter(packetId) ]; - var packet = packets[packetId]; - packet.forEach(function(fieldInfo) { - var value = params[fieldInfo.name]; - var Writer = writers[fieldInfo.type]; - assert.ok(Writer, "missing writer for data type: " + fieldInfo.type); - var field = new Writer(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]; - assert.ok(read, "missing reader for data type: " + 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, - }; -} function mcPubKeyToURsa(mcPubKeyBuffer) { var pem = "-----BEGIN PUBLIC KEY-----\n"; @@ -793,7 +266,7 @@ function getLoginSession(email, password, cb) { req.send({ user: email, password: password, - version: packets.meta.sessionVersion, + version: protocol.sessionVersion, }); req.end(function(err, resp) { var myErr; @@ -825,14 +298,3 @@ function getLoginSession(email, password, cb) { } }); } - -function hax(packet) { - // bullshit post-parsing hax that we have to do due to mojang incompetence - var tmp; - if (packet.id === 0x0d) { - // when 0x0d is sent from the server, the Y and the Stance fields are swapped. - tmp = packet.y; - packet.y = packet.stance; - packet.stance = tmp; - } -} diff --git a/lib/client.js b/lib/client.js new file mode 100644 index 0000000..9df9660 --- /dev/null +++ b/lib/client.js @@ -0,0 +1,62 @@ +var net = require('net') + , EventEmitter = require('events').EventEmitter + , util = require('util') + , protocol = require('./protocol') + , createPacketBuffer = protocol.createPacketBuffer + , parsePacket = protocol.parsePacket + +module.exports = Client; + +function Client(options) { + EventEmitter.call(this); + + this.isServer = options.isServer; + this.socket = null; + this.encryptionEnabled = false; + this.cipher = null; + this.decipher = null; +} +util.inherits(Client, EventEmitter); + +Client.prototype.setSocket = function(socket) { + var self = this; + self.socket = socket; + var incomingBuffer = new Buffer(0); + self.socket.on('data', function(data) { + if (self.encryptionEnabled) data = new Buffer(self.decipher.update(data), 'binary'); + incomingBuffer = Buffer.concat([incomingBuffer, data]); + var parsed, packet; + while (true) { + parsed = parsePacket(incomingBuffer, self.isServer); + if (! parsed) break; + packet = parsed.results; + incomingBuffer = incomingBuffer.slice(parsed.size); + self.emit(packet.id, packet); + } + }); + + self.socket.on('error', function(err) { + self.emit('error', err); + }); + + self.socket.on('close', function() { + self.emit('end'); + }); +}; + +Client.prototype.connect = function(port, host) { + var self = this; + self.setSocket(net.connect(port, host, function() { + self.emit('connect'); + })); +}; + +Client.prototype.end = function() { + this.socket.end(); +}; + +Client.prototype.write = function(packetId, params) { + var buffer = createPacketBuffer(packetId, params, this.isServer); + var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer; + this.socket.write(out); +}; diff --git a/lib/protocol.js b/lib/protocol.js new file mode 100644 index 0000000..370e55b --- /dev/null +++ b/lib/protocol.js @@ -0,0 +1,1020 @@ +var Iconv = require('iconv').Iconv + , assert = require('assert') + , toUcs2 = new Iconv('UTF-8', 'utf16be') + , fromUcs2 = new Iconv('utf16be', 'UTF-8') + +var STRING_MAX_LENGTH = 240; + +exports.version = 51; +exports.minecraftVersion = '1.4.6'; +exports.sessionVersion = 13; +exports.parsePacket = parsePacket; +exports.createPacketBuffer = createPacketBuffer; + +var packets = { + 0x00: [ + { name: "keepAliveId", type: "int" } + ], + 0x01: [ + { name: "entityId", type: "int" }, + { name: "levelType", type: "string" }, + { name: "gameMode", type: "byte" }, + { name: "dimension", type: "byte" }, + { name: "difficulty", type: "byte" }, + { name: null, type: "byte" }, + { name: "maxPlayers", type: "byte" } + ], + 0x02: [ + { name: "protocolVersion", type: "byte" }, + { name: "username", type: "string" }, + { name: "serverHost", type: "string" }, + { name: "serverPort", type: "int" } + ], + 0x03: [ + { name: "message", type: "string" } + ], + 0x04: [ + { name: "age", type: "long" }, + { name: "time", type: "long" } + ], + 0x05: [ + { name: "entityId", type: "int" }, + { name: "slot", type: "short" }, + { name: "item", type: "slot" } + ], + 0x06: [ + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" } + ], + 0x07: [ + { name: "user", type: "int" }, + { name: "target", type: "int" }, + { name: "leftClick", type: "bool" } + ], + 0x08: [ + { name: "health", type: "short" }, + { name: "food", type: "short" }, + { name: "foodSaturation", type: "float" } + ], + 0x09: [ + { name: "dimension", type: "int" }, + { name: "difficulty", type: "byte" }, + { name: "gameMode", type: "byte" }, + { name: "worldHeight", type: "short" }, + { name: "levelType", type: "string" } + ], + 0x0a: [ + { name: "onGround", type: "bool" } + ], + 0x0b: [ + { name: "x", type: "double" }, + { name: "y", type: "double" }, + { name: "stance", type: "double" }, + { name: "z", type: "double" }, + { name: "onGround", type: "bool" } + ], + 0x0c: [ + { name: "yaw", type: "float" }, + { name: "pitch", type: "float" }, + { name: "onGround", type: "bool" } + ], + 0x0d: { + toServer: [ + { name: "x", type: "double" }, + { name: "y", type: "double" }, + { name: "stance", type: "double" }, + { name: "z", type: "double" }, + { name: "yaw", type: "float" }, + { name: "pitch", type: "float" }, + { name: "onGround", type: "bool" } + ], + toClient: [ + { name: "x", type: "double" }, + { name: "stance", type: "double" }, + { name: "y", type: "double" }, + { name: "z", type: "double" }, + { name: "yaw", type: "float" }, + { name: "pitch", type: "float" }, + { name: "onGround", type: "bool" } + ], + }, + 0x0e: [ + { name: "status", type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "byte" }, + { name: "z", type: "int" }, + { name: "face", type: "byte" } + ], + 0x0f: [ + { name: "x", type: "int" }, + { name: "y", type: "ubyte" }, + { name: "z", type: "int" }, + { name: "direction", type: "byte" }, + { name: "heldItem", type: "slot" }, + { name: "cursorX", type: "byte" }, + { name: "cursorY", type: "byte" }, + { name: "cursorZ", type: "byte" } + ], + 0x10: [ + { name: "slotId", type: "short" } + ], + 0x11: [ + { name: "entityId", type: "int" }, + { name: null, type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "byte" }, + { name: "z", type: "int" } + ], + 0x12: [ + { name: "entityId", type: "int" }, + { name: "animation", type: "byte" } + ], + 0x13: [ + { name: "entityId", type: "int" }, + { name: "actionId", type: "byte" } + ], + 0x14: [ + { name: "entityId", type: "int" }, + { name: "name", type: "string" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" }, + { name: "currentItem", type: "short" }, + { name: "metadata", type: "entityMetadata" } + ], + 0x16: [ + { name: "collectedId", type: "int" }, + { name: "collectorId", type: "int" } + ], + 0x17: [ + { name: "entityId", type: "int" }, + { name: "type", type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" }, + { name: "objectData", type: "objectData" } + ], + 0x18: [ + { name: "entityId", type: "int" }, + { name: "type", type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" }, + { name: "headYaw", type: "byte" }, + { name: "velocityX", type: "short" }, + { name: "velocityY", type: "short" }, + { name: "velocityZ", type: "short" }, + { name: "metadata", type: "entityMetadata" } + ], + 0x19: [ + { name: "entityId", type: "int" }, + { name: "name", type: "string" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "direction", type: "int" } + ], + 0x1a: [ + { name: "entityId", type: "int" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "count", type: "short" } + ], + 0x1c: [ + { name: "entityId", type: "int" }, + { name: "velocityX", type: "short" }, + { name: "velocityY", type: "short" }, + { name: "velocityZ", type: "short" } + ], + 0x1d: [ + { name: "entityIds", type: "intArray8" } + ], + 0x1e: [ + { name: "entityId", type: "int" } + ], + 0x1f: [ + { name: "entityId", type: "int" }, + { name: "dx", type: "byte" }, + { name: "dy", type: "byte" }, + { name: "dz", type: "byte" } + ], + 0x20: [ + { name: "entityId", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" } + ], + 0x21: [ + { name: "entityId", type: "int" }, + { name: "dx", type: "byte" }, + { name: "dy", type: "byte" }, + { name: "dz", type: "byte" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" } + ], + 0x22: [ + { name: "entityId", type: "int" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" } + ], + 0x23: [ + { name: "entityId", type: "int" }, + { name: "headYaw", type: "byte" } + ], + 0x26: [ + { name: "entityId", type: "int" }, + { name: "status", type: "byte" } + ], + 0x27: [ + { name: "entityId", type: "int" }, + { name: "vehicleId", type: "int" } + ], + 0x28: [ + { name: "entityId", type: "int" }, + { name: "metadata", type: "entityMetadata" } + ], + 0x29: [ + { name: "entityId", type: "int" }, + { name: "effectId", type: "byte" }, + { name: "amplifier", type: "byte" }, + { name: "duration", type: "short" } + ], + 0x2a: [ + { name: "entityId", type: "int" }, + { name: "effectId", type: "byte" } + ], + 0x2b: [ + { name: "experienceBar", type: "float" }, + { name: "level", type: "short" }, + { name: "totalExperience", type: "short" } + ], + 0x33: [ + { name: "x", type: "int" }, + { name: "z", type: "int" }, + { name: "groundUp", type: "bool" }, + { name: "bitMap", type: "ushort" }, + { name: "addBitMap", type: "ushort" }, + { name: "compressedData", type: "byteArray32" } + ], + 0x34: [ + { name: "chunkX", type: "int" }, + { name: "chunkZ", type: "int" }, + { name: "recordCount", type: "short" }, + { name: "data", type: "byteArray32" } + ], + 0x35: [ + { name: "x", type: "int" }, + { name: "y", type: "byte" }, + { name: "z", type: "int" }, + { name: "type", type: "short" }, + { name: "metadata", type: "byte" } + ], + 0x36: [ + { name: "x", type: "int" }, + { name: "y", type: "short" }, + { name: "z", type: "int" }, + { name: "byte1", type: "byte" }, + { name: "byte2", type: "byte" }, + { name: "blockId", type: "short" } + ], + 0x37: [ + { name: "entityId", type: "int" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "destroyStage", type: "byte" } + ], + 0x38: [ + { name: "data", type: "mapChunkBulk" } + ], + 0x3c: [ + { name: "x", type: "double" }, + { name: "y", type: "double" }, + { name: "z", type: "double" }, + { name: "radius", type: "float" }, + { name: "affectedBlockOffsets", type: "byteVectorArray" }, + { name: "playerMotionX", type: "float" }, + { name: "playerMotionY", type: "float" }, + { name: "playerMotionZ", type: "float" } + ], + 0x3d: [ + { name: "effectId", type: "int" }, + { name: "x", type: "int" }, + { name: "y", type: "byte" }, + { name: "z", type: "int" }, + { name: "data", type: "int" }, + { name: "global", type: "bool" } + ], + 0x3e: [ + { name: "soundName", type: "string" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "volume", type: "float" }, + { name: "pitch", type: "byte" } + ], + 0x46: [ + { name: "reason", type: "byte" }, + { name: "gameMode", type: "byte" } + ], + 0x47: [ + { name: "entityId", type: "int" }, + { name: "type", type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" } + ], + 0x64: [ + { name: "windowId", type: "byte" }, + { name: "inventoryType", type: "byte" }, + { name: "windowTitle", type: "string" }, + { name: "slotCount", type: "byte" } + ], + 0x65: [ + { name: "windowId", type: "byte" } + ], + 0x66: [ + { name: "windowId", type: "byte" }, + { name: "slot", type: "short" }, + { name: "mouseButton", type: "byte" }, + { name: "action", type: "short" }, + { name: "shift", type: "bool" }, + { name: "item", type: "slot" } + ], + 0x67: [ + { name: "windowId", type: "byte" }, + { name: "slot", type: "short" }, + { name: "item", type: "slot" } + ], + 0x68: [ + { name: "windowId", type: "byte" }, + { name: "items", type: "slotArray" } + ], + 0x69: [ + { name: "windowId", type: "byte" }, + { name: "property", type: "short" }, + { name: "value", type: "short" } + ], + 0x6a: [ + { name: "windowId", type: "byte" }, + { name: "action", type: "short" }, + { name: "accepted", type: "bool" } + ], + 0x6b: [ + { name: "slot", type: "short" }, + { name: "item", type: "slot" } + ], + 0x6c: [ + { name: "windowId", type: "byte" }, + { name: "enchantment", type: "byte" } + ], + 0x82: [ + { name: "x", type: "int" }, + { name: "y", type: "short" }, + { name: "z", type: "int" }, + { name: "text1", type: "string" }, + { name: "text2", type: "string" }, + { name: "text3", type: "string" }, + { name: "text4", type: "string" } + ], + 0x83: [ + { name: "type", type: "short" }, + { name: "itemId", type: "short" }, + { name: "text", type: "ascii" } + ], + 0x84: [ + { name: "x", type: "int" }, + { name: "y", type: "short" }, + { name: "z", type: "int" }, + { name: "action", type: "byte" }, + { name: "nbtData", type: "byteArray16" } + ], + 0xc8: [ + { name: "statisticId", type: "int" }, + { name: "amount", type: "byte" } + ], + 0xc9: [ + { name: "playerName", type: "string" }, + { name: "online", type: "bool" }, + { name: "ping", type: "short" } + ], + 0xca: [ + { name: "flags", type: "byte" }, + { name: "flyingSpeed", type: "byte" }, + { name: "walkingSpeed", type: "byte" } + ], + 0xcb: [ + { name: "text", type: "string" } + ], + 0xcc: [ + { name: "locale", type: "string" }, + { name: "viewDistance", type: "byte" }, + { name: "chatFlags", type: "byte" }, + { name: "difficulty", type: "byte" }, + { name: "showCape", type: "bool" } + ], + 0xcd: [ + { name: "payload", type: "byte" } + ], + 0xfa: [ + { name: "channel", type: "string" }, + { name: "data", type: "byteArray16" } + ], + 0xfc: [ + { name: "sharedSecret", type: "byteArray16" }, + { name: "verifyToken", type: "byteArray16" } + ], + 0xfd: [ + { name: "serverId", type: "string" }, + { name: "publicKey", type: "byteArray16" }, + { name: "verifyToken", type: "byteArray16" } + ], + 0xfe: [ + { name: "magic", type: "byte" } + ], + 0xff: [ + { name: "reason", type: "string" } + ] +}; + +var writers = { + 'int': IntWriter, + 'short': ShortWriter, + 'byte': ByteWriter, + 'ubyte': UByteWriter, + 'string': StringWriter, + 'byteArray16': ByteArray16Writer, + 'bool': BoolWriter, + 'double': DoubleWriter, + 'float': FloatWriter, + 'slot': SlotWriter, +}; + +var readers = { + 'string': readString, + 'ascii': readAscii, + 'byteArray16': readByteArray16, + 'byteArray32': readByteArray32, + 'short': readShort, + 'ushort': readUShort, + 'int': readInt, + 'byte': readByte, + 'ubyte': readUByte, + 'long': readLong, + 'slot': readSlot, + 'bool': readBool, + 'double': readDouble, + 'float': readFloat, + 'slotArray': readSlotArray, + 'mapChunkBulk': readMapChunkBulk, + 'entityMetadata': readEntityMetadata, + 'objectData': readObjectData, + 'intArray8': readIntArray8, + 'intVector': readIntVector, + 'byteVector': readByteVector, + 'byteVectorArray': readByteVectorArray, +}; + +var entityMetadataReaders = { + 0: readByte, + 1: readShort, + 2: readInt, + 3: readFloat, + 4: readString, + 5: readSlot, + 6: readIntVector, +}; + +function readIntArray8(buffer, offset) { + var results = readByte(buffer, offset); + if (! results) return null; + var count = results.value; + var cursor = offset + results.size; + + var cursorEnd = cursor + 4 * count; + if (cursorEnd > buffer.length) return null; + var array = []; + for (var i = 0; i < count; ++i) { + array.push(buffer.readInt32BE(cursor)); + cursor += 4; + } + + return { + value: array, + size: cursorEnd - offset, + }; +} + + +function readByteVectorArray(buffer, offset) { + var results = readInt(buffer, offset); + if (! results) return null; + var count = results.value; + var cursor = offset + results.size; + var cursorEnd = cursor + 3 * count; + if (cursorEnd > buffer.length) return null; + + var array = []; + for (var i = 0; i < count; ++i) { + array.push({ + x: buffer.readInt8(cursor), + y: buffer.readInt8(cursor + 1), + z: buffer.readInt8(cursor + 2), + }); + cursor += 3; + } + return { + value: array, + size: cursorEnd - offset, + }; +} + +function readByteVector(buffer, offset) { + if (offset + 3 > buffer.length) return null; + return { + value: { + x: buffer.readInt8(offset), + y: buffer.readInt8(offset + 1), + z: buffer.readInt8(offset + 2), + }, + size: 3, + }; +} + +function readIntVector(buffer, offset) { + if (offset + 12 > buffer.length) return null; + return { + value: { + x: buffer.readInt32BE(offset), + y: buffer.readInt32BE(offset + 4), + z: buffer.readInt32BE(offset + 8), + }, + size: 12, + }; +} + +function readEntityMetadata(buffer, offset) { + var cursor = offset; + var metadata = {}; + var item, key, type, results, reader; + while (true) { + if (cursor + 1 > buffer.length) return null; + item = buffer.readUInt8(cursor); + cursor += 1; + if (item === 0x7f) break; + key = item & 0x1f; + type = item >> 5; + reader = entityMetadataReaders[type]; + assert.ok(reader, "missing reader for entity metadata type " + type); + results = reader(buffer, cursor); + if (! results) return null; + metadata[key] = results.value; + cursor += results.size; + } + return { + value: metadata, + size: cursor - offset, + }; +} + +function readObjectData(buffer, offset) { + var cursor = offset + 4; + if (cursor > buffer.length) return null; + var intField = buffer.readInt32BE(offset); + + if (intField === 0) { + return { + value: { + intField: intField, + }, + size: cursor - offset, + }; + } + + if (cursor + 6 > buffer.length) return null; + var velocityX = buffer.readInt16BE(cursor); + cursor += 2; + var velocityY = buffer.readInt16BE(cursor); + cursor += 2; + var velocityZ = buffer.readInt16BE(cursor); + cursor += 2; + + return { + value: { + intField: intField, + velocityX: velocityX, + velocityY: velocityY, + velocityZ: velocityZ, + }, + size: cursor - offset, + }; +} + +function readMapChunkBulk (buffer, offset) { + var cursor = offset + 7; + if (cursor > buffer.length) return null; + var chunkCount = buffer.readInt16BE(offset); + var dataSize = buffer.readInt32BE(offset + 2); + var skyLightSent = !!buffer.readInt8(offset + 6); + + var cursorEnd = cursor + dataSize + 12 * chunkCount; + if (cursorEnd > buffer.length) return null; + + var compressedChunkDataEnd = cursor + dataSize; + var compressedChunkData = buffer.slice(cursor, compressedChunkDataEnd); + cursor = compressedChunkDataEnd; + + var meta = []; + var i, chunkX, chunkZ, primaryBitMap, addBitMap; + for (i = 0; i < chunkCount; ++i) { + chunkX = buffer.readInt32BE(cursor); + cursor += 4; + chunkZ = buffer.readInt32BE(cursor); + cursor += 4; + primaryBitMap = buffer.readUInt16BE(cursor); + cursor += 2; + addBitMap = buffer.readUInt16BE(cursor); + cursor += 2; + + meta.push({ + chunkX: chunkX, + chunkZ: chunkZ, + primaryBitMap: primaryBitMap, + addBitMap: addBitMap, + }); + } + + return { + value: { + skyLightSent: skyLightSent, + compressedChunkData: compressedChunkData, + meta: meta, + }, + size: cursorEnd - offset, + }; +} + +function readAscii (buffer, offset) { + var results = readShort(buffer, offset); + if (! results) return null; + + var strBegin = offset + results.size; + var strLen = results.value; + var strEnd = strBegin + strLen; + if (strEnd > buffer.length) return null; + var str = buffer.slice(strBegin, strEnd).toString('ascii'); + + return { + value: str, + size: strEnd - offset, + }; +} + +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('utf8'); + + return { + value: str, + size: strEnd - offset, + }; +} + +function readByteArray16 (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 readByteArray32(buffer, offset) { + var results = readInt(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 readSlotArray (buffer, offset) { + var results = readShort(buffer, offset); + if (! results) return null; + var count = results.value; + var cursor = offset + results.size; + + var slotArray = []; + for (var i = 0; i < count; ++i) { + results = readSlot(buffer, cursor); + if (! results) return null; + slotArray.push(results.value); + cursor += results.size; + } + + return { + value: slotArray, + size: cursor - offset, + }; +} + +function readShort(buffer, offset) { + if (offset + 2 > buffer.length) return null; + var value = buffer.readInt16BE(offset); + return { + value: value, + size: 2, + }; +} + +function readUShort(buffer, offset) { + if (offset + 2 > buffer.length) return null; + var value = buffer.readUInt16BE(offset); + return { + value: value, + size: 2, + }; +} + +function readInt(buffer, offset) { + if (offset + 4 > buffer.length) return null; + var value = buffer.readInt32BE(offset); + return { + value: value, + size: 4, + }; +} + +function readFloat(buffer, offset) { + if (offset + 4 > buffer.length) return null; + var value = buffer.readFloatBE(offset); + return { + value: value, + size: 4, + }; +} + +function readDouble(buffer, offset) { + if (offset + 8 > buffer.length) return null; + var value = buffer.readDoubleBE(offset); + return { + value: value, + size: 8, + }; +} + +function readLong(buffer, offset) { + if (offset + 8 > buffer.length) return null; + return { + value: [buffer.readInt32BE(offset), buffer.readInt32BE(offset + 4)], + size: 8, + }; +} + +function readByte(buffer, offset) { + if (offset + 1 > buffer.length) return null; + var value = buffer.readInt8(offset); + return { + value: value, + size: 1, + }; +} + +function readUByte(buffer, offset) { + if (offset + 1 > buffer.length) return null; + var value = buffer.readUInt8(offset); + return { + value: value, + size: 1, + }; +} + +function readBool(buffer, offset) { + if (offset + 1 > buffer.length) return null; + var value = buffer.readInt8(offset); + return { + value: !!value, + size: 1, + }; +} + +function readSlot(buffer, offset) { + var results = readShort(buffer, offset); + if (! results) return null; + var blockId = results.value; + var cursor = offset + results.size; + + if (blockId === -1) { + return { + value: { id: blockId }, + size: cursor - offset, + }; + } + + var cursorEnd = cursor + 5; + if (cursorEnd > buffer.length) return null; + var itemCount = buffer.readInt8(cursor); + var itemDamage = buffer.readInt16BE(cursor + 1); + var nbtDataSize = buffer.readInt16BE(cursor + 3); + if (nbtDataSize === -1) nbtDataSize = 0; + var nbtDataEnd = cursorEnd + nbtDataSize; + var nbtData = buffer.slice(cursorEnd, nbtDataEnd); + + return { + value: { + id: blockId, + itemCount: itemCount, + itemDamage: itemDamage, + nbtData: nbtData, + }, + size: nbtDataEnd - offset, + }; +} + +function SlotWriter(value) { + this.value = value; + this.size = value.id === -1 ? 2 : 7 + this.value.nbtData.length; +} + +SlotWriter.prototype.write = function(buffer, offset) { + buffer.writeInt16BE(this.value.id, offset); + if (this.value.id === -1) return; + buffer.writeInt8(this.value.itemCount, offset + 2); + buffer.writeInt16BE(this.value.itemDamage, offset + 3); + var nbtDataSize = this.value.nbtData.length; + if (nbtDataSize === 0) nbtDataSize = -1; // I don't know wtf mojang smokes + buffer.writeInt16BE(nbtDataSize, offset + 5); + this.value.nbtData.copy(buffer, offset + 7); +}; + +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 ByteArray16Writer(value) { + assert.ok(Buffer.isBuffer(value), "non buffer passed to ByteArray16Writer"); + this.value = value; + this.size = 2 + value.length; +} + +ByteArray16Writer.prototype.write = function(buffer, offset) { + buffer.writeInt16BE(this.value.length, offset); + this.value.copy(buffer, offset + 2); +}; + +function ByteWriter(value) { + this.value = value == null ? 0 : value; + this.size = 1; +} + +ByteWriter.prototype.write = function(buffer, offset) { + buffer.writeInt8(this.value, offset); +} + +function BoolWriter(value) { + this.value = value; + this.size = 1; +} + +BoolWriter.prototype.write = function(buffer, offset) { + buffer.writeInt8(this.value ? 1 : 0, offset); +} + +function UByteWriter(value) { + this.value = value; + this.size = 1; +} + +UByteWriter.prototype.write = function(buffer, offset) { + buffer.writeUInt8(this.value, offset); +}; + +function FloatWriter(value) { + this.value = value; + this.size = 4; +} + +FloatWriter.prototype.write = function(buffer, offset) { + buffer.writeFloatBE(this.value, offset); +} + +function DoubleWriter(value) { + this.value = value; + this.size = 8; +} + +DoubleWriter.prototype.write = function(buffer, offset) { + buffer.writeDoubleBE(this.value, offset); +} + +function ShortWriter(value) { + this.value = value; + this.size = 2; +} + +ShortWriter.prototype.write = function(buffer, offset) { + buffer.writeInt16BE(this.value, offset); +} + +function IntWriter(value) { + this.value = value; + this.size = 4; +} + +IntWriter.prototype.write = function(buffer, offset) { + buffer.writeInt32BE(this.value, offset); +} + +function get(packetId, toServer) { + var packetInfo = packets[packetId]; + return Array.isArray(packetInfo) ? + packetInfo : + toServer ? + packetInfo.toServer : + packetInfo.toClient; +} + +function createPacketBuffer(packetId, params, isServer) { + var size = 1; + var fields = [ new UByteWriter(packetId) ]; + var packet = get(packetId, isServer); + packet.forEach(function(fieldInfo) { + var value = params[fieldInfo.name]; + var Writer = writers[fieldInfo.type]; + assert.ok(Writer, "missing writer for data type: " + fieldInfo.type); + var field = new Writer(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, isServer) { + if (buffer.length < 1) return null; + var packetId = buffer.readUInt8(0); + var size = 1; + var results = { id: packetId }; + var packetInfo = get(packetId, !isServer); + 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]; + assert.ok(read, "missing reader for data type: " + 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/lib/server.js b/lib/server.js new file mode 100644 index 0000000..afac721 --- /dev/null +++ b/lib/server.js @@ -0,0 +1,48 @@ +var net = require('net') + , EventEmitter = require('events').EventEmitter + , util = require('util') + , assert = require('assert') + , Client = require('./client') + +module.exports = Server; + +function Server(options) { + EventEmitter.call(this); + + this.maxPlayers = options['max-players'] || 20; + this.playerCount = 0 + + this.socket = null; + this.cipher = null; + this.decipher = null; +} +util.inherits(Server, EventEmitter); + +Server.prototype.listen = function(port, host) { + var self = this; + self.socket = net.createServer(); + self.socket.on('connection', function(socket) { + var client = new Client({ + isServer: true, + }); + client.on('error', function(err) { + self.emit('error', err); + }); + client.setSocket(socket); + self.emit('connection', client); + client.on('end', function() { + this.playerCount -= 1; + }); + this.playerCount += 1; + }); + self.socket.on('error', function(err) { + self.emit('error', err); + }); + self.socket.on('close', function() { + self.emit('end'); + }); + self.socket.on('listening', function() { + self.emit('listening'); + }); + self.socket.listen(port, host); +}; diff --git a/package.json b/package.json index c73b4b7..0d8c5ab 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ }, "dependencies": { "ursa": "~0.8.0", - "buffer-more-ints": "~0.0.1", "superagent": "~0.10.0", "iconv": "~1.2.4", "batch": "~0.2.1" diff --git a/packets.js b/packets.js deleted file mode 100644 index e6ebc29..0000000 --- a/packets.js +++ /dev/null @@ -1,1250 +0,0 @@ -module.exports = { - meta: { - protocolVersion: 51, - sessionVersion: 13 - }, - 0x00: [ - { - name: "keepAliveId", - type: "int" - } - ], - 0x01: [ - { - name: "entityId", - type: "int" - }, - { - name: "levelType", - type: "string" - }, - { - name: "gameMode", - type: "byte" - }, - { - name: "dimension", - type: "byte" - }, - { - name: "difficulty", - type: "byte" - }, - { - name: "_notUsed1", - type: "byte" - }, - { - name: "maxPlayers", - type: "byte" - } - ], - 0x02: [ - { - name: "protocolVersion", - type: "byte" - }, - { - name: "username", - type: "string" - }, - { - name: "serverHost", - type: "string" - }, - { - name: "serverPort", - type: "int" - } - ], - 0x03: [ - { - name: "message", - type: "string" - } - ], - 0x04: [ - { - name: "ageOfWorld", - type: "long" - }, - { - name: "timeOfDay", - type: "long" - } - ], - 0x05: [ - { - name: "entityId", - type: "int" - }, - { - name: "slot", - type: "short" - }, - { - name: "item", - type: "slot" - } - ], - 0x06: [ - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - } - ], - 0x07: [ - { - name: "user", - type: "int" - }, - { - name: "target", - type: "int" - }, - { - name: "mouseButton", - type: "bool" - } - ], - 0x08: [ - { - name: "health", - type: "short" - }, - { - name: "food", - type: "short" - }, - { - name: "foodSaturation", - type: "float" - } - ], - 0x09: [ - { - name: "dimension", - type: "int" - }, - { - name: "difficulty", - type: "byte" - }, - { - name: "gameMode", - type: "byte" - }, - { - name: "worldHeight", - type: "short" - }, - { - name: "levelType", - type: "string" - } - ], - 0x0a: [ - { - name: "onGround", - type: "bool" - } - ], - 0x0b: [ - { - name: "x", - type: "double" - }, - { - name: "y", - type: "double" - }, - { - name: "stance", - type: "double" - }, - { - name: "z", - type: "double" - }, - { - name: "onGround", - type: "bool" - } - ], - 0x0c: [ - { - name: "yaw", - type: "float" - }, - { - name: "pitch", - type: "float" - }, - { - name: "onGround", - type: "bool" - } - ], - 0x0d: [ - { - name: "x", - type: "double" - }, - { - name: "y", - type: "double" - }, - { - name: "stance", - type: "double" - }, - { - name: "z", - type: "double" - }, - { - name: "yaw", - type: "float" - }, - { - name: "pitch", - type: "float" - }, - { - name: "onGround", - type: "bool" - } - ], - 0x0e: [ - { - name: "status", - type: "byte" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "byte" - }, - { - name: "z", - type: "int" - }, - { - name: "face", - type: "byte" - } - ], - 0x0f: [ - { - name: "x", - type: "int" - }, - { - name: "y", - type: "ubyte" - }, - { - name: "z", - type: "int" - }, - { - name: "direction", - type: "byte" - }, - { - name: "heldItem", - type: "slot" - }, - { - name: "cursorX", - type: "byte" - }, - { - name: "cursorY", - type: "byte" - }, - { - name: "cursorZ", - type: "byte" - } - ], - 0x10: [ - { - name: "slotId", - type: "short" - } - ], - 0x11: [ - { - name: "entityId", - type: "int" - }, - { - name: "_unknown", - type: "byte" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "byte" - }, - { - name: "z", - type: "int" - } - ], - 0x12: [ - { - name: "entityId", - type: "int" - }, - { - name: "animation", - type: "byte" - } - ], - 0x13: [ - { - name: "entityId", - type: "int" - }, - { - name: "actionId", - type: "byte" - } - ], - 0x14: [ - { - name: "entityId", - type: "int" - }, - { - name: "playerName", - type: "string" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "yaw", - type: "byte" - }, - { - name: "pitch", - type: "byte" - }, - { - name: "currentItem", - type: "short" - }, - { - name: "metadata", - type: "entityMetadata" - } - ], - 0x16: [ - { - name: "collectedEntityId", - type: "int" - }, - { - name: "collectorEntityId", - type: "int" - } - ], - 0x17: [ - { - name: "entityId", - type: "int" - }, - { - name: "type", - type: "byte" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "yaw", - type: "byte" - }, - { - name: "pitch", - type: "byte" - }, - { - name: "objectData", - type: "objectData" - } - ], - 0x18: [ - { - name: "entityId", - type: "int" - }, - { - name: "type", - type: "byte" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "yaw", - type: "byte" - }, - { - name: "pitch", - type: "byte" - }, - { - name: "headYaw", - type: "byte" - }, - { - name: "velocityX", - type: "short" - }, - { - name: "velocityY", - type: "short" - }, - { - name: "velocityZ", - type: "short" - }, - { - name: "metadata", - type: "entityMetadata" - } - ], - 0x19: [ - { - name: "entityId", - type: "int" - }, - { - name: "title", - type: "string" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "direction", - type: "int" - } - ], - 0x1a: [ - { - name: "entityId", - type: "int" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "count", - type: "short" - } - ], - 0x1c: [ - { - name: "entityId", - type: "int" - }, - { - name: "velocityX", - type: "short" - }, - { - name: "velocityY", - type: "short" - }, - { - name: "velocityZ", - type: "short" - } - ], - 0x1d: [ - { - name: "entityIds", - type: "intArray" - } - ], - 0x1e: [ - { - name: "entityId", - type: "int" - } - ], - 0x1f: [ - { - name: "entityId", - type: "int" - }, - { - name: "deltaX", - type: "byte" - }, - { - name: "deltaY", - type: "byte" - }, - { - name: "deltaZ", - type: "byte" - } - ], - 0x20: [ - { - name: "entityId", - type: "int" - }, - { - name: "yaw", - type: "byte" - }, - { - name: "pitch", - type: "byte" - } - ], - 0x21: [ - { - name: "entityId", - type: "int" - }, - { - name: "deltaX", - type: "byte" - }, - { - name: "deltaY", - type: "byte" - }, - { - name: "deltaZ", - type: "byte" - }, - { - name: "yaw", - type: "byte" - }, - { - name: "pitch", - type: "byte" - } - ], - 0x22: [ - { - name: "entityId", - type: "int" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "yaw", - type: "byte" - }, - { - name: "pitch", - type: "byte" - } - ], - 0x23: [ - { - name: "entityId", - type: "int" - }, - { - name: "headYaw", - type: "byte" - } - ], - 0x26: [ - { - name: "entityId", - type: "int" - }, - { - name: "entityStatus", - type: "byte" - } - ], - 0x27: [ - { - name: "entityId", - type: "int" - }, - { - name: "vehicleId", - type: "int" - } - ], - 0x28: [ - { - name: "entityId", - type: "int" - }, - { - name: "metadata", - type: "entityMetadata" - } - ], - 0x29: [ - { - name: "entityId", - type: "int" - }, - { - name: "effectId", - type: "byte" - }, - { - name: "amplifier", - type: "byte" - }, - { - name: "duration", - type: "short" - } - ], - 0x2a: [ - { - name: "entityId", - type: "int" - }, - { - name: "effectId", - type: "byte" - } - ], - 0x2b: [ - { - name: "experienceBar", - type: "float" - }, - { - name: "level", - type: "short" - }, - { - name: "totalExperience", - type: "short" - } - ], - 0x33: [ - { - name: "x", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "groundUpContinuous", - type: "bool" - }, - { - name: "primaryBitMap", - type: "ushort" - }, - { - name: "addBitMap", - type: "ushort" - }, - { - name: "compressedData", - type: "byteArray32" - } - ], - 0x34: [ - { - name: "chunkX", - type: "int" - }, - { - name: "chunkZ", - type: "int" - }, - { - name: "recordCount", - type: "short" - }, - { - name: "data", - type: "byteArray32" - } - ], - 0x35: [ - { - name: "x", - type: "int" - }, - { - name: "y", - type: "byte" - }, - { - name: "z", - type: "int" - }, - { - name: "blockType", - type: "short" - }, - { - name: "blockMetadata", - type: "byte" - } - ], - 0x36: [ - { - name: "x", - type: "int" - }, - { - name: "y", - type: "short" - }, - { - name: "z", - type: "int" - }, - { - name: "byte1", - type: "byte" - }, - { - name: "byte2", - type: "byte" - }, - { - name: "blockId", - type: "short" - } - ], - 0x37: [ - { - name: "entityId", - type: "int" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "destroyStage", - type: "byte" - } - ], - 0x38: [ - { - name: "data", - type: "mapChunkBulk" - } - ], - 0x3c: [ - { - name: "x", - type: "double" - }, - { - name: "y", - type: "double" - }, - { - name: "z", - type: "double" - }, - { - name: "radius", - type: "float" - }, - { - name: "affectedBlockOffsets", - type: "byteVectorArray" - }, - { - name: "playerMotionX", - type: "float" - }, - { - name: "playerMotionY", - type: "float" - }, - { - name: "playerMotionZ", - type: "float" - } - ], - 0x3d: [ - { - name: "effectId", - type: "int" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "byte" - }, - { - name: "z", - type: "int" - }, - { - name: "data", - type: "int" - }, - { - name: "disableRelativeVolume", - type: "bool" - } - ], - 0x3e: [ - { - name: "soundName", - type: "string" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - }, - { - name: "volume", - type: "float" - }, - { - name: "pitch", - type: "byte" - } - ], - 0x46: [ - { - name: "reason", - type: "byte" - }, - { - name: "gameMode", - type: "byte" - } - ], - 0x47: [ - { - name: "entityId", - type: "int" - }, - { - name: "type", - type: "byte" - }, - { - name: "x", - type: "int" - }, - { - name: "y", - type: "int" - }, - { - name: "z", - type: "int" - } - ], - 0x64: [ - { - name: "windowId", - type: "byte" - }, - { - name: "inventoryType", - type: "byte" - }, - { - name: "windowTitle", - type: "string" - }, - { - name: "slotCount", - type: "byte" - } - ], - 0x65: [ - { - name: "windowId", - type: "byte" - } - ], - 0x66: [ - { - name: "windowId", - type: "byte" - }, - { - name: "slot", - type: "short" - }, - { - name: "mouseButton", - type: "byte" - }, - { - name: "actionNumber", - type: "short" - }, - { - name: "shift", - type: "bool" - }, - { - name: "clickedItem", - type: "slot" - } - ], - 0x67: [ - { - name: "windowId", - type: "byte" - }, - { - name: "slotId", - type: "short" - }, - { - name: "slot", - type: "slot" - } - ], - 0x68: [ - { - name: "windowId", - type: "byte" - }, - { - name: "slots", - type: "slotArray" - } - ], - 0x69: [ - { - name: "windowId", - type: "byte" - }, - { - name: "property", - type: "short" - }, - { - name: "value", - type: "short" - } - ], - 0x6a: [ - { - name: "windowId", - type: "byte" - }, - { - name: "actionNumber", - type: "short" - }, - { - name: "accepted", - type: "bool" - } - ], - 0x6b: [ - { - name: "slot", - type: "short" - }, - { - name: "clickedItem", - type: "slot" - } - ], - 0x6c: [ - { - name: "windowId", - type: "byte" - }, - { - name: "enchantment", - type: "byte" - } - ], - 0x82: [ - { - name: "x", - type: "int" - }, - { - name: "y", - type: "short" - }, - { - name: "z", - type: "int" - }, - { - name: "text1", - type: "string" - }, - { - name: "text2", - type: "string" - }, - { - name: "text3", - type: "string" - }, - { - name: "text4", - type: "string" - } - ], - 0x83: [ - { - name: "itemType", - type: "short" - }, - { - name: "itemId", - type: "short" - }, - { - name: "text", - type: "ascii" - } - ], - 0x84: [ - { - name: "x", - type: "int" - }, - { - name: "y", - type: "short" - }, - { - name: "z", - type: "int" - }, - { - name: "action", - type: "byte" - }, - { - name: "nbtData", - type: "byteArray16" - } - ], - 0xc8: [ - { - name: "statisticId", - type: "int" - }, - { - name: "amount", - type: "byte" - } - ], - 0xc9: [ - { - name: "playerName", - type: "string" - }, - { - name: "online", - type: "bool" - }, - { - name: "ping", - type: "short" - } - ], - 0xca: [ - { - name: "flags", - type: "byte" - }, - { - name: "flyingSpeed", - type: "byte" - }, - { - name: "walkingSpeed", - type: "byte" - } - ], - 0xcb: [ - { - name: "text", - type: "string" - } - ], - 0xcc: [ - { - name: "locale", - type: "string" - }, - { - name: "viewDistance", - type: "byte" - }, - { - name: "chatFlags", - type: "byte" - }, - { - name: "difficulty", - type: "byte" - }, - { - name: "showCape", - type: "bool" - } - ], - 0xcd: [ - { - name: "payload", - type: "byte" - } - ], - 0xfa: [ - { - name: "channel", - type: "string" - }, - { - name: "data", - type: "byteArray16" - } - ], - 0xfc: [ - { - name: "sharedSecret", - type: "byteArray16" - }, - { - name: "verifyToken", - type: "byteArray16" - } - ], - 0xfd: [ - { - name: "serverId", - type: "string" - }, - { - name: "publicKey", - type: "byteArray16" - }, - { - name: "verifyToken", - type: "byteArray16" - } - ], - 0xfe: [ - { - name: "magic", - type: "byte" - } - ], - 0xff: [ - { - name: "reason", - type: "string" - } - ] -};