refactor + createServer support

This commit is contained in:
Andrew Kelley 2013-01-04 01:45:57 -05:00
parent d38dad2d56
commit 3a8bfaf8e3
7 changed files with 283 additions and 126 deletions

View file

@ -8,8 +8,8 @@ Parse and serialize minecraft packets, plus authentication and encryption.
objects. objects.
* Send a packet by supplying fields as a JavaScript object. * Send a packet by supplying fields as a JavaScript object.
* Supports authenticating and logging in. * Supports authenticating and logging in.
- Supports encryption - Supports encryption (client only)
- Supports online mode - Supports online mode (client only)
- Supports offline mode - Supports offline mode
* Respond to keep-alive packets. * Respond to keep-alive packets.
* Test coverage * Test coverage
@ -50,17 +50,19 @@ client.on(0x03, function(packet) {
```js ```js
var mc = require('minecraft-protocol'); var mc = require('minecraft-protocol');
var server = mc.createServer({ var server = mc.createServer({
'online-mode': true, // optional 'online-mode': true, // optional
'encryption': 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, { client.write(0x01, {
entityId: 0, entityId: 0,
levelType: 'default', levelType: 'default',
gameMode: 0, gameMode: 0,
dimension: 0, dimension: 0,
difficulty: 2, difficulty: 2,
maxPlayers: 32 maxPlayers: server.maxPlayers
}); });
client.write(0x0d, { client.write(0x0d, {
x: 0, x: 0,
@ -71,10 +73,7 @@ server.on('connection', function(client) {
pitch: 0, pitch: 0,
onGround: true onGround: true
}); });
client.write(0x03, { message: 'Hello, world!' }); client.write(0x03, { message: 'Hello, ' + client.username });
});
server.listen();
console.log('Server listening on port', server.port);
}); });
``` ```

View file

@ -4,67 +4,67 @@ var yellow = '§e';
var players = []; var players = [];
var options = { var options = {
requireAuth: false, 'online-mode': false,
motd: 'Vox Industries', motd: 'Vox Industries',
maxPlayers: 128, 'max-players': 127,
port: 25565,
}; };
var server = mc.createServer(options); var server = mc.createServer(options);
server.on('connection', function(client) { server.on('login', function(client) {
var player = { var player = {
client: client, client: client,
username: handshake.username, username: client.username,
index: players.length index: players.length
}; };
players.push(player); players.push(player);
server.players = players.length; server.players = players.length;
broadcast(yellow + player.username+' joined the game.'); broadcast(yellow + player.username+' joined the game.');
var addr = client.socket.remoteAddress + ':' + client.socket.remotePort; var addr = client.socket.remoteAddress + ':' + client.socket.remotePort;
console.log(player.username+' connected', '('+addr+')'); console.log(player.username+' connected', '('+addr+')');
client.on('end', function() { client.on('end', function() {
players.splice(player.index, 1); players.splice(player.index, 1);
server.players = players.length; server.players = players.length;
broadcast(yellow + player.username+' left the game.', player); broadcast(yellow + player.username+' left the game.', player);
console.log(player.username+' disconnected', '('+addr+')'); console.log(player.username+' disconnected', '('+addr+')');
}); });
// send init data so client will start rendering world // send init data so client will start rendering world
client.write(0x01, { client.write(0x01, {
entityId: 0, entityId: 0,
levelType: 'default', levelType: 'default',
gameMode: 1, gameMode: 1,
dimension: 0, dimension: 0,
difficulty: 2, difficulty: 2,
maxPlayers: 128 maxPlayers: server.maxPlayers
}); });
client.write(0x0d, { client.write(0x0d, {
x: 0, x: 0,
y: 256, y: 256,
stance: 255, stance: 255,
z: 0, z: 0,
yaw: 0, yaw: 0,
pitch: 0, pitch: 0,
onGround: 1 onGround: true
}); });
client.on(0x03, function(data) { client.on(0x03, function(data) {
var message = '<'+player.username+'>' + ' ' + data.message; var message = '<'+player.username+'>' + ' ' + data.message;
broadcast(message); broadcast(message);
console.log(message); console.log(message);
}); });
} });
);
server.on('error', function(error) { server.on('error', function(error) {
console.log('Error:', error); console.log('Error:', error);
}); });
server.listen(function() { server.on('listening', function() {
console.log('Server listening on port', server.port); console.log('Server listening on port', server.socket.address().port);
}); });
function broadcast(message, exclude) { function broadcast(message, exclude) {

View file

@ -1,12 +1,12 @@
var mc = require('../'); var mc = require('../');
var options = { var options = {
'online-mode': false, 'online-mode': false, // optional
}; };
var server = mc.createServer(options); var server = mc.createServer(options);
server.on('connection', function(client) { server.on('login', function(client) {
var addr = client.socket.remoteAddress; var addr = client.socket.remoteAddress;
console.log('Incoming connection', '('+addr+')'); console.log('Incoming connection', '('+addr+')');
@ -15,6 +15,14 @@ server.on('connection', function(client) {
}); });
// send init data so client will start rendering world // 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, { client.write(0x0d, {
x: 0, x: 0,
y: 1.62, y: 1.62,
@ -32,6 +40,6 @@ server.on('error', function(error) {
console.log('Error:', error); console.log('Error:', error);
}); });
server.listen(function() { server.on('listening', function() {
console.log('Server listening on port', server.port); console.log('Server listening on port', server.socket.address().port);
}); });

143
index.js
View file

@ -1,5 +1,4 @@
var net = require('net') var EventEmitter = require('events').EventEmitter
, EventEmitter = require('events').EventEmitter
, util = require('util') , util = require('util')
, assert = require('assert') , assert = require('assert')
, ursa = require('ursa') , ursa = require('ursa')
@ -7,10 +6,92 @@ var net = require('net')
, superagent = require('superagent') , superagent = require('superagent')
, Batch = require('batch') , Batch = require('batch')
, protocol = require('./lib/protocol') , protocol = require('./lib/protocol')
, createPacketBuffer = protocol.createPacketBuffer , Client = require('./lib/client')
, parsePacket = protocol.parsePacket , Server = require('./lib/server')
exports.createClient = createClient; 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) { function createClient(options) {
// defaults // defaults
@ -20,7 +101,9 @@ function createClient(options) {
assert.ok(options.username, "username is required"); assert.ok(options.username, "username is required");
var haveCredentials = options.email && options.password; var haveCredentials = options.email && options.password;
var client = new Client(); var client = new Client({
isServer: false
});
client.username = options.username; client.username = options.username;
client.on('connect', function() { client.on('connect', function() {
client.write(0x02, { client.write(0x02, {
@ -30,6 +113,9 @@ function createClient(options) {
serverPort: port, serverPort: port,
}); });
}); });
client.on('packet', function(packet) {
console.log(packet.id, packet);
});
client.on(0x00, onKeepAlive); client.on(0x00, onKeepAlive);
client.once(0xFC, onEncryptionKeyResponse); client.once(0xFC, onEncryptionKeyResponse);
client.once(0xFD, onEncryptionKeyRequest); 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) { function mcPubKeyToURsa(mcPubKeyBuffer) {

62
lib/client.js Normal file
View file

@ -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);
};

View file

@ -6,6 +6,7 @@ var Iconv = require('iconv').Iconv
var STRING_MAX_LENGTH = 240; var STRING_MAX_LENGTH = 240;
exports.version = 51; exports.version = 51;
exports.minecraftVersion = '1.4.6';
exports.sessionVersion = 13; exports.sessionVersion = 13;
exports.parsePacket = parsePacket; exports.parsePacket = parsePacket;
exports.createPacketBuffer = createPacketBuffer; 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 = { var writers = {
'int': IntWriter, 'int': IntWriter,
'short': ShortWriter, 'short': ShortWriter,
@ -908,7 +900,7 @@ ByteArray16Writer.prototype.write = function(buffer, offset) {
}; };
function ByteWriter(value) { function ByteWriter(value) {
this.value = value; this.value = value == null ? 0 : value;
this.size = 1; this.size = 1;
} }
@ -970,10 +962,19 @@ IntWriter.prototype.write = function(buffer, offset) {
buffer.writeInt32BE(this.value, 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 size = 1;
var fields = [ new UByteWriter(packetId) ]; var fields = [ new UByteWriter(packetId) ];
var packet = get(packetId); var packet = get(packetId, isServer);
packet.forEach(function(fieldInfo) { packet.forEach(function(fieldInfo) {
var value = params[fieldInfo.name]; var value = params[fieldInfo.name];
var Writer = writers[fieldInfo.type]; var Writer = writers[fieldInfo.type];
@ -991,12 +992,12 @@ function createPacketBuffer(packetId, params) {
return buffer; return buffer;
} }
function parsePacket(buffer) { function parsePacket(buffer, isServer) {
if (buffer.length < 1) return null; if (buffer.length < 1) return null;
var packetId = buffer.readUInt8(0); var packetId = buffer.readUInt8(0);
var size = 1; var size = 1;
var results = { id: packetId }; var results = { id: packetId };
var packetInfo = get(packetId); var packetInfo = get(packetId, !isServer);
assert.ok(packetInfo, "Unrecognized packetId: " + packetId); assert.ok(packetInfo, "Unrecognized packetId: " + packetId);
var i, fieldInfo, read, readResults; var i, fieldInfo, read, readResults;
for (i = 0; i < packetInfo.length; ++i) { for (i = 0; i < packetInfo.length; ++i) {

48
lib/server.js Normal file
View file

@ -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);
};