From a2598cefda1ff303063f1dd7cf97a6239099f2b1 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Thu, 20 Jul 2017 23:32:25 +0200 Subject: [PATCH] split createServer in 4 plugins --- src/createServer.js | 293 ++++------------------------------------ src/server/handshake.js | 21 +++ src/server/keepalive.js | 37 +++++ src/server/login.js | 143 ++++++++++++++++++++ src/server/ping.js | 71 ++++++++++ src/version.js | 2 +- 6 files changed, 297 insertions(+), 270 deletions(-) create mode 100644 src/server/handshake.js create mode 100644 src/server/keepalive.js create mode 100644 src/server/login.js create mode 100644 src/server/ping.js diff --git a/src/createServer.js b/src/createServer.js index c9ca5de..8aec9c8 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,288 +1,43 @@ 'use strict'; -const crypto = require('crypto'); -const yggserver = require('yggdrasil').server({}); -const states = require("./states"); -const bufferEqual = require('buffer-equal'); const Server = require('./server'); -const UUID = require('uuid-1345'); -const endianToggle = require('endian-toggle'); -const pluginChannels = require('./client/pluginChannels'); const NodeRSA = require('node-rsa'); +const plugins = [ + require('./server/handshake'), + require('./server/keepalive'), + require('./server/login'), + require('./server/ping') +]; module.exports=createServer; -function createServer(options) { - options = options || {}; - const port = options.port != null ? - options.port : - options['server-port'] != null ? - options['server-port'] : - 25565; - const clientErrorHandler = options.errorHandler || function(client, err) { - client.end(); - }; - const host = options.host || '0.0.0.0'; - const kickTimeout = options.kickTimeout || 30 * 1000; - const checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000; - const onlineMode = options['online-mode'] == null ? true : options['online-mode']; - // a function receiving the default status object and the client - // and returning a modified response object. - const beforePing = options.beforePing || null; +function createServer(options={}) { + const { + host = '0.0.0.0', + 'server-port':serverPort, + port = serverPort || 25565, + motd = "A Minecraft server", + 'max-players' : maxPlayers = 20, + version : optVersion = require("./version").defaultVersion, + favicon, + customPackets + } = options; - const enableKeepAlive = options.keepAlive == null ? true : options.keepAlive; - - const optVersion = options.version || require("./version").defaultVersion; const mcData=require("minecraft-data")(optVersion); const version = mcData.version; - const serverKey = new NodeRSA({b: 1024}); - const server = new Server(version.minecraftVersion,options.customPackets); - server.motd = options.motd || "A Minecraft server"; - server.maxPlayers = options['max-players'] || 20; + const server = new Server(version.minecraftVersion,customPackets); + server.mcversion=version; + server.motd = motd; + server.maxPlayers = maxPlayers; server.playerCount = 0; server.onlineModeExceptions = {}; - server.favicon = options.favicon || undefined; + server.favicon = favicon; + server.serverKey = new NodeRSA({b: 1024}); server.on("connection", function(client) { - client.once('set_protocol', onHandshake); - client.once('login_start', onLogin); - client.once('ping_start', onPing); - client.once('legacy_server_list_ping', onLegacyPing); - client.on('error', function(err) { - clientErrorHandler(client, err); - }); - client.on('end', onEnd); - - let keepAlive = false; - let loggedIn = false; - let lastKeepAlive = null; - - let keepAliveTimer = null; - let loginKickTimer = setTimeout(kickForNotLoggingIn, kickTimeout); - - let serverId; - - let sendKeepAliveTime; - - function kickForNotLoggingIn() { - client.end('LoginTimeout'); - } - - function keepAliveLoop() { - if(!keepAlive) - return; - - // check if the last keepAlive was too long ago (kickTimeout) - const elapsed = new Date() - lastKeepAlive; - if(elapsed > kickTimeout) { - client.end('KeepAliveTimeout'); - return; - } - sendKeepAliveTime = new Date(); - client.write('keep_alive', { - keepAliveId: Math.floor(Math.random() * 2147483648) - }); - } - - function onKeepAlive() { - if(sendKeepAliveTime) client.latency = (new Date()) - sendKeepAliveTime; - lastKeepAlive = new Date(); - } - - function startKeepAlive() { - keepAlive = true; - lastKeepAlive = new Date(); - keepAliveTimer = setInterval(keepAliveLoop, checkTimeoutInterval); - client.on('keep_alive', onKeepAlive); - } - - function onEnd() { - clearInterval(keepAliveTimer); - clearTimeout(loginKickTimer); - } - - function onPing() { - const response = { - "version": { - "name": version.minecraftVersion, - "protocol": version.version - }, - "players": { - "max": server.maxPlayers, - "online": server.playerCount, - "sample": [] - }, - "description": {"text": server.motd}, - "favicon": server.favicon - }; - - function answerToPing(err, response) { - if ( err ) return; - client.write('server_info', {response: JSON.stringify(response)}); - } - - if(beforePing) { - if ( beforePing.length > 2 ) { - beforePing(response, client, answerToPing); - } else { - answerToPing(null, beforePing(response, client) || response); - } - } else { - answerToPing(null, response); - } - - client.once('ping', function(packet) { - client.write('ping', {time: packet.time}); - client.end(); - }); - } - - function onLegacyPing(packet) { - if (packet.payload === 1) { - const pingVersion = 1; - sendPingResponse('\xa7' + [pingVersion, version.version, version.minecraftVersion, - server.motd, server.playerCount.toString(), server.maxPlayers.toString()].join('\0')); - } else { - // ping type 0 - sendPingResponse([server.motd, server.playerCount.toString(), server.maxPlayers.toString()].join('\xa7')); - } - - function sendPingResponse(responseString) { - function utf16be(s) { - return endianToggle(new Buffer(s, 'utf16le'), 16); - } - - const responseBuffer = utf16be(responseString); - - const length = responseString.length; // UCS2 characters, not bytes - const lengthBuffer = new Buffer(2); - lengthBuffer.writeUInt16BE(length); - - const raw = Buffer.concat([new Buffer('ff', 'hex'), lengthBuffer, responseBuffer]); - - //client.writeRaw(raw); // not raw enough, it includes length - client.socket.write(raw); - } - - } - - function onLogin(packet) { - client.username = packet.username; - const isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; - const needToVerify = (onlineMode && !isException) || (!onlineMode && isException); - if(needToVerify) { - serverId = crypto.randomBytes(4).toString('hex'); - client.verifyToken = crypto.randomBytes(4); - const publicKeyStrArr = serverKey.exportKey('pkcs8-public-pem').split("\n"); - let publicKeyStr = ""; - for(let i = 1; i < publicKeyStrArr.length - 1; i++) { - publicKeyStr += publicKeyStrArr[i] - } - client.publicKey = new Buffer(publicKeyStr, 'base64'); - client.once('encryption_begin', onEncryptionKeyResponse); - client.write('encryption_begin', { - serverId: serverId, - publicKey: client.publicKey, - verifyToken: client.verifyToken - }); - } else { - loginClient(); - } - } - - function onHandshake(packet) { - client.serverHost = packet.serverHost; - client.serverPort = packet.serverPort; - client.protocolVersion = packet.protocolVersion; - if(packet.nextState == 1) { - client.state = states.STATUS; - } else if(packet.nextState == 2) { - client.state = states.LOGIN; - } - if(client.protocolVersion!=version.version) - { - client.end("Wrong protocol version, expected: "+version.version+" and you are using: "+client.protocolVersion); - } - } - - function onEncryptionKeyResponse(packet) { - let sharedSecret; - try { - const verifyToken = crypto.privateDecrypt({key:serverKey.exportKey(),padding:crypto.constants.RSA_PKCS1_PADDING},packet.verifyToken); - if(!bufferEqual(client.verifyToken, verifyToken)) { - client.end('DidNotEncryptVerifyTokenProperly'); - return; - } - sharedSecret = crypto.privateDecrypt({key:serverKey.exportKey(),padding:crypto.constants.RSA_PKCS1_PADDING},packet.sharedSecret); - } catch(e) { - client.end('DidNotEncryptVerifyTokenProperly'); - return; - } - client.setEncryption(sharedSecret); - - const isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; - const needToVerify = (onlineMode && !isException) || (!onlineMode && isException); - const nextStep = needToVerify ? verifyUsername : loginClient; - nextStep(); - - function verifyUsername() { - yggserver.hasJoined(client.username, serverId, sharedSecret, client.publicKey, function(err, profile) { - if(err) { - client.end("Failed to verify username!"); - return; - } - // Convert to a valid UUID until the session server updates and does - // it automatically - client.uuid = profile.id.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, "$1-$2-$3-$4-$5"); - client.profile = profile; - loginClient(); - }); - } - } - - - // https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163 - function javaUUID(s) - { - const hash = crypto.createHash("md5"); - hash.update(s, 'utf8'); - const buffer = hash.digest(); - buffer[6] = (buffer[6] & 0x0f) | 0x30; - buffer[8] = (buffer[8] & 0x3f) | 0x80; - return buffer; - } - - function nameToMcOfflineUUID(name) - { - return (new UUID(javaUUID("OfflinePlayer:"+name))).toString(); - } - - function loginClient() { - const isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; - if(onlineMode == false || isException) { - client.uuid = nameToMcOfflineUUID(client.username); - } - if (version.version >= 27) { // 14w28a (27) added whole-protocol compression (http://wiki.vg/Protocol_History#14w28a), earlier versions per-packet compressed TODO: refactor into minecraft-data - client.write('compress', { threshold: 256 }); // Default threshold is 256 - client.compressionThreshold = 256; - } - client.write('success', {uuid: client.uuid, username: client.username}); - client.state = states.PLAY; - loggedIn = true; - if(enableKeepAlive) startKeepAlive(); - - clearTimeout(loginKickTimer); - loginKickTimer = null; - - server.playerCount += 1; - client.once('end', function() { - server.playerCount -= 1; - }); - pluginChannels(client, options); - server.emit('login', client); - } + plugins.forEach(plugin => plugin(client,server,options)); }); server.listen(port, host); return server; diff --git a/src/server/handshake.js b/src/server/handshake.js new file mode 100644 index 0000000..c7e5be2 --- /dev/null +++ b/src/server/handshake.js @@ -0,0 +1,21 @@ +const states = require("../states"); + +module.exports=function(client,server) { + + client.once('set_protocol', onHandshake); + + function onHandshake(packet) { + client.serverHost = packet.serverHost; + client.serverPort = packet.serverPort; + client.protocolVersion = packet.protocolVersion; + if (packet.nextState === 1) { + client.state = states.STATUS; + } else if (packet.nextState === 2) { + client.state = states.LOGIN; + } + if (client.protocolVersion !== server.mcversion.version) { + client.end("Wrong protocol version, expected: " + server.mcversion.version + " and you are using: " + client.protocolVersion); + } + } + +}; diff --git a/src/server/keepalive.js b/src/server/keepalive.js new file mode 100644 index 0000000..f712a72 --- /dev/null +++ b/src/server/keepalive.js @@ -0,0 +1,37 @@ +module.exports=function(client,server,{kickTimeout = 30 * 1000,checkTimeoutInterval = 4 * 1000}) { + + let keepAlive = false; + let lastKeepAlive = null; + client._keepAliveTimer = null; + let sendKeepAliveTime; + + + function keepAliveLoop() { + if(!keepAlive) + return; + + // check if the last keepAlive was too long ago (kickTimeout) + const elapsed = new Date() - lastKeepAlive; + if(elapsed > kickTimeout) { + client.end('KeepAliveTimeout'); + return; + } + sendKeepAliveTime = new Date(); + client.write('keep_alive', { + keepAliveId: Math.floor(Math.random() * 2147483648) + }); + } + + function onKeepAlive() { + if(sendKeepAliveTime) client.latency = (new Date()) - sendKeepAliveTime; + lastKeepAlive = new Date(); + } + + client._startKeepAlive= () => { + keepAlive = true; + lastKeepAlive = new Date(); + client._keepAliveTimer = setInterval(keepAliveLoop, checkTimeoutInterval); + client.on('keep_alive', onKeepAlive); + } + +}; diff --git a/src/server/login.js b/src/server/login.js new file mode 100644 index 0000000..6ba82c7 --- /dev/null +++ b/src/server/login.js @@ -0,0 +1,143 @@ +const yggserver = require('yggdrasil').server({}); +const UUID = require('uuid-1345'); +const bufferEqual = require('buffer-equal'); +const crypto = require('crypto'); +const pluginChannels = require('../client/pluginChannels'); +const states = require("../states"); + +module.exports=function(client,server,options) { + const { + 'online-mode' : onlineMode = true, + kickTimeout = 30 * 1000, + keepAlive : enableKeepAlive = true, + errorHandler : clientErrorHandler=(client,err) => client.end(), + } = options; + + let serverId; + + client.on('error', function(err) { + clientErrorHandler(client, err); + }); + client.on('end', onEnd); + + + function onEnd() { + clearInterval(client._keepAliveTimer); + clearTimeout(loginKickTimer); + } + + + client.once('login_start', onLogin); + + let loggedIn = false; + + let loginKickTimer = setTimeout(kickForNotLoggingIn, kickTimeout); + + + function onLogin(packet) { + client.username = packet.username; + const isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; + const needToVerify = (onlineMode && !isException) || (!onlineMode && isException); + if(needToVerify) { + serverId = crypto.randomBytes(4).toString('hex'); + client.verifyToken = crypto.randomBytes(4); + const publicKeyStrArr = server.serverKey.exportKey('pkcs8-public-pem').split("\n"); + let publicKeyStr = ""; + for(let i = 1; i < publicKeyStrArr.length - 1; i++) { + publicKeyStr += publicKeyStrArr[i] + } + client.publicKey = new Buffer(publicKeyStr, 'base64'); + client.once('encryption_begin', onEncryptionKeyResponse); + client.write('encryption_begin', { + serverId: serverId, + publicKey: client.publicKey, + verifyToken: client.verifyToken + }); + } else { + loginClient(); + } + } + + function kickForNotLoggingIn() { + client.end('LoginTimeout'); + } + + + function onEncryptionKeyResponse(packet) { + let sharedSecret; + try { + const verifyToken = crypto.privateDecrypt({key:server.serverKey.exportKey(),padding:crypto.constants.RSA_PKCS1_PADDING},packet.verifyToken); + if(!bufferEqual(client.verifyToken, verifyToken)) { + client.end('DidNotEncryptVerifyTokenProperly'); + return; + } + sharedSecret = crypto.privateDecrypt({key:server.serverKey.exportKey(),padding:crypto.constants.RSA_PKCS1_PADDING},packet.sharedSecret); + } catch(e) { + client.end('DidNotEncryptVerifyTokenProperly'); + return; + } + client.setEncryption(sharedSecret); + + const isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; + const needToVerify = (onlineMode && !isException) || (!onlineMode && isException); + const nextStep = needToVerify ? verifyUsername : loginClient; + nextStep(); + + function verifyUsername() { + yggserver.hasJoined(client.username, serverId, sharedSecret, client.publicKey, function(err, profile) { + if(err) { + client.end("Failed to verify username!"); + return; + } + // Convert to a valid UUID until the session server updates and does + // it automatically + client.uuid = profile.id.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, "$1-$2-$3-$4-$5"); + client.profile = profile; + loginClient(); + }); + } + } + + + // https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163 + function javaUUID(s) + { + const hash = crypto.createHash("md5"); + hash.update(s, 'utf8'); + const buffer = hash.digest(); + buffer[6] = (buffer[6] & 0x0f) | 0x30; + buffer[8] = (buffer[8] & 0x3f) | 0x80; + return buffer; + } + + function nameToMcOfflineUUID(name) + { + return (new UUID(javaUUID("OfflinePlayer:"+name))).toString(); + } + + function loginClient() { + const isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; + if(onlineMode === false || isException) { + client.uuid = nameToMcOfflineUUID(client.username); + } + if (server.mcversion.version >= 27) { // 14w28a (27) added whole-protocol compression (http://wiki.vg/Protocol_History#14w28a), earlier versions per-packet compressed TODO: refactor into minecraft-data + client.write('compress', { threshold: 256 }); // Default threshold is 256 + client.compressionThreshold = 256; + } + client.write('success', {uuid: client.uuid, username: client.username}); + client.state = states.PLAY; + loggedIn = true; + if(enableKeepAlive) client._startKeepAlive(); + + clearTimeout(loginKickTimer); + loginKickTimer = null; + + server.playerCount += 1; + client.once('end', function() { + server.playerCount -= 1; + }); + pluginChannels(client, options); + server.emit('login', client); + } + +}; diff --git a/src/server/ping.js b/src/server/ping.js new file mode 100644 index 0000000..794c245 --- /dev/null +++ b/src/server/ping.js @@ -0,0 +1,71 @@ +const endianToggle = require('endian-toggle'); + +module.exports=function(client,server,{beforePing = null}) { + client.once('ping_start', onPing); + client.once('legacy_server_list_ping', onLegacyPing); + + function onPing() { + const response = { + "version": { + "name": server.mcversion.minecraftVersion, + "protocol": server.mcversion.version + }, + "players": { + "max": server.maxPlayers, + "online": server.playerCount, + "sample": [] + }, + "description": {"text": server.motd}, + "favicon": server.favicon + }; + + function answerToPing(err, response) { + if ( err ) return; + client.write('server_info', {response: JSON.stringify(response)}); + } + + if(beforePing) { + if ( beforePing.length > 2 ) { + beforePing(response, client, answerToPing); + } else { + answerToPing(null, beforePing(response, client) || response); + } + } else { + answerToPing(null, response); + } + + client.once('ping', function(packet) { + client.write('ping', {time: packet.time}); + client.end(); + }); + } + + function onLegacyPing(packet) { + if (packet.payload === 1) { + const pingVersion = 1; + sendPingResponse('\xa7' + [pingVersion, server.mcversion.version, server.mcversion.minecraftVersion, + server.motd, server.playerCount.toString(), server.maxPlayers.toString()].join('\0')); + } else { + // ping type 0 + sendPingResponse([server.motd, server.playerCount.toString(), server.maxPlayers.toString()].join('\xa7')); + } + + function sendPingResponse(responseString) { + function utf16be(s) { + return endianToggle(new Buffer(s, 'utf16le'), 16); + } + + const responseBuffer = utf16be(responseString); + + const length = responseString.length; // UCS2 characters, not bytes + const lengthBuffer = new Buffer(2); + lengthBuffer.writeUInt16BE(length); + + const raw = Buffer.concat([new Buffer('ff', 'hex'), lengthBuffer, responseBuffer]); + + //client.writeRaw(raw); // not raw enough, it includes length + client.socket.write(raw); + } + + } +}; diff --git a/src/version.js b/src/version.js index 8a9e949..85fe32a 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict'; module.exports={ - defaultVersion:'1.8', + defaultVersion:'1.12', supportedVersions:['1.7','1.8','1.9','1.10','1.11.2','1.12'] };