From 2483e6fba00f7aa62f00d1a79e978346547f23ac Mon Sep 17 00:00:00 2001 From: deathcap Date: Mon, 18 Jan 2016 00:29:20 -0800 Subject: [PATCH 1/8] Copy createClientStream.js from createClient.js --- src/createClientStream.js | 182 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/createClientStream.js diff --git a/src/createClientStream.js b/src/createClientStream.js new file mode 100644 index 0000000..8867249 --- /dev/null +++ b/src/createClientStream.js @@ -0,0 +1,182 @@ +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'); + +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(); + 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; + + + 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); + } + }; + + 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'); +} From 6125fb7303d010c4895ef2566bc01b8aad88c611 Mon Sep 17 00:00:00 2001 From: deathcap Date: Mon, 18 Jan 2016 01:39:27 -0800 Subject: [PATCH 2/8] createClientStream() takes a stream --- src/browser.js | 2 + src/createClientStream.js | 146 ++------------------------------------ src/index.js | 2 + 3 files changed, 11 insertions(+), 139 deletions(-) diff --git a/src/browser.js b/src/browser.js index f76c7c7..9c9d400 100644 --- a/src/browser.js +++ b/src/browser.js @@ -1,8 +1,10 @@ +var createClientStream = require('./createClientStream'); var Client = require('./client'); var Server = require('./server'); var serializer = require("./transforms/serializer"); module.exports = { + createClientStream: createClientStream, Client: Client, Server: Server, states: require("./states"), diff --git a/src/createClientStream.js b/src/createClientStream.js index 8867249..c5d047c 100644 --- a/src/createClientStream.js +++ b/src/createClientStream.js @@ -1,41 +1,15 @@ -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'); -module.exports=createClient; +module.exports=createClientStream; -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 createClientStream(options) { assert.ok(options, "options is required"); - var port = options.port || 25565; - var host = options.host || 'localhost'; - var clientToken = options.clientToken || uuid.v4(); - var accessToken; + var stream = options.stream; + assert.ok(stream, "stream 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; @@ -43,65 +17,19 @@ function createClient(options) { var mcData=require("minecraft-data")(optVersion); var version = mcData.version; - 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); - } - }; - 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); - } + client.username = options.username; + client.setSocket(stream); 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; } @@ -114,69 +42,9 @@ function createClient(options) { }); } - 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/index.js b/src/index.js index 8d90027..f75ed06 100644 --- a/src/index.js +++ b/src/index.js @@ -2,10 +2,12 @@ var Client = require('./client'); var Server = require('./server'); var serializer = require("./transforms/serializer"); var createClient = require("./createClient"); +var createClientStream = require("./createClientStream"); var createServer = require("./createServer"); module.exports = { createClient: createClient, + createClientStream: createClientStream, createServer: createServer, Client: Client, Server: Server, From 73d69e66d7590a0f7852a4fe6458c9f0b85dbd1c Mon Sep 17 00:00:00 2001 From: deathcap Date: Mon, 18 Jan 2016 01:41:08 -0800 Subject: [PATCH 3/8] Add noPacketFramer option to createClientStream() --- package.json | 1 + src/createClientStream.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 9d23285..d02704f 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "protodef": "0.2.5", "readable-stream": "^1.1.0", "superagent": "~0.10.0", + "through": "^2.3.8", "ursa-purejs": "0.0.3", "uuid": "^2.0.1", "uuid-1345": "^0.99.6", diff --git a/src/createClientStream.js b/src/createClientStream.js index c5d047c..72dce01 100644 --- a/src/createClientStream.js +++ b/src/createClientStream.js @@ -1,6 +1,7 @@ var Client = require('./client'); var assert = require('assert'); var states = require("./states"); +var EmptyTransformStream = require('through')(); module.exports=createClientStream; @@ -19,6 +20,12 @@ function createClientStream(options) { var client = new Client(false,version.majorVersion); + // Options to opt-out of MC protocol packet framing (useful since WS is alreay framed) + // TODO: refactor + if (options.noPacketFramer) { + client.framer = EmptyTransformStream; + } + if(keepAlive) client.on('keep_alive', onKeepAlive); client.once('success', onLogin); client.once("compress", onCompressionRequest); From b33dd88ac2fde492e3e3f822c84e2be5798890b8 Mon Sep 17 00:00:00 2001 From: deathcap Date: Mon, 18 Jan 2016 13:02:39 -0800 Subject: [PATCH 4/8] Make stream option optional, caller can setSocket() later --- src/createClientStream.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/createClientStream.js b/src/createClientStream.js index 72dce01..5da8e8e 100644 --- a/src/createClientStream.js +++ b/src/createClientStream.js @@ -8,7 +8,6 @@ module.exports=createClientStream; function createClientStream(options) { assert.ok(options, "options is required"); var stream = options.stream; - assert.ok(stream, "stream is required"); assert.ok(options.username, "username is required"); var keepAlive = options.keepAlive == null ? true : options.keepAlive; @@ -32,7 +31,7 @@ function createClientStream(options) { client.on("set_compression", onCompressionRequest); client.username = options.username; - client.setSocket(stream); + if (stream) client.setSocket(stream); var timeout = null; return client; From 9a781ca662da8c0d6775fbb2da0bfae89238be20 Mon Sep 17 00:00:00 2001 From: deathcap Date: Mon, 18 Jan 2016 13:13:20 -0800 Subject: [PATCH 5/8] Refactor createClient to use createClientStream --- src/createClient.js | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/createClient.js b/src/createClient.js index 8867249..5b32c63 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -2,6 +2,7 @@ var ursa=require("./ursa"); var net = require('net'); var dns = require('dns'); var Client = require('./client'); +var createClientStream = require('./createClientStream'); var assert = require('assert'); var crypto = require('crypto'); var yggdrasil = require('yggdrasil')({}); @@ -36,21 +37,14 @@ function createClient(options) { 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; - - var client = new Client(false,version.majorVersion); + var client = createClientStream(options); 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) { @@ -102,18 +96,6 @@ function createClient(options) { }); } - 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); @@ -159,12 +141,6 @@ function createClient(options) { } } } - - function onLogin(packet) { - client.state = states.PLAY; - client.uuid = packet.uuid; - client.username = packet.username; - } } From dd1d647d0e5bdae3c57c75272e2d73b03a4d03bf Mon Sep 17 00:00:00 2001 From: deathcap Date: Mon, 18 Jan 2016 13:39:22 -0800 Subject: [PATCH 6/8] Remove stream option, since callers can simply use client.setSocket() --- src/createClientStream.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/createClientStream.js b/src/createClientStream.js index 5da8e8e..4897131 100644 --- a/src/createClientStream.js +++ b/src/createClientStream.js @@ -7,7 +7,6 @@ module.exports=createClientStream; function createClientStream(options) { assert.ok(options, "options is required"); - var stream = options.stream; assert.ok(options.username, "username is required"); var keepAlive = options.keepAlive == null ? true : options.keepAlive; @@ -31,7 +30,6 @@ function createClientStream(options) { client.on("set_compression", onCompressionRequest); client.username = options.username; - if (stream) client.setSocket(stream); var timeout = null; return client; From 834170689ef2016f5dd5edb4473a06325c57c588 Mon Sep 17 00:00:00 2001 From: deathcap Date: Sun, 31 Jan 2016 15:27:26 -0800 Subject: [PATCH 7/8] Remove createClientStream --- package.json | 1 - src/browser.js | 2 -- src/createClient.js | 1 - src/createClientStream.js | 54 --------------------------------------- src/index.js | 2 -- 5 files changed, 60 deletions(-) delete mode 100644 src/createClientStream.js diff --git a/package.json b/package.json index 592b548..fd3abd5 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "prismarine-nbt": "0.1.0", "protodef": "0.2.5", "readable-stream": "^1.1.0", - "through": "^2.3.8", "ursa-purejs": "0.0.3", "uuid-1345": "^0.99.6", "yggdrasil": "0.1.0" diff --git a/src/browser.js b/src/browser.js index 9c9d400..f76c7c7 100644 --- a/src/browser.js +++ b/src/browser.js @@ -1,10 +1,8 @@ -var createClientStream = require('./createClientStream'); var Client = require('./client'); var Server = require('./server'); var serializer = require("./transforms/serializer"); module.exports = { - createClientStream: createClientStream, Client: Client, Server: Server, states: require("./states"), diff --git a/src/createClient.js b/src/createClient.js index 12e1357..40a3d13 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -1,5 +1,4 @@ var Client = require('./client'); -var createClientStream = require('./createClientStream'); var assert = require('assert'); var encrypt = require('./client/encrypt'); diff --git a/src/createClientStream.js b/src/createClientStream.js deleted file mode 100644 index 4897131..0000000 --- a/src/createClientStream.js +++ /dev/null @@ -1,54 +0,0 @@ -var Client = require('./client'); -var assert = require('assert'); -var states = require("./states"); -var EmptyTransformStream = require('through')(); - -module.exports=createClientStream; - -function createClientStream(options) { - assert.ok(options, "options is required"); - - assert.ok(options.username, "username is required"); - 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; - - var client = new Client(false,version.majorVersion); - - // Options to opt-out of MC protocol packet framing (useful since WS is alreay framed) - // TODO: refactor - if (options.noPacketFramer) { - client.framer = EmptyTransformStream; - } - - if(keepAlive) client.on('keep_alive', onKeepAlive); - client.once('success', onLogin); - client.once("compress", onCompressionRequest); - client.on("set_compression", onCompressionRequest); - - client.username = options.username; - - var timeout = null; - return client; - - 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 onLogin(packet) { - client.state = states.PLAY; - client.uuid = packet.uuid; - client.username = packet.username; - } -} diff --git a/src/index.js b/src/index.js index f75ed06..8d90027 100644 --- a/src/index.js +++ b/src/index.js @@ -2,12 +2,10 @@ var Client = require('./client'); var Server = require('./server'); var serializer = require("./transforms/serializer"); var createClient = require("./createClient"); -var createClientStream = require("./createClientStream"); var createServer = require("./createServer"); module.exports = { createClient: createClient, - createClientStream: createClientStream, createServer: createServer, Client: Client, Server: Server, From 39b7199ec6e130a43f1e9ca354a92eda345eba41 Mon Sep 17 00:00:00 2001 From: deathcap Date: Sun, 31 Jan 2016 15:33:06 -0800 Subject: [PATCH 8/8] Add 'stream' option to client/tcp_dns, alternative to host,port for TCP --- src/client/tcp_dns.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/tcp_dns.js b/src/client/tcp_dns.js index a8e3060..579cb80 100644 --- a/src/client/tcp_dns.js +++ b/src/client/tcp_dns.js @@ -6,7 +6,9 @@ module.exports = function(client, options) { options.host = options.host || 'localhost'; options.connect = (client) => { - if(options.port == 25565 && net.isIP(options.host) === 0) { + if (options.stream) { + client.setSocket(options.stream); + } else 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));