diff --git a/examples/client_chat/client_chat.js b/examples/client_chat/client_chat.js index 3c5d66d..d70b7ef 100644 --- a/examples/client_chat/client_chat.js +++ b/examples/client_chat/client_chat.js @@ -93,6 +93,10 @@ client.on('connect', function() { console.info(color('Successfully connected to ' + host + ':' + port, "blink+green")); }); +client.on('disconnect', function(packet) { + console.log('disconnected: '+ packet.reason); +}); + client.on('end', function() { console.log("Connection lost"); process.exit(); diff --git a/examples/client_echo/client_echo.js b/examples/client_echo/client_echo.js index 0caf0f1..1f64b4a 100644 --- a/examples/client_echo/client_echo.js +++ b/examples/client_echo/client_echo.js @@ -15,6 +15,9 @@ var client = mc.createClient({ client.on('connect', function() { console.info('connected'); }); +client.on('disconnect', function(packet) { + console.log('disconnected: '+ packet.reason); +}); client.on('chat', function(packet) { var jsonMsg = JSON.parse(packet.message); if(jsonMsg.translate == 'chat.type.announcement' || jsonMsg.translate == 'chat.type.text') { diff --git a/gulpfile.js b/gulpfile.js index 6053692..d3e62ff 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,8 +3,7 @@ var gulp = require('gulp'); var plumber = require('gulp-plumber'); var babel = require('gulp-babel'); var options = { - stage: 0, // Dat ES7 goodness - optional: ["runtime"] + presets: ['es2015'] }; var sourcemaps = require('gulp-sourcemaps'); diff --git a/package.json b/package.json index d02704f..592b548 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,12 @@ }, "browser": "browser.js", "devDependencies": { - "chai": "^2.3.0", + "babel-preset-es2015": "^6.3.13", "espower-loader": "^1.0.0", - "gulp": "^3.8.11", - "gulp-babel": "^5.1.0", + "gulp": "^3.9.0", + "gulp-babel": "^6.1.1", "gulp-plumber": "^1.0.1", - "gulp-sourcemaps": "^1.3.0", + "gulp-sourcemaps": "^1.6.0", "intelli-espower-loader": "^1.0.0", "minecraft-wrap": "~0.6.5", "mocha": "~2.3.3", @@ -42,18 +42,13 @@ "source-map-support": "^0.3.2" }, "dependencies": { - "babel-runtime": "^5.4.4", "buffer-equal": "0.0.1", - "lodash.reduce": "^3.1.2", "minecraft-data": "^0.16.1", - "node-uuid": "~1.4.1", "prismarine-nbt": "0.1.0", "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", "yggdrasil": "0.1.0" }, diff --git a/src/client.js b/src/client.js index 40eba1f..8a0a129 100644 --- a/src/client.js +++ b/src/client.js @@ -10,26 +10,25 @@ var createDeserializer=require("./transforms/serializer").createDeserializer; class Client extends EventEmitter { - packetsToParse={}; - serializer; - compressor=null; - framer=framing.createFramer(); - cipher=null; - decipher=null; - splitter=framing.createSplitter(); - decompressor=null; - deserializer; - isServer; - version; - protocolState=states.HANDSHAKING; - ended=true; - latency=0; - constructor(isServer,version) { super(); this.version=version; this.isServer = !!isServer; this.setSerializer(states.HANDSHAKING); + this.packetsToParse={}; + this.serializer; + this.compressor=null; + this.framer=framing.createFramer(); + this.cipher=null; + this.decipher=null; + this.splitter=framing.createSplitter(); + this.decompressor=null; + this.deserializer; + this.isServer; + this.version; + this.protocolState=states.HANDSHAKING; + this.ended=true; + this.latency=0; this.on('newListener', function(event, listener) { var direction = this.isServer ? 'toServer' : 'toClient'; @@ -76,6 +75,8 @@ class Client extends EventEmitter parsed.metadata.name=parsed.data.name; parsed.data=parsed.data.params; parsed.metadata.state=state; + debug("read packet " + state + "." + parsed.metadata.name); + debug(parsed.data); this.emit('packet', parsed.data, parsed.metadata); this.emit(parsed.metadata.name, parsed.data, parsed.metadata); this.emit('raw.' + parsed.metadata.name, parsed.buffer, parsed.metadata); @@ -214,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 5b32c63..12e1357 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -1,158 +1,36 @@ -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')({}); -var yggserver = require('yggdrasil').server({}); -var states = require("./states"); -var debug = require("./debug"); -var uuid = require('uuid'); + +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(); - var accessToken; - assert.ok(options.username, "username is required"); - var haveCredentials = options.password != null || (clientToken != null && options.session != null); 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 = createClientStream(options); - client.on('connect', onConnect); - client.once('encryption_begin', onEncryptionKeyRequest); - 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); - } - }; + var client = new Client(false, options.majorVersion); - 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); - } + tcp_dns(client, options); + setProtocol(client, options); + keepalive(client, options); + encrypt(client, options); + play(client, options); + compress(client, options); + caseCorrect(client, options); - 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 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 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/datatypes/minecraft.js b/src/datatypes/minecraft.js index 2b89b46..956212f 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -1,5 +1,5 @@ var nbt = require('prismarine-nbt'); -var uuid = require('node-uuid'); +var UUID = require('uuid-1345'); module.exports = { 'UUID': [readUUID, writeUUID, 16], @@ -11,13 +11,14 @@ module.exports = { function readUUID(buffer, offset) { return { - value: uuid.unparse(buffer, offset), + value: UUID.stringify(buffer.slice(offset,16)), size: 16 }; } function writeUUID(value, buffer, offset) { - uuid.parse(value, buffer, offset); + var buf=UUID.parse(value); + buf.copy(buffer,offset); return offset + 16; } 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); } diff --git a/src/server.js b/src/server.js index acc76f4..d604b9f 100644 --- a/src/server.js +++ b/src/server.js @@ -5,14 +5,13 @@ var states = require("./states"); class Server extends EventEmitter { - socketServer=null; - cipher=null; - decipher=null; - clients={}; - constructor(version) { super(); this.version=version; + this.socketServer=null; + this.cipher=null; + this.decipher=null; + this.clients={}; } listen(port, host) {