mirror of
https://github.com/unmojang/node-minecraft-protocol.git
synced 2025-09-27 21:24:25 -04:00
Merge pull request #196 from rom1504/divide_index
split index.js into mainly createClient.js and createServer.js
This commit is contained in:
commit
fb481210fa
170
src/createClient.js
Normal file
170
src/createClient.js
Normal file
@ -0,0 +1,170 @@
|
||||
var mcHexDigest=require("./mcHexDigest");
|
||||
var ursa=require("./ursa");
|
||||
var version = require("./version");
|
||||
var net = require('net');
|
||||
var dns = require('dns');
|
||||
var Client = require('./client');
|
||||
var assert = require('assert');
|
||||
var crypto = require('crypto');
|
||||
var Yggdrasil = require('./yggdrasil.js');
|
||||
var getSession = Yggdrasil.getSession;
|
||||
var joinServer = Yggdrasil.joinServer;
|
||||
var serializer = require("./transforms/serializer");
|
||||
var states = serializer.states;
|
||||
var debug = require("./debug");
|
||||
|
||||
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 || Yggdrasil.generateUUID();
|
||||
var accessToken = options.accessToken || null;
|
||||
|
||||
assert.ok(options.username, "username is required");
|
||||
var haveCredentials = options.password != null || (clientToken != null && accessToken != null);
|
||||
var keepAlive = options.keepAlive == null ? true : options.keepAlive;
|
||||
|
||||
|
||||
var client = new Client(false);
|
||||
client.on('connect', onConnect);
|
||||
if(keepAlive) client.on([states.PLAY, 0x00], onKeepAlive);
|
||||
client.once([states.LOGIN, 0x01], onEncryptionKeyRequest);
|
||||
client.once([states.LOGIN, 0x02], 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.username;
|
||||
accessToken = session.accessToken;
|
||||
client.emit('session');
|
||||
client.connect(port, host);
|
||||
}
|
||||
};
|
||||
|
||||
if(accessToken != null) getSession(options.username, accessToken, clientToken, true, cb);
|
||||
else getSession(options.username, options.password, clientToken, false, cb);
|
||||
} else {
|
||||
// assume the server is in offline mode and just go for it.
|
||||
client.username = options.username;
|
||||
client.connect(port, host);
|
||||
}
|
||||
|
||||
return client;
|
||||
|
||||
function onConnect() {
|
||||
client.write(0x00, {
|
||||
protocolVersion: version.version,
|
||||
serverHost: host,
|
||||
serverPort: port,
|
||||
nextState: 2
|
||||
});
|
||||
client.state = states.LOGIN;
|
||||
client.write(0x00, {
|
||||
username: client.username
|
||||
});
|
||||
}
|
||||
|
||||
function onCompressionRequest(packet) {
|
||||
client.compressionThreshold = packet.threshold;
|
||||
}
|
||||
|
||||
function onKeepAlive(packet) {
|
||||
client.write(0x00, {
|
||||
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) {
|
||||
var hash = crypto.createHash('sha1');
|
||||
hash.update(packet.serverId);
|
||||
hash.update(sharedSecret);
|
||||
hash.update(packet.publicKey);
|
||||
|
||||
var digest = mcHexDigest(hash);
|
||||
joinServer(client.username, digest, accessToken, client.session.selectedProfile.id, 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(0x01, {
|
||||
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');
|
||||
}
|
210
src/createServer.js
Normal file
210
src/createServer.js
Normal file
@ -0,0 +1,210 @@
|
||||
var mcHexDigest=require("./mcHexDigest");
|
||||
var ursa=require("./ursa");
|
||||
var version = require("./version");
|
||||
var crypto = require('crypto');
|
||||
var Yggdrasil = require('./yggdrasil.js');
|
||||
var validateSession = Yggdrasil.validateSession;
|
||||
var serializer = require("./transforms/serializer");
|
||||
var states = serializer.states;
|
||||
var bufferEqual = require('buffer-equal');
|
||||
var Server = require('./server');
|
||||
|
||||
module.exports=createServer;
|
||||
|
||||
function createServer(options) {
|
||||
options = options || {};
|
||||
var port = options.port != null ?
|
||||
options.port :
|
||||
options['server-port'] != null ?
|
||||
options['server-port'] :
|
||||
25565;
|
||||
var host = options.host || '0.0.0.0';
|
||||
var kickTimeout = options.kickTimeout || 10 * 1000;
|
||||
var checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000;
|
||||
var 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.
|
||||
var beforePing = options.beforePing || null;
|
||||
|
||||
var serverKey = ursa.generatePrivateKey(1024);
|
||||
|
||||
var server = new Server(options);
|
||||
server.motd = options.motd || "A Minecraft server";
|
||||
server.maxPlayers = options['max-players'] || 20;
|
||||
server.playerCount = 0;
|
||||
server.onlineModeExceptions = {};
|
||||
server.on("connection", function(client) {
|
||||
client.once([states.HANDSHAKING, 0x00], onHandshake);
|
||||
client.once([states.LOGIN, 0x00], onLogin);
|
||||
client.once([states.STATUS, 0x00], onPing);
|
||||
client.on('end', onEnd);
|
||||
|
||||
var keepAlive = false;
|
||||
var loggedIn = false;
|
||||
var lastKeepAlive = null;
|
||||
|
||||
var keepAliveTimer = null;
|
||||
var loginKickTimer = setTimeout(kickForNotLoggingIn, kickTimeout);
|
||||
|
||||
var hash;
|
||||
|
||||
function kickForNotLoggingIn() {
|
||||
client.end('LoginTimeout');
|
||||
}
|
||||
|
||||
function keepAliveLoop() {
|
||||
if(!keepAlive)
|
||||
return;
|
||||
|
||||
// check if the last keepAlive was too long ago (kickTimeout)
|
||||
var elapsed = new Date() - lastKeepAlive;
|
||||
if(elapsed > kickTimeout) {
|
||||
client.end('KeepAliveTimeout');
|
||||
return;
|
||||
}
|
||||
client.write(0x00, {
|
||||
keepAliveId: Math.floor(Math.random() * 2147483648)
|
||||
});
|
||||
}
|
||||
|
||||
function onKeepAlive(packet) {
|
||||
lastKeepAlive = new Date();
|
||||
}
|
||||
|
||||
function startKeepAlive() {
|
||||
keepAlive = true;
|
||||
lastKeepAlive = new Date();
|
||||
keepAliveTimer = setInterval(keepAliveLoop, checkTimeoutInterval);
|
||||
client.on(0x00, onKeepAlive);
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
clearInterval(keepAliveTimer);
|
||||
clearTimeout(loginKickTimer);
|
||||
}
|
||||
|
||||
function onPing(packet) {
|
||||
var response = {
|
||||
"version": {
|
||||
"name": version.minecraftVersion,
|
||||
"protocol": version.version
|
||||
},
|
||||
"players": {
|
||||
"max": server.maxPlayers,
|
||||
"online": server.playerCount,
|
||||
"sample": []
|
||||
},
|
||||
"description": {"text": server.motd},
|
||||
"favicon": server.favicon
|
||||
};
|
||||
|
||||
if(beforePing) {
|
||||
response = beforePing(response, client) || response;
|
||||
}
|
||||
|
||||
client.once([states.STATUS, 0x01], function(packet) {
|
||||
client.write(0x01, {time: packet.time});
|
||||
client.end();
|
||||
});
|
||||
client.write(0x00, {response: JSON.stringify(response)});
|
||||
}
|
||||
|
||||
function onLogin(packet) {
|
||||
client.username = packet.username;
|
||||
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
|
||||
var needToVerify = (onlineMode && !isException) || (!onlineMode && isException);
|
||||
if(needToVerify) {
|
||||
var serverId = crypto.randomBytes(4).toString('hex');
|
||||
client.verifyToken = crypto.randomBytes(4);
|
||||
var publicKeyStrArr = serverKey.toPublicPem("utf8").split("\n");
|
||||
var publicKeyStr = "";
|
||||
for(var i = 1; i < publicKeyStrArr.length - 2; i++) {
|
||||
publicKeyStr += publicKeyStrArr[i]
|
||||
}
|
||||
client.publicKey = new Buffer(publicKeyStr, 'base64');
|
||||
hash = crypto.createHash("sha1");
|
||||
hash.update(serverId);
|
||||
client.once([states.LOGIN, 0x01], onEncryptionKeyResponse);
|
||||
client.write(0x01, {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function onEncryptionKeyResponse(packet) {
|
||||
try {
|
||||
var verifyToken = serverKey.decrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING);
|
||||
if(!bufferEqual(client.verifyToken, verifyToken)) {
|
||||
client.end('DidNotEncryptVerifyTokenProperly');
|
||||
return;
|
||||
}
|
||||
var sharedSecret = serverKey.decrypt(packet.sharedSecret, undefined, undefined, ursa.RSA_PKCS1_PADDING);
|
||||
} catch(e) {
|
||||
client.end('DidNotEncryptVerifyTokenProperly');
|
||||
return;
|
||||
}
|
||||
client.setEncryption(sharedSecret);
|
||||
hash.update(sharedSecret);
|
||||
hash.update(client.publicKey);
|
||||
|
||||
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
|
||||
var needToVerify = (onlineMode && !isException) || (!onlineMode && isException);
|
||||
var nextStep = needToVerify ? verifyUsername : loginClient;
|
||||
nextStep();
|
||||
|
||||
function verifyUsername() {
|
||||
var digest = mcHexDigest(hash);
|
||||
validateSession(client.username, digest, function(err, uuid) {
|
||||
if(err) {
|
||||
client.end("Failed to verify username!");
|
||||
return;
|
||||
}
|
||||
client.uuid = uuid;
|
||||
// Convert to a valid UUID until the session server updates and does
|
||||
// it automatically
|
||||
client.uuid = client.uuid.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, "$1-$2-$3-$4-$5");
|
||||
loginClient();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loginClient() {
|
||||
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
|
||||
if(onlineMode == false || isException) {
|
||||
client.uuid = "0-0-0-0-0";
|
||||
}
|
||||
//client.write('compress', { threshold: 256 }); // Default threshold is 256
|
||||
//client.compressionThreshold = 256;
|
||||
client.write(0x02, {uuid: client.uuid, username: client.username});
|
||||
client.state = states.PLAY;
|
||||
loggedIn = true;
|
||||
startKeepAlive();
|
||||
|
||||
clearTimeout(loginKickTimer);
|
||||
loginKickTimer = null;
|
||||
|
||||
server.playerCount += 1;
|
||||
client.once('end', function() {
|
||||
server.playerCount -= 1;
|
||||
});
|
||||
server.emit('login', client);
|
||||
}
|
||||
});
|
||||
server.listen(port, host);
|
||||
return server;
|
||||
}
|
424
src/index.js
424
src/index.js
@ -1,42 +1,21 @@
|
||||
var assert = require('assert')
|
||||
, crypto = require('crypto')
|
||||
, bufferEqual = require('buffer-equal')
|
||||
, Client = require('./client')
|
||||
, dns = require('dns')
|
||||
, net = require('net')
|
||||
, Server = require('./server')
|
||||
, Yggdrasil = require('./yggdrasil.js')
|
||||
, getSession = Yggdrasil.getSession
|
||||
, validateSession = Yggdrasil.validateSession
|
||||
, joinServer = Yggdrasil.joinServer
|
||||
, serializer = require("./transforms/serializer")
|
||||
, states = serializer.states
|
||||
, debug = require("./debug")
|
||||
, utils = require("./utils")
|
||||
;
|
||||
|
||||
var ursa;
|
||||
try {
|
||||
ursa = require("ursa");
|
||||
} catch(e) {
|
||||
console.log("You are using a pure-javascript implementation of RSA.");
|
||||
console.log("Your performance might be subpar. Please consider installing URSA");
|
||||
ursa = require("ursa-purejs");
|
||||
}
|
||||
|
||||
var version = 47;
|
||||
var minecraftVersion = '1.8.1';
|
||||
|
||||
var Client = require('./client');
|
||||
var Server = require('./server');
|
||||
var Yggdrasil = require('./yggdrasil.js');
|
||||
var serializer = require("./transforms/serializer");
|
||||
var utils = require("./utils");
|
||||
var version = require("./version");
|
||||
var packets = require("../protocol/protocol");
|
||||
var readPackets = require("./packets").readPackets;
|
||||
var packetIndexes = readPackets(packets, states);
|
||||
var packetIndexes = readPackets(packets, serializer.states);
|
||||
var createClient = require("./createClient");
|
||||
var createServer = require("./createServer");
|
||||
|
||||
module.exports = {
|
||||
createClient: createClient,
|
||||
createServer: createServer,
|
||||
Client: Client,
|
||||
Server: Server,
|
||||
states: states,
|
||||
states: serializer.states,
|
||||
createPacketBuffer: serializer.createPacketBuffer,
|
||||
parsePacketData: serializer.parsePacketData,
|
||||
packetFields: packetIndexes.packetFields,
|
||||
@ -48,385 +27,6 @@ module.exports = {
|
||||
evalCondition: utils.evalCondition,
|
||||
ping: require('./ping'),
|
||||
yggdrasil: Yggdrasil,
|
||||
version: version,
|
||||
minecraftVersion: minecraftVersion
|
||||
version: version.version,
|
||||
minecraftVersion: version.minecraftVersion
|
||||
};
|
||||
|
||||
function createServer(options) {
|
||||
options = options || {};
|
||||
var port = options.port != null ?
|
||||
options.port :
|
||||
options['server-port'] != null ?
|
||||
options['server-port'] :
|
||||
25565;
|
||||
var host = options.host || '0.0.0.0';
|
||||
var kickTimeout = options.kickTimeout || 10 * 1000;
|
||||
var checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000;
|
||||
var 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.
|
||||
var beforePing = options.beforePing || null;
|
||||
|
||||
var serverKey = ursa.generatePrivateKey(1024);
|
||||
|
||||
var server = new Server(options);
|
||||
server.motd = options.motd || "A Minecraft server";
|
||||
server.maxPlayers = options['max-players'] || 20;
|
||||
server.playerCount = 0;
|
||||
server.onlineModeExceptions = {};
|
||||
server.on("connection", function(client) {
|
||||
client.once([states.HANDSHAKING, 0x00], onHandshake);
|
||||
client.once([states.LOGIN, 0x00], onLogin);
|
||||
client.once([states.STATUS, 0x00], onPing);
|
||||
client.on('end', onEnd);
|
||||
|
||||
var keepAlive = false;
|
||||
var loggedIn = false;
|
||||
var lastKeepAlive = null;
|
||||
|
||||
var keepAliveTimer = null;
|
||||
var loginKickTimer = setTimeout(kickForNotLoggingIn, kickTimeout);
|
||||
|
||||
var hash;
|
||||
|
||||
function kickForNotLoggingIn() {
|
||||
client.end('LoginTimeout');
|
||||
}
|
||||
|
||||
function keepAliveLoop() {
|
||||
if(!keepAlive)
|
||||
return;
|
||||
|
||||
// check if the last keepAlive was too long ago (kickTimeout)
|
||||
var elapsed = new Date() - lastKeepAlive;
|
||||
if(elapsed > kickTimeout) {
|
||||
client.end('KeepAliveTimeout');
|
||||
return;
|
||||
}
|
||||
client.write(0x00, {
|
||||
keepAliveId: Math.floor(Math.random() * 2147483648)
|
||||
});
|
||||
}
|
||||
|
||||
function onKeepAlive(packet) {
|
||||
lastKeepAlive = new Date();
|
||||
}
|
||||
|
||||
function startKeepAlive() {
|
||||
keepAlive = true;
|
||||
lastKeepAlive = new Date();
|
||||
keepAliveTimer = setInterval(keepAliveLoop, checkTimeoutInterval);
|
||||
client.on(0x00, onKeepAlive);
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
clearInterval(keepAliveTimer);
|
||||
clearTimeout(loginKickTimer);
|
||||
}
|
||||
|
||||
function onPing(packet) {
|
||||
var response = {
|
||||
"version": {
|
||||
"name": minecraftVersion,
|
||||
"protocol": version
|
||||
},
|
||||
"players": {
|
||||
"max": server.maxPlayers,
|
||||
"online": server.playerCount,
|
||||
"sample": []
|
||||
},
|
||||
"description": {"text": server.motd},
|
||||
"favicon": server.favicon
|
||||
};
|
||||
|
||||
if(beforePing) {
|
||||
response = beforePing(response, client) || response;
|
||||
}
|
||||
|
||||
client.once([states.STATUS, 0x01], function(packet) {
|
||||
client.write(0x01, {time: packet.time});
|
||||
client.end();
|
||||
});
|
||||
client.write(0x00, {response: JSON.stringify(response)});
|
||||
}
|
||||
|
||||
function onLogin(packet) {
|
||||
client.username = packet.username;
|
||||
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
|
||||
var needToVerify = (onlineMode && !isException) || (!onlineMode && isException);
|
||||
if(needToVerify) {
|
||||
var serverId = crypto.randomBytes(4).toString('hex');
|
||||
client.verifyToken = crypto.randomBytes(4);
|
||||
var publicKeyStrArr = serverKey.toPublicPem("utf8").split("\n");
|
||||
var publicKeyStr = "";
|
||||
for(var i = 1; i < publicKeyStrArr.length - 2; i++) {
|
||||
publicKeyStr += publicKeyStrArr[i]
|
||||
}
|
||||
client.publicKey = new Buffer(publicKeyStr, 'base64');
|
||||
hash = crypto.createHash("sha1");
|
||||
hash.update(serverId);
|
||||
client.once([states.LOGIN, 0x01], onEncryptionKeyResponse);
|
||||
client.write(0x01, {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function onEncryptionKeyResponse(packet) {
|
||||
try {
|
||||
var verifyToken = serverKey.decrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING);
|
||||
if(!bufferEqual(client.verifyToken, verifyToken)) {
|
||||
client.end('DidNotEncryptVerifyTokenProperly');
|
||||
return;
|
||||
}
|
||||
var sharedSecret = serverKey.decrypt(packet.sharedSecret, undefined, undefined, ursa.RSA_PKCS1_PADDING);
|
||||
} catch(e) {
|
||||
client.end('DidNotEncryptVerifyTokenProperly');
|
||||
return;
|
||||
}
|
||||
client.setEncryption(sharedSecret);
|
||||
hash.update(sharedSecret);
|
||||
hash.update(client.publicKey);
|
||||
|
||||
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
|
||||
var needToVerify = (onlineMode && !isException) || (!onlineMode && isException);
|
||||
var nextStep = needToVerify ? verifyUsername : loginClient;
|
||||
nextStep();
|
||||
|
||||
function verifyUsername() {
|
||||
var digest = mcHexDigest(hash);
|
||||
validateSession(client.username, digest, function(err, uuid) {
|
||||
if(err) {
|
||||
client.end("Failed to verify username!");
|
||||
return;
|
||||
}
|
||||
client.uuid = uuid;
|
||||
// Convert to a valid UUID until the session server updates and does
|
||||
// it automatically
|
||||
client.uuid = client.uuid.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, "$1-$2-$3-$4-$5");
|
||||
loginClient();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loginClient() {
|
||||
var isException = !!server.onlineModeExceptions[client.username.toLowerCase()];
|
||||
if(onlineMode == false || isException) {
|
||||
client.uuid = "0-0-0-0-0";
|
||||
}
|
||||
//client.write('compress', { threshold: 256 }); // Default threshold is 256
|
||||
//client.compressionThreshold = 256;
|
||||
client.write(0x02, {uuid: client.uuid, username: client.username});
|
||||
client.state = states.PLAY;
|
||||
loggedIn = true;
|
||||
startKeepAlive();
|
||||
|
||||
clearTimeout(loginKickTimer);
|
||||
loginKickTimer = null;
|
||||
|
||||
server.playerCount += 1;
|
||||
client.once('end', function() {
|
||||
server.playerCount -= 1;
|
||||
});
|
||||
server.emit('login', client);
|
||||
}
|
||||
});
|
||||
server.listen(port, host);
|
||||
return server;
|
||||
}
|
||||
|
||||
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 || Yggdrasil.generateUUID();
|
||||
var accessToken = options.accessToken || null;
|
||||
|
||||
assert.ok(options.username, "username is required");
|
||||
var haveCredentials = options.password != null || (clientToken != null && accessToken != null);
|
||||
var keepAlive = options.keepAlive == null ? true : options.keepAlive;
|
||||
|
||||
|
||||
var client = new Client(false);
|
||||
client.on('connect', onConnect);
|
||||
if(keepAlive) client.on([states.PLAY, 0x00], onKeepAlive);
|
||||
client.once([states.LOGIN, 0x01], onEncryptionKeyRequest);
|
||||
client.once([states.LOGIN, 0x02], 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.username;
|
||||
accessToken = session.accessToken;
|
||||
client.emit('session');
|
||||
client.connect(port, host);
|
||||
}
|
||||
};
|
||||
|
||||
if(accessToken != null) getSession(options.username, accessToken, clientToken, true, cb);
|
||||
else getSession(options.username, options.password, clientToken, false, cb);
|
||||
} else {
|
||||
// assume the server is in offline mode and just go for it.
|
||||
client.username = options.username;
|
||||
client.connect(port, host);
|
||||
}
|
||||
|
||||
return client;
|
||||
|
||||
function onConnect() {
|
||||
client.write(0x00, {
|
||||
protocolVersion: version,
|
||||
serverHost: host,
|
||||
serverPort: port,
|
||||
nextState: 2
|
||||
});
|
||||
client.state = states.LOGIN;
|
||||
client.write(0x00, {
|
||||
username: client.username
|
||||
});
|
||||
}
|
||||
|
||||
function onCompressionRequest(packet) {
|
||||
client.compressionThreshold = packet.threshold;
|
||||
}
|
||||
|
||||
function onKeepAlive(packet) {
|
||||
client.write(0x00, {
|
||||
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) {
|
||||
var hash = crypto.createHash('sha1');
|
||||
hash.update(packet.serverId);
|
||||
hash.update(sharedSecret);
|
||||
hash.update(packet.publicKey);
|
||||
|
||||
var digest = mcHexDigest(hash);
|
||||
joinServer(client.username, digest, accessToken, client.session.selectedProfile.id, 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(0x01, {
|
||||
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');
|
||||
}
|
||||
|
||||
function mcHexDigest(hash) {
|
||||
var buffer = new Buffer(hash.digest(), 'binary');
|
||||
// check for negative hashes
|
||||
var negative = buffer.readInt8(0) < 0;
|
||||
if(negative)
|
||||
performTwosCompliment(buffer);
|
||||
var digest = buffer.toString('hex');
|
||||
// trim leading zeroes
|
||||
digest = digest.replace(/^0+/g, '');
|
||||
if(negative)
|
||||
digest = '-' + digest;
|
||||
return digest;
|
||||
|
||||
function performTwosCompliment(buffer) {
|
||||
var carry = true;
|
||||
var i, newByte, value;
|
||||
for(i = buffer.length - 1; i >= 0; --i) {
|
||||
value = buffer.readUInt8(i);
|
||||
newByte = ~value & 0xff;
|
||||
if(carry) {
|
||||
carry = newByte === 0xff;
|
||||
buffer.writeUInt8((newByte + 1) & 0xff, i);
|
||||
} else {
|
||||
buffer.writeUInt8(newByte, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
src/mcHexDigest.js
Normal file
30
src/mcHexDigest.js
Normal file
@ -0,0 +1,30 @@
|
||||
module.exports=mcHexDigest;
|
||||
|
||||
function mcHexDigest(hash) {
|
||||
var buffer = new Buffer(hash.digest(), 'binary');
|
||||
// check for negative hashes
|
||||
var negative = buffer.readInt8(0) < 0;
|
||||
if(negative)
|
||||
performTwosCompliment(buffer);
|
||||
var digest = buffer.toString('hex');
|
||||
// trim leading zeroes
|
||||
digest = digest.replace(/^0+/g, '');
|
||||
if(negative)
|
||||
digest = '-' + digest;
|
||||
return digest;
|
||||
|
||||
function performTwosCompliment(buffer) {
|
||||
var carry = true;
|
||||
var i, newByte, value;
|
||||
for(i = buffer.length - 1; i >= 0; --i) {
|
||||
value = buffer.readUInt8(i);
|
||||
newByte = ~value & 0xff;
|
||||
if(carry) {
|
||||
carry = newByte === 0xff;
|
||||
buffer.writeUInt8((newByte + 1) & 0xff, i);
|
||||
} else {
|
||||
buffer.writeUInt8(newByte, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/ursa.js
Normal file
9
src/ursa.js
Normal file
@ -0,0 +1,9 @@
|
||||
var ursa;
|
||||
try {
|
||||
ursa = require("ursa");
|
||||
} catch(e) {
|
||||
console.log("You are using a pure-javascript implementation of RSA.");
|
||||
console.log("Your performance might be subpar. Please consider installing URSA");
|
||||
ursa = require("ursa-purejs");
|
||||
}
|
||||
module.exports=ursa;
|
4
src/version.js
Normal file
4
src/version.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports={
|
||||
version:47,
|
||||
minecraftVersion:'1.8.1'
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user