Merge branch 'master' into forge (client modularization)

Merging https://github.com/PrismarineJS/node-minecraft-protocol/pull/333
This commit is contained in:
deathcap 2016-01-27 20:25:55 -08:00
commit fd9caddb67
10 changed files with 231 additions and 175 deletions

View File

@ -215,6 +215,13 @@ class Client extends EventEmitter
else else
this.compressor.write(buffer); this.compressor.write(buffer);
} }
// TCP/IP-specific (not generic Stream) method for backwards-compatibility
connect(port, host) {
var options = {port, host};
require('./client/tcp_dns')(this, options);
options.connect(this);
}
} }
module.exports = Client; module.exports = Client;

43
src/client/caseCorrect.js Normal file
View File

@ -0,0 +1,43 @@
var yggdrasil = require('yggdrasil')({});
var UUID = require('uuid-1345');
module.exports = function(client, options) {
var clientToken = options.clientToken || UUID.v4().toString();
options.accessToken = null;
options.haveCredentials = options.password != null || (clientToken != null && options.session != null);
if(options.haveCredentials) {
// make a request to get the case-correct username before connecting.
var cb = function(err, session) {
if(err) {
client.emit('error', err);
} else {
client.session = session;
client.username = session.selectedProfile.name;
options.accessToken = session.accessToken;
client.emit('session');
options.connect(client);
}
};
if (options.session) {
yggdrasil.validate(options.session.accessToken, function(ok) {
if (ok)
cb(null, options.session);
else
yggdrasil.refresh(options.session.accessToken, options.session.clientToken, function(err, _, data) {
cb(err, data);
});
});
}
else yggdrasil.auth({
user: options.username,
pass: options.password,
token: clientToken
}, cb);
} else {
// assume the server is in offline mode and just go for it.
client.username = options.username;
options.connect(client);
}
};

9
src/client/compress.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = function(client, options) {
client.once("compress", onCompressionRequest);
client.on("set_compression", onCompressionRequest);
function onCompressionRequest(packet) {
client.compressionThreshold = packet.threshold;
}
// TODO: refactor with transforms/compression.js -- enable it here
};

66
src/client/encrypt.js Normal file
View File

@ -0,0 +1,66 @@
var crypto = require('crypto');
var yggserver = require('yggdrasil').server({});
var ursa=require("../ursa");
var debug = require("../debug");
module.exports = function(client, options) {
client.once('encryption_begin', onEncryptionKeyRequest);
function onEncryptionKeyRequest(packet) {
crypto.randomBytes(16, gotSharedSecret);
function gotSharedSecret(err, sharedSecret) {
if(err) {
debug(err);
client.emit('error', err);
client.end();
return;
}
if(options.haveCredentials) {
joinServerRequest(onJoinServerResponse);
} else {
if(packet.serverId != '-') {
debug('This server appears to be an online server and you are providing no password, the authentication will probably fail');
}
sendEncryptionKeyResponse();
}
function onJoinServerResponse(err) {
if(err) {
client.emit('error', err);
client.end();
} else {
sendEncryptionKeyResponse();
}
}
function joinServerRequest(cb) {
yggserver.join(options.accessToken, client.session.selectedProfile.id,
packet.serverId, sharedSecret, packet.publicKey, cb);
}
function sendEncryptionKeyResponse() {
var pubKey = mcPubKeyToURsa(packet.publicKey);
var encryptedSharedSecretBuffer = pubKey.encrypt(sharedSecret, undefined, undefined, ursa.RSA_PKCS1_PADDING);
var encryptedVerifyTokenBuffer = pubKey.encrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING);
client.write('encryption_begin', {
sharedSecret: encryptedSharedSecretBuffer,
verifyToken: encryptedVerifyTokenBuffer
});
client.setEncryption(sharedSecret);
}
}
}
};
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');
}

20
src/client/keepalive.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = function(client, options) {
var keepAlive = options.keepAlive == null ? true : options.keepAlive;
if (!keepAlive) return;
var checkTimeoutInterval = options.checkTimeoutInterval || 10 * 1000;
client.on('keep_alive', onKeepAlive);
var timeout = null;
function onKeepAlive(packet) {
if (timeout)
clearTimeout(timeout);
timeout = setTimeout(() => client.end(), checkTimeoutInterval);
client.write('keep_alive', {
keepAliveId: packet.keepAliveId
});
}
};

11
src/client/play.js Normal file
View File

@ -0,0 +1,11 @@
var states = require("../states");
module.exports = function(client, options) {
client.once('success', onLogin);
function onLogin(packet) {
client.state = states.PLAY;
client.uuid = packet.uuid;
client.username = packet.username;
}
};

24
src/client/setProtocol.js Normal file
View File

@ -0,0 +1,24 @@
var states = require("../states");
module.exports = function(client, options) {
client.on('connect', onConnect);
function onConnect() {
var taggedHost = options.host;
if (options.tagHost) taggedHost += options.tagHost;
client.write('set_protocol', {
protocolVersion: options.protocolVersion,
serverHost: taggedHost,
serverPort: options.port,
nextState: 2
});
client.state = states.LOGIN;
client.write('login_start', {
username: client.username
});
}
}

21
src/client/tcp_dns.js Normal file
View File

@ -0,0 +1,21 @@
var net = require('net');
var dns = require('dns');
module.exports = function(client, options) {
options.port = options.port || 25565;
options.host = options.host || 'localhost';
options.connect = (client) => {
if(options.port == 25565 && net.isIP(options.host) === 0) {
dns.resolveSrv("_minecraft._tcp." + options.host, function(err, addresses) {
if(addresses && addresses.length > 0) {
client.setSocket(net.connect(addresses[0].port, addresses[0].name));
} else {
client.setSocket(net.connect(options.port, options.host));
}
});
} else {
client.setSocket(net.connect(options.port, options.host));
}
};
};

View File

@ -1,185 +1,35 @@
var ursa=require("./ursa");
var net = require('net');
var dns = require('dns');
var Client = require('./client'); var Client = require('./client');
var assert = require('assert'); var assert = require('assert');
var crypto = require('crypto');
var yggdrasil = require('yggdrasil')({}); var encrypt = require('./client/encrypt');
var yggserver = require('yggdrasil').server({}); var keepalive = require('./client/keepalive');
var states = require("./states"); var compress = require('./client/compress');
var debug = require("./debug"); var caseCorrect = require('./client/caseCorrect');
var UUID = require('uuid-1345'); var setProtocol = require('./client/setProtocol');
var play = require('./client/play');
var tcp_dns = require('./client/tcp_dns');
module.exports=createClient; module.exports=createClient;
Client.prototype.connect = function(port, host) {
var self = this;
if(port == 25565 && net.isIP(host) === 0) {
dns.resolveSrv("_minecraft._tcp." + host, function(err, addresses) {
if(addresses && addresses.length > 0) {
self.setSocket(net.connect(addresses[0].port, addresses[0].name));
} else {
self.setSocket(net.connect(port, host));
}
});
} else {
self.setSocket(net.connect(port, host));
}
};
function createClient(options) { function createClient(options) {
assert.ok(options, "options is required"); assert.ok(options, "options is required");
var port = options.port || 25565;
var host = options.host || 'localhost';
var clientToken = options.clientToken || UUID.v4().toString();
var accessToken;
assert.ok(options.username, "username is required"); assert.ok(options.username, "username is required");
var haveCredentials = options.password != null || (clientToken != null && options.session != null);
var keepAlive = options.keepAlive == null ? true : options.keepAlive;
var checkTimeoutInterval = options.checkTimeoutInterval || 10 * 1000;
var optVersion = options.version || require("./version").defaultVersion; var optVersion = options.version || require("./version").defaultVersion;
var mcData=require("minecraft-data")(optVersion); var mcData=require("minecraft-data")(optVersion);
var version = mcData.version; var version = mcData.version;
options.majorVersion = version.majorVersion;
options.protocolVersion = version.version;
var client = new Client(false, options.majorVersion);
var client = new Client(false,version.majorVersion); tcp_dns(client, options);
client.on('connect', onConnect); setProtocol(client, options);
if(keepAlive) client.on('keep_alive', onKeepAlive); keepalive(client, options);
client.once('encryption_begin', onEncryptionKeyRequest); encrypt(client, options);
client.once('success', onLogin); play(client, options);
client.once("compress", onCompressionRequest); compress(client, options);
client.on("set_compression", onCompressionRequest); caseCorrect(client, options);
if(haveCredentials) {
// make a request to get the case-correct username before connecting.
var cb = function(err, session) {
if(err) {
client.emit('error', err);
} else {
client.session = session;
client.username = session.selectedProfile.name;
accessToken = session.accessToken;
client.emit('session');
client.connect(port, host);
}
};
if (options.session) {
yggdrasil.validate(options.session.accessToken, function(ok) {
if (ok)
cb(null, options.session);
else
yggdrasil.refresh(options.session.accessToken, options.session.clientToken, function(err, _, data) {
cb(err, data);
});
});
}
else yggdrasil.auth({
user: options.username,
pass: options.password,
token: clientToken
}, cb);
} else {
// assume the server is in offline mode and just go for it.
client.username = options.username;
client.connect(port, host);
}
var timeout = null;
return client; return client;
function onConnect() {
var taggedHost = host;
if (options.tagHost) taggedHost += options.tagHost;
client.write('set_protocol', {
protocolVersion: version.version,
serverHost: taggedHost,
serverPort: port,
nextState: 2
});
client.state = states.LOGIN;
client.write('login_start', {
username: client.username
});
}
function onCompressionRequest(packet) {
client.compressionThreshold = packet.threshold;
}
function onKeepAlive(packet) {
if (timeout)
clearTimeout(timeout);
timeout = setTimeout(() => client.end(), checkTimeoutInterval);
client.write('keep_alive', {
keepAliveId: packet.keepAliveId
});
}
function onEncryptionKeyRequest(packet) {
crypto.randomBytes(16, gotSharedSecret);
function gotSharedSecret(err, sharedSecret) {
if(err) {
debug(err);
client.emit('error', err);
client.end();
return;
}
if(haveCredentials) {
joinServerRequest(onJoinServerResponse);
} else {
if(packet.serverId != '-') {
debug('This server appears to be an online server and you are providing no password, the authentication will probably fail');
}
sendEncryptionKeyResponse();
}
function onJoinServerResponse(err) {
if(err) {
client.emit('error', err);
client.end();
} else {
sendEncryptionKeyResponse();
}
}
function joinServerRequest(cb) {
yggserver.join(accessToken, client.session.selectedProfile.id,
packet.serverId, sharedSecret, packet.publicKey, cb);
}
function sendEncryptionKeyResponse() {
var pubKey = mcPubKeyToURsa(packet.publicKey);
var encryptedSharedSecretBuffer = pubKey.encrypt(sharedSecret, undefined, undefined, ursa.RSA_PKCS1_PADDING);
var encryptedVerifyTokenBuffer = pubKey.encrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING);
client.write('encryption_begin', {
sharedSecret: encryptedSharedSecretBuffer,
verifyToken: encryptedVerifyTokenBuffer
});
client.setEncryption(sharedSecret);
}
}
}
function onLogin(packet) {
client.state = states.PLAY;
client.uuid = packet.uuid;
client.username = packet.username;
}
}
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');
} }

View File

@ -1,17 +1,20 @@
var net = require('net'); var net = require('net');
var Client = require('./client'); var Client = require('./client');
var states = require("./states"); var states = require("./states");
var tcp_dns = require('./client/tcp_dns');
module.exports = ping; module.exports = ping;
function ping(options, cb) { function ping(options, cb) {
var host = options.host || 'localhost'; options.host = options.host || 'localhost';
var port = options.port || 25565; options.port = options.port || 25565;
var optVersion = options.version || require("./version").defaultVersion; var optVersion = options.version || require("./version").defaultVersion;
var mcData=require("minecraft-data")(optVersion); var mcData=require("minecraft-data")(optVersion);
var version = mcData.version; var version = mcData.version;
options.majorVersion = version.majorVersion;
options.protocolVersion = version.version;
var client = new Client(false,version.majorVersion); var client = new Client(false,options.majorVersion);
client.on('error', function(err) { client.on('error', function(err) {
cb(err); cb(err);
}); });
@ -32,15 +35,17 @@ function ping(options, cb) {
client.write('ping_start', {}); client.write('ping_start', {});
}); });
// TODO: refactor with src/client/setProtocol.js
client.on('connect', function() { client.on('connect', function() {
client.write('set_protocol', { client.write('set_protocol', {
protocolVersion: version.version, protocolVersion: options.protocolVersion,
serverHost: host, serverHost: options.host,
serverPort: port, serverPort: options.port,
nextState: 1 nextState: 1
}); });
client.state = states.STATUS; client.state = states.STATUS;
}); });
client.connect(port, host); tcp_dns(client, options);
options.connect(client);
} }