mirror of
https://github.com/unmojang/node-minecraft-protocol.git
synced 2025-09-28 05:35:56 -04:00
Merge branch 'server'
This commit is contained in:
commit
d6bd6feb9a
36
README.md
36
README.md
@ -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
|
||||||
@ -45,6 +45,38 @@ client.on(0x03, function(packet) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Hello World server example
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mc = require('minecraft-protocol');
|
||||||
|
var server = mc.createServer({
|
||||||
|
'online-mode': true, // optional
|
||||||
|
encryption: true, // optional
|
||||||
|
host: '0.0.0.0', // optional
|
||||||
|
port: 25565, // optional
|
||||||
|
});
|
||||||
|
server.on('login', function(client) {
|
||||||
|
client.write(0x01, {
|
||||||
|
entityId: 0,
|
||||||
|
levelType: 'default',
|
||||||
|
gameMode: 0,
|
||||||
|
dimension: 0,
|
||||||
|
difficulty: 2,
|
||||||
|
maxPlayers: server.maxPlayers
|
||||||
|
});
|
||||||
|
client.write(0x0d, {
|
||||||
|
x: 0,
|
||||||
|
y: 1.62,
|
||||||
|
stance: 0,
|
||||||
|
z: 0,
|
||||||
|
yaw: 0,
|
||||||
|
pitch: 0,
|
||||||
|
onGround: true
|
||||||
|
});
|
||||||
|
client.write(0x03, { message: 'Hello, ' + client.username });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
* Ensure your system has the `java` executable in `PATH`.
|
* Ensure your system has the `java` executable in `PATH`.
|
||||||
|
76
examples/server.js
Normal file
76
examples/server.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
var mc = require('../');
|
||||||
|
|
||||||
|
var yellow = '§e';
|
||||||
|
var players = [];
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
'online-mode': false,
|
||||||
|
motd: 'Vox Industries',
|
||||||
|
'max-players': 127,
|
||||||
|
port: 25565,
|
||||||
|
};
|
||||||
|
|
||||||
|
var server = mc.createServer(options);
|
||||||
|
|
||||||
|
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+')');
|
||||||
|
|
||||||
|
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+')');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('error', function(error) {
|
||||||
|
console.log('Error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('listening', function() {
|
||||||
|
console.log('Server listening on port', server.socket.address().port);
|
||||||
|
});
|
||||||
|
|
||||||
|
function broadcast(message, exclude) {
|
||||||
|
for(var i = 0; i < players.length; i++) {
|
||||||
|
if(players[i].username !== exclude && i !== exclude && players[i] !== exclude) {
|
||||||
|
players[i].client.write(0x03, { message: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
examples/server_helloworld.js
Normal file
45
examples/server_helloworld.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
var mc = require('../');
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
'online-mode': false, // optional
|
||||||
|
};
|
||||||
|
|
||||||
|
var server = mc.createServer(options);
|
||||||
|
|
||||||
|
server.on('login', function(client) {
|
||||||
|
var addr = client.socket.remoteAddress;
|
||||||
|
console.log('Incoming connection', '('+addr+')');
|
||||||
|
|
||||||
|
client.on('end', function() {
|
||||||
|
console.log('Connection closed', '('+addr+')');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
stance: 0,
|
||||||
|
z: 0,
|
||||||
|
yaw: 0,
|
||||||
|
pitch: 0,
|
||||||
|
onGround: true
|
||||||
|
});
|
||||||
|
|
||||||
|
client.write(0x03, { message: 'Hello, world!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('error', function(error) {
|
||||||
|
console.log('Error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('listening', function() {
|
||||||
|
console.log('Server listening on port', server.socket.address().port);
|
||||||
|
});
|
726
index.js
726
index.js
@ -1,19 +1,97 @@
|
|||||||
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')
|
||||||
, Iconv = require('iconv').Iconv
|
|
||||||
, ursa = require('ursa')
|
, ursa = require('ursa')
|
||||||
, crypto = require('crypto')
|
, crypto = require('crypto')
|
||||||
, superagent = require('superagent')
|
, superagent = require('superagent')
|
||||||
, Batch = require('batch')
|
, Batch = require('batch')
|
||||||
, packets = require('./packets')
|
, protocol = require('./lib/protocol')
|
||||||
, toUcs2 = new Iconv('UTF-8', 'utf16be')
|
, Client = require('./lib/client')
|
||||||
, fromUcs2 = new Iconv('utf16be', 'UTF-8')
|
, Server = require('./lib/server')
|
||||||
|
|
||||||
require('buffer-more-ints');
|
|
||||||
|
|
||||||
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
|
||||||
@ -23,16 +101,21 @@ 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, {
|
||||||
protocolVersion: packets.meta.protocolVersion,
|
protocolVersion: protocol.version,
|
||||||
username: options.username,
|
username: options.username,
|
||||||
serverHost: host,
|
serverHost: host,
|
||||||
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);
|
||||||
@ -136,617 +219,7 @@ 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;
|
|
||||||
hax(packet); // fuck you, notch
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
var writers = {
|
|
||||||
'int': IntWriter,
|
|
||||||
'short': ShortWriter,
|
|
||||||
'byte': ByteWriter,
|
|
||||||
'ubyte': UByteWriter,
|
|
||||||
'string': StringWriter,
|
|
||||||
'byteArray16': ByteArray16Writer,
|
|
||||||
'bool': BoolWriter,
|
|
||||||
'double': DoubleWriter,
|
|
||||||
'float': FloatWriter,
|
|
||||||
'slot': SlotWriter,
|
|
||||||
};
|
|
||||||
|
|
||||||
var readers = {
|
|
||||||
'string': readString,
|
|
||||||
'ascii': readAscii,
|
|
||||||
'byteArray16': readByteArray16,
|
|
||||||
'byteArray32': readByteArray32,
|
|
||||||
'short': readShort,
|
|
||||||
'ushort': readUShort,
|
|
||||||
'int': readInt,
|
|
||||||
'byte': readByte,
|
|
||||||
'ubyte': readUByte,
|
|
||||||
'long': readLong,
|
|
||||||
'slot': readSlot,
|
|
||||||
'bool': readBool,
|
|
||||||
'double': readDouble,
|
|
||||||
'float': readFloat,
|
|
||||||
'slotArray': readSlotArray,
|
|
||||||
'mapChunkBulk': readMapChunkBulk,
|
|
||||||
'entityMetadata': readEntityMetadata,
|
|
||||||
'objectData': readObjectData,
|
|
||||||
'intArray': readIntArray,
|
|
||||||
'intVector': readIntVector,
|
|
||||||
'byteVector': readByteVector,
|
|
||||||
'byteVectorArray': readByteVectorArray,
|
|
||||||
};
|
|
||||||
|
|
||||||
function readIntArray(buffer, offset) {
|
|
||||||
var results = readByte(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
var count = results.value;
|
|
||||||
var cursor = offset + results.size;
|
|
||||||
|
|
||||||
var cursorEnd = cursor + 4 * count;
|
|
||||||
if (cursorEnd > buffer.length) return null;
|
|
||||||
var array = [];
|
|
||||||
for (var i = 0; i < count; ++i) {
|
|
||||||
array.push(buffer.readInt32BE(cursor));
|
|
||||||
cursor += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: array,
|
|
||||||
size: cursorEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var entityMetadataReaders = {
|
|
||||||
0: readByte,
|
|
||||||
1: readShort,
|
|
||||||
2: readInt,
|
|
||||||
3: readFloat,
|
|
||||||
4: readString,
|
|
||||||
5: readSlot,
|
|
||||||
6: readIntVector,
|
|
||||||
};
|
|
||||||
|
|
||||||
function readByteVectorArray(buffer, offset) {
|
|
||||||
var results = readInt(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
var count = results.value;
|
|
||||||
var cursor = offset + results.size;
|
|
||||||
var cursorEnd = cursor + 3 * count;
|
|
||||||
if (cursorEnd > buffer.length) return null;
|
|
||||||
|
|
||||||
var array = [];
|
|
||||||
for (var i = 0; i < count; ++i) {
|
|
||||||
array.push({
|
|
||||||
x: buffer.readInt8(cursor),
|
|
||||||
y: buffer.readInt8(cursor + 1),
|
|
||||||
z: buffer.readInt8(cursor + 2),
|
|
||||||
});
|
|
||||||
cursor += 3;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: array,
|
|
||||||
size: cursorEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readByteVector(buffer, offset) {
|
|
||||||
if (offset + 3 > buffer.length) return null;
|
|
||||||
return {
|
|
||||||
value: {
|
|
||||||
x: buffer.readInt8(offset),
|
|
||||||
y: buffer.readInt8(offset + 1),
|
|
||||||
z: buffer.readInt8(offset + 2),
|
|
||||||
},
|
|
||||||
size: 3,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readIntVector(buffer, offset) {
|
|
||||||
if (offset + 12 > buffer.length) return null;
|
|
||||||
return {
|
|
||||||
value: {
|
|
||||||
x: buffer.readInt32BE(offset),
|
|
||||||
y: buffer.readInt32BE(offset + 4),
|
|
||||||
z: buffer.readInt32BE(offset + 8),
|
|
||||||
},
|
|
||||||
size: 12,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readEntityMetadata(buffer, offset) {
|
|
||||||
var cursor = offset;
|
|
||||||
var metadata = {};
|
|
||||||
var item, key, type, results, reader;
|
|
||||||
while (true) {
|
|
||||||
if (cursor + 1 > buffer.length) return null;
|
|
||||||
item = buffer.readUInt8(cursor);
|
|
||||||
cursor += 1;
|
|
||||||
if (item === 0x7f) break;
|
|
||||||
key = item & 0x1f;
|
|
||||||
type = item >> 5;
|
|
||||||
reader = entityMetadataReaders[type];
|
|
||||||
assert.ok(reader, "missing reader for entity metadata type " + type);
|
|
||||||
results = reader(buffer, cursor);
|
|
||||||
if (! results) return null;
|
|
||||||
metadata[key] = results.value;
|
|
||||||
cursor += results.size;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: metadata,
|
|
||||||
size: cursor - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readObjectData(buffer, offset) {
|
|
||||||
var cursor = offset + 4;
|
|
||||||
if (cursor > buffer.length) return null;
|
|
||||||
var intField = buffer.readInt32BE(offset);
|
|
||||||
|
|
||||||
if (intField === 0) {
|
|
||||||
return {
|
|
||||||
value: {
|
|
||||||
intField: intField,
|
|
||||||
},
|
|
||||||
size: cursor - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor + 6 > buffer.length) return null;
|
|
||||||
var velocityX = buffer.readInt16BE(cursor);
|
|
||||||
cursor += 2;
|
|
||||||
var velocityY = buffer.readInt16BE(cursor);
|
|
||||||
cursor += 2;
|
|
||||||
var velocityZ = buffer.readInt16BE(cursor);
|
|
||||||
cursor += 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: {
|
|
||||||
intField: intField,
|
|
||||||
velocityX: velocityX,
|
|
||||||
velocityY: velocityY,
|
|
||||||
velocityZ: velocityZ,
|
|
||||||
},
|
|
||||||
size: cursor - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readMapChunkBulk (buffer, offset) {
|
|
||||||
var cursor = offset + 7;
|
|
||||||
if (cursor > buffer.length) return null;
|
|
||||||
var chunkCount = buffer.readInt16BE(offset);
|
|
||||||
var dataSize = buffer.readInt32BE(offset + 2);
|
|
||||||
var skyLightSent = !!buffer.readInt8(offset + 6);
|
|
||||||
|
|
||||||
var cursorEnd = cursor + dataSize + 12 * chunkCount;
|
|
||||||
if (cursorEnd > buffer.length) return null;
|
|
||||||
|
|
||||||
var compressedChunkDataEnd = cursor + dataSize;
|
|
||||||
var compressedChunkData = buffer.slice(cursor, compressedChunkDataEnd);
|
|
||||||
cursor = compressedChunkDataEnd;
|
|
||||||
|
|
||||||
var meta = [];
|
|
||||||
var i, chunkX, chunkZ, primaryBitMap, addBitMap;
|
|
||||||
for (i = 0; i < chunkCount; ++i) {
|
|
||||||
chunkX = buffer.readInt32BE(cursor);
|
|
||||||
cursor += 4;
|
|
||||||
chunkZ = buffer.readInt32BE(cursor);
|
|
||||||
cursor += 4;
|
|
||||||
primaryBitMap = buffer.readUInt16BE(cursor);
|
|
||||||
cursor += 2;
|
|
||||||
addBitMap = buffer.readUInt16BE(cursor);
|
|
||||||
cursor += 2;
|
|
||||||
|
|
||||||
meta.push({
|
|
||||||
chunkX: chunkX,
|
|
||||||
chunkZ: chunkZ,
|
|
||||||
primaryBitMap: primaryBitMap,
|
|
||||||
addBitMap: addBitMap,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: {
|
|
||||||
skyLightSent: skyLightSent,
|
|
||||||
compressedChunkData: compressedChunkData,
|
|
||||||
meta: meta,
|
|
||||||
},
|
|
||||||
size: cursorEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readAscii (buffer, offset) {
|
|
||||||
var results = readShort(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
|
|
||||||
var strBegin = offset + results.size;
|
|
||||||
var strLen = results.value;
|
|
||||||
var strEnd = strBegin + strLen;
|
|
||||||
if (strEnd > buffer.length) return null;
|
|
||||||
var str = buffer.slice(strBegin, strEnd).toString('ascii');
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: str,
|
|
||||||
size: strEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readString (buffer, offset) {
|
|
||||||
var results = readShort(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
|
|
||||||
var strBegin = offset + results.size;
|
|
||||||
var strLen = results.value;
|
|
||||||
var strEnd = strBegin + strLen * 2;
|
|
||||||
if (strEnd > buffer.length) return null;
|
|
||||||
var str = fromUcs2.convert(buffer.slice(strBegin, strEnd)).toString('utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: str,
|
|
||||||
size: strEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readByteArray16 (buffer, offset) {
|
|
||||||
var results = readShort(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
|
|
||||||
var bytesBegin = offset + results.size;
|
|
||||||
var bytesSize = results.value;
|
|
||||||
var bytesEnd = bytesBegin + bytesSize;
|
|
||||||
if (bytesEnd > buffer.length) return null;
|
|
||||||
var bytes = buffer.slice(bytesBegin, bytesEnd);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: bytes,
|
|
||||||
size: bytesEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readByteArray32(buffer, offset) {
|
|
||||||
var results = readInt(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
|
|
||||||
var bytesBegin = offset + results.size;
|
|
||||||
var bytesSize = results.value;
|
|
||||||
var bytesEnd = bytesBegin + bytesSize;
|
|
||||||
if (bytesEnd > buffer.length) return null;
|
|
||||||
var bytes = buffer.slice(bytesBegin, bytesEnd);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: bytes,
|
|
||||||
size: bytesEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readSlotArray (buffer, offset) {
|
|
||||||
var results = readShort(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
var count = results.value;
|
|
||||||
var cursor = offset + results.size;
|
|
||||||
|
|
||||||
var slotArray = [];
|
|
||||||
for (var i = 0; i < count; ++i) {
|
|
||||||
results = readSlot(buffer, cursor);
|
|
||||||
if (! results) return null;
|
|
||||||
slotArray.push(results.value);
|
|
||||||
cursor += results.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: slotArray,
|
|
||||||
size: cursor - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readShort(buffer, offset) {
|
|
||||||
if (offset + 2 > buffer.length) return null;
|
|
||||||
var value = buffer.readInt16BE(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readUShort(buffer, offset) {
|
|
||||||
if (offset + 2 > buffer.length) return null;
|
|
||||||
var value = buffer.readUInt16BE(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readInt(buffer, offset) {
|
|
||||||
if (offset + 4 > buffer.length) return null;
|
|
||||||
var value = buffer.readInt32BE(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readFloat(buffer, offset) {
|
|
||||||
if (offset + 4 > buffer.length) return null;
|
|
||||||
var value = buffer.readFloatBE(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readDouble(buffer, offset) {
|
|
||||||
if (offset + 8 > buffer.length) return null;
|
|
||||||
var value = buffer.readDoubleBE(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 8,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readLong(buffer, offset) {
|
|
||||||
if (offset + 8 > buffer.length) return null;
|
|
||||||
var value = buffer.readInt64BE(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 8,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readByte(buffer, offset) {
|
|
||||||
if (offset + 1 > buffer.length) return null;
|
|
||||||
var value = buffer.readInt8(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readUByte(buffer, offset) {
|
|
||||||
if (offset + 1 > buffer.length) return null;
|
|
||||||
var value = buffer.readUInt8(offset);
|
|
||||||
return {
|
|
||||||
value: value,
|
|
||||||
size: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBool(buffer, offset) {
|
|
||||||
if (offset + 1 > buffer.length) return null;
|
|
||||||
var value = buffer.readInt8(offset);
|
|
||||||
return {
|
|
||||||
value: !!value,
|
|
||||||
size: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readSlot(buffer, offset) {
|
|
||||||
var results = readShort(buffer, offset);
|
|
||||||
if (! results) return null;
|
|
||||||
var blockId = results.value;
|
|
||||||
var cursor = offset + results.size;
|
|
||||||
|
|
||||||
if (blockId === -1) {
|
|
||||||
return {
|
|
||||||
value: { id: blockId },
|
|
||||||
size: cursor - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var cursorEnd = cursor + 5;
|
|
||||||
if (cursorEnd > buffer.length) return null;
|
|
||||||
var itemCount = buffer.readInt8(cursor);
|
|
||||||
var itemDamage = buffer.readInt16BE(cursor + 1);
|
|
||||||
var nbtDataSize = buffer.readInt16BE(cursor + 3);
|
|
||||||
if (nbtDataSize === -1) nbtDataSize = 0;
|
|
||||||
var nbtDataEnd = cursorEnd + nbtDataSize;
|
|
||||||
var nbtData = buffer.slice(cursorEnd, nbtDataEnd);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: {
|
|
||||||
id: blockId,
|
|
||||||
itemCount: itemCount,
|
|
||||||
itemDamage: itemDamage,
|
|
||||||
nbtData: nbtData,
|
|
||||||
},
|
|
||||||
size: nbtDataEnd - offset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function SlotWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = value.id === -1 ? 2 : 7 + this.value.nbtData.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
SlotWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeInt16BE(this.value.id, offset);
|
|
||||||
if (this.value.id === -1) return;
|
|
||||||
buffer.writeInt8(this.value.itemCount, offset + 2);
|
|
||||||
buffer.writeInt16BE(this.value.itemDamage, offset + 3);
|
|
||||||
var nbtDataSize = this.value.nbtData.length;
|
|
||||||
if (nbtDataSize === 0) nbtDataSize = -1; // I don't know wtf mojang smokes
|
|
||||||
buffer.writeInt16BE(nbtDataSize, offset + 5);
|
|
||||||
this.value.nbtData.copy(buffer, offset + 7);
|
|
||||||
};
|
|
||||||
|
|
||||||
function StringWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.encoded = toUcs2.convert(value);
|
|
||||||
this.size = 2 + this.encoded.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeInt16BE(this.value.length, offset);
|
|
||||||
this.encoded.copy(buffer, offset + 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ByteArray16Writer(value) {
|
|
||||||
assert.ok(Buffer.isBuffer(value), "non buffer passed to ByteArray16Writer");
|
|
||||||
this.value = value;
|
|
||||||
this.size = 2 + value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArray16Writer.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeInt16BE(this.value.length, offset);
|
|
||||||
this.value.copy(buffer, offset + 2);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ByteWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeInt8(this.value, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BoolWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
BoolWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeInt8(this.value ? 1 : 0, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UByteWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
UByteWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeUInt8(this.value, offset);
|
|
||||||
};
|
|
||||||
|
|
||||||
function FloatWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
FloatWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeFloatBE(this.value, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DoubleWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
DoubleWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeDoubleBE(this.value, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShortWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShortWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeInt16BE(this.value, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function IntWriter(value) {
|
|
||||||
this.value = value;
|
|
||||||
this.size = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
IntWriter.prototype.write = function(buffer, offset) {
|
|
||||||
buffer.writeInt32BE(this.value, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPacketBuffer(packetId, params) {
|
|
||||||
var size = 1;
|
|
||||||
var fields = [ new UByteWriter(packetId) ];
|
|
||||||
var packet = packets[packetId];
|
|
||||||
packet.forEach(function(fieldInfo) {
|
|
||||||
var value = params[fieldInfo.name];
|
|
||||||
var Writer = writers[fieldInfo.type];
|
|
||||||
assert.ok(Writer, "missing writer for data type: " + fieldInfo.type);
|
|
||||||
var field = new Writer(value);
|
|
||||||
size += field.size;
|
|
||||||
fields.push(field);
|
|
||||||
});
|
|
||||||
var buffer = new Buffer(size);
|
|
||||||
var cursor = 0;
|
|
||||||
fields.forEach(function(field) {
|
|
||||||
field.write(buffer, cursor);
|
|
||||||
cursor += field.size;
|
|
||||||
});
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePacket(buffer) {
|
|
||||||
if (buffer.length < 1) return null;
|
|
||||||
var packetId = buffer.readUInt8(0);
|
|
||||||
var size = 1;
|
|
||||||
var results = { id: packetId };
|
|
||||||
var packetInfo = packets[packetId];
|
|
||||||
assert.ok(packetInfo, "Unrecognized packetId: " + packetId);
|
|
||||||
var i, fieldInfo, read, readResults;
|
|
||||||
for (i = 0; i < packetInfo.length; ++i) {
|
|
||||||
fieldInfo = packetInfo[i];
|
|
||||||
read = readers[fieldInfo.type];
|
|
||||||
assert.ok(read, "missing reader for data type: " + fieldInfo.type);
|
|
||||||
readResults = read(buffer, size);
|
|
||||||
if (readResults) {
|
|
||||||
results[fieldInfo.name] = readResults.value;
|
|
||||||
size += readResults.size;
|
|
||||||
} else {
|
|
||||||
// buffer needs to be more full
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
size: size,
|
|
||||||
results: results,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mcPubKeyToURsa(mcPubKeyBuffer) {
|
function mcPubKeyToURsa(mcPubKeyBuffer) {
|
||||||
var pem = "-----BEGIN PUBLIC KEY-----\n";
|
var pem = "-----BEGIN PUBLIC KEY-----\n";
|
||||||
@ -793,7 +266,7 @@ function getLoginSession(email, password, cb) {
|
|||||||
req.send({
|
req.send({
|
||||||
user: email,
|
user: email,
|
||||||
password: password,
|
password: password,
|
||||||
version: packets.meta.sessionVersion,
|
version: protocol.sessionVersion,
|
||||||
});
|
});
|
||||||
req.end(function(err, resp) {
|
req.end(function(err, resp) {
|
||||||
var myErr;
|
var myErr;
|
||||||
@ -825,14 +298,3 @@ function getLoginSession(email, password, cb) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hax(packet) {
|
|
||||||
// bullshit post-parsing hax that we have to do due to mojang incompetence
|
|
||||||
var tmp;
|
|
||||||
if (packet.id === 0x0d) {
|
|
||||||
// when 0x0d is sent from the server, the Y and the Stance fields are swapped.
|
|
||||||
tmp = packet.y;
|
|
||||||
packet.y = packet.stance;
|
|
||||||
packet.stance = tmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
62
lib/client.js
Normal file
62
lib/client.js
Normal 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);
|
||||||
|
};
|
1020
lib/protocol.js
Normal file
1020
lib/protocol.js
Normal file
File diff suppressed because it is too large
Load Diff
48
lib/server.js
Normal file
48
lib/server.js
Normal 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);
|
||||||
|
};
|
@ -32,7 +32,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ursa": "~0.8.0",
|
"ursa": "~0.8.0",
|
||||||
"buffer-more-ints": "~0.0.1",
|
|
||||||
"superagent": "~0.10.0",
|
"superagent": "~0.10.0",
|
||||||
"iconv": "~1.2.4",
|
"iconv": "~1.2.4",
|
||||||
"batch": "~0.2.1"
|
"batch": "~0.2.1"
|
||||||
|
1250
packets.js
1250
packets.js
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user