UNPKG

total5

Version:
1,980 lines (1,538 loc) 48 kB
// Total.js WebSocket // The MIT License // Copyright 2016-2023 (c) Peter Širka <petersirka@gmail.com> & Jozef Gula <gula.jozef@gmail.com> const SOCKET_RESPONSE = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\n\r\n'; const SOCKET_RESPONSE_COMPRESS = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nSec-WebSocket-Extensions: permessage-deflate\r\n\r\n'; const SOCKET_RESPONSE_PROTOCOL = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nSec-WebSocket-Protocol: {1}\r\n\r\n'; const SOCKET_RESPONSE_PROTOCOL_COMPRESS = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nSec-WebSocket-Protocol: {1}\r\nSec-WebSocket-Extensions: permessage-deflate\r\n\r\n'; const SOCKET_HASH = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; const SOCKET_ALLOW_VERSION = ['13']; const SOCKET_COMPRESS = Buffer.from([0x00, 0x00, 0xFF, 0xFF]); const SOCKET_COMPRESS_OPTIONS = { windowBits: F.Zlib.Z_DEFAULT_WINDOWBITS }; const CACHE_GML1 = [null, null, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; const CACHE_GML2 = [null, null, null, null, null, null, null, null]; const CONCAT = [null, null]; const REG_WEBSOCKET = /websocket/i; const REG_WEBSOCKET_ERROR = /ECONNRESET|EHOSTUNREACH|EPIPE|is closed/i; const REG_EMPTYBUFFER = /\0|%00|\\u0000/g; const REG_EMPTYBUFFER_TEST = /\0|%00|\\u0000/; const REG_ROBOT = /search|agent|bot|crawler|spider/i; const REG_MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Tablet/i; var WSCLIENTSID = 0; // WSClient var CALLBACKS = {}; var CALLBACKSCOUNTER = 1; function Controller(req, socket, head) { var ctrl = this; ctrl.req = req; ctrl.method = 'SOCKET'; ctrl.res = ctrl.socket = socket; ctrl.route = null; ctrl.head = head; ctrl.uri = F.TUtils.parseURI2(req.url); ctrl.headers = req.headers; ctrl.query = ctrl.uri.search.parseEncoded(); ctrl.split = ctrl.uri.split; ctrl.split2 = []; ctrl.url = ctrl.uri.key; ctrl.released = false; ctrl.params = {}; ctrl.current = {}; ctrl.masking = false; ctrl.iswebsocket = true; ctrl.protocol = req.connection.encrypted || req.headers['x-forwarded-ssl'] === 'on' || req.headers['x-forwarded-port'] === '443' || (req.headers['x-forwarded-proto'] || req.headers['x-forwarded-protocol']) === 'https' ? 'https' : 'http'; for (let path of ctrl.split) ctrl.split2.push(path.toLowerCase()); ctrl.datatype = 'json'; // json|text|binary } Controller.prototype = { get mobile() { let ua = this.headers['user-agent']; return ua ? REG_MOBILE.test(ua) : false; }, get robot() { let ua = this.headers['user-agent']; return ua ? REG_ROBOT.test(ua) : false; }, get ua() { if (this.$ua != null) return this.$ua; this.$ua = this.headers['user-agent'] || ''; if (this.$ua) this.$ua = this.$ua.parseUA(); return this.$ua; }, get ip() { if (this.$ip != null) return this.$ip; // x-forwarded-for: client, proxy1, proxy2, ... let proxy = this.headers['x-forwarded-for']; if (proxy) this.$ip = proxy.split(',', 1)[0] || this.req.connection.remoteAddress; else if (!this.$ip) this.$ip = this.req.connection.remoteAddress; return this.$ip; }, get referrer() { return this.headers.referer; }, get host() { return this.headers.host; }, get address() { return (this.protocol + '://' + this.headers?.host || '') + (this.req?.url || ''); } }; Controller.prototype.upgrade = function(websocket) { var ctrl = this; F.stats.performance.online++; ctrl.parent = websocket; ctrl.socket.$controller = this; ctrl.req.on('abort', websocket_onerror); ctrl.req.on('aborted', websocket_onerror); ctrl.req.on('error', websocket_onerror); ctrl.socket.on('data', websocket_ondata); ctrl.socket.on('error', websocket_onerror); ctrl.socket.on('close', websocket_close); ctrl.socket.on('end', websocket_close); ctrl.parent.add(ctrl); websocket.online++; // Deprecated: // F.$events.websocket_begin && F.emit('websocket_begin', ctrl.parent, ctrl); if (F.$events.websocket) { /* @Path: Framework @Event: ON('websocket', function(ctrl) { ... }); #ctrl {Controller}; The event captures all incoming WebSocket connections. The next processing __can't be__ canceled via the `ctrl.cancel()` method. */ F.emit('websocket', ctrl); } ctrl.parent.$events.open && ctrl.parent.emit('open', ctrl); }; // Readonly Controller.prototype.cookie = function(name) { var ctrl = this; var arr; if (ctrl.cookies) return F.TUtils.decodeURIComponent(ctrl.cookies[name] || ''); var cookie = ctrl.headers.cookie; if (!cookie) { ctrl.cookies = F.EMPTYOBJECT; return ''; } ctrl.cookies = {}; arr = cookie.split(';'); for (let i = 0; i < arr.length; i++) { let line = arr[i].trim(); let index = line.indexOf('='); if (index !== -1) ctrl.cookies[line.substring(0, index)] = line.substring(index + 1); } return name ? F.TUtils.decodeURIComponent(ctrl.cookies[name] || '') : ''; }; function websocket_ondata(chunk) { this.$controller.ondata(chunk); } function websocket_onerror(e) { this.destroy && this.destroy(); this.$controller && this.$controller.onerror(e); } function websocket_close() { this.$controller && this.$controller.onclose(); this.destroy && this.destroy(); } Controller.prototype.destroy = function() { var ctrl = this; ctrl.socket.destroy(); ctrl.req.destroy(); F.TUtils.destroystream(ctrl.socket); F.TUtils.destroystream(ctrl.req); }; Controller.prototype.onerror = function(err) { var ctrl = this; if (ctrl.isclosed) return; if (REG_WEBSOCKET_ERROR.test(err.stack)) { ctrl.isclosed = true; ctrl.onclose(); } else ctrl.parent.$events.error && ctrl.parent.emit('error', err, ctrl); }; Controller.prototype.onclose = function() { var ctrl = this; if (ctrl.isclosed2) return; F.stats.performance.online--; ctrl.isclosed = true; ctrl.isclosed2 = true; if (ctrl.inflate) { ctrl.inflate.removeAllListeners(); delete ctrl.inflate; delete ctrl.inflatechunks; } if (ctrl.deflate) { ctrl.deflate.removeAllListeners(); delete ctrl.deflate; delete ctrl.deflatechunks; } delete ctrl.parent.connections[ctrl.ID]; ctrl.parent.online--; ctrl.parent.$events.close && ctrl.parent.emit('close', ctrl, ctrl.closecode, ctrl.closemessage); ctrl.socket.removeAllListeners(); F.$events.websocket_end && EMIT('websocket_end', ctrl.parent, ctrl); }; Controller.prototype.ondata = function(data) { var ctrl = this; var current = ctrl.current; if (data) { if (current.buffer) { CONCAT[0] = current.buffer; CONCAT[1] = data; current.buffer = Buffer.concat(CONCAT); } else current.buffer = data; } if (!ctrl.parse()) return; if (!current.final && current.type !== 0x00) current.type2 = current.type; var decompress = current.compressed && ctrl.inflate; switch (current.type === 0x00 ? current.type2 : current.type) { case 0x01: // text if (decompress) { current.final && ctrl.parseinflate(); } else { if (current.body) { CONCAT[0] = current.body; CONCAT[1] = current.data; current.body = Buffer.concat(CONCAT); } else current.body = current.data; current.final && ctrl.decode(); } break; case 0x02: // binary if (decompress) { current.final && ctrl.parseinflate(); } else { if (current.body) { CONCAT[0] = current.body; CONCAT[1] = current.data; current.body = Buffer.concat(CONCAT); } else current.body = current.data; current.final && ctrl.decode(); } break; case 0x08: // close if (current.data) { ctrl.closemessage = current.data.slice(2).toString('utf8'); ctrl.closecode = current.data[0] << 8 | current.data[1]; } if (ctrl.closemessage && ctrl.parent.encodedecode) ctrl.closemessage = F.TUtils.decodeURIComponent(ctrl.closemessage); ctrl.close(); current.buffer = null; current.inflatedata = null; return; case 0x09: // ping, response pong ctrl.socket.write(getWebSocketFrame(0, 'PONG', 0x0A, false, ctrl.masking)); current.buffer = null; current.inflatedata = null; break; case 0x0a: // pong ctrl.latency = Date.now() - ctrl.$ping; current.buffer = null; current.inflatedata = null; break; } if (current.buffer) { current.buffer = current.buffer.slice(current.length, current.buffer.length); current.buffer.length && setImmediate(ctrl.ondata2); } }; // MIT // Written by Jozef Gula // Optimized by Peter Sirka Controller.prototype.parse = function() { var ctrl = this; var current = ctrl.current; // Fixed a problem with parsing of long messages, the code bellow 0x80 still returns 0 when the message is longer // if (!current.buffer || current.buffer.length <= 2 || ((current.buffer[0] & 0x80) >> 7) !== 1) if (!current.buffer || current.buffer.length <= 2) return; // WebSocket - Opcode current.type = current.buffer[0] & 0x0f; // Compression // Type must be greater than 0 if (current.type) 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 = getMessageLength(current.buffer, F.isLE); // index for data // Solving a problem with The value "-1" is invalid for option "size" if (length <= 0) return current.final; 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; if (mlength > ctrl.route.size) { ctrl.close(1009, 'Frame is too large'); return; } // 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 && ctrl.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; ctrl.inflatepending.push(buf); } else { current.data = Buffer.alloc(length); current.buffer.copy(current.data, 0, index, mlength); if (current.isMask) { for (var i = 0; i < length; i++) current.data[i] = current.data[i] ^ current.mask[i % 4]; } } } return true; }; Controller.prototype.decode = function() { var ctrl = this; var data = ctrl.current.body; var size = data.length; F.stats.performance.message++; F.stats.performance.download += size / 1024 / 1024; switch (ctrl.datatype) { case 'binary': ctrl.parent.$events.message && ctrl.parent.emit('message', ctrl, data, size); break; case 'json': if (data instanceof Buffer) data = data.toString('utf8'); if (ctrl.parent.encodedecode === true) data = F.TUtils.decodeURIComponent(data); if (ctrl.parent.encryptdecrypt && F.config.secret_encryption) data = F.TUtils.decrypt_data(data, F.config.secret_encryption); if (data.isJSON()) { let tmp = data.parseJSON(true); if (REG_EMPTYBUFFER_TEST.test(tmp)) tmp = tmp.replace(REG_EMPTYBUFFER, ''); if (tmp !== undefined && ctrl.parent.$events.message) ctrl.parent.emit('message', this, tmp, size); } break; default: // TEXT if (data instanceof Buffer) data = data.toString('utf8'); if (ctrl.parent.encodedecode === true) data = F.TUtils.decodeURIComponent(data); if (ctrl.parent.encryptdecrypt && F.config.secret_encryption) data = F.TUtils.decrypt_data(data, F.config.secret_encryption); if (REG_EMPTYBUFFER_TEST.test(data)) data = data.replace(REG_EMPTYBUFFER, ''); ctrl.parent.$events.message && ctrl.parent.emit('message', ctrl, data, size); break; } ctrl.current.body = null; }; Controller.prototype.parseinflate = function() { var ctrl = this; if (ctrl.inflatelock) return; var buf = ctrl.inflatepending.shift(); if (buf) { ctrl.inflatechunks = []; ctrl.inflatechunkslength = 0; ctrl.inflatelock = true; ctrl.inflate.write(buf); if (!buf.$continue) ctrl.inflate.write(Buffer.from(SOCKET_COMPRESS)); ctrl.inflate.flush(function() { if (!ctrl.inflatechunks) return; var data = concat(ctrl.inflatechunks, ctrl.inflatechunkslength); ctrl.inflatechunks = null; ctrl.inflatelock = false; if (data.length > ctrl.route.size) { ctrl.close(1009, 'Frame is too large'); return; } if (ctrl.current.body) { CONCAT[0] = ctrl.current.body; CONCAT[1] = data; ctrl.current.body = Buffer.concat(CONCAT); } else ctrl.current.body = data; !buf.$continue && ctrl.decode(); ctrl.parseinflate(); }); } }; Controller.prototype.send = function(message, raw, replacer) { var ctrl = this; if (ctrl.isclosed) return ctrl; var buffer; if (ctrl.datatype !== 'binary') { var data = ctrl.datatype === 'text' ? (raw ? message : JSON.stringify(message, replacer == true ? F.TUtils.json2replacer : replacer)) : typeof(message) === 'object' ? JSON.stringify(message, replacer == true ? F.TUtils.json2replacer : replacer) : (message + ''); if (ctrl.parent.encryptdecrypt && F.config.secret_encryption) data = F.TUtils.encrypt_data(data, F.config.secret_encryption); if (ctrl.parent.encodedecode === true && data) data = encodeURIComponent(data); if (ctrl.deflate) { buffer = Buffer.from(data, 'utf8'); ctrl.deflatepending.push(buffer); ctrl.senddeflate(); } else { buffer = Buffer.from(data, 'utf8'); ctrl.socket.write(getWebSocketFrame(0, buffer, 0x01, false, ctrl.masking)); } } else if (message) { buffer = message; if (ctrl.deflate) { ctrl.deflatepending.push(message); ctrl.senddeflate(); } else ctrl.socket.write(getWebSocketFrame(0, message, 0x02, false, ctrl.masking)); } if (buffer) F.stats.performance.upload += buffer.length / 1024 / 1024; }; Controller.prototype.senddeflate = function() { var ctrl = this; if (ctrl.deflatelock) return; var buf = ctrl.deflatepending.shift(); if (buf) { ctrl.deflatechunks = []; ctrl.deflatechunkslength = 0; ctrl.deflatelock = true; ctrl.deflate.write(buf); ctrl.deflate.flush(function() { if (ctrl.deflatechunks) { var data = concat(ctrl.deflatechunks, ctrl.deflatechunkslength); data = data.slice(0, data.length - 4); ctrl.deflatelock = false; ctrl.deflatechunks = null; ctrl.socket.write(getWebSocketFrame(0, data, ctrl.type === 'binary' ? 0x02 : 0x01, true, ctrl.masking)); ctrl.senddeflate(); } }); } }; Controller.prototype.ping = function(ts) { var ctrl = this; if (!ctrl.isclosed) { try { ctrl.$ping = ts || Date.now(); ctrl.socket.write(getWebSocketFrame(0, 'PING', 0x09, false, ctrl.masking)); } catch (e) { // Socket error ctrl.onerror(e); } } return ctrl; }; Controller.prototype.hostname = function(path) { var ctrl = this; return ctrl.protocol + '://' + ctrl.headers.host + (path ? path : ''); }; function websocketclientdestroy(ctrl) { ctrl.socket.destroy(); F.TUtils.destroystream(ctrl.socket); F.TUtils.destroystream(ctrl.req); } function websocketclientsendfin(ctrl) { ctrl.socket.end(getWebSocketFrame(ctrl.closecode, ctrl.closemessage, 0x08, false, ctrl.masking)); setImmediate(websocketclientdestroy, ctrl); } Controller.prototype.close = function(code, message) { var ctrl = this; if (!ctrl.isclosed) { ctrl.isclosed = true; if (ctrl.ready) { if (message && ctrl.parent && ctrl.parent.encodedecode) message = encodeURIComponent(message); if (ctrl.closecode) { setImmediate(websocketclientdestroy, ctrl); } else { ctrl.closecode = code || 1000; ctrl.closemessage = message || ''; setTimeout(websocketclientsendfin, 1000, ctrl); } } else if (!ctrl.closecode) { ctrl.socket.end(); setImmediate(websocketclientdestroy, ctrl); } } }; Controller.prototype.sign = function(ctrl) { var sha1 = F.Crypto.createHash('sha1'); sha1.update((ctrl.headers['sec-websocket-key'] || '') + SOCKET_HASH); return sha1.digest('base64'); }; function 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; } function WebSocket(url, route, params) { var t = this; t.url = url; t.online = 0; t.connections = {}; t.route = route; t.params = params; // t.autocloseid = null; F.TUtils.EventEmitter2.extend(t); } WebSocket.prototype.encrypt = function(enable) { this.encryptdecrypt = enable === true || enable == null; }; WebSocket.prototype.find = function(fn) { var self = this; for (var key in self.connections) { var ctrl = self.connections[key]; if (fn(ctrl)) return ctrl; } }; WebSocket.prototype.send = function(message, comparer, replacer, params) { var self = this; if (message === undefined) return self; if (!params && replacer != null && typeof(replacer) !== 'function') { params = replacer; replacer = null; } var raw = false; var data = null; for (var key in self.connections) { var ctrl = self.connections[key]; if (data == null) { if (ctrl.datatype === 'json') { raw = true; data = JSON.stringify(message, replacer == true ? F.TUtils.json2replacer : replacer); } else data = message; } if (comparer && !comparer(ctrl, message, params)) continue; ctrl.send(data, raw); F.stats.response.websocket++; } return self; }; // Ping all connections WebSocket.prototype.ping = function() { var self = this; self.$ping = true; F.stats.other.websocketping++; var ts = Date.now(); for (var key in self.connections) self.connections[key].ping(ts); return self; }; WebSocket.prototype.api = function(api) { var self = this; if (!api.startsWith('/@')) { if (api[0] !== '@') api = '@' + api; api = '/' + api + '/'; } self.on('message', function(client, msg) { if (msg && msg.TYPE === 'api') client.exec(api, msg); }); return self; }; WebSocket.prototype.close = function(code, message) { var self = this; for (var key in self.connections) { self.connections[key].close(code, message); delete self.connections[key]; } self.online = 0; return self; }; WebSocket.prototype.error = function(err) { var self = this; F.error(typeof(err) === 'string' ? new Error(err) : err, self.name, self.url); }; WebSocket.prototype.destroy = function() { var self = this; if (!self.connections) return self; self.close(); self.$events.destroy && self.emit('destroy'); delete F.connections[self.url]; setTimeout(function(self) { for (var key in self.connections) { var conn = self.connections[key]; if (conn) { conn.isclosed2 = true; conn.socket.removeAllListeners(); } } var index = self.route.connections.indexOf(self); if (index !== -1) self.route.connections.splice(index, 1); }, 1000, self); }; WebSocket.prototype.add = function(ctrl) { this.connections[ctrl.ID] = ctrl; }; WebSocket.prototype.check = function() { var self = this; if (self.$ping) { for (var key in self.connections) { var ctrl = self.connections[key]; if (ctrl.$ping && (ctrl.latency == null || ctrl.latency > F.config.$wsmaxlatency)) { ctrl.close(); F.stats.other.websocketcleaner++; } } } }; function wsdestroy_open() { var self = this; if (self.autocloseid) { clearTimeout(self.autocloseid); self.autocloseid = null; } } function wsdestroy_close(self) { // Checks again online state if (self.online) { self.autocloseid = null; return; } if (self.autodestroyitems) { for (var fn of self.autodestroyitems) fn.call(self); self.autodestroyitems = null; } self.destroy(); } WebSocket.prototype.autodestroy = function(callback) { var self = this; if (self.autodestroyitems) { self.autodestroyitems.push(callback); return self; } self.autodestroyitems = []; callback && self.autodestroyitems.push(callback); self.on('open', wsdestroy_open); self.on('close', function() { if (!self.online) self.autocloseid = setTimeout(wsdestroy_close, 5000, self); }); return self; }; function authorize(ctrl) { if (F.def.onAuthorize) { var opt = new F.TBuilders.Options(ctrl); opt.TYPE = 'auth'; // important opt.query = ctrl.query; opt.iswebsocket = true; opt.next = opt.callback; opt.$callback = function(err, user) { let auth = user ? 1 : 2; ctrl.user = user; if (ctrl.route.auth === auth) { execute(ctrl); } else { ctrl.route = F.TRouting.lookupwebsocket(ctrl, auth); if (ctrl.route) execute(ctrl); else ctrl.close(4001); } }; F.def.onAuthorize(opt); } else { ctrl.route = F.TRouting.lookupwebsocket(ctrl, 0); if (ctrl.route) execute(ctrl); else ctrl.close(4004); } } function middleware(ctrl) { var run = function(index) { let key = ctrl.route.middleware[index]; if (key) { let fn = F.routes.middleware[key]; if (fn) fn(ctrl, () => run(index + 1)); else prepare(ctrl); } else prepare(ctrl); }; run(0); } function prepare(ctrl) { ctrl.ondata2 = () => ctrl.ondata(); var compress = (F.config.$wscompress && ctrl.headers['sec-websocket-extensions'] || '').indexOf('permessage-deflate') !== -1; var header = ctrl.route.protocols && ctrl.route.protocols.length ? (compress ? SOCKET_RESPONSE_PROTOCOL_COMPRESS : SOCKET_RESPONSE_PROTOCOL).format(ctrl.sign(ctrl), ctrl.route.protocols.join(', ')) : (compress ? SOCKET_RESPONSE_COMPRESS : SOCKET_RESPONSE).format(ctrl.sign(ctrl)); ctrl.socket.write(Buffer.from(header, 'binary')); ctrl.ready = true; if (compress) { ctrl.inflatepending = []; ctrl.inflatelock = false; ctrl.inflate = F.Zlib.createInflateRaw(SOCKET_COMPRESS_OPTIONS); ctrl.inflate.$controller = ctrl; ctrl.inflate.on('error', function() { if (!ctrl.$uerror) { ctrl.$uerror = true; ctrl.close(1003, 'Invalid data'); } }); ctrl.inflate.on('data', inflate); ctrl.deflatepending = []; ctrl.deflatelock = false; ctrl.deflate = F.Zlib.createDeflateRaw(SOCKET_COMPRESS_OPTIONS); ctrl.deflate.$controller = ctrl; ctrl.deflate.on('error', function() { if (!ctrl.$uerror) { ctrl.$uerror = true; ctrl.close(1003, 'Invalid data'); } }); ctrl.deflate.on('data', deflate); } if (WSCLIENTSID++ > 999999999) WSCLIENTSID = 1; ctrl.ID = F.TUtils.random_text(3) + WSCLIENTSID; ctrl.id = ctrl.ID; if (F.connections[ctrl.url]) { ctrl.upgrade(F.connections[ctrl.url]); return; } var websocket = new WebSocket(ctrl.url, ctrl.route, ctrl.params); F.connections[ctrl.url] = websocket; websocket.encodedecode = F.config.$wsencodedecode === true; if (!ctrl.route.connections) ctrl.route.connections = []; ctrl.route.connections.push(websocket); ctrl.route.action.call(websocket, websocket); setImmediate(upgradecontinue, ctrl, websocket); } function upgradecontinue(ctrl, websocket) { ctrl.upgrade(websocket); } function inflate(data) { var ctrl = this.$controller; if (ctrl && ctrl.inflatechunks) { ctrl.inflatechunks.push(data); ctrl.inflatechunkslength += data.length; } } function deflate(data) { var ctrl = this.$controller; if (ctrl && ctrl.deflatechunks) { ctrl.deflatechunks.push(data); ctrl.deflatechunkslength += data.length; } } function execute(ctrl) { for (let param of ctrl.route.params) { let value = ctrl.split[param.index]; ctrl.params[param.name] = value; } if (F.def.onLocalize) ctrl.language = F.def.onLocalize(ctrl); if (ctrl.route.flags.binary) ctrl.datatype = 'binary'; else if (ctrl.route.flags.text) ctrl.datatype = 'text'; if (ctrl.route.middleware.length) middleware(ctrl); else prepare(ctrl); } // MIT // Written by Jozef Gula <gula.jozef@gmail.com> function getWebSocketFrame(code, message, type, compress, mask) { if (mask) mask = ((Math.random() * 214748364) >> 0) + 1; var messageBuffer = getWebSocketFrameMessageBytes(code, message); var lengthBuffer = getWebSocketFrameLengthBytes(messageBuffer.length); var lengthMask = mask ? 4 : 0; var frameBuffer = Buffer.alloc(1 + lengthBuffer.length + messageBuffer.length + lengthMask); frameBuffer[0] = 0x80 | type; if (compress) frameBuffer[0] |= 0x40; lengthBuffer.copy(frameBuffer, 1, 0, lengthBuffer.length); if (mask) { var offset = lengthBuffer.length + 1; frameBuffer[1] |= 0x80; frameBuffer.writeInt32BE(mask, offset); for (var i = 0; i < messageBuffer.length; i++) messageBuffer[i] = messageBuffer[i] ^ frameBuffer[offset + (i % 4)]; } messageBuffer.copy(frameBuffer, lengthBuffer.length + 1 + lengthMask, 0, messageBuffer.length); return frameBuffer; } // MIT // Written by Jozef Gula <gula.jozef@gmail.com> function getWebSocketFrameMessageBytes(code, message) { var index = code ? 2 : 0; var binary = message instanceof Int8Array || message instanceof Buffer; var length = message.length; var messageBuffer = Buffer.alloc(length + index); for (var i = 0; i < length; i++) messageBuffer[i + index] = binary ? message[i] : message.charCodeAt(i); if (code) { messageBuffer[0] = code >> 8; messageBuffer[1] = code; } return messageBuffer; } // MIT // Written by Jozef Gula <gula.jozef@gmail.com> function getWebSocketFrameLengthBytes(length) { var lengthBuffer = null; if (length <= 125) { lengthBuffer = Buffer.alloc(1); lengthBuffer[0] = length; return lengthBuffer; } if (length <= 65535) { lengthBuffer = Buffer.alloc(3); lengthBuffer[0] = 126; lengthBuffer[1] = (length >> 8) & 255; lengthBuffer[2] = (length) & 255; return lengthBuffer; } lengthBuffer = Buffer.alloc(9); lengthBuffer[0] = 127; lengthBuffer[1] = 0x00; lengthBuffer[2] = 0x00; lengthBuffer[3] = 0x00; lengthBuffer[4] = 0x00; lengthBuffer[5] = (length >> 24) & 255; lengthBuffer[6] = (length >> 16) & 255; lengthBuffer[7] = (length >> 8) & 255; lengthBuffer[8] = (length) & 255; return lengthBuffer; } // MIT // Written by Jozef Gula // Optimized by Peter Sirka function getMessageLength(data, isLE) { var length = data[1] & 0x7f; if (length === 126) { if (data.length < 4) return -1; CACHE_GML1[0] = data[3]; CACHE_GML1[1] = data[2]; return converBytesToInt64(CACHE_GML1, 0, isLE); } if (length === 127) { if (data.Length < 10) return -1; CACHE_GML2[0] = data[9]; CACHE_GML2[1] = data[8]; CACHE_GML2[2] = data[7]; CACHE_GML2[3] = data[6]; CACHE_GML2[4] = data[5]; CACHE_GML2[5] = data[4]; CACHE_GML2[6] = data[3]; CACHE_GML2[7] = data[2]; return converBytesToInt64(CACHE_GML2, 0, isLE); } return length; } // MIT // Written by Jozef Gula function converBytesToInt64(data, startIndex, isLE) { return isLE ? (data[startIndex] | (data[startIndex + 1] << 0x08) | (data[startIndex + 2] << 0x10) | (data[startIndex + 3] << 0x18) | (data[startIndex + 4] << 0x20) | (data[startIndex + 5] << 0x28) | (data[startIndex + 6] << 0x30) | (data[startIndex + 7] << 0x38)) : ((data[startIndex + 7] << 0x20) | (data[startIndex + 6] << 0x28) | (data[startIndex + 5] << 0x30) | (data[startIndex + 4] << 0x38) | (data[startIndex + 3]) | (data[startIndex + 2] << 0x08) | (data[startIndex + 1] << 0x10) | (data[startIndex] << 0x18)); } exports.ping = function() { for (let key in F.connections) { let socket = F.connections[key]; socket.check(); socket.ping(); } }; exports.listen = function(req, socket, head) { if (!req.headers.upgrade || !F.routes.websockets.length || !REG_WEBSOCKET.test(req.headers.upgrade)) return; var ctrl = new Controller(req, socket, head); if (F.paused.length) { ctrl.destroy(); return; } if (SOCKET_ALLOW_VERSION.indexOf(ctrl.headers['sec-websocket-version'] || '') === -1) { ctrl.destroy(); return; } if (F.config.$blacklist && F.config.$blacklist.indexOf(ctrl.ip) !== -1) { F.stats.request.blocked++; ctrl.destroy(); return; } if (F.routes.proxies.length && F.TRouting.lookupproxy(ctrl)) return; socket.setTimeout(0); socket.on('error', NOOP); ctrl.route = F.TRouting.lookupwebsocket(ctrl, 0, true); if (!ctrl.route) { ctrl.destroy(); return; } if (ctrl.route.flags.csrf && !F.def.onCSRFcheck(ctrl)) { ctrl.destroy(); return; } F.$events.websocket && F.emit('websocket', ctrl); F.stats.request.websocket++; authorize(ctrl); }; function WebSocketClient() { var t = this; t.iswsclient = true; t.current = {}; t.pending = []; t.reconnect = 0; t.closed = true; // type: json, text, binary t.headers = {}; t.options = { type: 'json', size: 0, masking: false, compress: true, reconnect: 3000, encodedecode: false, encryptdecrypt: false, rejectunauthorized: false }; // key: Buffer, cert: Buffer, dhparam: Buffer t.cookies = {}; t.ondata2 = () => t.ondata(); F.TUtils.EventEmitter2.extend(t); } WebSocketClient.prototype.connect = function(url, protocol, origin) { setImmediate(this.connectforce, this, url, protocol, origin); }; WebSocketClient.prototype.connectforce = function(self, url, protocol, origin) { var options = {}; self.url = url; self.origin = origin; self.protocol = protocol; self.secret = F.Crypto.randomBytes(16).toString('base64'); delete self.isclosed2; delete self.isclosed; var secured = false; if (typeof(url) === 'string') { url = F.Url.parse(url); options.host = url.hostname; options.path = url.path; options.query = url.query; secured = url.protocol === 'wss:'; options.port = url.port || (secured ? 443 : 80); } else { options.socketPath = url.socket; 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'] = self.secret; options.headers['Sec-Websocket-Extensions'] = (self.options.compress ? 'permessage-deflate, ' : '') + 'client_max_window_bits'; if (protocol) options.headers['Sec-WebSocket-Protocol'] = protocol; if (origin) options.headers['Sec-WebSocket-Origin'] = origin; options.headers.Connection = 'Upgrade'; options.headers.Upgrade = 'websocket'; // options.agent = options.port === 443 ? KeepAliveHttps : KeepAlive; // options.agent = options.port === 443 ? new Https.Agent() : new Http.Agent(); options.agent = false; if (self.options.key) options.key = self.options.key; if (self.options.cert) options.cert = self.options.cert; if (self.options.dhparam) options.dhparam = self.options.dhparam; if (self.options.rejectUnauthorized || self.options.rejectunauthorized) options.rejectUnauthorized = true; for (let key in self.headers) options.headers[key] = self.headers[key]; var tmp = []; for (let key in self.cookies) tmp.push(key + '=' + self.cookies[key]); options.headers.Cookie = tmp.join(', '); F.stats.performance.online++; self.req = (secured ? F.Https : F.Http).get(options); self.req.$main = self; self.req.on('error', function(e) { self.$events.error && self.emit('error', e); self.onclose(); self.$api && wsclient_closecallbacks(self, e); self.options.reconnectserver && wsclient_reconnect_timer(self); }); self.req.on('response', function(res) { if (res.statusCode === 301 || res.statusCode === 302) { self.connect(res.headers.location, self.protocol, self.origin); return; } self.$events.error && self.emit('error', new Error('Unexpected server response (' + res.statusCode + ')')); if (self.options.reconnectserver) { self.onclose(); wsclient_reconnect_timer(self); } self.onclose(); }); self.req.on('upgrade', function(response, socket) { self.socket = socket; self.socket.$controller = self; var compress = self.options.compress && (response.headers['sec-websocket-extensions'] || '').indexOf('-deflate') !== -1; var digest = F.Crypto.createHash('sha1').update(self.secret + SOCKET_HASH, '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'), response); self.free(); return; } self.closed = false; self.socket.on('data', websocket_ondata); self.socket.on('error', websocket_onerror); self.socket.on('close', wsclient_close); self.socket.on('end', wsclient_close); if (compress) { self.inflatepending = []; self.inflatelock = false; self.inflate = F.Zlib.createInflateRaw(SOCKET_COMPRESS_OPTIONS); self.inflate.$controller = self; self.inflate.on('error', F.error()); self.inflate.on('data', inflate); self.deflatepending = []; self.deflatelock = false; self.deflate = F.Zlib.createDeflateRaw(SOCKET_COMPRESS_OPTIONS); self.deflate.$controller = self; self.deflate.on('error', F.error()); self.deflate.on('data', deflate); } self.$events.open && self.emit('open'); }); }; WebSocketClient.prototype.ping = function(timeout) { var self = this; if (!self.isclosed && !self.timeout) { self.timeout = setTimeout(wsclient_timeout, timeout || 3000); self.socket.write(getWebSocketFrame(0, 'PING', 0x09, false, self.options.masking)); self.$ping = Date.now(); } return self; }; WebSocketClient.prototype.ondata = function(data) { var self = this; if (self.isclosed) return; if (data) F.stats.performance.download += data.length / 1024 / 1024; var current = self.current; if (data) { if (current.buffer) { CONCAT[0] = current.buffer; CONCAT[1] = data; current.buffer = Buffer.concat(CONCAT); } else current.buffer = data; } if (!self.parse()) return; if (!current.final && current.type !== 0x00) current.type2 = current.type; var decompress = current.compressed && self.inflate; switch (current.type === 0x00 ? current.type2 : current.type) { case 0x01: // text if (decompress) { current.final && self.parseinflate(); } else { if (current.body) { CONCAT[0] = current.body; CONCAT[1] = current.data; current.body = Buffer.concat(CONCAT); } else current.body = current.data; current.final && self.decode(); } break; case 0x02: // binary if (decompress) { current.final && self.parseinflate(); } else { if (current.body) { CONCAT[0] = current.body; CONCAT[1] = current.data; current.body = Buffer.concat(CONCAT); } else current.body = current.data; current.final && self.decode(); } break; case 0x08: // close self.closemessage = current.data.slice(2).toString('utf8'); self.closecode = current.data[0] << 8 | current.data[1]; if (self.closemessage && self.options.encodedecode) self.closemessage = F.TUtils.decodeURIComponent(self.closemessage); self.$api && wsclient_closecallbacks(self, self.closecode); wsclient_closeforce(self); break; case 0x09: // ping, response pong self.socket.write(getWebSocketFrame(0, 'PONG', 0x0A, false, self.options.masking)); current.buffer = null; current.inflatedata = null; break; case 0x0a: // pong self.timeout && clearTimeout(self.timeout); self.timeout = null; self.latency = Date.now() - self.$ping; current.buffer = null; current.inflatedata = null; break; } if (current.buffer) { current.buffer = current.buffer.slice(current.length, current.buffer.length); current.buffer.length && setImmediate(self.ondata2); } }; // MIT // Written by Jozef Gula // Optimized by Peter Sirka WebSocketClient.prototype.parse = function() { var self = this; var current = self.current; // Fixed a problem with parsing of long messages, the code bellow 0x80 still returns 0 when the message is longer // if (!current.buffer || current.buffer.length <= 2 || ((current.buffer[0] & 0x80) >> 7) !== 1) if (!current.buffer || current.buffer.length <= 2) 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 = getMessageLength(current.buffer, F.isLE); // index for data // Solving a problem with The value "-1" is invalid for option "size" if (length <= 0) return current.final; 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; if (self.options.length && mlength > self.options.length) { self.close('Frame is too large', 1009); return; } // 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 && self.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; self.inflatepending.push(buf); } else { current.data = Buffer.alloc(length); current.buffer.copy(current.data, 0, index, mlength); } } return true; }; WebSocketClient.prototype.readbody = function() { var self = this; var current = self.current; var length = current.data.length; var 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; }; WebSocketClient.prototype.decode = function() { var self = this; var data = self.current.body; var size = data.length; F.stats.performance.message++; switch (self.options.type) { case 'binary': self.emit('message', data, size); break; case 'json': if (data instanceof Buffer) data = data.toString('utf8'); if (self.options.encodedecode === true) data = F.TUtils.decodeURIComponent(data); if (self.options.encrypt) data = F.TUtils.decrypt_data(data, self.options.encrypt); if (data.isJSON()) { var tmp = data.parseJSON(true); if (tmp !== undefined) self.emit('message', tmp, size); } break; default: // TEXT if (data instanceof Buffer) data = data.toString('utf8'); if (self.options.encodedecode === true) data = F.TUtils.decodeURIComponent(data); if (self.options.encrypt) data = F.TUtils.decrypt_data(data, self.options.encrypt); self.emit('message', data, size); break; } self.current.body = null; }; WebSocketClient.prototype.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(SOCKET_COMPRESS)); self.inflate.flush(function() { if (!self.inflatechunks) return; let data = concat(self.inflatechunks, self.inflatechunkslength); self.inflatechunks = null; self.inflatelock = false; if (self.options.size && data.length > self.options.size) { self.close(1009, 'Frame is too large'); return; } if (self.current.body) { CONCAT[0] = self.current.body; CONCAT[1] = data; self.current.body = Buffer.concat(CONCAT); } else self.current.body = data; if (!buf.$continue) self.decode(); self.parseinflate(); }); } }; WebSocketClient.prototype.onerror = function(err) { var self = this; self.$events.error && self.emit('error', err); if (!self.isclosed) { self.$api && wsclient_closecallbacks(self, err); self.isclosed = true; self.onclose(); } }; WebSocketClient.prototype.onclose = function() { var self = this; if (self.isclosed2) return; self.isclosed = true; self.isclosed2 = true; F.stats.performance.online--; if (self.inflate) { self.inflate.removeAllListeners(); self.inflate = null; self.inflatechunks = null; } if (self.deflate) { self.deflate.removeAllListeners(); self.deflate = null; self.deflatechunks = null; } self.free(); }; WebSocketClient.prototype.destroy = function() { var self = this; self.free(); self.options.reconnect = 0; if (self.reconnecting) { clearTimeout(self.reconnecting); self.reconnecting = null; } self.$events.destroy && self.emit('destroy'); }; WebSocketClient.prototype.free = function() { var self = this; if (self.req) { self.req.connection && self.req.connection.destroy(); self.req.removeAllListeners(); self.req.destroy(); F.cleanup(self.req); } if (self.socket) { self.socket.removeAllListeners(); self.socket.destroy(); F.cleanup(self.socket); } self.socket = null; self.req = null; return self; }; WebSocketClient.prototype.send = function(message, raw, replacer) { var self = this; if (self.isclosed || self.closed) return false; var type = self.options.type; if (type != 'binary') { var data = type === 'json' ? (raw ? message : JSON.stringify(message, replacer == true ? F.TUtils.json2replacer : replacer)) : typeof(message) === 'object' ? JSON.stringify(message, replacer == true ? F.TUtils.json2replacer : replacer) : (message + ''); var buffer; if (self.options.encrypt) data = F.TUtils.encrypt_data(data, self.options.encrypt); if (self.options.encodedecode === true && data) data = encodeURIComponent(data); if (self.deflate) { buffer = Buffer.from(data, 'utf8'); self.deflatepending.push(buffer); self.senddeflate(); } else { buffer = Buffer.from(data, 'utf8'); self.socket.write(getWebSocketFrame(0, buffer, 0x01, false, self.options.masking)); } F.stats.performance.upload += buffer.length / 1024 / 1024; } else if (message) { if (!(message instanceof Buffer)) message = Buffer.from(message, 'utf8'); if (self.deflate) { self.deflatepending.push(message); self.senddeflate(); } else self.socket.write(getWebSocketFrame(0, message, 0x02, false, self.options.masking)); F.stats.performance.upload += message.length / 1024 / 1024; } F.stats.response.websocket++; return true; }; WebSocketClient.prototype.sendcustom = function(type, message) { var self = this; if (self.isclosed || self.closed || !self.socket) return false; if (type !== 'binary') { var data = (message == null ? '' : message) + ''; if (self.options.encrypt) data = F.TUtils.encrypt_data(data, self.options.encrypt); if (self.options.encodedecode && data) data = encodeURIComponent(data); if (self.deflate) { self.deflatepending.push(Buffer.from(data)); self.senddeflate(); } else self.socket.write(getWebSocketFrame(0, data, 0x01, false, self.options.masking)); } else { if (!(message instanceof Buffer)) message = Buffer.from(message); if (self.deflate) { self.deflatepending.push(message); self.senddeflate(); } else self.socket.write(getWebSocketFrame(0, message, 0x02, false, self.options.masking)); } F.stats.response.websocket++; return true; }; WebSocketClient.prototype.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 = concat(self.deflatechunks, self.deflatechunkslength); data = data.slice(0, data.length - 4); self.deflatelock = false; self.deflatechunks = null; if (self.socket) { self.socket.write(getWebSocketFrame(0, data, self.options.type === 'binary' ? 0x02 : 0x01, true, self.options.masking)); self.senddeflate(); } } }); } }; WebSocketClient.prototype.close = function(code, message) { var self = this; if (message !== true) { self.options.reconnect = 0; self.reconnecting && clearTimeout(self.reconnecting); self.reconnecting = null; } else message = undefined; if (!self.isclosed && self.socket) { self.isclosed = true; if (message && self.options.encodedecode) message = encodeURIComponent(message); self.socket.end(getWebSocketFrame(code || 1000, message || '', 0x08, false, self.options.masking)); } return self; }; function registerapi(client) { if (client.$api) return; client.$api = true; client.on('message', function(msg) { if (msg.TYPE === 'api') { var obj = CALLBACKS[msg.callbackid]; if (obj) { delete CALLBACKS[msg.callbackid]; clearTimeout(obj.timeout); if (msg.error) obj.callback(msg.data instanceof Array ? ErrorBuilder.assign(msg.data) : msg.data); else obj.callback(null, msg.data); } } }); } function timeoutapi(id) { var obj = CALLBACKS[id]; if (obj) { obj.callback(408); delete CALLBACKS[id]; } } WebSocketClient.prototype.api = function(schema, data, callback, timeout) { var self = this; if (!schema) { registerapi(self); return self; } if (!self.$api) self.api(); if (typeof(data) === 'function') { timeout = callback; callback = data; data = null; } if (callback == null) return new Promise((resolve, reject) => self.api(schema, data, (err, response) => err ? reject(err) : resolve(response), timeout)); var msg = { TYPE: 'api', data: { schema: schema, data: data }}; msg.callbackid = (CALLBACKSCOUNTER++) + ''; if (CALLBACKSCOUNTER > 9999999999) CALLBACKSCOUNTER = 1; var obj = {}; obj.client = self; obj.callback = callback; obj.timeout = setTimeout(timeoutapi, timeout || 5000, msg.callbackid); CALLBACKS[msg.callbackid] = obj; self.send(msg); }; function wsclient_timeout(self) { self.timeout = null; self.onerror('Timeout'); } function wsclient_close() { wsclient_closeforce(this.$controller); } function wsclient_closeforce(client) { if (!client.closed) { client.$events.close && client.emit('close', client.closecode, client.closemessage); client.closed = true; client.onclose(); wsclient_reconnect_timer(client); } } function inflate(data) { var ctrl = this.$controller; if (ctrl && ctrl.inflatechunks) { ctrl.inflatechunks.push(data); ctrl.inflatechunkslength += data.length; } } function deflate(data) { var ctrl = this.$controller; if (ctrl && ctrl.deflatechunks) { ctrl.deflatechunks.push(data); ctrl.deflatechunkslength += data.length; } } function wsclient_reconnect_client(client) { client.isclosed = false; client.isclosed2 = false; client.reconnecting = null; client.reconnect++; client.connect(client.url, client.protocol, client.origin); } function wsclient_reconnect_timer(client) { if (client.options.reconnect) { client.reconnecting && clearTimeout(client.reconnecting); client.reconnecting = setTimeout(wsclient_reconnect_client, client.options.reconnect, client); } } function wsclient_closecallbacks(client, e) { for (var key in CALLBACKS) { var obj = CALLBACKS[key]; if (obj.client === client) { clearTimeout(obj.timeout); obj.callback(e); delete CALLBACKS[key]; } } } exports.createclient = function(callback) { var client = new WebSocketClient(); callback && callback(client); return client; }; exports.Controller = Controller;