mirror of
https://github.com/unmojang/node-minecraft-protocol.git
synced 2025-10-04 16:40:25 -04:00

This change breaks backwards compatibility. `createClient` no longer takes an `email` argument. Instead, the `username` argument is used to authenticate and determine the case correct username. There is a special case if you leave out the `password` argument. In this case, `username` is used to connect directly to the server, and you may get kicked if the server is in online mode.
591 lines
17 KiB
JavaScript
591 lines
17 KiB
JavaScript
var mc = require('../')
|
|
, protocol = mc.protocol
|
|
, Client = mc.Client
|
|
, Server = mc.Server
|
|
, spawn = require('child_process').spawn
|
|
, path = require('path')
|
|
, fs = require('fs')
|
|
, net = require('net')
|
|
, assert = require('assert')
|
|
, mkdirp = require('mkdirp')
|
|
, rimraf = require('rimraf')
|
|
, Batch = require('batch')
|
|
, zfill = require('zfill')
|
|
, MC_SERVER_JAR = process.env.MC_SERVER_JAR
|
|
, SURVIVE_TIME = 10000
|
|
, MC_SERVER_PATH = path.join(__dirname, 'server')
|
|
|
|
var defaultServerProps = {
|
|
'generator-settings': "",
|
|
'allow-nether': 'true',
|
|
'level-name': 'world',
|
|
'enable-query': 'false',
|
|
'allow-flight': 'false',
|
|
'server-port': '25565',
|
|
'level-type': 'DEFAULT',
|
|
'enable-rcon': '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': '',
|
|
'online-mode': 'true',
|
|
'pvp': 'true',
|
|
'difficulty': '1',
|
|
'gamemode': '0',
|
|
'max-players': '20',
|
|
'spawn-monsters': 'true',
|
|
'generate-structures': 'true',
|
|
'view-distance': '10',
|
|
'spawn-protection': '16',
|
|
'motd': 'A Minecraft Server',
|
|
};
|
|
|
|
var values = {
|
|
'int': 123456,
|
|
'short': -123,
|
|
'ushort': 123,
|
|
'byte': -10,
|
|
'ubyte': 8,
|
|
'string': "hi hi this is my string",
|
|
'byteArray16': new Buffer(8),
|
|
'bool': true,
|
|
'double': 99999.2222,
|
|
'float': -333.444,
|
|
'slot': {
|
|
id: 5,
|
|
itemCount: 56,
|
|
itemDamage: 2,
|
|
nbtData: new Buffer(90),
|
|
},
|
|
'ascii': "hello",
|
|
'byteArray32': new Buffer(10),
|
|
'long': [0, 1],
|
|
'slotArray': [{
|
|
id: 41,
|
|
itemCount: 2,
|
|
itemDamage: 3,
|
|
nbtData: new Buffer(0),
|
|
}],
|
|
'mapChunkBulk': {
|
|
skyLightSent: true,
|
|
compressedChunkData: new Buffer(1234),
|
|
meta: [{
|
|
x: 23,
|
|
z: 64,
|
|
bitMap: 3,
|
|
addBitMap: 10,
|
|
}],
|
|
},
|
|
'entityMetadata': [{
|
|
type: 'int',
|
|
key: 3,
|
|
value: 100,
|
|
}],
|
|
'objectData': {
|
|
intField: 9,
|
|
velocityX: 1,
|
|
velocityY: 2,
|
|
velocityZ: 3,
|
|
},
|
|
'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}],
|
|
};
|
|
|
|
describe("packets", function() {
|
|
var client, server, serverClient;
|
|
before(function(done) {
|
|
server = new Server();
|
|
server.once('listening', function() {
|
|
server.once('connection', function(c) {
|
|
serverClient = c;
|
|
done();
|
|
});
|
|
client = new Client();
|
|
client.setSocket(net.connect(25565, 'localhost'));
|
|
});
|
|
server.listen(25565, 'localhost');
|
|
});
|
|
after(function(done) {
|
|
client.on('end', function() {
|
|
server.on('close', done);
|
|
server.close();
|
|
});
|
|
client.end();
|
|
});
|
|
var packetId, packetInfo, field;
|
|
for(packetId in protocol.packets) {
|
|
packetId = parseInt(packetId, 10);
|
|
packetInfo = protocol.packets[packetId];
|
|
it("0x" + zfill(parseInt(packetId, 10).toString(16), 2), callTestPacket(packetId, packetInfo));
|
|
}
|
|
function callTestPacket(packetId, packetInfo) {
|
|
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(done);
|
|
};
|
|
}
|
|
function testPacket(packetId, packetInfo, done) {
|
|
// empty object uses default values
|
|
var packet = {};
|
|
packetInfo.forEach(function(field) {
|
|
var value = field.type;
|
|
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);
|
|
done();
|
|
});
|
|
serverClient.write(packetId, receivedPacket);
|
|
});
|
|
client.write(packetId, packet);
|
|
}
|
|
function assertPacketsMatch(p1, p2) {
|
|
packetInfo.forEach(function(field) {
|
|
assert.deepEqual(p1[field], p2[field]);
|
|
});
|
|
var field, cmp;
|
|
for (field in p1) {
|
|
assert.ok(field in p2, "field " + field + " missing in p2");
|
|
}
|
|
for (field in p2) {
|
|
assert.ok(field in p1, "field " + field + " missing in p1");
|
|
}
|
|
}
|
|
});
|
|
|
|
describe("client", function() {
|
|
this.timeout(20000);
|
|
|
|
var mcServer;
|
|
function startServer(propOverrides, done) {
|
|
var props = {};
|
|
var prop;
|
|
for (prop in defaultServerProps) {
|
|
props[prop] = defaultServerProps[prop];
|
|
}
|
|
for (prop in propOverrides) {
|
|
props[prop] = propOverrides[prop];
|
|
}
|
|
var batch = new Batch();
|
|
batch.push(function(cb) { mkdirp(MC_SERVER_PATH, cb); });
|
|
batch.push(function(cb) {
|
|
var str = "";
|
|
for (var prop in props) {
|
|
str += prop + "=" + props[prop] + "\n";
|
|
}
|
|
fs.writeFile(path.join(MC_SERVER_PATH, "server.properties"), str, cb);
|
|
});
|
|
batch.end(function(err) {
|
|
if (err) return done(err);
|
|
mcServer = spawn('java', [ '-jar', MC_SERVER_JAR, 'nogui'], {
|
|
stdio: 'pipe',
|
|
cwd: MC_SERVER_PATH,
|
|
});
|
|
mcServer.stdin.setEncoding('utf8');
|
|
mcServer.stdout.setEncoding('utf8');
|
|
mcServer.stderr.setEncoding('utf8');
|
|
var buffer = "";
|
|
mcServer.stdout.on('data', onData);
|
|
mcServer.stderr.on('data', onData);
|
|
function onData(data) {
|
|
buffer += data;
|
|
var lines = buffer.split("\n");
|
|
var len = lines.length - 1;
|
|
for (var i = 0; i < len; ++i) {
|
|
mcServer.emit('line', lines[i]);
|
|
}
|
|
buffer = lines[lines.length - 1];
|
|
}
|
|
mcServer.on('line', onLine);
|
|
mcServer.on('line', function(line) {
|
|
process.stderr.write('.');
|
|
// uncomment this line when debugging for more insight as to what is
|
|
// happening on the minecraft server
|
|
//console.error("[MC]", line);
|
|
});
|
|
function onLine(line) {
|
|
if (/\[INFO\] Done/.test(line)) {
|
|
mcServer.removeListener('line', onLine);
|
|
done();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
afterEach(function(done) {
|
|
mcServer.stdin.write("stop\n");
|
|
mcServer.on('exit', function() {
|
|
mcServer = null;
|
|
done();
|
|
});
|
|
});
|
|
after(function(done) {
|
|
rimraf(MC_SERVER_PATH, done);
|
|
});
|
|
it("pings the server", function(done) {
|
|
startServer({
|
|
motd: 'test1234',
|
|
'max-players': 120,
|
|
}, function() {
|
|
mc.ping({}, function(err, results) {
|
|
if (err) return done(err);
|
|
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
|
|
});
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it("connects successfully - online mode", function(done) {
|
|
startServer({ 'online-mode': 'true' }, function() {
|
|
var client = mc.createClient({
|
|
username: process.env.MC_USERNAME,
|
|
password: process.env.MC_PASSWORD,
|
|
});
|
|
mcServer.on('line', function(line) {
|
|
var match = line.match(/\[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) {
|
|
assert.strictEqual(packet.levelType, 'default');
|
|
assert.strictEqual(packet.difficulty, 1);
|
|
assert.strictEqual(packet.dimension, 0);
|
|
assert.strictEqual(packet.gameMode, 0);
|
|
client.write(0x03, {
|
|
message: "hello everyone; I have logged in."
|
|
});
|
|
});
|
|
client.on(0x03, function(packet) {
|
|
chatCount += 1;
|
|
assert.ok(chatCount <= 2);
|
|
if (chatCount === 1) {
|
|
assert.strictEqual(packet.message, "<" + client.session.username + ">" + " hello everyone; I have logged in.");
|
|
} else if (chatCount === 2) {
|
|
assert.strictEqual(packet.message, "[Server] hello");
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
it("connects successfully - offline mode", function(done) {
|
|
startServer({ 'online-mode': 'false' }, function() {
|
|
var client = mc.createClient({
|
|
username: 'Player',
|
|
});
|
|
mcServer.on('line', function(line) {
|
|
var match = line.match(/\[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) {
|
|
assert.strictEqual(packet.levelType, 'default');
|
|
assert.strictEqual(packet.difficulty, 1);
|
|
assert.strictEqual(packet.dimension, 0);
|
|
assert.strictEqual(packet.gameMode, 0);
|
|
client.write(0x03, {
|
|
message: "hello everyone; I have logged in."
|
|
});
|
|
});
|
|
client.on(0x03, function(packet) {
|
|
chatCount += 1;
|
|
assert.ok(chatCount <= 2);
|
|
if (chatCount === 1) {
|
|
assert.strictEqual(packet.message, "<Player>" + " hello everyone; I have logged in.");
|
|
} else if (chatCount === 2) {
|
|
assert.strictEqual(packet.message, "[Server] hello");
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
it("gets kicked when no credentials supplied in online mode", function(done) {
|
|
startServer({ 'online-mode': 'true' }, function() {
|
|
var client = mc.createClient({
|
|
username: 'Player',
|
|
});
|
|
var gotKicked = false;
|
|
client.on(0xff, function(packet) {
|
|
assert.strictEqual(packet.reason, "Failed to verify username!");
|
|
gotKicked = true;
|
|
});
|
|
client.on('end', function() {
|
|
assert.ok(gotKicked);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it("does not crash for " + SURVIVE_TIME + "ms", function(done) {
|
|
startServer({ 'online-mode': 'false' }, function() {
|
|
var client = mc.createClient({
|
|
username: 'Player',
|
|
});
|
|
client.on(0x01, function(packet) {
|
|
client.write(0x03, {
|
|
message: "hello everyone; I have logged in."
|
|
});
|
|
});
|
|
client.on(0x03, function(packet) {
|
|
assert.strictEqual(packet.message, "<Player>" + " 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");
|
|
});
|
|
});
|
|
});
|
|
});
|
|
describe("mc-server", function() {
|
|
it("starts listening and shuts down cleanly", function(done) {
|
|
var server = mc.createServer({ 'online-mode': false });
|
|
var listening = false;
|
|
server.on('listening', function() {
|
|
listening = true;
|
|
server.close();
|
|
});
|
|
server.on('close', function() {
|
|
assert.ok(listening);
|
|
done();
|
|
});
|
|
});
|
|
it("kicks clients that do not log in", function(done) {
|
|
var server = mc.createServer({
|
|
'online-mode': false,
|
|
kickTimeout: 100,
|
|
checkTimeoutInterval: 10,
|
|
});
|
|
var count = 2;
|
|
server.on('connection', function(client) {
|
|
client.on('end', function(reason) {
|
|
assert.strictEqual(reason, "LoginTimeout");
|
|
server.close();
|
|
});
|
|
});
|
|
server.on('close', function() {
|
|
resolve();
|
|
});
|
|
server.on('listening', function() {
|
|
var client = new mc.Client();
|
|
client.on('end', function() {
|
|
resolve();
|
|
});
|
|
client.connect(25565, 'localhost');
|
|
});
|
|
|
|
function resolve() {
|
|
count -= 1;
|
|
if (count <= 0) done();
|
|
}
|
|
});
|
|
it("kicks clients that do not send keepalive packets", function(done) {
|
|
var server = mc.createServer({
|
|
'online-mode': false,
|
|
kickTimeout: 100,
|
|
checkTimeoutInterval: 10,
|
|
});
|
|
var count = 2;
|
|
server.on('connection', function(client) {
|
|
client.on('end', function(reason) {
|
|
assert.strictEqual(reason, "KeepAliveTimeout");
|
|
server.close();
|
|
});
|
|
});
|
|
server.on('close', function() {
|
|
resolve();
|
|
});
|
|
server.on('listening', function() {
|
|
var client = mc.createClient({
|
|
username: 'superpants',
|
|
keepAlive: false,
|
|
});
|
|
client.on('end', function() {
|
|
resolve();
|
|
});
|
|
});
|
|
function resolve() {
|
|
count -= 1;
|
|
if (count <= 0) done();
|
|
}
|
|
});
|
|
it("responds to ping requests", function(done) {
|
|
var server = mc.createServer({
|
|
'online-mode': false,
|
|
motd: 'test1234',
|
|
'max-players': 120,
|
|
});
|
|
server.on('listening', function() {
|
|
mc.ping({}, function(err, results) {
|
|
if (err) return done(err);
|
|
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
|
|
});
|
|
server.close();
|
|
});
|
|
});
|
|
server.on('close', done);
|
|
});
|
|
it("clients can log in and chat", function(done) {
|
|
var server = mc.createServer({ 'online-mode': false, });
|
|
var username = ['player1', 'player2'];
|
|
var index = 0;
|
|
server.on('login', function(client) {
|
|
assert.notEqual(client.id, null);
|
|
assert.strictEqual(client.username, username[index++]);
|
|
broadcast(client.username + ' joined the game.');
|
|
client.on('end', function() {
|
|
broadcast(client.username + ' left the game.', client);
|
|
if (client.username === 'player2') server.close();
|
|
});
|
|
client.write(0x01, {
|
|
entityId: client.id,
|
|
levelType: 'default',
|
|
gameMode: 1,
|
|
dimension: 0,
|
|
difficulty: 2,
|
|
maxPlayers: server.maxPlayers
|
|
});
|
|
client.on(0x03, function(packet) {
|
|
var message = '<' + client.username + '>' + ' ' + packet.message;
|
|
broadcast(message);
|
|
});
|
|
});
|
|
server.on('close', done);
|
|
server.on('listening', function() {
|
|
var player1 = mc.createClient({ username: 'player1' });
|
|
player1.on(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);
|
|
function fn(packet) {
|
|
if (/^<player2>/.test(packet.message)) {
|
|
player2.once(0x03, fn);
|
|
return;
|
|
}
|
|
assert.strictEqual(packet.message, '<player1> hello');
|
|
player1.once(0x03, function(packet) {
|
|
assert.strictEqual(packet.message, 'player2 left the game.');
|
|
player1.end();
|
|
});
|
|
player2.end();
|
|
}
|
|
player1.write(0x03, { message: "hello" } );
|
|
});
|
|
player2.write(0x03, { message: "hi" } );
|
|
});
|
|
var player2 = mc.createClient({ username: 'player2' });
|
|
});
|
|
});
|
|
|
|
function broadcast(message, exclude) {
|
|
var client;
|
|
for (var clientId in server.clients) {
|
|
client = server.clients[clientId];
|
|
if (client !== exclude) client.write(0x03, { message: message });
|
|
}
|
|
}
|
|
});
|
|
it("kicks clients when invalid credentials", function(done) {
|
|
var server = mc.createServer();
|
|
var count = 4;
|
|
server.on('connection', function(client) {
|
|
client.on('end', function(reason) {
|
|
resolve();
|
|
server.close();
|
|
});
|
|
});
|
|
server.on('close', function() {
|
|
resolve();
|
|
});
|
|
server.on('listening', function() {
|
|
resolve();
|
|
var client = mc.createClient({
|
|
username: 'lalalal',
|
|
});
|
|
client.on('end', function() {
|
|
resolve();
|
|
});
|
|
});
|
|
function resolve() {
|
|
count -= 1;
|
|
if (count <= 0) done();
|
|
}
|
|
});
|
|
it("gives correct reason for kicking clients when shutting down", function(done) {
|
|
var server = mc.createServer({ 'online-mode': false, });
|
|
var count = 2;
|
|
server.on('login', function(client) {
|
|
client.on('end', function(reason) {
|
|
assert.strictEqual(reason, 'ServerShutdown');
|
|
resolve();
|
|
});
|
|
client.write(0x01, {
|
|
entityId: client.id,
|
|
levelType: 'default',
|
|
gameMode: 1,
|
|
dimension: 0,
|
|
difficulty: 2,
|
|
maxPlayers: server.maxPlayers
|
|
});
|
|
});
|
|
server.on('close', function() {
|
|
resolve();
|
|
});
|
|
server.on('listening', function() {
|
|
var client = mc.createClient({ username: 'lalalal', });
|
|
client.on(0x01, function() {
|
|
server.close();
|
|
});
|
|
});
|
|
function resolve() {
|
|
count -= 1;
|
|
if (count <= 0) done();
|
|
}
|
|
});
|
|
});
|