diff --git a/src/client.js b/src/client.js index 200d738..8a0a129 100644 --- a/src/client.js +++ b/src/client.js @@ -215,6 +215,13 @@ class Client extends EventEmitter else 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; diff --git a/src/client/caseCorrect.js b/src/client/caseCorrect.js new file mode 100644 index 0000000..2eb634a --- /dev/null +++ b/src/client/caseCorrect.js @@ -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); + } +}; diff --git a/src/client/compress.js b/src/client/compress.js new file mode 100644 index 0000000..cebe34f --- /dev/null +++ b/src/client/compress.js @@ -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 +}; diff --git a/src/client/encrypt.js b/src/client/encrypt.js new file mode 100644 index 0000000..696ddfc --- /dev/null +++ b/src/client/encrypt.js @@ -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'); +} diff --git a/src/client/keepalive.js b/src/client/keepalive.js new file mode 100644 index 0000000..3fc817b --- /dev/null +++ b/src/client/keepalive.js @@ -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 + }); + } + +}; diff --git a/src/client/play.js b/src/client/play.js new file mode 100644 index 0000000..ebc491e --- /dev/null +++ b/src/client/play.js @@ -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; + } +}; diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js new file mode 100644 index 0000000..865b44e --- /dev/null +++ b/src/client/setProtocol.js @@ -0,0 +1,21 @@ + +var states = require("../states"); + +module.exports = function(client, options) { + client.on('connect', onConnect); + + function onConnect() { + client.write('set_protocol', { + protocolVersion: options.protocolVersion, + serverHost: options.host, + serverPort: options.port, + nextState: 2 + }); + client.state = states.LOGIN; + client.write('login_start', { + username: client.username + }); + } + + +} diff --git a/src/client/tcp_dns.js b/src/client/tcp_dns.js new file mode 100644 index 0000000..a8e3060 --- /dev/null +++ b/src/client/tcp_dns.js @@ -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)); + } + }; +}; diff --git a/src/createClient.js b/src/createClient.js index 83dc1b7..40a3d13 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -1,182 +1,35 @@ -var ursa=require("./ursa"); -var net = require('net'); -var dns = require('dns'); var Client = require('./client'); var assert = require('assert'); -var crypto = require('crypto'); -var yggdrasil = require('yggdrasil')({}); -var yggserver = require('yggdrasil').server({}); -var states = require("./states"); -var debug = require("./debug"); -var UUID = require('uuid-1345'); + +var encrypt = require('./client/encrypt'); +var keepalive = require('./client/keepalive'); +var compress = require('./client/compress'); +var caseCorrect = require('./client/caseCorrect'); +var setProtocol = require('./client/setProtocol'); +var play = require('./client/play'); +var tcp_dns = require('./client/tcp_dns'); 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) { 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"); - 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 mcData=require("minecraft-data")(optVersion); 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); - client.on('connect', onConnect); - if(keepAlive) client.on('keep_alive', onKeepAlive); - client.once('encryption_begin', onEncryptionKeyRequest); - client.once('success', onLogin); - client.once("compress", onCompressionRequest); - client.on("set_compression", onCompressionRequest); - 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); - } - }; + tcp_dns(client, options); + setProtocol(client, options); + keepalive(client, options); + encrypt(client, options); + play(client, options); + compress(client, options); + caseCorrect(client, options); - 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; - - function onConnect() { - client.write('set_protocol', { - protocolVersion: version.version, - serverHost: host, - 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'); } diff --git a/src/ping.js b/src/ping.js index cf03f2f..4a58353 100644 --- a/src/ping.js +++ b/src/ping.js @@ -1,17 +1,20 @@ var net = require('net'); var Client = require('./client'); var states = require("./states"); +var tcp_dns = require('./client/tcp_dns'); module.exports = ping; function ping(options, cb) { - var host = options.host || 'localhost'; - var port = options.port || 25565; + options.host = options.host || 'localhost'; + options.port = options.port || 25565; var optVersion = options.version || require("./version").defaultVersion; var mcData=require("minecraft-data")(optVersion); 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) { cb(err); }); @@ -32,15 +35,17 @@ function ping(options, cb) { client.write('ping_start', {}); }); + // TODO: refactor with src/client/setProtocol.js client.on('connect', function() { client.write('set_protocol', { - protocolVersion: version.version, - serverHost: host, - serverPort: port, + protocolVersion: options.protocolVersion, + serverHost: options.host, + serverPort: options.port, nextState: 1 }); client.state = states.STATUS; }); - client.connect(port, host); + tcp_dns(client, options); + options.connect(client); }