'use strict'; var protocol = require('./constants') , empty = new Buffer(0) function generate(packet) { switch (packet.cmd) { case 'connect': return connect(packet) case 'connack': return connack(packet) case 'publish': return publish(packet) case 'puback': case 'pubrec': case 'pubrel': case 'pubcomp': case 'unsuback': return confirmation(packet) case 'subscribe': return subscribe(packet) case 'suback': return suback(packet) case 'unsubscribe': return unsubscribe(packet) case 'pingreq': case 'pingresp': case 'disconnect': return emptyPacket(packet) default: throw new Error('unknown command') } } function connect(opts) { var opts = opts || {} , protocolId = opts.protocolId || 'MQTT' , protocolVersion = opts.protocolVersion || 4 , will = opts.will , clean = opts.clean , keepalive = opts.keepalive || 0 , clientId = opts.clientId || "" , username = opts.username , 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))) { throw new Error('Invalid protocol id') } else { length += protocolId.length + 2 } // Must be a 1 byte number if (!protocolVersion || 'number' !== typeof protocolVersion || protocolVersion > 255 || protocolVersion < 0) { throw new Error('Invalid protocol version') } 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) { throw new Error('clientId must be supplied before 3.1.1'); } if(clean == 0) { throw new Error('clientId must be given if cleanSession set to 0'); } } // Must be a two byte number if ('number' !== typeof keepalive || keepalive < 0 || keepalive > 65535) { throw new Error('Invalid keepalive') } else { length += 2 } // Connect flags length += 1 // If will exists... if (will) { // It must be an object if ('object' !== typeof will) { throw new Error('Invalid will') } // It must have topic typeof string if (!will.topic || 'string' !== typeof will.topic) { throw new Error('Invalid will topic') } else { length += Buffer.byteLength(will.topic) + 2 } // Payload if (will.payload && will.payload) { if (will.payload.length >= 0) { if ('string' === typeof will.payload) { length += Buffer.byteLength(will.payload) + 2 } else { length += will.payload.length + 2 } } else { throw new Error('Invalid will payload') } } else { length += 2 } } // Username if (username) { if (username.length) { length += Buffer.byteLength(username) + 2 } else { throw new Error('Invalid username') } } // Password if (password) { if (password.length) { length += byteLength(password) + 2 } else { throw new Error('Invalid password') } } var buffer = new Buffer(1 + calcLengthLength(length) + length) , pos = 0 // Generate header buffer.writeUInt8(protocol.codes['connect'] << protocol.CMD_SHIFT, pos++, true) // Generate length pos += writeLength(buffer, pos, length) // Generate protocol ID pos += writeStringOrBuffer(buffer, pos, protocolId) buffer.writeUInt8(protocolVersion, pos++, true) // Connect flags var flags = 0 flags |= username ? protocol.USERNAME_MASK : 0 flags |= password ? protocol.PASSWORD_MASK : 0 flags |= (will && will.retain) ? protocol.WILL_RETAIN_MASK : 0 flags |= (will && will.qos) ? will.qos << protocol.WILL_QOS_SHIFT : 0 flags |= will ? protocol.WILL_FLAG_MASK : 0 flags |= clean ? protocol.CLEAN_SESSION_MASK : 0 buffer.writeUInt8(flags, pos++, true) // Keepalive pos += writeNumber(buffer, pos, keepalive) // Client ID pos += writeStringOrBuffer(buffer, pos, clientId) // Will if (will) { pos += writeString(buffer, pos, will.topic) pos += writeStringOrBuffer(buffer, pos, will.payload) } // Username and password if (username) pos += writeStringOrBuffer(buffer, pos, username) if (password) pos += writeStringOrBuffer(buffer, pos, password) return buffer } function connack(opts) { var opts = opts || {} , rc = opts.returnCode; // Check return code if ('number' !== typeof rc) throw new Error('Invalid return code'); var buffer = new Buffer(4) , pos = 0; buffer.writeUInt8(protocol.codes['connack'] << protocol.CMD_SHIFT, pos++, true); pos += writeLength(buffer, pos, 2); buffer.writeUInt8(opts.sessionPresent && protocol.SESSIONPRESENT_MASK || 0, pos++, true); buffer.writeUInt8(rc, pos++, true); return buffer; } function publish(opts) { var opts = opts || {} , dup = opts.dup ? protocol.DUP_MASK : 0 , qos = opts.qos , retain = opts.retain ? protocol.RETAIN_MASK : 0 , topic = opts.topic , payload = opts.payload || empty , id = opts.messageId; 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 throw new Error('Invalid topic'); // 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 && 'number' !== typeof id) { throw new Error('Invalid message id') } else if (qos) { length += 2; } var buffer = new Buffer(1 + calcLengthLength(length) + length) , pos = 0; // Header buffer.writeUInt8( protocol.codes['publish'] << protocol.CMD_SHIFT | dup | qos << protocol.QOS_SHIFT | retain, pos++, true); // Remaining length pos += writeLength(buffer, pos, length); // Topic pos += writeStringOrBuffer(buffer, pos, topic); // Message ID if (qos > 0) { pos += writeNumber(buffer, pos, id); } // Payload if (!Buffer.isBuffer(payload)) { writeStringNoPos(buffer, pos, payload); } else { writeBuffer(buffer, pos, payload); } return buffer; } /* Puback, pubrec, pubrel and pubcomp */ function confirmation(opts) { var opts = opts || {} , type = opts.cmd || 'puback' , id = opts.messageId , dup = (opts.dup && type === 'pubrel') ? protocol.DUP_MASK : 0 , qos = 0 if (type === 'pubrel') qos = 1 // Check message ID if ('number' !== typeof id) throw new Error('Invalid message id'); var buffer = new Buffer(4) , pos = 0; // Header buffer[pos++] = protocol.codes[type] << protocol.CMD_SHIFT | dup | qos << protocol.QOS_SHIFT; // Length pos += writeLength(buffer, pos, 2); // Message ID pos += writeNumber(buffer, pos, id); return buffer; } function subscribe(opts) { var opts = opts || {} , dup = opts.dup ? protocol.DUP_MASK : 0 , qos = opts.qos || 0 , id = opts.messageId , subs = opts.subscriptions; var length = 0; // Check mid if ('number' !== typeof id) { throw new Error('Invalid message id'); } else { length += 2; } // Check subscriptions if ('object' === typeof subs && subs.length) { for (var i = 0; i < subs.length; i += 1) { var topic = subs[i].topic , qos = subs[i].qos; if ('string' !== typeof topic) { throw new Error('Invalid subscriptions - invalid topic'); } if ('number' !== typeof qos) { throw new Error('Invalid subscriptions - invalid qos'); } length += Buffer.byteLength(topic) + 2 + 1; } } else { throw new Error('Invalid subscriptions'); } var buffer = new Buffer(1 + calcLengthLength(length) + length) , pos = 0; // Generate header buffer.writeUInt8( protocol.codes['subscribe'] << protocol.CMD_SHIFT | dup | 1 << protocol.QOS_SHIFT, pos++, true); // Generate length pos += writeLength(buffer, pos, length); // Generate message ID pos += writeNumber(buffer, pos, id); // Generate subs for (var i = 0; i < subs.length; i++) { var sub = subs[i] , topic = sub.topic , qos = sub.qos; // Write topic string pos += writeString(buffer, pos, topic); // Write qos buffer.writeUInt8(qos, pos++, true); } return buffer; } function suback(opts) { var opts = opts || {} , id = opts.messageId , granted = opts.granted; var length = 0; // Check message id if ('number' !== typeof id) { throw new Error('Invalid message id'); } else { length += 2; } // Check granted qos vector if ('object' === typeof granted && granted.length) { for (var i = 0; i < granted.length; i += 1) { if ('number' !== typeof granted[i]) { throw new Error('Invalid qos vector'); } length += 1; } } else { throw new Error('Invalid qos vector'); } var buffer = new Buffer(1 + calcLengthLength(length) + length) , pos = 0; // Header buffer.writeUInt8(protocol.codes['suback'] << protocol.CMD_SHIFT, pos++, true); // Length pos += writeLength(buffer, pos, length); // Message ID pos += writeNumber(buffer, pos, id); // Subscriptions for (var i = 0; i < granted.length; i++) { buffer.writeUInt8(granted[i], pos++, true); } return buffer; } function unsubscribe(opts) { var opts = opts || {} , id = opts.messageId , dup = opts.dup ? protocol.DUP_MASK : 0 , unsubs = opts.unsubscriptions; var length = 0; // Check message id if ('number' !== typeof id) { throw new Error('Invalid message id'); } else { length += 2; } // Check unsubs if ('object' === typeof unsubs && unsubs.length) { for (var i = 0; i < unsubs.length; i += 1) { if ('string' !== typeof unsubs[i]) { throw new Error('Invalid unsubscriptions'); } length += Buffer.byteLength(unsubs[i]) + 2; } } else { throw new Error('Invalid unsubscriptions'); } var buffer = new Buffer(1 + calcLengthLength(length) + length) , pos = 0; // Header buffer[pos++] = protocol.codes['unsubscribe'] << protocol.CMD_SHIFT | dup | 1 << protocol.QOS_SHIFT; // Length pos += writeLength(buffer, pos, length); // Message ID pos += writeNumber(buffer, pos, id); // Unsubs for (var i = 0; i < unsubs.length; i++) { pos += writeString(buffer, pos, unsubs[i]); } return buffer; } function emptyPacket(opts) { var buf = new Buffer(2); buf[0] = protocol.codes[opts.cmd] << 4; buf[1] = 0; return buf; } /** * 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 } } /** * writeLength - write an MQTT style length field to the buffer * * @param buffer - destination * @param pos - offset * @param length - length (>0) * @returns number of bytes written * * @api private */ function writeLength(buffer, pos, length) { var digit = 0 , origPos = pos do { digit = length % 128 | 0 length = length / 128 | 0 if (length > 0) { digit = digit | 0x80 } buffer.writeUInt8(digit, pos++, true) } while (length > 0) return pos - origPos } /** * writeString - write a utf8 string to the buffer * * @param buffer - destination * @param pos - offset * @param string - string to write * @return number of bytes written * * @api private */ function writeString(buffer, pos, string) { var strlen = Buffer.byteLength(string) writeNumber(buffer, pos, strlen) writeStringNoPos(buffer, pos + 2, string) return strlen + 2 } function writeStringNoPos(buffer, pos, string) { buffer.write(string, pos) } /** * write_buffer - write buffer to buffer * * @param buffer - dest buffer * @param pos - offset * @param src - source buffer * @return number of bytes written * * @api private */ function writeBuffer(buffer, pos, src) { src.copy(buffer, pos) return src.length } /** * writeNumber - write a two byte number to the buffer * * @param buffer - destination * @param pos - offset * @param number - number to write * @return number of bytes written * * @api private */ function writeNumber(buffer, pos, number) { buffer.writeUInt8(number >> 8, pos, true) buffer.writeUInt8(number & 0x00FF, pos + 1, true) return 2 } /** * writeStringOrBuffer - write a String or Buffer with the its length prefix * * @param buffer - destination * @param pos - offset * @param toWrite - String or Buffer * @return number of bytes written */ function writeStringOrBuffer(buffer, pos, toWrite) { var written = 0 if (toWrite && typeof toWrite === 'string') { written += writeString(buffer, pos + written, toWrite) } else if (toWrite) { written += writeNumber(buffer, pos + written, toWrite.length) written += writeBuffer(buffer, pos + written, toWrite) } else { written += writeNumber(buffer, pos + written, 0) } return written } function byteLength(bufOrString) { if (Buffer.isBuffer(bufOrString)) { return bufOrString.length } else { return Buffer.byteLength(bufOrString) } } module.exports = generate