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.
615 lines
14 KiB
615 lines
14 KiB
5 years ago
|
|
||
|
'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
|