total.js
Version:
MVC framework for Node.js
690 lines (568 loc) • 17.1 kB
JavaScript
// Copyright 2012-2020 (c) Peter Širka <petersirka@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
/**
* @module WebSocketClient
* @version 3.4.4
*/
if (!global.framework_utils)
global.framework_utils = require('./utils');
const Crypto = require('crypto');
const Https = require('https');
const Http = require('http');
const Url = require('url');
const Zlib = require('zlib');
const ENCODING = 'utf8';
const WEBSOCKET_COMPRESS = Buffer.from([0x00, 0x00, 0xFF, 0xFF]);
const WEBSOCKET_COMPRESS_OPTIONS = { windowBits: Zlib.Z_DEFAULT_WINDOWBITS };
const CONCAT = [null, null];
function WebSocketClient() {
this.current = {};
this.$events = {};
this.pending = [];
this.reconnect = 0;
this.closed = true;
// type: json, text, binary
this.options = { type: 'json', compress: true, reconnect: 3000, encodedecode: true };
this.cookies = {};
this.headers = {};
}
const WebSocketClientProto = WebSocketClient.prototype;
WebSocketClientProto.connect = function(url, protocol, origin) {
var self = this;
var options = {};
var key = Crypto.randomBytes(16).toString('base64');
self.url = url;
self.origin = origin;
self.protocol = protocol;
url = Url.parse(url);
var isSecure = url.protocol === 'wss:';
options.port = url.port || (isSecure ? 443 : 80);
options.host = url.hostname;
options.path = url.path;
options.query = url.query;
options.headers = {};
options.headers['User-Agent'] = 'Total.js/v' + F.version_header;
options.headers['Sec-WebSocket-Version'] = '13';
options.headers['Sec-WebSocket-Key'] = key;
options.headers['Sec-Websocket-Extensions'] = (self.options.compress ? 'permessage-deflate, ' : '') + 'client_max_window_bits';
protocol && (options.headers['Sec-WebSocket-Protocol'] = protocol);
origin && (options.headers['Sec-WebSocket-Origin'] = origin);
options.headers.Connection = 'Upgrade';
options.headers.Upgrade = 'websocket';
var keys = Object.keys(self.headers);
for (var i = 0, length = keys.length; i < length; i++)
options.headers[keys[i]] = self.headers[keys[i]];
keys = Object.keys(self.cookies);
if (keys.length) {
var tmp = [];
for (var i = 0, length = keys.length; i < length; i++)
tmp.push(keys[i] + '=' + self.cookies[keys[i]]);
options.headers['Cookie'] = tmp.join(', ');
}
self.req = (isSecure ? Https : Http).get(options);
self.req.$main = self;
F.stats.performance.online++;
self.req.on('error', function(e) {
self.$events.error && self.emit('error', e);
});
self.req.on('response', function() {
self.$events.error && self.emit('error', new Error('Unexpected server response.'));
if (self.options.reconnectserver)
self.connect(url, protocol, origin);
else
self.free();
});
self.req.on('upgrade', function(response, socket) {
self.socket = socket;
self.socket.$websocket = self;
var compress = (response.headers['sec-websocket-extensions'] || '').indexOf('-deflate') !== -1;
var digest = Crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64');
if (response.headers['sec-websocket-accept'] !== digest) {
socket.destroy();
self.closed = true;
self.$events.error && self.emit('error', new Error('Invalid server key.'));
self.free();
return;
}
self.closed = false;
self.socket.setTimeout(0);
self.socket.setNoDelay();
self.socket.on('data', websocket_ondata);
self.socket.on('error', websocket_onerror);
self.socket.on('close', websocket_close);
self.socket.on('end', websocket_close);
if (compress) {
self.inflatepending = [];
self.inflatelock = false;
self.inflate = Zlib.createInflateRaw(WEBSOCKET_COMPRESS_OPTIONS);
self.inflate.$websocket = self;
self.inflate.on('error', F.error());
self.inflate.on('data', websocket_inflate);
self.deflatepending = [];
self.deflatelock = false;
self.deflate = Zlib.createDeflateRaw(WEBSOCKET_COMPRESS_OPTIONS);
self.deflate.$websocket = self;
self.deflate.on('error', F.error());
self.deflate.on('data', websocket_deflate);
}
self.$events.open && self.emit('open');
});
};
function websocket_ondata(chunk) {
this.$websocket.$ondata(chunk);
}
function websocket_onerror(e) {
this.$websocket.$onerror(e);
}
function websocket_close() {
var ws = this.$websocket;
ws.closed = true;
ws.$onclose();
F.stats.performance.online--;
ws.options.reconnect && setTimeout(function(ws) {
ws.isClosed = false;
ws._isClosed = false;
ws.reconnect++;
ws.connect(ws.url, ws.protocol, ws.origin);
}, ws.options.reconnect, ws);
}
WebSocketClientProto.emit = function(name, a, b, c, d, e, f, g) {
var evt = this.$events[name];
if (evt) {
var clean = false;
for (var i = 0, length = evt.length; i < length; i++) {
if (evt[i].$once)
clean = true;
evt[i].call(this, a, b, c, d, e, f, g);
}
if (clean) {
evt = evt.remove(n => n.$once);
if (evt.length)
this.$events[name] = evt;
else
this.$events[name] = undefined;
}
}
return this;
};
WebSocketClientProto.on = function(name, fn) {
if (this.$events[name])
this.$events[name].push(fn);
else
this.$events[name] = [fn];
return this;
};
WebSocketClientProto.once = function(name, fn) {
fn.$once = true;
return this.on(name, fn);
};
WebSocketClientProto.removeListener = function(name, fn) {
var evt = this.$events[name];
if (evt) {
evt = evt.remove(n => n === fn);
if (evt.length)
this.$events[name] = evt;
else
this.$events[name] = undefined;
}
return this;
};
WebSocketClientProto.removeAllListeners = function(name) {
if (name === true)
this.$events = EMPTYOBJECT;
else if (name)
this.$events[name] = undefined;
else
this.$events = {};
return this;
};
WebSocketClientProto.free = function() {
var self = this;
self.socket && self.socket.destroy();
self.socket = null;
self.req && self.req.abort();
self.req = null;
return self;
};
/**
* Internal handler written by Jozef Gula
* @param {Buffer} data
* @return {Framework}
*/
WebSocketClientProto.$ondata = function(data) {
if (this.isClosed)
return;
var current = this.current;
if (data) {
if (current.buffer) {
CONCAT[0] = current.buffer;
CONCAT[1] = data;
current.buffer = Buffer.concat(CONCAT);
} else
current.buffer = data;
}
if (!this.$parse())
return;
if (!current.final && current.type !== 0x00)
current.type2 = current.type;
var tmp;
var decompress = current.compressed && this.inflate;
switch (current.type === 0x00 ? current.type2 : current.type) {
case 0x01:
// text
if (decompress) {
current.final && this.parseInflate();
} else {
tmp = this.$readbody();
if (current.body)
current.body += tmp;
else
current.body = tmp;
current.final && this.$decode();
}
break;
case 0x02:
// binary
if (decompress) {
current.final && this.parseInflate();
} else {
tmp = this.$readbody();
if (current.body) {
CONCAT[0] = current.body;
CONCAT[1] = tmp;
current.body = Buffer.concat(CONCAT);
} else
current.body = tmp;
current.final && this.$decode();
}
break;
case 0x08:
this.closemessage = current.buffer.slice(4).toString('utf8');
this.closecode = current.buffer[2] << 8 | current.buffer[3];
if (this.closemessage && this.options.encodedecode)
this.closemessage = $decodeURIComponent(this.closemessage);
this.close(true);
break;
case 0x09:
// ping, response pong
this.socket.write(U.getWebSocketFrame(0, 'PONG', 0x0A));
current.buffer = null;
current.inflatedata = null;
break;
case 0x0A:
// pong
current.buffer = null;
current.inflatedata = null;
break;
}
if (current.buffer) {
current.buffer = current.buffer.slice(current.length, current.buffer.length);
current.buffer.length && this.$ondata();
}
};
function buffer_concat(buffers, length) {
var buffer = Buffer.alloc(length);
var offset = 0;
for (var i = 0, n = buffers.length; i < n; i++) {
buffers[i].copy(buffer, offset);
offset += buffers[i].length;
}
return buffer;
}
// MIT
// Written by Jozef Gula
// Optimized by Peter Sirka
WebSocketClientProto.$parse = function() {
var self = this;
var current = self.current;
// check end message
if (!current.buffer || current.buffer.length <= 2 || ((current.buffer[0] & 0x80) >> 7) !== 1)
return;
// WebSocket - Opcode
current.type = current.buffer[0] & 0x0f;
current.compressed = (current.buffer[0] & 0x40) === 0x40;
// is final message?
current.final = ((current.buffer[0] & 0x80) >> 7) === 0x01;
// does frame contain mask?
current.isMask = ((current.buffer[1] & 0xfe) >> 7) === 0x01;
// data length
var length = U.getMessageLength(current.buffer, F.isLE);
// index for data
var index = current.buffer[1] & 0x7f;
index = ((index === 126) ? 4 : (index === 127 ? 10 : 2)) + (current.isMask ? 4 : 0);
// total message length (data + header)
var mlength = index + length;
// Check length of data
if (current.buffer.length < mlength)
return;
current.length = mlength;
// Not Ping & Pong
if (current.type !== 0x09 && current.type !== 0x0A) {
// does frame contain mask?
if (current.isMask) {
current.mask = Buffer.alloc(4);
current.buffer.copy(current.mask, 0, index - 4, index);
}
if (current.compressed && this.inflate) {
var buf = Buffer.alloc(length);
current.buffer.copy(buf, 0, index, mlength);
// does frame contain mask?
if (current.isMask) {
for (var i = 0; i < length; i++)
buf[i] = buf[i] ^ current.mask[i % 4];
}
// Does the buffer continue?
buf.$continue = current.final === false;
this.inflatepending.push(buf);
} else {
current.data = Buffer.alloc(length);
current.buffer.copy(current.data, 0, index, mlength);
}
}
return true;
};
WebSocketClientProto.$readbody = function() {
var current = this.current;
var length = current.data.length;
var buf;
if (current.type === 1) {
buf = Buffer.alloc(length);
for (var i = 0; i < length; i++) {
if (current.isMask)
buf[i] = current.data[i] ^ current.mask[i % 4];
else
buf[i] = current.data[i];
}
return buf.toString('utf8');
} else {
buf = Buffer.alloc(length);
for (var i = 0; i < length; i++) {
// does frame contain mask?
if (current.isMask)
buf[i] = current.data[i] ^ current.mask[i % 4];
else
buf[i] = current.data[i];
}
return buf;
}
};
WebSocketClientProto.$decode = function() {
var data = this.current.body;
if (global.F)
global.F.stats.performance.message++;
switch (this.options.type) {
case 'buffer': // Buffer
case 'binary': // Binary
// this.emit('message', Buffer.from(new Uint8Array(data)));
// break;
this.emit('message', data);
break;
case 'json': // JSON
if (data instanceof Buffer)
data = data.toString(ENCODING);
this.options.encodedecode && (data = $decodeURIComponent(data));
data.isJSON() && this.emit('message', F.onParseJSON(data, this.req));
break;
default: // TEXT
if (data instanceof Buffer)
data = data.toString(ENCODING);
this.emit('message', this.options.encodedecode ? $decodeURIComponent(data) : data);
break;
}
this.current.body = null;
};
WebSocketClientProto.parseInflate = function() {
var self = this;
if (self.inflatelock)
return;
var buf = self.inflatepending.shift();
if (buf) {
self.inflatechunks = [];
self.inflatechunkslength = 0;
self.inflatelock = true;
self.inflate.write(buf);
!buf.$continue && self.inflate.write(Buffer.from(WEBSOCKET_COMPRESS));
self.inflate.flush(function() {
if (!self.inflatechunks)
return;
var data = buffer_concat(self.inflatechunks, self.inflatechunkslength);
self.inflatechunks = null;
self.inflatelock = false;
if (self.current.body) {
CONCAT[0] = self.current.body;
CONCAT[1] = data;
self.current.body = Buffer.concat(CONCAT);
} else
self.current.body = data;
!buf.$continue && self.$decode();
self.parseInflate();
});
}
};
WebSocketClientProto.$onerror = function(err) {
this.$events.error && this.emit('error', err);
if (!this.isClosed) {
this.isClosed = true;
this.$onclose();
}
};
WebSocketClientProto.$onclose = function() {
if (this._isClosed)
return;
this.isClosed = true;
this._isClosed = true;
if (this.inflate) {
this.inflate.removeAllListeners();
this.inflate = null;
this.inflatechunks = null;
}
if (this.deflate) {
this.deflate.removeAllListeners();
this.deflate = null;
this.deflatechunks = null;
}
this.$events.close && this.emit('close', this.closecode, this.closemessage);
this.socket && this.socket.removeAllListeners();
};
/**
* Sends a message
* @param {String/Object} message
* @param {Boolean} raw The message won't be converted e.g. to JSON.
* @return {WebSocketClient}
*/
WebSocketClientProto.send = function(message, raw, replacer) {
if (this.isClosed)
return this;
var t = this.options.type;
if (t !== 'binary' && t !== 'buffer') {
var data = t === 'json' ? (raw ? message : JSON.stringify(message, replacer)) : ((message == null ? '' : message) + '');
if (this.options.encodedecode && data)
data = encodeURIComponent(data);
if (this.deflate) {
this.deflatepending.push(Buffer.from(data, ENCODING));
this.sendDeflate();
} else
this.socket.write(U.getWebSocketFrame(0, data, 0x01));
} else if (message) {
if (this.deflate) {
this.deflatepending.push(message);
this.sendDeflate();
} else
this.socket.write(U.getWebSocketFrame(0, message, 0x02));
}
return this;
};
/**
* Sends a message
* @param {String/Object} message
* @param {Boolean} raw The message won't be converted e.g. to JSON.
* @return {WebSocketClient}
*/
WebSocketClientProto.sendcustom = function(type, message) {
if (this.isClosed)
return this;
if (type === 'binary' || type === 'buffer') {
if (this.deflate) {
this.deflatepending.push(message);
this.sendDeflate();
} else
this.socket.write(U.getWebSocketFrame(0, message, 0x02));
} else {
var data = (message == null ? '' : message) + '';
if (this.options.encodedecode && data)
data = encodeURIComponent(data);
if (this.deflate) {
this.deflatepending.push(Buffer.from(data));
this.sendDeflate();
} else
this.socket.write(U.getWebSocketFrame(0, data, 0x01));
}
return this;
};
WebSocketClientProto.sendDeflate = function() {
var self = this;
if (self.deflatelock)
return;
var buf = self.deflatepending.shift();
if (buf) {
self.deflatechunks = [];
self.deflatechunkslength = 0;
self.deflatelock = true;
self.deflate.write(buf);
self.deflate.flush(function() {
if (self.deflatechunks) {
var data = buffer_concat(self.deflatechunks, self.deflatechunkslength);
data = data.slice(0, data.length - 4);
self.deflatelock = false;
self.deflatechunks = null;
self.socket.write(U.getWebSocketFrame(0, data, self.type === 1 ? 0x02 : 0x01, true));
self.sendDeflate();
}
});
}
};
/**
* Ping message
* @return {WebSocketClient}
*/
WebSocketClientProto.ping = function() {
if (!this.isClosed) {
this.socket.write(U.getWebSocketFrame(0, '', 0x09));
this.$ping = false;
}
return this;
};
function websocket_inflate(data) {
this.$websocket.inflatechunks.push(data);
this.$websocket.inflatechunkslength += data.length;
}
function websocket_deflate(data) {
this.$websocket.deflatechunks.push(data);
this.$websocket.deflatechunkslength += data.length;
}
/**
* Close connection
* @param {String} message Message.
* @param {Number} code WebSocket code.
* @return {WebSocketClient}
*/
WebSocketClientProto.close = function(message, code) {
if (message !== true) {
this.options.reconnect = 0;
} else
message = undefined;
if (!this.isClosed) {
this.isClosed = true;
this.socket.end(U.getWebSocketFrame(code || 1000, message ? (this.options.encodedecode ? encodeURIComponent(message) : message) : '', 0x08));
}
return this;
};
function $decodeURIComponent(value) {
try
{
return decodeURIComponent(value);
} catch (e) {
return value;
}
}
exports.create = function() {
return new WebSocketClient();
};