diff --git a/examples/server_helloworld.js b/examples/server_helloworld.js index f2661a1..fdc8eff 100644 --- a/examples/server_helloworld.js +++ b/examples/server_helloworld.js @@ -1,7 +1,7 @@ var mc = require('../'); var options = { - // 'online-mode': false, // optional + 'online-mode': true, }; var server = mc.createServer(options); @@ -21,15 +21,17 @@ server.on('login', function(client) { gameMode: 0, dimension: 0, difficulty: 2, - maxPlayers: server.maxPlayers + maxPlayers: server.maxPlayers, + reducedDebugInfo: false }); + client.write('position', { x: 0, y: 1.62, z: 0, yaw: 0, pitch: 0, - onGround: true + flags: 0x00 }); var msg = { @@ -39,7 +41,7 @@ server.on('login', function(client) { 'Hello, world!' ] }; - client.write('chat', { message: JSON.stringify(msg) }); + client.write('chat', { message: JSON.stringify(msg), position: 0 }); }); server.on('error', function(error) { diff --git a/index.js b/index.js index a63a88c..e8cc4a5 100644 --- a/index.js +++ b/index.js @@ -44,7 +44,6 @@ function createServer(options) { var kickTimeout = options.kickTimeout || 10 * 1000; var checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000; var onlineMode = options['online-mode'] == null ? true : options['online-mode']; - var encryptionEnabled = options.encryption == null ? true : options.encryption; var serverKey = ursa.generatePrivateKey(1024); @@ -128,8 +127,8 @@ function createServer(options) { function onLogin(packet) { client.username = packet.username; var isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; - var needToVerify = (onlineMode && ! isException) || (! onlineMode && isException); - if (encryptionEnabled || needToVerify) { + var needToVerify = (onlineMode && !isException) || (! onlineMode && isException); + if (needToVerify) { var serverId = crypto.randomBytes(4).toString('hex'); client.verifyToken = crypto.randomBytes(4); var publicKeyStrArr = serverKey.toPublicPem("utf8").split("\n"); @@ -141,8 +140,6 @@ function createServer(options) { hash = crypto.createHash("sha1"); hash.update(serverId); client.once([states.LOGIN, 0x01], onEncryptionKeyResponse); - client.once([states.LOGIN, 0x03], onCompressionRequest); - client.on( [states.PLAY, 0x46], onCompressionRequest); client.write(0x01, { serverId: serverId, publicKey: client.publicKey, @@ -195,15 +192,13 @@ function createServer(options) { } } - function onCompressionRequest(packet) { - client.compressionThreshold = packet.threshold; - } - function loginClient() { - var isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; + var isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; if (onlineMode == false || isException) { client.uuid = "0-0-0-0-0"; } + //client.write('compress', { threshold: 256 }); // Default threshold is 256 + //client.compressionThreshold = 256; client.write(0x02, {uuid: client.uuid, username: client.username}); client.state = states.PLAY; loggedIn = true; @@ -240,7 +235,8 @@ function createClient(options) { if (keepAlive) client.on([states.PLAY, 0x00], onKeepAlive); client.once([states.LOGIN, 0x01], onEncryptionKeyRequest); client.once([states.LOGIN, 0x02], onLogin); - + client.once("compress", onCompressionRequest); + client.once("set_compression", onCompressionRequest); if (haveCredentials) { // make a request to get the case-correct username before connecting. var cb = function(err, session) { @@ -254,7 +250,7 @@ function createClient(options) { client.connect(port, host); } }; - + if (accessToken != null) getSession(options.username, accessToken, options.clientToken, true, cb); else getSession(options.username, options.password, options.clientToken, false, cb); } else { @@ -279,6 +275,10 @@ function createClient(options) { }); } + function onCompressionRequest(packet) { + client.compressionThreshold = packet.threshold; + } + function onKeepAlive(packet) { client.write(0x00, { keepAliveId: packet.keepAliveId diff --git a/lib/client.js b/lib/client.js index c4a9171..da25223 100644 --- a/lib/client.js +++ b/lib/client.js @@ -5,7 +5,11 @@ var net = require('net') , dns = require('dns') , createPacketBuffer = protocol.createPacketBuffer , compressPacketBuffer = protocol.compressPacketBuffer + , oldStylePacket = protocol.oldStylePacket + , newStylePacket = protocol.newStylePacket , parsePacket = protocol.parsePacket + , packetIds = protocol.packetIds + , packetNames = protocol.packetNames , states = protocol.states , debug = protocol.debug ; @@ -31,7 +35,7 @@ function Client(isServer) { this.encryptionEnabled = false; this.cipher = null; this.decipher = null; - this.compressionThreshold = -1; + this.compressionThreshold = -2; this.packetsToParse = {}; this.on('newListener', function(event, listener) { var direction = this.isServer ? 'toServer' : 'toClient'; @@ -154,28 +158,36 @@ Client.prototype.write = function(packetId, params) { return false; packetId = packetId[1]; } - + if (typeof packetId === "string") + packetId = packetIds[this.state][this.isServer ? "toClient" : "toServer"][packetId]; var that = this; - // TODO: Which comes first, encryption or compression? - var finishWriting = function(buffer) { - debug("writing packetId " + packetId + " (0x" + packetId.toString(16) + ")"); - debug(params); - var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer; - that.socket.write(out); - return true; + var packetName = packetNames[that.state][that.isServer ? "toClient" : "toServer"][packetId]; + debug("writing packetId " + that.state + "." + packetName + " (0x" + packetId.toString(16) + ")"); + debug(params); + debug(buffer); + var out = that.encryptionEnabled ? new Buffer(that.cipher.update(buffer), 'binary') : buffer; + that.socket.write(out); + return true; } var buffer = createPacketBuffer(packetId, this.state, params, this.isServer); - if(this.compressionThreshold != -1 && buffer.length > this.compressionThreshold) { - compressPacketBuffer(buffer, finishWriting); + if (this.compressionThreshold >= 0 && buffer.length >= this.compressionThreshold) { + debug("Compressing packet"); + compressPacketBuffer(buffer, finishWriting); + } else if (this.compressionThreshold >= -1) { + debug("New-styling packet"); + finishWriting(newStylePacket(buffer)); } else { - finishWriting(buffer); + debug("Old-styling packet"); + finishWriting(oldStylePacket(buffer)); } - }; +// TODO : Perhaps this should only accept buffers without length, so we can +// handle compression ourself ? Needs to ask peopl who actually use this feature +// like @deathcap Client.prototype.writeRaw = function(buffer, shouldEncrypt) { if (shouldEncrypt === null) { shouldEncrypt = true; diff --git a/lib/protocol.js b/lib/protocol.js index f3f121d..62f64b1 100644 --- a/lib/protocol.js +++ b/lib/protocol.js @@ -51,9 +51,9 @@ var packets = { ]}, encryption_begin: {id: 0x01, fields: [ { name: "serverId", type: "string" }, - { name: "publicKeyLength", type: "count", typeArgs: { type: "short", countFor: "publicKey" } }, + { name: "publicKeyLength", type: "count", typeArgs: { type: "varint", countFor: "publicKey" } }, { name: "publicKey", type: "buffer", typeArgs: { count: "publicKeyLength" } }, - { name: "verifyTokenLength", type: "count", typeArgs: { type: "short", countFor: "verifyToken" } }, + { name: "verifyTokenLength", type: "count", typeArgs: { type: "varint", countFor: "verifyToken" } }, { name: "verifyToken", type: "buffer", typeArgs: { count: "verifyTokenLength" } }, ]}, success: {id: 0x02, fields: [ @@ -69,9 +69,9 @@ var packets = { { name: "username", type: "string" } ]}, encryption_begin: {id: 0x01, fields: [ - { name: "sharedSecretLength", type: "count", typeArgs: { type: "short", countFor: "sharedSecret" } }, + { name: "sharedSecretLength", type: "count", typeArgs: { type: "varint", countFor: "sharedSecret" } }, { name: "sharedSecret", type: "buffer", typeArgs: { count: "sharedSecretLength" } }, - { name: "verifyTokenLength", type: "count", typeArgs: { type: "short", countFor: "verifyToken" } }, + { name: "verifyTokenLength", type: "count", typeArgs: { type: "varint", countFor: "verifyToken" } }, { name: "verifyToken", type: "buffer", typeArgs: { count: "verifyTokenLength" } }, ]} } @@ -131,7 +131,7 @@ var packets = { ]}, bed: {id: 0x0a, fields: [ { name: "entityId", type: "int" }, - { name: "position", type: "location" } + { name: "location", type: "position" } ]}, animation: {id: 0x0b, fields: [ { name: "entityId", type: "varint" }, @@ -841,6 +841,7 @@ var types = { 'restBuffer': [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], 'count': [readCount, writeCount, sizeOfCount], // TODO : remove type-specific, replace with generic containers and arrays. + 'position': [readPosition, writePosition, 8], 'slot': [readSlot, writeSlot, sizeOfSlot], 'entityMetadata': [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], }; @@ -1055,6 +1056,17 @@ function readBool(buffer, offset) { }; } +function readPosition(buffer, offset) { + var longVal = readLong(buffer, offset).value; // I wish I could do destructuring... + var x = longVal[0] >> 6; + var y = ((longVal[0] & 0x3F) << 6) | (longVal[1] >> 26); + var z = longVal[1] << 6 >> 6 + return { + value: { x: x, y: y, z: z }, + size: 8 + }; +} + function readSlot(buffer, offset) { var results = readShort(buffer, offset); if (! results) return null; @@ -1093,6 +1105,13 @@ function sizeOfSlot(value) { return value.id === -1 ? 2 : 7 + value.nbtData.length; } +function writePosition(value, buffer, offset) { + var longVal = []; + longVal[0] = ((value.x & 0x3FFFFFF) << 6) | ((value.y & 0xFC0) >> 6); + longVal[1] = ((value.y & 0x3F) << 26) | (value.z & 0x3FFFFFF); + return writeLong(longVal, buffer, offset); +} + function writeSlot(value, buffer, offset) { buffer.writeInt16BE(value.id, offset); if (value.id === -1) return offset + 2; @@ -1233,6 +1252,8 @@ function readContainer(buffer, offset, typeArgs, rootNode) { function writeContainer(value, buffer, offset, typeArgs, rootNode) { rootNode.this = value; for (var index in typeArgs.fields) { + if (!value.hasOwnProperty(typeArgs.fields[index].name && typeArgs.fields[index].type != "count" && !typeArgs.fields[index].condition)) + debug(new Error("Missing Property " + typeArgs.fields[index].name).stack); offset = write(value[typeArgs.fields[index].name], buffer, offset, typeArgs.fields[index], rootNode); } delete rootNode.this; @@ -1384,6 +1405,7 @@ function get(packetId, state, toServer) { return packetInfo; } +// TODO : This does NOT contain the length prefix anymore. function createPacketBuffer(packetId, state, params, isServer) { var length = 0; if (typeof packetId === 'string' && typeof state !== 'string' && !params) { @@ -1400,29 +1422,48 @@ function createPacketBuffer(packetId, state, params, isServer) { length += sizeOf(params[fieldInfo.name], fieldInfo, params); }); length += sizeOfVarInt(packetId); - var size = length + sizeOfVarInt(length); + var size = length;// + sizeOfVarInt(length); var buffer = new Buffer(size); - var offset = writeVarInt(length, buffer, 0); + var offset = 0;//writeVarInt(length, buffer, 0); offset = writeVarInt(packetId, buffer, offset); packet.forEach(function(fieldInfo) { var value = params[fieldInfo.name]; - if(typeof value === "undefined") value = 0; // TODO : Why ? + // TODO : A better check is probably needed + if(typeof value === "undefined" && fieldInfo.type != "count" && !fieldInfo.condition) + debug(new Error("Missing Property " + fieldInfo.name).stack); offset = write(value, buffer, offset, fieldInfo, params); }); return buffer; } function compressPacketBuffer(buffer, callback) { - var dataLength = buffer.length; - var packetLength = dataLength + sizeOfVarInt(dataLength); - var data = zlib.deflateRaw(buffer, function(compressedBuffer) { - var size = sizeOfVarInt(packetLength) + sizeOfVarInt(packetLength) + compressedBuffer.length; - var packetBuffer = new Buffer(size); - var offset = writeVarInt(packetLength, packetBuffer, 0); - offset = writeVarInt(dataLength, packetBuffer, offset); - writeVarInt(compressedBuffer, packetBuffer, offset); - callback(packetBuffer); - }); + var dataLength = buffer.size; + zlib.deflateRaw(buffer, function(compressedBuffer) { + var packetLength = sizeOfVarInt(dataLength) + compressedBuffer.length; + var size = sizeOfVarInt(packetLength) + packetLength; + var packetBuffer = new Buffer(size); + var offset = writeVarInt(packetLength, packetBuffer, 0); + offset = writeVarInt(dataLength, packetBuffer, offset); + writeBuffer(compressedBuffer, packetBuffer, offset); + callback(packetBuffer); + }); +} + +function oldStylePacket(buffer) { + var packet = new Buffer(sizeOfVarInt(buffer.length) + buffer.length); + var cursor = writeVarInt(buffer.length, packet, 0); + writeBuffer(buffer, packet, cursor); + return packet; +} + +function newStylePacket(buffer) { + var sizeOfO = sizeOfVarInt(0); + var size = sizeOfVarInt(buffer.length + sizeOfO) + sizeOfO + buffer.length; + var packet = new Buffer(size); + var cursor = writeVarInt(buffer.length, packet, 0); + cursor = writeVarInt(0, packet, cursor); + writeBuffer(buffer, packet, cursor); + return packet; } function parsePacket(buffer, state, isServer, packetsToParse) { @@ -1461,7 +1502,8 @@ function parsePacket(buffer, state, isServer, packetsToParse) { results: results }; } else { - debug("read packetId " + packetId + " (0x" + packetId.toString(16) + ")"); + var packetName = packetNames[state][isServer ? "toServer" : "toClient"][packetId]; + debug("read packetId " + state + "." + packetName + " (0x" + packetId.toString(16) + ")"); } var i, fieldInfo, readResults; @@ -1502,6 +1544,8 @@ module.exports = { parsePacket: parsePacket, createPacketBuffer: createPacketBuffer, compressPacketBuffer: compressPacketBuffer, + oldStylePacket: oldStylePacket, + newStylePacket: newStylePacket, STRING_MAX_LENGTH: STRING_MAX_LENGTH, packetIds: packetIds, packetNames: packetNames,