diff --git a/README.md b/README.md index ecb1e0a..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 @@ -50,17 +50,19 @@ client.on(0x03, function(packet) { ```js var mc = require('minecraft-protocol'); var server = mc.createServer({ - 'online-mode': true, // optional - 'encryption': true, // optional + 'online-mode': true, // optional + encryption: true, // optional + host: '0.0.0.0', // optional + port: 25565, // optional }); -server.on('connection', function(client) { +server.on('login', function(client) { client.write(0x01, { entityId: 0, levelType: 'default', gameMode: 0, dimension: 0, difficulty: 2, - maxPlayers: 32 + maxPlayers: server.maxPlayers }); client.write(0x0d, { x: 0, @@ -71,10 +73,7 @@ server.on('connection', function(client) { pitch: 0, onGround: true }); - client.write(0x03, { message: 'Hello, world!' }); -}); -server.listen(); - console.log('Server listening on port', server.port); + client.write(0x03, { message: 'Hello, ' + client.username }); }); ``` diff --git a/examples/server.js b/examples/server.js index 8f4f8c0..1d0fcf6 100644 --- a/examples/server.js +++ b/examples/server.js @@ -4,67 +4,67 @@ var yellow = '§e'; var players = []; var options = { - requireAuth: false, + 'online-mode': false, motd: 'Vox Industries', - maxPlayers: 128, + 'max-players': 127, + port: 25565, }; var server = mc.createServer(options); -server.on('connection', function(client) { - var player = { - client: client, - username: handshake.username, - index: players.length - }; - players.push(player); - server.players = players.length; +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+')'); + 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; + 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+')'); - }); + 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: 128 - }); - client.write(0x0d, { - x: 0, - y: 256, - stance: 255, - z: 0, - yaw: 0, - pitch: 0, - onGround: 1 - }); + // 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); - }); - } -); + 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.listen(function() { - console.log('Server listening on port', server.port); +server.on('listening', function() { + console.log('Server listening on port', server.socket.address().port); }); function broadcast(message, exclude) { diff --git a/examples/server_helloworld.js b/examples/server_helloworld.js index e7b1134..90a0d50 100644 --- a/examples/server_helloworld.js +++ b/examples/server_helloworld.js @@ -1,12 +1,12 @@ var mc = require('../'); var options = { - 'online-mode': false, + 'online-mode': false, // optional }; var server = mc.createServer(options); -server.on('connection', function(client) { +server.on('login', function(client) { var addr = client.socket.remoteAddress; console.log('Incoming connection', '('+addr+')'); @@ -15,6 +15,14 @@ server.on('connection', function(client) { }); // 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, @@ -32,6 +40,6 @@ server.on('error', function(error) { console.log('Error:', error); }); -server.listen(function() { - console.log('Server listening on port', server.port); +server.on('listening', function() { + console.log('Server listening on port', server.socket.address().port); }); diff --git a/index.js b/index.js index 5f25446..e9a0dc0 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,4 @@ -var net = require('net') - , EventEmitter = require('events').EventEmitter +var EventEmitter = require('events').EventEmitter , util = require('util') , assert = require('assert') , ursa = require('ursa') @@ -7,10 +6,92 @@ var net = require('net') , superagent = require('superagent') , Batch = require('batch') , protocol = require('./lib/protocol') - , createPacketBuffer = protocol.createPacketBuffer - , parsePacket = protocol.parsePacket + , 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 @@ -20,7 +101,9 @@ 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, { @@ -30,6 +113,9 @@ function createClient(options) { serverPort: port, }); }); + client.on('packet', function(packet) { + console.log(packet.id, packet); + }); client.on(0x00, onKeepAlive); client.once(0xFC, onEncryptionKeyResponse); client.once(0xFD, onEncryptionKeyRequest); @@ -133,53 +219,6 @@ 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; - 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); -}; function mcPubKeyToURsa(mcPubKeyBuffer) { 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 index a6467be..370e55b 100644 --- a/lib/protocol.js +++ b/lib/protocol.js @@ -6,6 +6,7 @@ var Iconv = require('iconv').Iconv var STRING_MAX_LENGTH = 240; exports.version = 51; +exports.minecraftVersion = '1.4.6'; exports.sessionVersion = 13; exports.parsePacket = parsePacket; exports.createPacketBuffer = createPacketBuffer; @@ -446,15 +447,6 @@ var packets = { ] }; -function get(packetId, toServer) { - var packetInfo = packets[packetId]; - return Array.isArray(packetInfo) ? - packetInfo : - toServer ? - packetInfo.toServer : - packetInfo.toClient; -} - var writers = { 'int': IntWriter, 'short': ShortWriter, @@ -908,7 +900,7 @@ ByteArray16Writer.prototype.write = function(buffer, offset) { }; function ByteWriter(value) { - this.value = value; + this.value = value == null ? 0 : value; this.size = 1; } @@ -970,10 +962,19 @@ IntWriter.prototype.write = function(buffer, offset) { buffer.writeInt32BE(this.value, offset); } -function createPacketBuffer(packetId, params) { +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); + var packet = get(packetId, isServer); packet.forEach(function(fieldInfo) { var value = params[fieldInfo.name]; var Writer = writers[fieldInfo.type]; @@ -991,12 +992,12 @@ function createPacketBuffer(packetId, params) { return buffer; } -function parsePacket(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); + var packetInfo = get(packetId, !isServer); assert.ok(packetInfo, "Unrecognized packetId: " + packetId); var i, fieldInfo, read, readResults; for (i = 0; i < packetInfo.length; ++i) { 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); +};