2013-01-04 01:45:57 -05:00
|
|
|
var net = require('net')
|
|
|
|
, EventEmitter = require('events').EventEmitter
|
|
|
|
, util = require('util')
|
|
|
|
, protocol = require('./protocol')
|
2013-04-13 16:23:31 -04:00
|
|
|
, dns = require('dns')
|
2013-01-04 01:45:57 -05:00
|
|
|
, createPacketBuffer = protocol.createPacketBuffer
|
2014-12-30 03:55:04 -05:00
|
|
|
, compressPacketBuffer = protocol.compressPacketBuffer
|
2015-01-01 17:20:47 -05:00
|
|
|
, oldStylePacket = protocol.oldStylePacket
|
|
|
|
, newStylePacket = protocol.newStylePacket
|
2013-01-04 01:45:57 -05:00
|
|
|
, parsePacket = protocol.parsePacket
|
2015-01-02 12:50:54 -05:00
|
|
|
, parseNewStylePacket = protocol.parseNewStylePacket
|
2015-01-01 17:20:47 -05:00
|
|
|
, packetIds = protocol.packetIds
|
|
|
|
, packetNames = protocol.packetNames
|
2013-12-30 10:05:22 -05:00
|
|
|
, states = protocol.states
|
2013-04-05 23:02:36 -04:00
|
|
|
, debug = protocol.debug
|
2013-11-19 18:06:57 -05:00
|
|
|
;
|
2013-01-04 01:45:57 -05:00
|
|
|
|
|
|
|
module.exports = Client;
|
|
|
|
|
2013-01-07 23:36:14 -05:00
|
|
|
function Client(isServer) {
|
2013-01-04 01:45:57 -05:00
|
|
|
EventEmitter.call(this);
|
|
|
|
|
2013-12-30 10:05:22 -05:00
|
|
|
this._state = states.HANDSHAKING;
|
|
|
|
Object.defineProperty(this, "state", {
|
|
|
|
get: function() {
|
|
|
|
return this._state;
|
|
|
|
},
|
|
|
|
set: function(newProperty) {
|
|
|
|
var oldProperty = this._state;
|
|
|
|
this._state = newProperty;
|
|
|
|
this.emit('state', newProperty, oldProperty);
|
|
|
|
}
|
|
|
|
});
|
2013-01-07 23:36:14 -05:00
|
|
|
this.isServer = !!isServer;
|
2013-01-04 01:45:57 -05:00
|
|
|
this.socket = null;
|
|
|
|
this.encryptionEnabled = false;
|
|
|
|
this.cipher = null;
|
|
|
|
this.decipher = null;
|
2015-01-01 17:20:47 -05:00
|
|
|
this.compressionThreshold = -2;
|
2014-04-10 19:48:06 -04:00
|
|
|
this.packetsToParse = {};
|
|
|
|
this.on('newListener', function(event, listener) {
|
|
|
|
var direction = this.isServer ? 'toServer' : 'toClient';
|
2014-04-10 20:08:17 -04:00
|
|
|
if (protocol.packetStates[direction].hasOwnProperty(event) || event === "packet") {
|
2014-04-10 19:48:06 -04:00
|
|
|
if (typeof this.packetsToParse[event] === "undefined") this.packetsToParse[event] = 1;
|
|
|
|
else this.packetsToParse[event] += 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.on('removeListener', function(event, listener) {
|
|
|
|
var direction = this.isServer ? 'toServer' : 'toClient';
|
2014-04-10 20:08:17 -04:00
|
|
|
if (protocol.packetStates[direction].hasOwnProperty(event) || event === "packet") {
|
2014-04-10 19:48:06 -04:00
|
|
|
this.packetsToParse[event] -= 1;
|
|
|
|
}
|
|
|
|
});
|
2013-01-04 01:45:57 -05:00
|
|
|
}
|
2014-04-10 19:48:06 -04:00
|
|
|
|
2013-01-04 01:45:57 -05:00
|
|
|
util.inherits(Client, EventEmitter);
|
|
|
|
|
2014-04-10 19:48:06 -04:00
|
|
|
// Transform weird "packet" types into string representing their type. Should be mostly retro-compatible
|
|
|
|
Client.prototype.on = function(type, func) {
|
|
|
|
var direction = this.isServer ? 'toServer' : 'toClient';
|
|
|
|
if (Array.isArray(type)) {
|
|
|
|
arguments[0] = protocol.packetNames[type[0]][direction][type[1]];
|
|
|
|
} else if (typeof type === "number") {
|
|
|
|
arguments[0] = protocol.packetNames[this.state][direction][type];
|
|
|
|
}
|
|
|
|
EventEmitter.prototype.on.apply(this, arguments);
|
|
|
|
};
|
|
|
|
|
|
|
|
Client.prototype.onRaw = function(type, func) {
|
|
|
|
var arg = "raw.";
|
|
|
|
if (Array.isArray(type)) {
|
|
|
|
arg += protocol.packetNames[type[0]][direction][type[1]];
|
|
|
|
} else if (typeof type === "number") {
|
|
|
|
arg += protocol.packetNames[this.state][direction][type];
|
|
|
|
} else {
|
|
|
|
arg += type;
|
|
|
|
}
|
|
|
|
arguments[0] = arg;
|
|
|
|
EventEmitter.prototype.on.apply(this, arguments);
|
|
|
|
};
|
|
|
|
|
2013-01-04 01:45:57 -05:00
|
|
|
Client.prototype.setSocket = function(socket) {
|
|
|
|
var self = this;
|
2015-02-05 13:02:34 -05:00
|
|
|
function afterParse(err, parsed) {
|
|
|
|
if (err || (parsed && parsed.error)) {
|
2015-02-19 20:04:43 -05:00
|
|
|
self.emit('error', err || parsed.error);
|
|
|
|
self.end("ProtocolError");
|
2015-02-05 13:02:34 -05:00
|
|
|
return;
|
|
|
|
}
|
2015-02-19 15:36:37 -05:00
|
|
|
if (! parsed) { return; }
|
2015-02-19 20:04:43 -05:00
|
|
|
var packet = parsed.results;
|
2015-02-05 13:02:34 -05:00
|
|
|
incomingBuffer = incomingBuffer.slice(parsed.size);
|
|
|
|
|
|
|
|
var packetName = protocol.packetNames[self.state][self.isServer ? 'toServer' : 'toClient'][packet.id];
|
|
|
|
self.emit(packetName, packet);
|
|
|
|
self.emit('packet', packet);
|
|
|
|
self.emit('raw.' + packetName, parsed.buffer);
|
|
|
|
self.emit('raw', parsed.buffer);
|
2015-02-19 15:36:37 -05:00
|
|
|
prepareParse();
|
2015-02-05 13:02:34 -05:00
|
|
|
}
|
2015-02-19 15:36:37 -05:00
|
|
|
|
|
|
|
function prepareParse() {
|
|
|
|
if(self.compressionThreshold == -2)
|
|
|
|
afterParse(null, parsePacket(incomingBuffer, self.state, self.isServer, self.packetsToParse));
|
|
|
|
else
|
|
|
|
parseNewStylePacket(incomingBuffer, self.state, self.isServer, self.packetsToParse, afterParse);
|
|
|
|
}
|
|
|
|
|
2013-01-04 01:45:57 -05:00
|
|
|
self.socket = socket;
|
2014-10-28 19:30:33 -04:00
|
|
|
if (self.socket.setNoDelay)
|
|
|
|
self.socket.setNoDelay(true);
|
2013-01-04 01:45:57 -05:00
|
|
|
var incomingBuffer = new Buffer(0);
|
|
|
|
self.socket.on('data', function(data) {
|
|
|
|
if (self.encryptionEnabled) data = new Buffer(self.decipher.update(data), 'binary');
|
|
|
|
incomingBuffer = Buffer.concat([incomingBuffer, data]);
|
2015-02-19 15:36:37 -05:00
|
|
|
prepareParse()
|
2013-01-04 01:45:57 -05:00
|
|
|
});
|
|
|
|
|
2013-01-04 21:33:19 -05:00
|
|
|
self.socket.on('connect', function() {
|
|
|
|
self.emit('connect');
|
|
|
|
});
|
2013-02-03 15:36:55 -05:00
|
|
|
|
|
|
|
self.socket.on('error', onError);
|
|
|
|
self.socket.on('close', endSocket);
|
|
|
|
self.socket.on('end', endSocket);
|
|
|
|
self.socket.on('timeout', endSocket);
|
|
|
|
|
|
|
|
function onError(err) {
|
|
|
|
self.emit('error', err);
|
|
|
|
endSocket();
|
|
|
|
}
|
|
|
|
|
2013-02-03 22:13:15 -05:00
|
|
|
var ended = false;
|
2013-02-03 15:36:55 -05:00
|
|
|
function endSocket() {
|
2013-02-03 22:13:15 -05:00
|
|
|
if (ended) return;
|
|
|
|
ended = true;
|
2013-02-03 15:36:55 -05:00
|
|
|
self.socket.removeListener('close', endSocket);
|
|
|
|
self.socket.removeListener('end', endSocket);
|
|
|
|
self.socket.removeListener('timeout', endSocket);
|
|
|
|
self.emit('end', self._endReason);
|
|
|
|
}
|
2013-01-04 01:45:57 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
Client.prototype.connect = function(port, host) {
|
|
|
|
var self = this;
|
2013-04-14 23:09:04 -04:00
|
|
|
if (port == 25565) {
|
|
|
|
dns.resolveSrv("_minecraft._tcp." + host, function(err, addresses) {
|
2014-10-28 19:30:33 -04:00
|
|
|
if (addresses && addresses.length > 0) {
|
2013-04-14 23:09:04 -04:00
|
|
|
self.setSocket(net.connect(addresses[0].port, addresses[0].name));
|
|
|
|
} else {
|
|
|
|
self.setSocket(net.connect(port, host));
|
|
|
|
}
|
2013-12-30 10:05:22 -05:00
|
|
|
});
|
2013-04-14 23:09:04 -04:00
|
|
|
} else {
|
|
|
|
self.setSocket(net.connect(port, host));
|
|
|
|
}
|
2013-01-04 01:45:57 -05:00
|
|
|
};
|
|
|
|
|
2013-01-04 20:55:53 -05:00
|
|
|
Client.prototype.end = function(reason) {
|
|
|
|
this._endReason = reason;
|
2013-01-04 01:45:57 -05:00
|
|
|
this.socket.end();
|
|
|
|
};
|
|
|
|
|
|
|
|
Client.prototype.write = function(packetId, params) {
|
2013-12-30 10:05:22 -05:00
|
|
|
if (Array.isArray(packetId)) {
|
|
|
|
if (packetId[0] !== this.state)
|
|
|
|
return false;
|
|
|
|
packetId = packetId[1];
|
|
|
|
}
|
2015-01-01 17:20:47 -05:00
|
|
|
if (typeof packetId === "string")
|
|
|
|
packetId = packetIds[this.state][this.isServer ? "toClient" : "toServer"][packetId];
|
2014-12-31 13:26:59 -05:00
|
|
|
var that = this;
|
|
|
|
|
2014-12-30 03:55:04 -05:00
|
|
|
var finishWriting = function(buffer) {
|
2015-01-01 17:20:47 -05:00
|
|
|
var packetName = packetNames[that.state][that.isServer ? "toClient" : "toServer"][packetId];
|
|
|
|
debug("writing packetId " + that.state + "." + packetName + " (0x" + packetId.toString(16) + ")");
|
|
|
|
debug(params);
|
|
|
|
var out = that.encryptionEnabled ? new Buffer(that.cipher.update(buffer), 'binary') : buffer;
|
|
|
|
that.socket.write(out);
|
|
|
|
return true;
|
2014-12-30 03:55:04 -05:00
|
|
|
}
|
|
|
|
|
2013-12-30 10:05:22 -05:00
|
|
|
var buffer = createPacketBuffer(packetId, this.state, params, this.isServer);
|
2015-01-01 17:20:47 -05:00
|
|
|
if (this.compressionThreshold >= 0 && buffer.length >= this.compressionThreshold) {
|
|
|
|
debug("Compressing packet");
|
|
|
|
compressPacketBuffer(buffer, finishWriting);
|
|
|
|
} else if (this.compressionThreshold >= -1) {
|
|
|
|
debug("New-styling packet");
|
|
|
|
finishWriting(newStylePacket(buffer));
|
2014-12-30 03:55:04 -05:00
|
|
|
} else {
|
2015-01-01 17:20:47 -05:00
|
|
|
debug("Old-styling packet");
|
|
|
|
finishWriting(oldStylePacket(buffer));
|
2014-12-30 03:55:04 -05:00
|
|
|
}
|
2013-01-04 01:45:57 -05:00
|
|
|
};
|
2014-04-10 19:48:06 -04:00
|
|
|
|
2015-01-01 17:20:47 -05:00
|
|
|
// TODO : Perhaps this should only accept buffers without length, so we can
|
|
|
|
// handle compression ourself ? Needs to ask peopl who actually use this feature
|
|
|
|
// like @deathcap
|
2014-04-10 19:48:06 -04:00
|
|
|
Client.prototype.writeRaw = function(buffer, shouldEncrypt) {
|
|
|
|
if (shouldEncrypt === null) {
|
|
|
|
shouldEncrypt = true;
|
|
|
|
}
|
2014-12-30 03:55:04 -05:00
|
|
|
|
2014-12-31 13:26:59 -05:00
|
|
|
var that = this;
|
|
|
|
|
2014-12-30 03:55:04 -05:00
|
|
|
var finishWriting = function(buffer) {
|
|
|
|
var out = (shouldEncrypt && this.encryptionEnabled) ? new Buffer(this.cipher.update(buffer), 'binary') : buffer;
|
2014-12-31 13:26:59 -05:00
|
|
|
that.socket.write(out);
|
2014-12-30 03:55:04 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
if(this.compressionThreshold != -1 && buffer.length > this.compressionThreshold) {
|
|
|
|
compressPacketBuffer(buffer, finishWriting);
|
|
|
|
} else {
|
|
|
|
finishWriting(buffer);
|
|
|
|
}
|
2014-10-28 19:30:33 -04:00
|
|
|
};
|