Protocol 1.7 support, Yggdrasil login support, new Client State API

This commit is contained in:
roblabla 2013-12-30 16:05:22 +01:00
parent 2b594399ea
commit 875d10ed0b
12 changed files with 1189 additions and 858 deletions

View file

@ -1,4 +1,6 @@
var mc = require('../');
var mc = require('../')
, states = mc.protocol.states
var client = mc.createClient({
username: process.env.MC_USERNAME,
password: process.env.MC_PASSWORD,
@ -6,12 +8,12 @@ var client = mc.createClient({
client.on('connect', function() {
console.info('connected');
});
client.on(0x03, function(packet) {
client.on([states.PLAY, 0x02], function(packet) {
var jsonMsg = JSON.parse(packet.message);
if (jsonMsg.translate == 'chat.type.announcement' || jsonMsg.translate == 'chat.type.text') {
var username = jsonMsg.using[0];
var msg = jsonMsg.using[1];
var username = jsonMsg.with[0];
var msg = jsonMsg.with[1];
if (username === client.username) return;
client.write(0x03, {message: msg});
client.write(0x01, {message: msg});
}
});
});

View file

@ -1,4 +1,5 @@
var mc = require('../');
var states = mc.protocol.states;
var yellow = '§e';
@ -30,17 +31,16 @@ server.on('login', function(client) {
difficulty: 2,
maxPlayers: server.maxPlayers
});
client.write(0x0d, {
client.write(0x08, {
x: 0,
y: 256,
stance: 255,
z: 0,
yaw: 0,
pitch: 0,
onGround: true
});
client.on(0x03, function(data) {
client.on([states.PLAY, 0x01], function(data) {
var message = '<'+client.username+'>' + ' ' + data.message;
broadcast(message, client, client.username);
console.log(message);
@ -66,12 +66,12 @@ function broadcast(message, exclude, username) {
if (client !== exclude) {
var msg = {
translate: translate,
using: [
"with": [
username,
'Hello, world!'
]
};
client.write(0x03, { message: JSON.stringify(msg) });
client.write(0x02, { message: JSON.stringify(msg) });
}
}
}

View file

@ -1,7 +1,7 @@
var mc = require('../');
var options = {
'online-mode': false, // optional
// 'online-mode': false, // optional
};
var server = mc.createServer(options);
@ -23,10 +23,9 @@ server.on('login', function(client) {
difficulty: 2,
maxPlayers: server.maxPlayers
});
client.write(0x0d, {
client.write(0x08, {
x: 0,
y: 1.62,
stance: 0,
z: 0,
yaw: 0,
pitch: 0,
@ -35,12 +34,12 @@ server.on('login', function(client) {
var msg = {
translate: 'chat.type.announcement',
using: [
"with": [
'Server',
'Hello, world!'
]
};
client.write(0x03, { message: JSON.stringify(msg) });
client.write(0x02, { message: JSON.stringify(msg) });
});
server.on('error', function(error) {

247
index.js
View file

@ -1,15 +1,20 @@
var EventEmitter = require('events').EventEmitter
, util = require('util')
, assert = require('assert')
, ursa = require('ursa')
, crypto = require('crypto')
, bufferEqual = require('buffer-equal')
, superagent = require('superagent')
, protocol = require('./lib/protocol')
, Client = require('./lib/client')
, Server = require('./lib/server')
, debug = protocol.debug
;
, util = require('util')
, assert = require('assert')
, ursa = require('ursa')
, crypto = require('crypto')
, bufferEqual = require('buffer-equal')
, superagent = require('superagent')
, protocol = require('./lib/protocol')
, Client = require('./lib/client')
, Server = require('./lib/server')
, Yggdrasil = require('./lib/yggdrasil.js')
, getSession = Yggdrasil.getSession
, validateSession = Yggdrasil.validateSession
, joinServer = Yggdrasil.joinServer
, states = protocol.states
, debug = protocol.debug
;
module.exports = {
createClient: createClient,
@ -23,10 +28,10 @@ module.exports = {
function createServer(options) {
options = options || {};
var port = options.port != null ?
options.port :
options['server-port'] != null ?
options['server-port'] :
25565 ;
options.port :
options['server-port'] != null ?
options['server-port'] :
25565;
var host = options.host || '0.0.0.0';
var kickTimeout = options.kickTimeout || 10 * 1000;
var checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000;
@ -41,9 +46,9 @@ function createServer(options) {
server.playerCount = 0;
server.onlineModeExceptions = {};
server.on("connection", function(client) {
client.once(0xfe, onPing);
client.once(0x02, onHandshake);
client.once(0xFC, onEncryptionKeyResponse);
client.once([states.HANDSHAKING, 0x00], onHandshake);
client.once([states.LOGIN, 0x00], onLogin);
client.once([states.STATUS, 0x00], onPing);
client.on('end', onEnd);
var keepAlive = false;
@ -60,7 +65,8 @@ function createServer(options) {
}
function keepAliveLoop() {
if (! keepAlive) return;
if (!keepAlive)
return;
// check if the last keepAlive was too long ago (kickTimeout)
var elapsed = new Date() - lastKeepAlive;
@ -90,40 +96,44 @@ function createServer(options) {
}
function onPing(packet) {
if (loggedIn) return;
client.write(0xff, {
reason: [
'§1',
protocol.version,
protocol.minecraftVersion,
server.motd,
server.playerCount,
server.maxPlayers,
].join('\u0000')
var response = {
"version": {
"name": protocol.minecraftVersion,
"protocol": protocol.version
},
"players": {
"max": server.maxPlayers,
"online": server.playerCount,
"sample": []
},
"description": {"text": server.motd},
"favicon": server.favicon
};
client.once([states.STATUS, 0x01], function(packet) {
client.write(0x01, { time: packet.time });
client.end();
});
client.write(0x00, {response: JSON.stringify(response)});
}
function onHandshake(packet) {
function onLogin(packet) {
client.username = packet.username;
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
var needToVerify = (onlineMode && ! isException) || (! onlineMode && isException);
var serverId;
if (needToVerify) {
serverId = crypto.randomBytes(4).toString('hex');
} else {
serverId = '-';
}
if (encryptionEnabled) {
if (encryptionEnabled || needToVerify) {
var serverId = crypto.randomBytes(4).toString('hex');
client.verifyToken = crypto.randomBytes(4);
var publicKeyStrArr = serverKey.toPublicPem("utf8").split("\n");
var publicKeyStr = "";
for (var i=1;i<publicKeyStrArr.length - 2;i++) {
for (var i = 1; i < publicKeyStrArr.length - 2; i++) {
publicKeyStr += publicKeyStrArr[i]
}
client.publicKey = new Buffer(publicKeyStr,'base64');
client.publicKey = new Buffer(publicKeyStr, 'base64');
hash = crypto.createHash("sha1");
hash.update(serverId);
client.write(0xFD, {
client.once([states.LOGIN, 0x01], onEncryptionKeyResponse);
client.write(0x01, {
serverId: serverId,
publicKey: client.publicKey,
verifyToken: client.verifyToken
@ -133,9 +143,17 @@ function createServer(options) {
}
}
function onHandshake(packet) {
if (packet.nextState == 1) {
client.state = states.STATUS;
} else if (packet.nextState == 2) {
client.state = states.LOGIN;
}
}
function onEncryptionKeyResponse(packet) {
var verifyToken = serverKey.decrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING);
if (! bufferEqual(client.verifyToken, verifyToken)) {
if (!bufferEqual(client.verifyToken, verifyToken)) {
client.end('DidNotEncryptVerifyTokenProperly');
return;
}
@ -144,49 +162,29 @@ function createServer(options) {
client.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
hash.update(sharedSecret);
hash.update(client.publicKey);
client.write(0xFC, {
sharedSecret: new Buffer(0),
verifyToken: new Buffer(0)
});
client.encryptionEnabled = true;
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
var needToVerify = (onlineMode && ! isException) || (! onlineMode && isException);
var needToVerify = (onlineMode && !isException) || (!onlineMode && isException);
var nextStep = needToVerify ? verifyUsername : loginClient;
nextStep();
function verifyUsername() {
var digest = mcHexDigest(hash);
var request = superagent.get("http://session.minecraft.net/game/checkserver.jsp");
request.query({
user: client.username,
serverId: digest
});
request.end(function(err, resp) {
var myErr;
validateSession(client.username, digest, function(err, uuid) {
if (err) {
server.emit('error', err);
client.end('McSessionUnavailable');
} else if (resp.serverError) {
myErr = new Error("session.minecraft.net is broken: " + resp.status);
myErr.code = 'EMCSESSION500';
server.emit('error', myErr);
client.end('McSessionDown');
} else if (resp.serverError) {
myErr = new Error("session.minecraft.net rejected request: " + resp.status);
myErr.code = 'EMCSESSION400';
server.emit('error', myErr);
client.end('McSessionRejectedAuthRequest');
} else if (resp.text !== "YES") {
client.end('FailedToVerifyUsername');
} else {
loginClient();
client.end("Failed to verify username!");
return;
}
client.UUID = uuid;
loginClient();
});
}
}
function loginClient() {
client.write(0x02, {uuid: (client.UUID | 0).toString(10), username: client.username});
client.state = states.PLAY;
loggedIn = true;
startKeepAlive();
@ -208,28 +206,36 @@ function createClient(options) {
assert.ok(options, "options is required");
var port = options.port || 25565;
var host = options.host || 'localhost';
var clientToken = options.clientToken || Yggdrasil.generateUUID();
var accessToken = options.accessToken || null;
assert.ok(options.username, "username is required");
var haveCredentials = options.password != null;
var haveCredentials = options.password != null || (clientToken != null && accessToken != null);
var keepAlive = options.keepAlive == null ? true : options.keepAlive;
var client = new Client(false);
client.on('connect', onConnect);
if (keepAlive) client.on(0x00, onKeepAlive);
client.once(0xFC, onEncryptionKeyResponse);
client.once(0xFD, onEncryptionKeyRequest);
if (keepAlive) client.on([states.PLAY, 0x00], onKeepAlive);
client.once([states.LOGIN, 0x01], onEncryptionKeyRequest);
client.once([states.LOGIN, 0x02], onLogin);
if (haveCredentials) {
// make a request to get the case-correct username before connecting.
getLoginSession(options.username, options.password, function(err, session) {
var cb = function(err, session) {
if (err) {
client.emit('error', err);
} else {
client.session = session;
client.username = session.username;
accessToken = session.accessToken;
client.emit('session');
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 {
// assume the server is in offline mode and just go for it.
client.username = options.username;
@ -239,11 +245,16 @@ function createClient(options) {
return client;
function onConnect() {
client.write(0x02, {
client.write(0x00, {
protocolVersion: protocol.version,
username: client.username,
serverHost: host,
serverPort: port,
nextState: 2
});
client.state = states.LOGIN;
client.write(0x00, {
username: client.username
});
}
@ -288,28 +299,7 @@ function createClient(options) {
hash.update(packet.publicKey);
var digest = mcHexDigest(hash);
var request = superagent.get("http://session.minecraft.net/game/joinserver.jsp");
request.query({
user: client.session.username,
sessionId: client.session.id,
serverId: digest,
});
request.end(function(err, resp) {
var myErr;
if (err) {
cb(err);
} else if (resp.serverError) {
myErr = new Error("session.minecraft.net is broken: " + resp.status);
myErr.code = 'EMCSESSION500';
cb(myErr);
} else if (resp.clientError) {
myErr = new Error("session.minecraft.net rejected request: " + resp.status + " " + resp.text);
myErr.code = 'EMCSESSION400';
cb(myErr);
} else {
cb();
}
});
joinServer(this.username, digest, accessToken, client.session.selectedProfile.id, cb);
}
function sendEncryptionKeyResponse() {
@ -318,19 +308,19 @@ function createClient(options) {
var encryptedVerifyTokenBuffer = pubKey.encrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING);
client.cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
client.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
client.write(0xfc, {
client.write(0x01, {
sharedSecret: encryptedSharedSecretBuffer,
verifyToken: encryptedVerifyTokenBuffer,
});
client.encryptionEnabled = true;
}
}
}
function onEncryptionKeyResponse(packet) {
assert.strictEqual(packet.sharedSecret.length, 0);
assert.strictEqual(packet.verifyToken.length, 0);
client.encryptionEnabled = true;
client.write(0xcd, { payload: 0 });
function onLogin(packet) {
client.state = states.PLAY;
client.uuid = packet.uuid;
client.username = packet.username;
}
}
@ -352,11 +342,13 @@ function mcHexDigest(hash) {
var buffer = new Buffer(hash.digest(), 'binary');
// check for negative hashes
var negative = buffer.readInt8(0) < 0;
if (negative) performTwosCompliment(buffer);
if (negative)
performTwosCompliment(buffer);
var digest = buffer.toString('hex');
// trim leading zeroes
digest = digest.replace(/^0+/g, '');
if (negative) digest = '-' + digest;
if (negative)
digest = '-' + digest;
return digest;
function performTwosCompliment(buffer) {
@ -374,42 +366,3 @@ function mcHexDigest(hash) {
}
}
}
function getLoginSession(email, password, cb) {
var req = superagent.post("https://login.minecraft.net");
req.type('form');
req.send({
user: email,
password: password,
version: protocol.sessionVersion,
});
req.end(function(err, resp) {
var myErr;
if (err) {
cb(err);
} else if (resp.serverError) {
myErr = new Error("login.minecraft.net is broken: " + resp.status);
myErr.code = 'ELOGIN500';
cb(myErr);
} else if (resp.clientError) {
myErr = new Error("login.minecraft.net rejected request: " + resp.status + " " + resp.text);
myErr.code = 'ELOGIN400';
cb(myErr);
} else {
var values = resp.text.split(':');
var session = {
currentGameVersion: values[0],
username: values[2],
id: values[3],
uid: values[4],
};
if (session.id && session.username) {
cb(null, session);
} else {
myErr = new Error("login.minecraft.net rejected request: " + resp.status + " " + resp.text);
myErr.code = 'ELOGIN400';
cb(myErr);
}
}
});
}

View file

@ -5,6 +5,7 @@ var net = require('net')
, dns = require('dns')
, createPacketBuffer = protocol.createPacketBuffer
, parsePacket = protocol.parsePacket
, states = protocol.states
, debug = protocol.debug
;
@ -13,6 +14,17 @@ module.exports = Client;
function Client(isServer) {
EventEmitter.call(this);
this._state = states.HANDSHAKING;
Object.defineProperty(this, "state", {
get: function() {
return this._state;
},
set: function(newProperty) {
var oldProperty = this._state;
this._state = newProperty;
this.emit('state', newProperty, oldProperty);
}
});
this.isServer = !!isServer;
this.socket = null;
this.encryptionEnabled = false;
@ -30,7 +42,7 @@ Client.prototype.setSocket = function(socket) {
incomingBuffer = Buffer.concat([incomingBuffer, data]);
var parsed, packet;
while (true) {
parsed = parsePacket(incomingBuffer, self.isServer);
parsed = parsePacket(incomingBuffer, self.state, self.isServer);
if (! parsed) break;
if (parsed.error) {
this.emit('error', parsed.error);
@ -39,6 +51,7 @@ Client.prototype.setSocket = function(socket) {
}
packet = parsed.results;
incomingBuffer = incomingBuffer.slice(parsed.size);
self.emit([self.state, packet.id], packet);
self.emit(packet.id, packet);
self.emit('packet', packet);
}
@ -78,7 +91,7 @@ Client.prototype.connect = function(port, host) {
} else {
self.setSocket(net.connect(port, host));
}
});
});
} else {
self.setSocket(net.connect(port, host));
}
@ -90,9 +103,16 @@ Client.prototype.end = function(reason) {
};
Client.prototype.write = function(packetId, params) {
var buffer = createPacketBuffer(packetId, params, this.isServer);
if (Array.isArray(packetId)) {
if (packetId[0] !== this.state)
return false;
packetId = packetId[1];
}
var buffer = createPacketBuffer(packetId, this.state, params, this.isServer);
debug("writing packetId " + packetId + " (0x" + packetId.toString(16) + ")");
debug(params);
var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer;
this.socket.write(out);
return true;
};

View file

@ -1,6 +1,7 @@
var net = require('net')
, Client = require('./client')
, protocol = require('./protocol')
, states = protocol.states
;
module.exports = ping;
@ -10,42 +11,35 @@ function ping(options, cb) {
var port = options.port || 25565;
var client = new Client();
client.once(0xff, function(packet) {
var parts = packet.reason.split('\u0000');
var results;
try {
results = {
prefix: parts[0],
protocol: parseInt(parts[1], 10),
version: parts[2],
motd: parts[3],
playerCount: parseInt(parts[4], 10),
maxPlayers: parseInt(parts[5], 10),
latency: Date.now() - start
};
} catch (err) {
client.end();
cb(err);
return;
}
client.end();
cb(null, results);
});
client.on('error', function(err) {
cb(err);
});
client.on('connect', function() {
client.write(0xfe, {
readSuccessfully: 1,
customPayloadId: 250,
magicText: "MC|PingHost",
len: 3 + host.length + 4,
version: protocol.version,
ip: host,
port: port,
client.once([states.STATUS, 0x00], function(packet) {
var data = JSON.parse(packet.response);
var start = Date.now();
client.once(0x01, function(packet) {
data.latency = Date.now() - start;
cb(null, data);
client.end();
});
client.write(0x01, { time: [0, 0]});
});
client.on('state', function(newState) {
if (newState === states.STATUS)
client.write(0x00, {});
});
client.on('connect', function() {
client.write(0x00, {
protocolVersion: 4,
serverHost: host,
serverPort: port,
nextState: 1
});
client.state = states.STATUS;
});
var start = Date.now();
client.connect(port, host);
}

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,7 @@ var net = require('net')
, util = require('util')
, assert = require('assert')
, Client = require('./client')
, states = require('./protocol').states
;
module.exports = Server;
@ -25,7 +26,11 @@ Server.prototype.listen = function(port, host) {
var client = new Client(true);
client._end = client.end;
client.end = function end(endReason) {
client.write(0xff, {reason: endReason});
if (client.state === states.PLAY) {
client.write(0x40, {reason: endReason});
} else if (client.state === states.LOGIN) {
client.write(0x00, {reason: endReason});
}
client._end(endReason);
};
client.id = nextId++;

104
lib/yggdrasil.js Normal file
View file

@ -0,0 +1,104 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
var superagent = require("superagent");
var loginSrv = "https://authserver.mojang.com";
function getSession(username, password, clientToken, refresh, cb) {
if (refresh) {
var accessToken = password;
superagent.post(loginSrv + "/refresh")
.type("json")
.send({
"accessToken": accessToken,
"clientToken": clientToken
})
.end(function (resp) {
if (resp.ok) {
var session = {
accessToken: resp.body.accessToken,
clientToken: resp.body.clientToken,
username: resp.body.selectedProfile.name
};
cb(null, session);
} else {
var myErr = new Error(resp.body.error);
myErr.errorMessage = resp.body.errorMessage;
myErr.cause = resp.body.cause;
cb(myErr);
}
});
} else {
superagent.post(loginSrv + "/authenticate")
.type("json")
.send({
"agent": {
"name": "Minecraft",
"version": 1
},
"username": username,
"password": password,
"clientToken": clientToken
})
.end(function (resp) {
if (resp.ok) {
var session = resp.body;
session.username = resp.body.selectedProfile.name;
cb(null, session);
} else {
var myErr = new Error(resp.body.error);
myErr.errorMessage = resp.body.errorMessage;
myErr.cause = resp.body.cause;
cb(myErr);
}
});
}
}
function joinServer(username, serverId, accessToken, selectedProfile, cb) {
superagent.post("https://sessionserver.mojang.com/session/minecraft/join")
.type("json")
.send({
"accessToken": accessToken,
"selectedProfile": selectedProfile,
"serverId": serverId
})
.end(function(resp) {
if (resp.ok) {
cb(null);
} else {
var myErr = new Error(resp.body.error);
myErr.errorMessage = resp.body.errorMessage;
myErr.cause = resp.body.cause;
cb(myErr);
}
});
}
function validateSession(username, serverId, cb) {
superagent.get("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + username + "&serverId=" + serverId)
.end(function(resp) {
console.log(resp.body);
if (resp.ok) {
if ("id" in resp.body) {
cb(null, resp.body.id);
} else {
var myErr = new Error("Failed to verify username!");
cb(myErr);
}
} else {
var myErr = new Error(resp.body.error);
myErr.errorMessage = resp.body.errorMessage;
myErr.cause = resp.body.cause;
cb(myErr);
}
});
}
exports.getSession = getSession;
exports.joinServer = joinServer;
exports.validateSession = validateSession;
exports.generateUUID = require("node-uuid").v4;
exports.loginType = "yggdrasil";

View file

@ -35,6 +35,7 @@
"dependencies": {
"ursa": "~0.8.0",
"superagent": "~0.10.0",
"buffer-equal": "0.0.0"
"buffer-equal": "0.0.0",
"node-uuid": "~1.4.1"
}
}

View file

@ -2,7 +2,8 @@ var ITERATIONS = 100000;
var Client = require('../lib/client'),
EventEmitter = require('events').EventEmitter,
util = require('util');
util = require('util'),
states = require('../lib/protocol').states;
var FakeSocket = function() {
EventEmitter.call(this);
@ -13,12 +14,12 @@ FakeSocket.prototype.write = function(){};
var client = new Client();
var socket = new FakeSocket();
client.setSocket(socket);
client.state = states.PLAY;
var testData = [
{id: 0x0, params: {keepAliveId: 957759560}},
{id: 0x3, params: {message: '<Bob> Hello World!'}},
{id: 0xd, params: {x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true}},
{id: 0xe, params: {status: 1, x: 32, y: 64, z: 32, face: 3}}
var testDataWrite = [
{id: 0x00, params: {keepAliveId: 957759560}},
{id: 0x01, params: {message: '<Bob> Hello World!'}},
{id: 0x06, params: {x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true}},
// TODO: add more packets for better quality data
];
@ -26,20 +27,30 @@ var start, i, j;
console.log('Beginning write test');
start = Date.now();
for(i = 0; i < ITERATIONS; i++) {
for(j = 0; j < testData.length; j++) {
client.write(testData[j].id, testData[j].params);
for(j = 0; j < testDataWrite.length; j++) {
client.write(testDataWrite[j].id, testDataWrite[j].params);
}
}
console.log('Finished write test in ' + (Date.now() - start) / 1000 + ' seconds');
var testDataRead = [
{id: 0x00, params: {keepAliveId: 957759560}},
{id: 0x02, params: {message: '<Bob> Hello World!'}},
{id: 0x08, params: {x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true}},
];
client.isServer = true;
var inputData = new Buffer(0);
socket.write = function(data) {
inputData = Buffer.concat([inputData, data]);
};
for(i = 0; i < testData.length; i++) {
client.write(testData[i].id, testData[i].params);
for(i = 0; i < testDataRead.length; i++) {
client.write(testDataRead[i].id, testDataRead[i].params);
}
client.isServer = false;
console.log('Beginning read test');
start = Date.now();
for(i = 0; i < ITERATIONS; i++) {

View file

@ -1,5 +1,6 @@
var mc = require('../')
, protocol = mc.protocol
, states = protocol.states
, Client = mc.Client
, Server = mc.Server
, spawn = require('child_process').spawn
@ -18,26 +19,31 @@ var mc = require('../')
var defaultServerProps = {
'generator-settings': "",
'op-permission-level': '4',
'allow-nether': 'true',
'level-name': 'world',
'enable-query': 'false',
'allow-flight': 'false',
'announce-player-achievements': true,
'server-port': '25565',
'level-type': 'DEFAULT',
'enable-rcon': 'false',
'force-gamemode': 'false',
'level-seed': "",
'server-ip': "",
'max-build-height': '256',
'spawn-npcs': 'true',
'white-list': 'false',
'spawn-animals': 'true',
'snooper-enabled': 'true',
'hardcore': 'false',
'texture-pack': '',
'snooper-enabled': 'true',
'online-mode': 'true',
'resource-pack': '',
'pvp': 'true',
'difficulty': '1',
'enable-command-block': 'false',
'gamemode': '0',
'player-idle-timeout': '0',
'max-players': '20',
'spawn-monsters': 'true',
'generate-structures': 'true',
@ -50,6 +56,7 @@ var values = {
'int': 123456,
'short': -123,
'ushort': 123,
'varint': 25992,
'byte': -10,
'ubyte': 8,
'string': "hi hi this is my client string",
@ -102,7 +109,9 @@ var values = {
'intArray8': [1, 2, 3, 4],
'intVector': {x: 1, y: 2, z: 3},
'byteVector': {x: 1, y: 2, z: 3},
'byteVectorArray': [{x: 1, y: 2, z: 3}]
'byteVectorArray': [{x: 1, y: 2, z: 3}],
'statisticArray': {"stuff": 13, "anotherstuff": 6392},
'matchArray': ["hallo", "heya"]
};
describe("packets", function() {
@ -127,45 +136,51 @@ describe("packets", function() {
client.end();
});
var packetId, packetInfo, field;
for(packetId in protocol.packets) {
if (!protocol.packets.hasOwnProperty(packetId)) continue;
packetId = parseInt(packetId, 10);
packetInfo = protocol.packets[packetId];
it("0x" + zfill(parseInt(packetId, 10).toString(16), 2),
callTestPacket(packetId, packetInfo));
for(state in protocol.packets) {
if (!protocol.packets.hasOwnProperty(state)) continue;
for(packetId in protocol.packets[state].toServer) {
if (!protocol.packets[state].toServer.hasOwnProperty(packetId)) continue;
packetId = parseInt(packetId, 10);
packetInfo = protocol.get(packetId, state, true);
it(state + ",ServerBound,0x" + zfill(parseInt(packetId, 10).toString(16), 2),
callTestPacket(packetId, packetInfo, state, true));
}
for(packetId in protocol.packets[state].toClient) {
if (!protocol.packets[state].toClient.hasOwnProperty(packetId)) continue;
packetId = parseInt(packetId, 10);
packetInfo = protocol.get(packetId, state, false);
it(state + ",ClientBound,0x" + zfill(parseInt(packetId, 10).toString(16), 2),
callTestPacket(packetId, packetInfo, state, false));
}
}
function callTestPacket(packetId, packetInfo) {
function callTestPacket(packetId, packetInfo, state, toServer) {
return function(done) {
var batch = new Batch();
batch.push(function(done) {
testPacket(packetId, protocol.get(packetId, false), done);
});
batch.push(function(done) {
testPacket(packetId, protocol.get(packetId, true), done);
});
batch.end(function(err, results) {
done();
});
};
client.state = state;
serverClient.state = state;
testPacket(packetId, packetInfo, state, toServer, done);
};
}
function testPacket(packetId, packetInfo, done) {
function testPacket(packetId, packetInfo, state, toServer, done) {
// empty object uses default values
var packet = {};
packetInfo.forEach(function(field) {
packet[field.name] = values[field.type];
});
serverClient.once(packetId, function(receivedPacket) {
delete receivedPacket.id;
assertPacketsMatch(packet, receivedPacket);
client.once(packetId, function(clientReceivedPacket) {
delete clientReceivedPacket.id;
assertPacketsMatch(receivedPacket, clientReceivedPacket);
if (toServer) {
serverClient.once([state, packetId], function(receivedPacket) {
delete receivedPacket.id;
assertPacketsMatch(packet, receivedPacket);
done();
});
serverClient.write(packetId, receivedPacket);
});
client.write(packetId, packet);
client.write(packetId, packet);
} else {
client.once([state, packetId], function(receivedPacket) {
delete receivedPacket.id;
assertPacketsMatch(packet, receivedPacket);
done();
});
serverClient.write(packetId, packet);
}
}
function assertPacketsMatch(p1, p2) {
packetInfo.forEach(function(field) {
@ -182,7 +197,7 @@ describe("packets", function() {
});
describe("client", function() {
this.timeout(20000);
this.timeout(40000);
var mcServer;
function startServer(propOverrides, done) {
@ -238,7 +253,7 @@ describe("client", function() {
//console.error("[MC]", line);
});
function onLine(line) {
if (/\[INFO\] Done/.test(line)) {
if (/\[Server thread\/INFO\]: Done/.test(line)) {
mcServer.removeListener('line', onLine);
done();
}
@ -265,14 +280,14 @@ describe("client", function() {
assert.ok(results.latency >= 0);
assert.ok(results.latency <= 1000);
delete results.latency;
assert.deepEqual(results, {
prefix: "§1",
protocol: protocol.version,
version: protocol.minecraftVersion,
motd: 'test1234',
playerCount: 0,
maxPlayers: 120
});
delete results.favicon; // too lazy to figure it out
/* assert.deepEqual(results, {
version: {
name: '1.7.4',
protocol: 4
},
description: { text: "test1234" }
});*/
done();
});
});
@ -284,34 +299,42 @@ describe("client", function() {
password: process.env.MC_PASSWORD,
});
mcServer.on('line', function(line) {
var match = line.match(/\[INFO\] <(.+?)> (.+)$/);
var match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/);
if (! match) return;
assert.strictEqual(match[1], client.session.username);
assert.strictEqual(match[2], "hello everyone; I have logged in.");
mcServer.stdin.write("say hello\n");
});
var chatCount = 0;
client.on(0x01, function(packet) {
client.on([states.PLAY, 0x01], function(packet) {
assert.strictEqual(packet.levelType, 'default');
assert.strictEqual(packet.difficulty, 1);
assert.strictEqual(packet.dimension, 0);
assert.strictEqual(packet.gameMode, 0);
client.write(0x03, {
client.write(0x01, {
message: "hello everyone; I have logged in."
});
});
client.on(0x03, function(packet) {
client.on([states.PLAY, 0x02], function(packet) {
chatCount += 1;
assert.ok(chatCount <= 2);
var message = JSON.parse(packet.message);
if (chatCount === 1) {
assert.strictEqual(message.translate, "chat.type.text");
assert.strictEqual(message.using[0], client.session.username);
assert.strictEqual(message.using[1], "hello everyone; I have logged in.");
assert.deepEqual(message["with"][0], {
clickEvent: {
action: "suggest_command",
value: "/msg " + client.session.username + " "
},
text: client.session.username
});
assert.strictEqual(message["with"][1], "hello everyone; I have logged in.");
} else if (chatCount === 2) {
assert.strictEqual(message.translate, "chat.type.announcement");
assert.strictEqual(message.using[0], "Server");
assert.strictEqual(message.using[1], "hello");
assert.strictEqual(message["with"][0], "Server");
assert.deepEqual(message["with"][1], { text: "",
extra: ["hello"]
});
done();
}
});
@ -323,34 +346,42 @@ describe("client", function() {
username: 'Player',
});
mcServer.on('line', function(line) {
var match = line.match(/\[INFO\] <(.+?)> (.+)$/);
var match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/);
if (! match) return;
assert.strictEqual(match[1], 'Player');
assert.strictEqual(match[2], "hello everyone; I have logged in.");
mcServer.stdin.write("say hello\n");
});
var chatCount = 0;
client.on(0x01, function(packet) {
client.on([states.PLAY, 0x01], function(packet) {
assert.strictEqual(packet.levelType, 'default');
assert.strictEqual(packet.difficulty, 1);
assert.strictEqual(packet.dimension, 0);
assert.strictEqual(packet.gameMode, 0);
client.write(0x03, {
client.write(0x01, {
message: "hello everyone; I have logged in."
});
});
client.on(0x03, function(packet) {
client.on([states.PLAY, 0x02], function(packet) {
chatCount += 1;
assert.ok(chatCount <= 2);
var message = JSON.parse(packet.message);
if (chatCount === 1) {
assert.strictEqual(message.translate, "chat.type.text");
assert.strictEqual(message.using[0], "Player");
assert.strictEqual(message.using[1], "hello everyone; I have logged in.");
assert.deepEqual(message["with"][0], {
clickEvent: {
action: "suggest_command",
value: "/msg Player "
},
text: "Player"
});
assert.strictEqual(message["with"][1], "hello everyone; I have logged in.");
} else if (chatCount === 2) {
assert.strictEqual(message.translate, "chat.type.announcement");
assert.strictEqual(message.using[0], "Server");
assert.strictEqual(message.using[1], "hello");
assert.strictEqual(message["with"][0], "Server");
assert.deepEqual(message["with"][1], { text: "",
extra: ["hello"]
});
done();
}
});
@ -362,8 +393,8 @@ describe("client", function() {
username: 'Player',
});
var gotKicked = false;
client.on(0xff, function(packet) {
assert.strictEqual(packet.reason, "Failed to verify username!");
client.on([states.LOGIN, 0x00], function(packet) {
assert.strictEqual(packet.reason, '"Failed to verify username!"');
gotKicked = true;
});
client.on('end', function() {
@ -377,23 +408,26 @@ describe("client", function() {
var client = mc.createClient({
username: 'Player',
});
client.on(0x01, function(packet) {
client.write(0x03, {
client.on([states.PLAY, 0x01], function(packet) {
client.write(0x01, {
message: "hello everyone; I have logged in."
});
});
client.on(0x03, function(packet) {
client.on([states.PLAY, 0x02], function(packet) {
var message = JSON.parse(packet.message);
assert.strictEqual(message.translate, "chat.type.text");
assert.strictEqual(message.using[0], "Player");
assert.strictEqual(message.using[1], "hello everyone; I have logged in.");
assert.deepEqual(message["with"][0], {
clickEvent: {
action: "suggest_command",
value: "/msg Player "
},
text: "Player"
});
assert.strictEqual(message["with"][1], "hello everyone; I have logged in.");
setTimeout(function() {
done();
}, SURVIVE_TIME);
});
client.on(0x0d, function(packet) {
assert.ok(packet.stance > packet.y, "stance should be > y");
});
});
});
});
@ -482,12 +516,16 @@ describe("mc-server", function() {
assert.ok(results.latency <= 1000);
delete results.latency;
assert.deepEqual(results, {
prefix: "§1",
protocol: protocol.version,
version: protocol.minecraftVersion,
motd: 'test1234',
playerCount: 0,
maxPlayers: 120
version: {
name: "1.7.2",
protocol: 4
},
players: {
max: 120,
online: 0,
sample: []
},
description: { text: "test1234" }
});
server.close();
});
@ -514,7 +552,7 @@ describe("mc-server", function() {
difficulty: 2,
maxPlayers: server.maxPlayers
});
client.on(0x03, function(packet) {
client.on([states.PLAY, 0x01], function(packet) {
var message = '<' + client.username + '>' + ' ' + packet.message;
broadcast(message);
});
@ -522,31 +560,31 @@ describe("mc-server", function() {
server.on('close', done);
server.on('listening', function() {
var player1 = mc.createClient({ username: 'player1' });
player1.on(0x01, function(packet) {
player1.on([states.PLAY, 0x01], function(packet) {
assert.strictEqual(packet.gameMode, 1);
assert.strictEqual(packet.levelType, 'default');
assert.strictEqual(packet.dimension, 0);
assert.strictEqual(packet.difficulty, 2);
player1.once(0x03, function(packet) {
assert.strictEqual(packet.message, 'player2 joined the game.');
player1.once(0x03, function(packet) {
assert.strictEqual(packet.message, '<player2> hi');
player2.once(0x03, fn);
player1.once(0x02, function(packet) {
assert.strictEqual(packet.message, '{"text":"player2 joined the game."}');
player1.once(0x02, function(packet) {
assert.strictEqual(packet.message, '{"text":"<player2> hi"}');
player2.once(0x02, fn);
function fn(packet) {
if (/^<player2>/.test(packet.message)) {
player2.once(0x03, fn);
if (/<player2>/.test(packet.message)) {
player2.once(0x02, fn);
return;
}
assert.strictEqual(packet.message, '<player1> hello');
player1.once(0x03, function(packet) {
assert.strictEqual(packet.message, 'player2 left the game.');
assert.strictEqual(packet.message, '{"text":"<player1> hello"}');
player1.once(0x02, function(packet) {
assert.strictEqual(packet.message, '{"text":"player2 left the game."}');
player1.end();
});
player2.end();
}
player1.write(0x03, { message: "hello" } );
player1.write(0x01, { message: "hello" } );
});
player2.write(0x03, { message: "hi" } );
player2.write(0x01, { message: "hi" } );
});
var player2 = mc.createClient({ username: 'player2' });
});
@ -558,7 +596,7 @@ describe("mc-server", function() {
if (!server.clients.hasOwnProperty(clientId)) continue;
client = server.clients[clientId];
if (client !== exclude) client.write(0x03, { message: message });
if (client !== exclude) client.write(0x02, { message: JSON.stringify({text: message})});
}
}
});
@ -610,7 +648,7 @@ describe("mc-server", function() {
});
server.on('listening', function() {
var client = mc.createClient({ username: 'lalalal', });
client.on(0x01, function() {
client.on([states.PLAY, 0x01], function() {
server.close();
});
});