mirror of
https://github.com/unmojang/node-minecraft-protocol.git
synced 2025-10-01 07:00:34 -04:00
refactor into nice npm module. closes #7
This commit is contained in:
parent
dc2aa5222c
commit
c405ac21f8
24
README.md
24
README.md
@ -20,6 +20,26 @@ Parse and serialize minecraft packets, plus authentication and encryption.
|
|||||||
|
|
||||||
Supports Minecraft version 1.4.6
|
Supports Minecraft version 1.4.6
|
||||||
|
|
||||||
## Try it out so far
|
## Usage
|
||||||
|
|
||||||
`MC_EMAIL=you@example.com MC_PASSWORD=your_pass node test.js`
|
### Echo example
|
||||||
|
|
||||||
|
Listen for chat messages and echo them back.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var mc = require('minecraft-protocol');
|
||||||
|
var client = mc.createClient({
|
||||||
|
host: "localhost", // optional
|
||||||
|
port: 25565, // optional
|
||||||
|
username: "player",
|
||||||
|
email: "email@example.com", // email and password are required only for
|
||||||
|
password: "12345678", // encrypted and online servers
|
||||||
|
});
|
||||||
|
client.on('packet', function(packet) {
|
||||||
|
if (packet.id !== 0x03) return;
|
||||||
|
if (packet.message.indexOf(client.session.username) !== -1) return;
|
||||||
|
client.writePacket(0x03, {
|
||||||
|
message: packet.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
@ -3,31 +3,149 @@ var net = require('net')
|
|||||||
, util = require('util')
|
, util = require('util')
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, Iconv = require('iconv').Iconv
|
, Iconv = require('iconv').Iconv
|
||||||
, packets = require('../packets.json')
|
, ursa = require('ursa')
|
||||||
|
, crypto = require('crypto')
|
||||||
|
, superagent = require('superagent')
|
||||||
|
, Batch = require('batch')
|
||||||
|
, packets = require('./packets.json')
|
||||||
, toUcs2 = new Iconv('UTF-8', 'utf16be')
|
, toUcs2 = new Iconv('UTF-8', 'utf16be')
|
||||||
, fromUcs2 = new Iconv('utf16be', 'UTF-8')
|
, fromUcs2 = new Iconv('utf16be', 'UTF-8')
|
||||||
|
|
||||||
require('buffer-more-ints');
|
require('buffer-more-ints');
|
||||||
|
|
||||||
module.exports = Parser;
|
exports.createClient = createClient;
|
||||||
|
|
||||||
function Parser(options) {
|
function createClient(options) {
|
||||||
|
// defaults
|
||||||
|
options = options || {};
|
||||||
|
var port = options.port || 25565;
|
||||||
|
var host = options.host || 'localhost';
|
||||||
|
assert.ok(options.username, "username is required");
|
||||||
|
var haveCredentials = options.email && options.password;
|
||||||
|
|
||||||
|
var packetHandlers = {
|
||||||
|
0x00: onKeepAlive,
|
||||||
|
0xFC: onEncryptionKeyResponse,
|
||||||
|
0xFD: onEncryptionKeyRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
var client = new Client();
|
||||||
|
client.on('connect', function() {
|
||||||
|
client.writePacket(0x02, {
|
||||||
|
protocolVersion: packets.meta.protocolVersion,
|
||||||
|
username: options.username,
|
||||||
|
serverHost: host,
|
||||||
|
serverPort: port,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
client.on('packet', function(packet) {
|
||||||
|
var handler = packetHandlers[packet.id];
|
||||||
|
if (handler) handler(packet);
|
||||||
|
});
|
||||||
|
client.connect(port, host);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
|
||||||
|
function onKeepAlive(packet) {
|
||||||
|
client.writePacket(0x00, {
|
||||||
|
keepAliveId: packet.keepAliveId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEncryptionKeyRequest(packet) {
|
||||||
|
if (! haveCredentials) {
|
||||||
|
var err = new Error("server is in online mode and no credentials supplied");
|
||||||
|
err.code = 'ENOCRED';
|
||||||
|
client.emit('error', err);
|
||||||
|
client.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var hash = crypto.createHash('sha1');
|
||||||
|
hash.update(packet.serverId);
|
||||||
|
var batch = new Batch();
|
||||||
|
batch.push(function(cb) { getLoginSession(options.email, options.password, cb); });
|
||||||
|
batch.push(function(cb) { crypto.randomBytes(16, cb); });
|
||||||
|
batch.end(function (err, results) {
|
||||||
|
if (err) {
|
||||||
|
client.emit('error', err);
|
||||||
|
client.end();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.session = results[0];
|
||||||
|
client.emit('session');
|
||||||
|
|
||||||
|
var sharedSecret = results[1];
|
||||||
|
hash.update(sharedSecret);
|
||||||
|
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) {
|
||||||
|
client.emit('error', err);
|
||||||
|
client.end();
|
||||||
|
} else if (resp.serverError) {
|
||||||
|
myErr = new Error("session.minecraft.net is broken: " + resp.status);
|
||||||
|
myErr.code = 'EMCSESSION500';
|
||||||
|
client.emit('error', myErr);
|
||||||
|
client.end();
|
||||||
|
} else if (resp.clientError) {
|
||||||
|
myErr = new Error("session.minecraft.net rejected request: " + resp.status + " " + resp.text);
|
||||||
|
myErr.code = 'EMCSESSION400';
|
||||||
|
client.emit('error', myErr);
|
||||||
|
client.end();
|
||||||
|
} else {
|
||||||
|
sendEncryptionKeyResponse();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendEncryptionKeyResponse() {
|
||||||
|
var pubKey = mcPubKeyToURsa(packet.publicKey);
|
||||||
|
var encryptedSharedSecret = pubKey.encrypt(sharedSecret, 'binary', 'base64', ursa.RSA_PKCS1_PADDING);
|
||||||
|
var encryptedSharedSecretBuffer = new Buffer(encryptedSharedSecret, 'base64');
|
||||||
|
var encryptedVerifyToken = pubKey.encrypt(packet.verifyToken, 'binary', 'base64', ursa.RSA_PKCS1_PADDING);
|
||||||
|
var encryptedVerifyTokenBuffer = new Buffer(encryptedVerifyToken, 'base64');
|
||||||
|
client.cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
|
||||||
|
client.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
|
||||||
|
client.writePacket(0xfc, {
|
||||||
|
sharedSecret: encryptedSharedSecretBuffer,
|
||||||
|
verifyToken: encryptedVerifyTokenBuffer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEncryptionKeyResponse(packet) {
|
||||||
|
assert.strictEqual(packet.sharedSecret.length, 0);
|
||||||
|
assert.strictEqual(packet.verifyToken.length, 0);
|
||||||
|
client.encryptionEnabled = true;
|
||||||
|
client.writePacket(0xcd, { payload: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Client(options) {
|
||||||
EventEmitter.call(this);
|
EventEmitter.call(this);
|
||||||
|
|
||||||
this.client = null;
|
this.socket = null;
|
||||||
this.encryptionEnabled = false;
|
this.encryptionEnabled = false;
|
||||||
this.cipher = null;
|
this.cipher = null;
|
||||||
this.decipher = null;
|
this.decipher = null;
|
||||||
}
|
}
|
||||||
util.inherits(Parser, EventEmitter);
|
util.inherits(Client, EventEmitter);
|
||||||
|
|
||||||
Parser.prototype.connect = function(port, host) {
|
Client.prototype.connect = function(port, host) {
|
||||||
var self = this;
|
var self = this;
|
||||||
self.client = net.connect(port, host, function() {
|
self.socket = net.connect(port, host, function() {
|
||||||
self.emit('connect');
|
self.emit('connect');
|
||||||
});
|
});
|
||||||
var incomingBuffer = new Buffer(0);
|
var incomingBuffer = new Buffer(0);
|
||||||
self.client.on('data', function(data) {
|
self.socket.on('data', function(data) {
|
||||||
if (self.encryptionEnabled) data = new Buffer(self.decipher.update(data), 'binary');
|
if (self.encryptionEnabled) data = new Buffer(self.decipher.update(data), 'binary');
|
||||||
incomingBuffer = Buffer.concat([incomingBuffer, data]);
|
incomingBuffer = Buffer.concat([incomingBuffer, data]);
|
||||||
var parsed;
|
var parsed;
|
||||||
@ -39,24 +157,23 @@ Parser.prototype.connect = function(port, host) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client.on('error', function(err) {
|
self.socket.on('error', function(err) {
|
||||||
self.emit('error', err);
|
self.emit('error', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.client.on('end', function() {
|
self.socket.on('close', function() {
|
||||||
self.emit('end');
|
self.emit('end');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser.prototype.end = function() {
|
Client.prototype.end = function() {
|
||||||
this.client.end();
|
this.socket.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser.prototype.writePacket = function(packetId, params) {
|
Client.prototype.writePacket = function(packetId, params) {
|
||||||
var buffer = createPacketBuffer(packetId, params);
|
var buffer = createPacketBuffer(packetId, params);
|
||||||
var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer;
|
var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer;
|
||||||
if (this.encryptionEnabled) console.log("writing", packetId, "packet with encryption");
|
this.socket.write(out);
|
||||||
this.client.write(out);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var writers = {
|
var writers = {
|
||||||
@ -598,7 +715,6 @@ function createPacketBuffer(packetId, params) {
|
|||||||
function parsePacket(buffer) {
|
function parsePacket(buffer) {
|
||||||
if (buffer.length < 1) return null;
|
if (buffer.length < 1) return null;
|
||||||
var packetId = buffer.readUInt8(0);
|
var packetId = buffer.readUInt8(0);
|
||||||
console.log("parsing packet " + packetId);
|
|
||||||
var size = 1;
|
var size = 1;
|
||||||
var results = { id: packetId };
|
var results = { id: packetId };
|
||||||
var packetInfo = packets[packetId];
|
var packetInfo = packets[packetId];
|
||||||
@ -623,78 +739,80 @@ function parsePacket(buffer) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// packet ids
|
function mcPubKeyToURsa(mcPubKeyBuffer) {
|
||||||
Parser.KEEP_ALIVE = 0x00;
|
var pem = "-----BEGIN PUBLIC KEY-----\n";
|
||||||
Parser.LOGIN_REQUEST = 0x01;
|
var base64PubKey = mcPubKeyBuffer.toString('base64');
|
||||||
Parser.HANDSHAKE = 0x02;
|
var maxLineLength = 65;
|
||||||
Parser.CHAT_MESSAGE = 0x03;
|
while (base64PubKey.length > 0) {
|
||||||
Parser.TIME_UPDATE = 0x04;
|
pem += base64PubKey.substring(0, maxLineLength) + "\n";
|
||||||
Parser.ENTITY_EQUIPMENT = 0x05;
|
base64PubKey = base64PubKey.substring(maxLineLength);
|
||||||
Parser.SPAWN_POSITION = 0x06;
|
}
|
||||||
Parser.USE_ENTITY = 0x07;
|
pem += "-----END PUBLIC KEY-----\n";
|
||||||
Parser.UPDATE_HEALTH = 0x08;
|
return ursa.createPublicKey(pem, 'utf8');
|
||||||
Parser.RESPAWN = 0x09;
|
}
|
||||||
Parser.PLAYER = 0x0A;
|
|
||||||
Parser.PLAYER_POSITION = 0x0B;
|
function mcHexDigest(hash) {
|
||||||
Parser.PLAYER_LOOK = 0x0C;
|
var buffer = new Buffer(hash.digest(), 'binary');
|
||||||
Parser.PLAYER_POSITION_AND_LOOK = 0x0D;
|
// check for negative hashes
|
||||||
Parser.PLAYER_DIGGING = 0x0E;
|
var negative = buffer.readInt8(0) < 0;
|
||||||
Parser.PLAYER_BLOCK_PLACEMENT = 0x0F;
|
if (negative) performTwosCompliment(buffer);
|
||||||
Parser.HELD_ITEM_CHANGE = 0x10;
|
var digest = buffer.toString('hex');
|
||||||
Parser.USE_BED = 0x11;
|
// trim leading zeroes
|
||||||
Parser.ANIMATION = 0x12;
|
digest = digest.replace(/^0+/g, '');
|
||||||
Parser.ENTITY_ACTION = 0x13;
|
if (negative) digest = '-' + digest;
|
||||||
Parser.SPAWN_NAMED_ENTITY = 0x14;
|
return digest;
|
||||||
Parser.COLLECT_ITEM = 0x16;
|
|
||||||
Parser.SPAWN_OBJECT_VEHICLE = 0x17;
|
function performTwosCompliment(buffer) {
|
||||||
Parser.SPAWN_MOB = 0x18;
|
var carry = true;
|
||||||
Parser.SPAWN_PAINTING = 0x19;
|
var i, newByte, value;
|
||||||
Parser.SPAWN_EXPERIENCE_ORB = 0x1A;
|
for (i = buffer.length - 1; i >= 0; --i) {
|
||||||
Parser.ENTITY_VELOCITY = 0x1C;
|
value = buffer.readUInt8(i);
|
||||||
Parser.DESTROY_ENTITY = 0x1D;
|
newByte = ~value & 0xff;
|
||||||
Parser.ENTITY = 0x1E;
|
if (carry) {
|
||||||
Parser.ENTITY_RELATIVE_MOVE = 0x1F;
|
carry = newByte === 0xff;
|
||||||
Parser.ENTITY_LOOK = 0x20;
|
buffer.writeUInt8(newByte + 1, i);
|
||||||
Parser.ENTITY_LOOK_AND_RELATIVE_MOVE = 0x21;
|
} else {
|
||||||
Parser.ENTITY_TELEPORT = 0x22;
|
buffer.writeUInt8(newByte, i);
|
||||||
Parser.ENTITY_HEAD_LOOK = 0x23;
|
}
|
||||||
Parser.ENTITY_STATUS = 0x26;
|
}
|
||||||
Parser.ATTACH_ENTITY = 0x27;
|
}
|
||||||
Parser.ENTITY_METADATA = 0x28;
|
}
|
||||||
Parser.ENTITY_EFFECT = 0x29;
|
|
||||||
Parser.REMOVE_ENTITY_EFFECT = 0x2A;
|
function getLoginSession(email, password, cb) {
|
||||||
Parser.SET_EXPERIENCE = 0x2B;
|
var req = superagent.post("https://login.minecraft.net");
|
||||||
Parser.CHUNK_DATA = 0x33;
|
req.type('form');
|
||||||
Parser.MULTI_BLOCK_CHANGE = 0x34;
|
req.send({
|
||||||
Parser.BLOCK_CHANGE = 0x35;
|
user: email,
|
||||||
Parser.BLOCK_ACTION = 0x36;
|
password: password,
|
||||||
Parser.BLOCK_BREAK_ANIMATION = 0x37;
|
version: packets.meta.sessionVersion,
|
||||||
Parser.MAP_CHUNK_BULK = 0x38;
|
});
|
||||||
Parser.EXPLOSION = 0x3C;
|
req.end(function(err, resp) {
|
||||||
Parser.SOUND_OR_PARTICLE_EFFECT = 0x3D;
|
var myErr;
|
||||||
Parser.NAMED_SOUND_EFFECT = 0x3E;
|
if (err) {
|
||||||
Parser.CHANGE_GAME_STATE = 0x46;
|
cb(err);
|
||||||
Parser.SPAWN_GLOBAL_ENTITY = 0x47;
|
} else if (resp.serverError) {
|
||||||
Parser.OPEN_WINDOW = 0x64;
|
myErr = new Error("login.minecraft.net is broken: " + resp.status);
|
||||||
Parser.CLOSE_WINDOW = 0x65;
|
myErr.code = 'ELOGIN500';
|
||||||
Parser.CLICK_WINDOW = 0x66;
|
cb(myErr);
|
||||||
Parser.SET_SLOT = 0x67;
|
} else if (resp.clientError) {
|
||||||
Parser.SET_WINDOW_ITEMS = 0x68;
|
myErr = new Error("login.minecraft.net rejected request: " + resp.status + " " + resp.text);
|
||||||
Parser.UPDATE_WINDOW_PROPERTY = 0x69;
|
myErr.code = 'ELOGIN400';
|
||||||
Parser.CONFIRM_TRANSACTION = 0x6A;
|
cb(myErr);
|
||||||
Parser.CREATIVE_INVENTORY_ACTION = 0x6B;
|
} else {
|
||||||
Parser.ENCHANT_ITEM = 0x6C;
|
var values = resp.text.split(':');
|
||||||
Parser.UPDATE_SIGN = 0x82;
|
var session = {
|
||||||
Parser.ITEM_DATA = 0x83;
|
currentGameVersion: values[0],
|
||||||
Parser.UPDATE_TILE_ENTITY = 0x84;
|
username: values[2],
|
||||||
Parser.INCREMENT_STATISTIC = 0xC8;
|
id: values[3],
|
||||||
Parser.PLAYER_LIST_ITEM = 0xC9;
|
uid: values[4],
|
||||||
Parser.PLAYER_ABILITIES = 0xCA;
|
};
|
||||||
Parser.TAB_COMPLETE = 0xCB;
|
if (session.id && session.username) {
|
||||||
Parser.CLIENT_SETTINGS = 0xCC;
|
cb(null, session);
|
||||||
Parser.CLIENT_STATUSES = 0xCD;
|
} else {
|
||||||
Parser.PLUGIN_MESSAGE = 0xFA;
|
myErr = new Error("login.minecraft.net rejected request: " + resp.status + " " + resp.text);
|
||||||
Parser.ENCRYPTION_KEY_RESPONSE = 0xFC;
|
myErr.code = 'ELOGIN400';
|
||||||
Parser.ENCRYPTION_KEY_REQUEST = 0xFD;
|
cb(myErr);
|
||||||
Parser.SERVER_LIST_PING = 0xFE;
|
}
|
||||||
Parser.DISCONNECT_KICK = 0xFF;
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -32,6 +32,7 @@
|
|||||||
"ursa": "~0.8.0",
|
"ursa": "~0.8.0",
|
||||||
"buffer-more-ints": "~0.0.1",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"meta": {
|
||||||
|
"protocolVersion": 51,
|
||||||
|
"sessionVersion": 13
|
||||||
|
},
|
||||||
"0": [
|
"0": [
|
||||||
{
|
{
|
||||||
"name": "keepAliveId",
|
"name": "keepAliveId",
|
||||||
|
201
test.js
201
test.js
@ -1,200 +1,13 @@
|
|||||||
var Parser = require('./lib/parser')
|
var mc = require('./');
|
||||||
, ursa = require('ursa')
|
var client = mc.createClient({
|
||||||
, crypto = require('crypto')
|
username: process.env.MC_USERNAME,
|
||||||
, assert = require('assert')
|
|
||||||
, superagent = require('superagent')
|
|
||||||
|
|
||||||
var input = {
|
|
||||||
email: process.env.MC_EMAIL,
|
email: process.env.MC_EMAIL,
|
||||||
password: process.env.MC_PASSWORD,
|
password: process.env.MC_PASSWORD,
|
||||||
serverHost: 'localhost',
|
|
||||||
serverPort: 25565,
|
|
||||||
};
|
|
||||||
|
|
||||||
var parser = new Parser();
|
|
||||||
var loginSession = null;
|
|
||||||
parser.on('connect', function() {
|
|
||||||
console.info("connect");
|
|
||||||
parser.writePacket(Parser.HANDSHAKE, {
|
|
||||||
protocolVersion: 51,
|
|
||||||
username: loginSession.username,
|
|
||||||
serverHost: input.serverHost,
|
|
||||||
serverPort: input.serverPort,
|
|
||||||
});
|
});
|
||||||
});
|
client.on('packet', function(packet) {
|
||||||
parser.on('packet', function(packet) {
|
if (packet.id !== 0x03) return;
|
||||||
var handler = packetHandlers[packet.id];
|
if (packet.message.indexOf(client.session.username) !== -1) return;
|
||||||
if (handler) {
|
client.writePacket(0x03, {
|
||||||
handler(packet);
|
|
||||||
} else {
|
|
||||||
console.warn("No packet handler for", packet.id, "fields", packet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
parser.on('error', function(err) {
|
|
||||||
console.error("error connecting", err.stack);
|
|
||||||
});
|
|
||||||
parser.on('end', function() {
|
|
||||||
console.info("disconnect");
|
|
||||||
});
|
|
||||||
|
|
||||||
getLoginSession(function() {
|
|
||||||
parser.connect(input.serverPort, input.serverHost);
|
|
||||||
});
|
|
||||||
|
|
||||||
function getLoginSession(cb) {
|
|
||||||
console.log("logging in to minecraft.net");
|
|
||||||
var req = superagent.post("https://login.minecraft.net");
|
|
||||||
req.type('form');
|
|
||||||
req.send({
|
|
||||||
user: input.email,
|
|
||||||
password: input.password,
|
|
||||||
version: 13,
|
|
||||||
});
|
|
||||||
req.end(function(err, resp) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
} else if (! resp.ok) {
|
|
||||||
cb(new Error("login.minecraft.net status " + resp.status + ": " + resp.text));
|
|
||||||
} 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) {
|
|
||||||
loginSession = session;
|
|
||||||
console.info("logged in as", session.username);
|
|
||||||
cb();
|
|
||||||
} else {
|
|
||||||
cb(new Error("login.minecraft.net says " + session.currentGameVersion));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var packetHandlers = {
|
|
||||||
0x00: onKeepAlive,
|
|
||||||
0x01: onLoginRequest,
|
|
||||||
0x03: onChatMessage,
|
|
||||||
0xFC: onEncryptionKeyResponse,
|
|
||||||
0xFD: onEncryptionKeyRequest,
|
|
||||||
0xFF: onKick,
|
|
||||||
};
|
|
||||||
|
|
||||||
function onKeepAlive(packet) {
|
|
||||||
parser.writePacket(Parser.KEEP_ALIVE, {
|
|
||||||
keepAliveId: packet.keepAliveId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKick(packet) {
|
|
||||||
console.log("kick", packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLoginRequest(packet) {
|
|
||||||
console.log("login request", packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChatMessage(packet) {
|
|
||||||
console.log("chat message", packet);
|
|
||||||
if (packet.message.indexOf(loginSession.username) === -1) {
|
|
||||||
parser.writePacket(Parser.CHAT_MESSAGE, {
|
|
||||||
message: packet.message,
|
message: packet.message,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEncryptionKeyRequest(packet) {
|
|
||||||
console.log("enc key request");
|
|
||||||
var hash = crypto.createHash('sha1');
|
|
||||||
hash.update(packet.serverId);
|
|
||||||
crypto.randomBytes(16, function (err, sharedSecret) {
|
|
||||||
assert.ifError(err);
|
|
||||||
hash.update(sharedSecret);
|
|
||||||
hash.update(packet.publicKey);
|
|
||||||
var digest = mcHexDigest(hash);
|
|
||||||
var request = superagent.get("http://session.minecraft.net/game/joinserver.jsp");
|
|
||||||
|
|
||||||
request.query({
|
|
||||||
user: loginSession.username,
|
|
||||||
sessionId: loginSession.id,
|
|
||||||
serverId: digest,
|
|
||||||
});
|
});
|
||||||
request.end(function(err, resp) {
|
|
||||||
if (err) {
|
|
||||||
console.error("session.minecraft.net not available");
|
|
||||||
// TODO emit error
|
|
||||||
} else if (! resp.ok) {
|
|
||||||
console.error("session.minecraft.net returned error:", resp.status, resp.text);
|
|
||||||
// TODO emit error
|
|
||||||
} else {
|
|
||||||
sendEncryptionKeyResponse();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function sendEncryptionKeyResponse() {
|
|
||||||
var pubKey = mcPubKeyToURsa(packet.publicKey);
|
|
||||||
var encryptedSharedSecret = pubKey.encrypt(sharedSecret, 'binary', 'base64', ursa.RSA_PKCS1_PADDING);
|
|
||||||
var encryptedSharedSecretBuffer = new Buffer(encryptedSharedSecret, 'base64');
|
|
||||||
var encryptedVerifyToken = pubKey.encrypt(packet.verifyToken, 'binary', 'base64', ursa.RSA_PKCS1_PADDING);
|
|
||||||
var encryptedVerifyTokenBuffer = new Buffer(encryptedVerifyToken, 'base64');
|
|
||||||
parser.cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
|
|
||||||
parser.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret);
|
|
||||||
console.log("write enc key response");
|
|
||||||
parser.writePacket(Parser.ENCRYPTION_KEY_RESPONSE, {
|
|
||||||
sharedSecret: encryptedSharedSecretBuffer,
|
|
||||||
verifyToken: encryptedVerifyTokenBuffer,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEncryptionKeyResponse(packet) {
|
|
||||||
console.log("confirmation enc key response");
|
|
||||||
assert.strictEqual(packet.sharedSecret.length, 0);
|
|
||||||
assert.strictEqual(packet.verifyToken.length, 0);
|
|
||||||
parser.encryptionEnabled = true;
|
|
||||||
parser.writePacket(Parser.CLIENT_STATUSES, { payload: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function mcPubKeyToURsa(mcPubKeyBuffer) {
|
|
||||||
var pem = "-----BEGIN PUBLIC KEY-----\n";
|
|
||||||
var base64PubKey = mcPubKeyBuffer.toString('base64');
|
|
||||||
var maxLineLength = 65;
|
|
||||||
while (base64PubKey.length > 0) {
|
|
||||||
pem += base64PubKey.substring(0, maxLineLength) + "\n";
|
|
||||||
base64PubKey = base64PubKey.substring(maxLineLength);
|
|
||||||
}
|
|
||||||
pem += "-----END PUBLIC KEY-----\n";
|
|
||||||
return ursa.createPublicKey(pem, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function mcHexDigest(hash) {
|
|
||||||
var buffer = new Buffer(hash.digest(), 'binary');
|
|
||||||
// check for negative hashes
|
|
||||||
var negative = buffer.readInt8(0) < 0;
|
|
||||||
if (negative) performTwosCompliment(buffer);
|
|
||||||
var digest = buffer.toString('hex');
|
|
||||||
// trim leading zeroes
|
|
||||||
digest = digest.replace(/^0+/g, '');
|
|
||||||
if (negative) digest = '-' + digest;
|
|
||||||
return digest;
|
|
||||||
|
|
||||||
function performTwosCompliment(buffer) {
|
|
||||||
var carry = true;
|
|
||||||
var i, newByte, value;
|
|
||||||
for (i = buffer.length - 1; i >= 0; --i) {
|
|
||||||
value = buffer.readUInt8(i);
|
|
||||||
newByte = ~value & 0xff;
|
|
||||||
if (carry) {
|
|
||||||
carry = newByte === 0xff;
|
|
||||||
buffer.writeUInt8(newByte + 1, i);
|
|
||||||
} else {
|
|
||||||
buffer.writeUInt8(newByte, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user