node-minecraft-protocol/src/createServer.js

290 lines
9.6 KiB
JavaScript
Raw Normal View History

2016-04-27 19:27:32 -04:00
'use strict';
2016-02-01 04:45:13 -05:00
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');
module.exports=createServer;
function createServer(options) {
options = options || {};
2016-02-01 04:45:13 -05:00
const port = options.port != null ?
options.port :
options['server-port'] != null ?
options['server-port'] :
25565;
2017-01-28 17:50:33 -05:00
const clientErrorHandler = options.errorHandler || function(client, err) {
client.end();
};
2016-02-01 04:45:13 -05:00
const host = options.host || '0.0.0.0';
const kickTimeout = options.kickTimeout || 30 * 1000;
2016-02-01 04:45:13 -05:00
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.
2016-02-01 04:45:13 -05:00
const beforePing = options.beforePing || null;
2016-02-01 04:45:13 -05:00
const enableKeepAlive = options.keepAlive == null ? true : options.keepAlive;
2015-08-04 15:45:46 -04:00
2016-02-01 04:45:13 -05:00
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;
server.playerCount = 0;
server.onlineModeExceptions = {};
2016-04-10 16:23:25 -04:00
server.favicon = options.favicon || undefined;
server.on("connection", function(client) {
2015-09-14 13:51:36 -04:00
client.once('set_protocol', onHandshake);
client.once('login_start', onLogin);
client.once('ping_start', onPing);
client.once('legacy_server_list_ping', onLegacyPing);
2017-01-28 17:50:33 -05:00
client.on('error', function(err) {
clientErrorHandler(client, err);
});
client.on('end', onEnd);
2016-02-01 04:45:13 -05:00
let keepAlive = false;
let loggedIn = false;
let lastKeepAlive = null;
2016-02-01 04:45:13 -05:00
let keepAliveTimer = null;
let loginKickTimer = setTimeout(kickForNotLoggingIn, kickTimeout);
2016-02-01 04:45:13 -05:00
let serverId;
let sendKeepAliveTime;
function kickForNotLoggingIn() {
client.end('LoginTimeout');
}
function keepAliveLoop() {
if(!keepAlive)
return;
// check if the last keepAlive was too long ago (kickTimeout)
2016-02-01 04:45:13 -05:00
const elapsed = new Date() - lastKeepAlive;
if(elapsed > kickTimeout) {
client.end('KeepAliveTimeout');
return;
}
2015-12-21 17:53:09 -05:00
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() {
2016-02-01 04:45:13 -05:00
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);
}
2015-09-14 13:51:36 -04:00
client.once('ping', function(packet) {
client.write('ping', {time: packet.time});
client.end();
});
}
function onLegacyPing(packet) {
if (packet.payload === 1) {
2016-02-01 04:45:13 -05:00
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);
}
2016-02-01 04:45:13 -05:00
const responseBuffer = utf16be(responseString);
2016-02-01 04:45:13 -05:00
const length = responseString.length; // UCS2 characters, not bytes
const lengthBuffer = new Buffer(2);
lengthBuffer.writeUInt16BE(length);
2016-02-01 04:45:13 -05:00
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;
2016-02-01 04:45:13 -05:00
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");
2016-02-01 04:45:13 -05:00
let publicKeyStr = "";
for(let i = 1; i < publicKeyStrArr.length - 1; i++) {
publicKeyStr += publicKeyStrArr[i]
}
client.publicKey = new Buffer(publicKeyStr, 'base64');
2015-09-14 13:51:36 -04:00
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);
2016-02-01 04:45:13 -05:00
const isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
const needToVerify = (onlineMode && !isException) || (!onlineMode && isException);
const nextStep = needToVerify ? verifyUsername : loginClient;
nextStep();
function verifyUsername() {
2015-10-07 20:50:38 -04:00
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
2015-10-07 20:50:38 -04:00
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)
{
2016-02-01 04:45:13 -05:00
const hash = crypto.createHash("md5");
hash.update(s, 'utf8');
2016-02-01 04:45:13 -05:00
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() {
2016-02-01 04:45:13 -05:00
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;
}
2015-09-14 13:51:36 -04:00
client.write('success', {uuid: client.uuid, username: client.username});
client.state = states.PLAY;
loggedIn = true;
2015-08-04 15:45:46 -04:00
if(enableKeepAlive) startKeepAlive();
clearTimeout(loginKickTimer);
loginKickTimer = null;
server.playerCount += 1;
client.once('end', function() {
server.playerCount -= 1;
});
pluginChannels(client, options);
server.emit('login', client);
}
});
server.listen(port, host);
return server;
}