UNPKG

nxkit

Version:

This is a collection of tools, independent of any other libraries

568 lines (567 loc) 18.1 kB
"use strict"; /* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2015, xuewen.chu * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of xuewen.chu nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL xuewen.chu BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ Object.defineProperty(exports, "__esModule", { value: true }); const constants_1 = require("./constants"); const numbers_1 = require("./numbers"); const empty = Buffer.allocUnsafe(0); const zeroBuf = Buffer.from([0]); const nextTick = process.nextTick; const numCache = numbers_1.default.cache; const generateNumber = numbers_1.default.generateNumber; const generateCache = numbers_1.default.generateCache; var writeNumber = writeNumberCached; var toGenerate = true; const generate = function (packet, stream) { if (stream.cork) { stream.cork(); nextTick(uncork, stream); } if (toGenerate) { toGenerate = false; generateCache(); } switch (packet.cmd) { case 'connect': return connect(packet, stream); case 'connack': return connack(packet, stream); case 'publish': return publish(packet, stream); case 'puback': case 'pubrec': case 'pubrel': case 'pubcomp': case 'unsuback': return confirmation(packet, stream); case 'subscribe': return subscribe(packet, stream); case 'suback': return suback(packet, stream); case 'unsubscribe': return unsubscribe(packet, stream); case 'pingreq': case 'pingresp': case 'disconnect': return emptyPacket(packet, stream); default: stream.emit('error', new Error('Unknown command')); return false; } }; /** * Controls numbers cache. * Set to "false" to allocate buffers on-the-flight instead of pre-generated cache */ Object.defineProperty(generate, 'cacheNumbers', { get() { return writeNumber === writeNumberCached; }, set(value) { if (value) { if (!numCache || Object.keys(numCache).length === 0) toGenerate = true; writeNumber = writeNumberCached; } else { toGenerate = false; writeNumber = writeNumberGenerated; } } }); exports.default = generate; function uncork(stream) { stream.uncork(); } function connect(opts, stream) { var protocolId = opts.protocolId || 'MQTT'; var protocolVersion = opts.protocolVersion || 4; var will = opts.will; var clean = opts.clean; var keepalive = opts.keepalive || 0; var clientId = opts.clientId || ''; var username = opts.username; var password = opts.password; if (clean === undefined) clean = true; var length = 0; // Must be a string and non-falsy if (!protocolId || (typeof protocolId !== 'string' && !Buffer.isBuffer(protocolId))) { stream.emit('error', new Error('Invalid protocolId')); return false; } else { length += protocolId.length + 2; } // Must be 3 or 4 if (protocolVersion !== 3 && protocolVersion !== 4) { stream.emit('error', new Error('Invalid protocol version')); return false; } else length += 1; // ClientId might be omitted in 3.1.1, but only if cleanSession is set to 1 if ((typeof clientId === 'string' || Buffer.isBuffer(clientId)) && (clientId || protocolVersion === 4) && (clientId || clean)) { length += clientId.length + 2; } else { if (protocolVersion < 4) { stream.emit('error', new Error('clientId must be supplied before 3.1.1')); return false; } if (!clean) { stream.emit('error', new Error('clientId must be given if cleanSession set to 0')); return false; } } // Must be a two byte number if (typeof keepalive !== 'number' || keepalive < 0 || keepalive > 65535 || keepalive % 1 !== 0) { stream.emit('error', new Error('Invalid keepalive')); return false; } else length += 2; // Connect flags length += 1; // If will exists... if (will) { // It must be an object if (typeof will !== 'object') { stream.emit('error', new Error('Invalid will')); return false; } // It must have topic typeof string if (!will.topic || typeof will.topic !== 'string') { stream.emit('error', new Error('Invalid will topic')); return false; } else { length += Buffer.byteLength(will.topic) + 2; } // Payload if (will.payload && will.payload) { if (will.payload.length >= 0) { if (typeof will.payload === 'string') { length += Buffer.byteLength(will.payload) + 2; } else { length += will.payload.length + 2; } } else { stream.emit('error', new Error('Invalid will payload')); return false; } } else { length += 2; } } // Username var providedUsername = false; if (username != null) { if (isStringOrBuffer(username)) { providedUsername = true; length += Buffer.byteLength(username) + 2; } else { stream.emit('error', new Error('Invalid username')); return false; } } // Password if (password != null) { if (!providedUsername) { stream.emit('error', new Error('Username is required to use password')); return false; } if (isStringOrBuffer(password)) { length += byteLength(password) + 2; } else { stream.emit('error', new Error('Invalid password')); return false; } } // Generate header stream.write(constants_1.default.CONNECT_HEADER); // Generate length writeLength(stream, length); // Generate protocol ID writeStringOrBuffer(stream, protocolId); stream.write(protocolVersion === 4 ? constants_1.default.VERSION4 : constants_1.default.VERSION3); // Connect flags var flags = 0; flags |= (username != null) ? constants_1.default.USERNAME_MASK : 0; flags |= (password != null) ? constants_1.default.PASSWORD_MASK : 0; flags |= (will && will.retain) ? constants_1.default.WILL_RETAIN_MASK : 0; flags |= (will && will.qos) ? will.qos << constants_1.default.WILL_QOS_SHIFT : 0; flags |= will ? constants_1.default.WILL_FLAG_MASK : 0; flags |= clean ? constants_1.default.CLEAN_SESSION_MASK : 0; stream.write(Buffer.from([flags])); // Keepalive writeNumber(stream, keepalive); // Client ID writeStringOrBuffer(stream, clientId); // Will if (will) { writeString(stream, will.topic); writeStringOrBuffer(stream, will.payload); } // Username and password if (username != null) { writeStringOrBuffer(stream, username); } if (password != null) { writeStringOrBuffer(stream, password); } // This is a small packet that happens only once on a stream // We assume the stream is always free to receive more data after this return true; } function connack(opts, stream) { var rc = opts.returnCode; // Check return code if (typeof rc !== 'number') { stream.emit('error', new Error('Invalid return code')); return false; } stream.write(constants_1.default.CONNACK_HEADER); writeLength(stream, 2); stream.write(opts.sessionPresent ? constants_1.default.SESSIONPRESENT_HEADER : zeroBuf); return stream.write(Buffer.from([rc])); } function publish(opts, stream) { var qos = opts.qos || 0; var retain = opts.retain ? constants_1.default.RETAIN_MASK : 0; var topic = opts.topic; var payload = opts.payload || empty; var id = opts.messageId || 0; var length = 0; // Topic must be a non-empty string or Buffer if (typeof topic === 'string') length += Buffer.byteLength(topic) + 2; else if (Buffer.isBuffer(topic)) length += topic.length + 2; else { stream.emit('error', new Error('Invalid topic')); return false; } // Get the payload length if (!Buffer.isBuffer(payload)) length += Buffer.byteLength(payload); else length += payload.length; // Message ID must a number if qos > 0 if (qos && typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')); return false; } else if (qos) length += 2; // Header stream.write(constants_1.default.PUBLISH_HEADER[qos][opts.dup ? 1 : 0][retain ? 1 : 0]); // Remaining length writeLength(stream, length); // Topic writeNumber(stream, byteLength(topic)); stream.write(topic); // Message ID if (qos > 0) writeNumber(stream, id); // Payload return stream.write(payload); } /* Puback, pubrec, pubrel and pubcomp */ function confirmation(opts, stream) { var type = opts.cmd || 'puback'; var id = opts.messageId; var dup = (opts.dup && type === 'pubrel') ? constants_1.default.DUP_MASK : 0; var qos = 0; if (type === 'pubrel') qos = 1; // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')); return false; } // Header stream.write(constants_1.default.ACKS[type][qos][dup][0]); // Length writeLength(stream, 2); // Message ID return writeNumber(stream, id); } function subscribe(opts, stream) { var dup = opts.dup ? constants_1.default.DUP_MASK : 0; var id = opts.messageId; var subs = opts.subscriptions; var length = 0; // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')); return false; } else length += 2; // Check subscriptions if (typeof subs === 'object' && subs.length) { for (var i = 0; i < subs.length; i += 1) { var itopic = subs[i].topic; var iqos = subs[i].qos; if (typeof itopic !== 'string') { stream.emit('error', new Error('Invalid subscriptions - invalid topic')); return false; } if (typeof iqos !== 'number') { stream.emit('error', new Error('Invalid subscriptions - invalid qos')); return false; } length += Buffer.byteLength(itopic) + 2 + 1; } } else { stream.emit('error', new Error('Invalid subscriptions')); return false; } // Generate header stream.write(constants_1.default.SUBSCRIBE_HEADER[1][dup ? 1 : 0][0]); // Generate length writeLength(stream, length); // Generate message ID writeNumber(stream, id); var result = true; // Generate subs for (var j = 0; j < subs.length; j++) { var sub = subs[j]; var jtopic = sub.topic; var jqos = sub.qos; // Write topic string writeString(stream, jtopic); // Write qos result = stream.write(constants_1.default.QOS[jqos]); } return result; } function suback(opts, stream) { var id = opts.messageId; var granted = opts.granted; var length = 0; // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')); return false; } else length += 2; // Check granted qos vector if (typeof granted === 'object' && granted.length) { for (var i = 0; i < granted.length; i += 1) { if (typeof granted[i] !== 'number') { stream.emit('error', new Error('Invalid qos vector')); return false; } length += 1; } } else { stream.emit('error', new Error('Invalid qos vector')); return false; } // header stream.write(constants_1.default.SUBACK_HEADER); // Length writeLength(stream, length); // Message ID writeNumber(stream, id); return stream.write(Buffer.from(granted)); } function unsubscribe(opts, stream) { var id = opts.messageId; var dup = opts.dup ? constants_1.default.DUP_MASK : 0; var unsubs = opts.unsubscriptions; var length = 0; // Check message ID if (typeof id !== 'number') { stream.emit('error', new Error('Invalid messageId')); return false; } else { length += 2; } // Check unsubs if (typeof unsubs === 'object' && unsubs.length) { for (var i = 0; i < unsubs.length; i += 1) { if (typeof unsubs[i] !== 'string') { stream.emit('error', new Error('Invalid unsubscriptions')); return false; } length += Buffer.byteLength(unsubs[i]) + 2; } } else { stream.emit('error', new Error('Invalid unsubscriptions')); return false; } // Header stream.write(constants_1.default.UNSUBSCRIBE_HEADER[1][dup ? 1 : 0][0]); // Length writeLength(stream, length); // Message ID writeNumber(stream, id); // Unsubs var result = true; for (var j = 0; j < unsubs.length; j++) { writeString(stream, unsubs[j]); result = false; } return result; } function emptyPacket(opts, stream) { var cmd = opts.cmd; return stream.write(constants_1.default.EMPTY[cmd]); } /** * calcLengthLength - calculate the length of the remaining * length field * * @api private */ function calcLengthLength(length) { if (length >= 0 && length < 128) return 1; else if (length >= 128 && length < 16384) return 2; else if (length >= 16384 && length < 2097152) return 3; else if (length >= 2097152 && length < 268435456) return 4; else return 0; } function genBufLength(length) { var digit = 0; var pos = 0; var buffer = Buffer.allocUnsafe(calcLengthLength(length)); do { digit = length % 128 | 0; length = length / 128 | 0; if (length > 0) digit = digit | 0x80; buffer.writeUInt8(digit, pos++); } while (length > 0); return buffer; } /** * writeLength - write an MQTT style length field to the buffer * * @param <Buffer> buffer - destination * @param <Number> pos - offset * @param <Number> length - length (>0) * @returns <Number> number of bytes written * * @api private */ const lengthCache = {}; function writeLength(stream, length) { var buffer = lengthCache[length]; if (!buffer) { buffer = genBufLength(length); if (length < 16384) lengthCache[length] = buffer; } stream.write(buffer); } /** * writeString - write a utf8 string to the buffer * * @param <Buffer> buffer - destination * @param <Number> pos - offset * @param <String> string - string to write * @return <Number> number of bytes written * * @api private */ function writeString(stream, string) { var strlen = Buffer.byteLength(string); writeNumber(stream, strlen); stream.write(string, 'utf8'); } /** * writeNumber - write a two byte number to the buffer * * @param <Buffer> buffer - destination * @param <Number> pos - offset * @param <String> number - number to write * @return <Number> number of bytes written * * @api private */ function writeNumberCached(stream, number) { return stream.write(numCache[number]); } function writeNumberGenerated(stream, number) { return stream.write(generateNumber(number)); } /** * writeStringOrBuffer - write a String or Buffer with the its length prefix * * @param <Buffer> buffer - destination * @param <Number> pos - offset * @param <String> toWrite - String or Buffer * @return <Number> number of bytes written */ function writeStringOrBuffer(stream, toWrite) { if (typeof toWrite === 'string') { writeString(stream, toWrite); } else if (toWrite) { writeNumber(stream, toWrite.length); stream.write(toWrite); } else writeNumber(stream, 0); } function byteLength(bufOrString) { if (!bufOrString) return 0; else if (bufOrString instanceof Buffer) return bufOrString.length; else return Buffer.byteLength(bufOrString); } function isStringOrBuffer(field) { return typeof field === 'string' || field instanceof Buffer; }