imt-gateway
Version:
Integromat Gateway Client
254 lines (208 loc) • 5.5 kB
JavaScript
'use strict'
const debug = require('debug')('imt:gateway:protocol');
const EventEmitter = require('events');
const msgpack = require('msgpack-lite');
const os = require('os');
const tls = require('tls');
const HEADER_LENGTH = 6;
const BUFFER_SIZE = 10485760; // 10MB
const TOKEN = {
HELLO: 0x01,
WELCOME: 0x02,
BYE: 0x03,
EVENT: 0x04,
ACTION: 0x05,
ACKNOWLEDGEMENT: 0x06,
DRAINED: 0x07
};
/**
* Protocol implementation.
*
* @property {Boolean} closed If `true`, connection was closed.
*
* @event data Emitted on data.
* @event error Emitted on error.
* @event end Emitted when socked is closed.
*/
class Protocol extends EventEmitter {
constructor(socket) {
super();
this.authorized = false;
this.drained = false;
this.closed = false;
this.closing = false;
this._secure = socket instanceof tls.TLSSocket;
this._data = this._data.bind(this);
this._close = this._close.bind(this);
this._error = this._error.bind(this);
this._connect = this._connect.bind(this);
this.buffer = Buffer.alloc(0);
this.socket = socket;
this.socket.on('data', this._data);
this.socket.on('error', this._error);
this.socket.on('close', this._close);
this.socket.on(this._secure ? 'secureConnect' : 'connect', this._connect);
}
/**
* Release protocol and it's event listeners from memory.
*
* @returns {Protocol}
*/
destroy() {
this.removeAllListeners();
this.buffer = null;
if (this.socket) {
this.socket.removeListener('data', this._data);
this.socket.removeListener('error', this._error);
this.socket.removeListener('close', this._close);
this.socket.removeListener(this._secure ? 'secureConnect' : 'connect', this._connect);
this.socket = null;
}
this.authorized = false;
return this;
}
/**
* Socket connect handler.
*
* @private
*/
_connect() {
if (this._secure && !this.socket.authorized) return this.socket.destroy();
if (this._secure) this.certificate = this.socket.getPeerCertificate();
this.emit('connect');
this.send(TOKEN.HELLO, {
metadata: {
device_name: os.hostname()
}
});
}
/**
* Socket data handler.
*
* @private
*/
_data(chunk) {
if (this.closed || this.closing) return; // don't receive anything when closed or error
debug('chunk', chunk);
this.buffer = Buffer.concat([this.buffer, chunk]);
this.emit('stats', chunk.length, 0);
if (this.buffer.length > BUFFER_SIZE) {
return this.end(TOKEN.BYE, {
error: {
code: "IM491",
message: "Maximum buffer size exceeded."
}
});
}
while (this.buffer.length >= HEADER_LENGTH) {
let token = this.buffer.readUInt8(0);
let length = this.buffer.readUInt32LE(2);
if (this.allowedTokens != null && this.allowedTokens.indexOf(token) === -1) {
return this.end(TOKEN.BYE, {
error: {
code: "IM480",
message: "Invalid token in current state."
}
});
}
if (this.buffer.length >= HEADER_LENGTH + length) {
let body = this.buffer.slice(6, HEADER_LENGTH + length);
this.buffer = this.buffer.slice(HEADER_LENGTH + length);
if (length) {
try {
var data = msgpack.decode(body);
} catch (e) {
return this._error(new Error(`Failed to deserialize packet. ${e.message}`));
}
} else {
var data = null;
}
this.emit('data', token, data);
} else {
break;
}
}
}
/**
* Socket end handler.
*
* @private
*/
_close() {
if (this.closed) return;
this.closed = true;
if (this._destroyTimeout) {
clearTimeout(this._destroyTimeout);
this._destroyTimeout = null;
}
this.emit('close');
this.destroy();
}
/**
* Socket error handler.
*
* @private
*/
_error(err) {
this.closing = true;
console.error(`[ PROTOCOL ERROR ]`, err);
this.emit('error', err);
if (this.socket) this.socket.destroy();
// don't this.destroy() here, it will be destroyed on the 'close' event
}
/**
* Close TCP connection and optionaly send final token.
*
* @param {Number} [token] Token ID.
* @param {Object} [data] Data structure.
* @returns {Protocol}
*/
end(token, data) {
if (!this.socket) return this;
if (token != null) this.send(token, data);
this.closing = true;
this.socket.end();
this._destroyTimeout = setTimeout(() => {
if (this.socket) this.socket.destroy();
this._destroyTimeout = null;
}, 1000);
return this;
}
/**
* Send token over TCP.
*
* @param {Number} token Token ID.
* @parma {Object} data Data structure.
* @returns {Protocol}
*/
send(token, data) {
if (!this.socket || this.closing) return this;
if (token === TOKEN.DRAINED) this.drained = true;
if (data != null) {
try {
var body = msgpack.encode(data);
var length = body.length;
} catch (e) {
if (token === TOKEN.BYE) {
return console.error(new Error(`Failed to serialize packet. ${e.message}`));
} else {
return this._error(new Error(`Failed to serialize packet. ${e.message}`));
}
}
} else {
var length = 0;
}
let packet = Buffer.alloc(HEADER_LENGTH + length);
packet.writeUInt8(token, 0);
packet.writeUInt8(0x00, 1);
packet.writeUInt32LE(length, 2);
if (length) body.copy(packet, HEADER_LENGTH);
debug('sent', packet);
this.socket.write(packet);
this.emit('stats', 0, packet.length);
this.emit('sent', token, data);
return this;
}
}
exports.TOKEN = TOKEN;
exports.Protocol = Protocol;