You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
614 lines
14 KiB
614 lines
14 KiB
|
|
'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> buffer - destination |
|
* @param <Number> pos - offset |
|
* @param <Number> length - length (>0) |
|
* @returns <Number> 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> buffer - destination |
|
* @param <Number> pos - offset |
|
* @param <String> string - string to write |
|
* @return <Number> 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> buffer - dest buffer |
|
* @param <Number> pos - offset |
|
* @param <Buffer> src - source buffer |
|
* @return <Number> 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> buffer - destination |
|
* @param <Number> pos - offset |
|
* @param <String> number - number to write |
|
* @return <Number> 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> buffer - destination |
|
* @param <Number> pos - offset |
|
* @param <String> toWrite - String or Buffer |
|
* @return <Number> 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
|
|
|