2012-12-31 20:33:35 -05:00
|
|
|
var net = require('net')
|
|
|
|
, EventEmitter = require('events').EventEmitter
|
|
|
|
, util = require('util')
|
|
|
|
, assert = require('assert')
|
2013-01-01 23:39:31 -05:00
|
|
|
, ursa = require('ursa')
|
|
|
|
, crypto = require('crypto')
|
|
|
|
, superagent = require('superagent')
|
|
|
|
, Batch = require('batch')
|
2013-01-03 23:14:19 -05:00
|
|
|
, protocol = require('./lib/protocol')
|
|
|
|
, createPacketBuffer = protocol.createPacketBuffer
|
|
|
|
, parsePacket = protocol.parsePacket
|
2012-12-31 20:33:35 -05:00
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
exports.createClient = createClient;
|
2012-12-31 20:33:35 -05:00
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
function createClient(options) {
|
|
|
|
// defaults
|
2013-01-02 04:46:01 -05:00
|
|
|
assert.ok(options, "options is required");
|
2013-01-01 23:39:31 -05:00
|
|
|
var port = options.port || 25565;
|
|
|
|
var host = options.host || 'localhost';
|
|
|
|
assert.ok(options.username, "username is required");
|
|
|
|
var haveCredentials = options.email && options.password;
|
|
|
|
|
|
|
|
var client = new Client();
|
2013-01-02 03:30:33 -05:00
|
|
|
client.username = options.username;
|
2013-01-01 23:39:31 -05:00
|
|
|
client.on('connect', function() {
|
2013-01-03 21:42:35 -05:00
|
|
|
client.write(0x02, {
|
2013-01-03 23:14:19 -05:00
|
|
|
protocolVersion: protocol.version,
|
2013-01-01 23:39:31 -05:00
|
|
|
username: options.username,
|
|
|
|
serverHost: host,
|
|
|
|
serverPort: port,
|
|
|
|
});
|
|
|
|
});
|
2013-01-03 20:59:58 -05:00
|
|
|
client.on(0x00, onKeepAlive);
|
2013-01-03 21:42:35 -05:00
|
|
|
client.once(0xFC, onEncryptionKeyResponse);
|
|
|
|
client.once(0xFD, onEncryptionKeyRequest);
|
2013-01-01 23:39:31 -05:00
|
|
|
client.connect(port, host);
|
|
|
|
|
|
|
|
return client;
|
|
|
|
|
|
|
|
function onKeepAlive(packet) {
|
2013-01-03 21:42:35 -05:00
|
|
|
client.write(0x00, {
|
2013-01-01 23:39:31 -05:00
|
|
|
keepAliveId: packet.keepAliveId
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function onEncryptionKeyRequest(packet) {
|
|
|
|
var batch = new Batch();
|
2013-01-02 02:19:47 -05:00
|
|
|
var hash;
|
|
|
|
if (haveCredentials) {
|
|
|
|
hash = crypto.createHash('sha1');
|
|
|
|
hash.update(packet.serverId);
|
|
|
|
batch.push(function(cb) { getLoginSession(options.email, options.password, cb); });
|
|
|
|
}
|
2013-01-01 23:39:31 -05:00
|
|
|
batch.push(function(cb) { crypto.randomBytes(16, cb); });
|
|
|
|
batch.end(function (err, results) {
|
|
|
|
if (err) {
|
|
|
|
client.emit('error', err);
|
|
|
|
client.end();
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-01-02 02:19:47 -05:00
|
|
|
var sharedSecret;
|
|
|
|
if (haveCredentials) {
|
|
|
|
client.session = results[0];
|
2013-01-02 03:30:33 -05:00
|
|
|
client.username = client.session.username;
|
2013-01-02 02:19:47 -05:00
|
|
|
client.emit('session');
|
|
|
|
sharedSecret = results[1];
|
|
|
|
joinServerRequest(onJoinServerResponse);
|
|
|
|
} else {
|
|
|
|
sharedSecret = results[0];
|
|
|
|
sendEncryptionKeyResponse();
|
|
|
|
}
|
2013-01-01 23:39:31 -05:00
|
|
|
|
2013-01-02 02:19:47 -05:00
|
|
|
function onJoinServerResponse(err) {
|
2013-01-01 23:39:31 -05:00
|
|
|
if (err) {
|
|
|
|
client.emit('error', err);
|
|
|
|
client.end();
|
|
|
|
} else {
|
|
|
|
sendEncryptionKeyResponse();
|
|
|
|
}
|
2013-01-02 02:19:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function joinServerRequest(cb) {
|
|
|
|
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) {
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2013-01-01 23:39:31 -05:00
|
|
|
|
|
|
|
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);
|
2013-01-03 21:42:35 -05:00
|
|
|
client.write(0xfc, {
|
2013-01-01 23:39:31 -05:00
|
|
|
sharedSecret: encryptedSharedSecretBuffer,
|
|
|
|
verifyToken: encryptedVerifyTokenBuffer,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function onEncryptionKeyResponse(packet) {
|
|
|
|
assert.strictEqual(packet.sharedSecret.length, 0);
|
|
|
|
assert.strictEqual(packet.verifyToken.length, 0);
|
|
|
|
client.encryptionEnabled = true;
|
2013-01-03 21:42:35 -05:00
|
|
|
client.write(0xcd, { payload: 0 });
|
2013-01-01 23:39:31 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function Client(options) {
|
2012-12-31 20:33:35 -05:00
|
|
|
EventEmitter.call(this);
|
2013-01-01 01:49:38 -05:00
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
this.socket = null;
|
2013-01-01 01:49:38 -05:00
|
|
|
this.encryptionEnabled = false;
|
|
|
|
this.cipher = null;
|
|
|
|
this.decipher = null;
|
2012-12-31 20:33:35 -05:00
|
|
|
}
|
2013-01-01 23:39:31 -05:00
|
|
|
util.inherits(Client, EventEmitter);
|
2012-12-31 20:33:35 -05:00
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
Client.prototype.connect = function(port, host) {
|
2012-12-31 20:33:35 -05:00
|
|
|
var self = this;
|
2013-01-01 23:39:31 -05:00
|
|
|
self.socket = net.connect(port, host, function() {
|
2012-12-31 20:33:35 -05:00
|
|
|
self.emit('connect');
|
|
|
|
});
|
|
|
|
var incomingBuffer = new Buffer(0);
|
2013-01-01 23:39:31 -05:00
|
|
|
self.socket.on('data', function(data) {
|
2013-01-01 01:49:38 -05:00
|
|
|
if (self.encryptionEnabled) data = new Buffer(self.decipher.update(data), 'binary');
|
2012-12-31 20:33:35 -05:00
|
|
|
incomingBuffer = Buffer.concat([incomingBuffer, data]);
|
2013-01-02 21:57:31 -05:00
|
|
|
var parsed, packet;
|
2012-12-31 20:33:35 -05:00
|
|
|
while (true) {
|
|
|
|
parsed = parsePacket(incomingBuffer);
|
|
|
|
if (! parsed) break;
|
2013-01-02 21:57:31 -05:00
|
|
|
packet = parsed.results;
|
2012-12-31 20:33:35 -05:00
|
|
|
incomingBuffer = incomingBuffer.slice(parsed.size);
|
2013-01-03 20:59:58 -05:00
|
|
|
self.emit(packet.id, packet);
|
2012-12-31 20:33:35 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
self.socket.on('error', function(err) {
|
2012-12-31 20:33:35 -05:00
|
|
|
self.emit('error', err);
|
|
|
|
});
|
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
self.socket.on('close', function() {
|
2012-12-31 20:33:35 -05:00
|
|
|
self.emit('end');
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
Client.prototype.end = function() {
|
|
|
|
this.socket.end();
|
2013-01-01 04:14:38 -05:00
|
|
|
};
|
|
|
|
|
2013-01-03 21:42:35 -05:00
|
|
|
Client.prototype.write = function(packetId, params) {
|
2012-12-31 20:33:35 -05:00
|
|
|
var buffer = createPacketBuffer(packetId, params);
|
2013-01-01 01:49:38 -05:00
|
|
|
var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer;
|
2013-01-01 23:39:31 -05:00
|
|
|
this.socket.write(out);
|
2012-12-31 20:33:35 -05:00
|
|
|
};
|
|
|
|
|
2012-12-31 23:53:36 -05:00
|
|
|
|
2013-01-01 23:39:31 -05:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getLoginSession(email, password, cb) {
|
|
|
|
var req = superagent.post("https://login.minecraft.net");
|
|
|
|
req.type('form');
|
|
|
|
req.send({
|
|
|
|
user: email,
|
|
|
|
password: password,
|
2013-01-03 23:14:19 -05:00
|
|
|
version: protocol.sessionVersion,
|
2013-01-01 23:39:31 -05:00
|
|
|
});
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|