serverless-spy
Version:
CDK-based library for writing elegant integration tests on AWS serverless architecture and an additional web console to monitor events in real time.
943 lines (859 loc) • 31.5 kB
JavaScript
/*
* Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
//node.js deps
var events = require('events');
var inherits = require('util').inherits;
//npm deps
var mqtt = require('mqtt');
var hmacSHA256 = require('crypto-js/hmac-sha256');
var sha256 = require('crypto-js/sha256');
//app deps
var exceptions = require('./lib/exceptions');
var isUndefined = require('../common/lib/is-undefined');
var tlsReader = require('../common/lib/tls-reader');
var path = require('path');
var fs = require('fs');
//begin module
function makeTwoDigits(n) {
if (n > 9) {
return n;
} else {
return '0' + n;
}
}
function getDateTimeString() {
var d = new Date();
//
// The additional ''s are used to force JavaScript to interpret the
// '+' operator as string concatenation rather than arithmetic.
//
return d.getUTCFullYear() + '' +
makeTwoDigits(d.getUTCMonth() + 1) + '' +
makeTwoDigits(d.getUTCDate()) + 'T' + '' +
makeTwoDigits(d.getUTCHours()) + '' +
makeTwoDigits(d.getUTCMinutes()) + '' +
makeTwoDigits(d.getUTCSeconds()) + 'Z';
}
function getDateString(dateTimeString) {
return dateTimeString.substring(0, dateTimeString.indexOf('T'));
}
function getSignatureKey(key, dateStamp, regionName, serviceName) {
var kDate = hmacSHA256(dateStamp, 'AWS4' + key, {
asBytes: true
});
var kRegion = hmacSHA256(regionName, kDate, {
asBytes: true
});
var kService = hmacSHA256(serviceName, kRegion, {
asBytes: true
});
var kSigning = hmacSHA256('aws4_request', kService, {
asBytes: true
});
return kSigning;
}
function signUrl(method, scheme, hostname, path, queryParams, accessId, secretKey,
region, serviceName, payload, today, now, debug, awsSTSToken) {
var signedHeaders = 'host';
var canonicalHeaders = 'host:' + hostname.toLowerCase() + '\n';
var canonicalRequest = method + '\n' + // method
path + '\n' + // path
queryParams + '\n' + // query params
canonicalHeaders + // headers
'\n' + // required
signedHeaders + '\n' + // signed header list
sha256(payload, {
asBytes: true
}); // hash of payload (empty string)
if (debug === true) {
console.log('canonical request: ' + canonicalRequest + '\n');
}
var hashedCanonicalRequest = sha256(canonicalRequest, {
asBytes: true
});
if (debug === true) {
console.log('hashed canonical request: ' + hashedCanonicalRequest + '\n');
}
var stringToSign = 'AWS4-HMAC-SHA256\n' +
now + '\n' +
today + '/' + region + '/' + serviceName + '/aws4_request\n' +
hashedCanonicalRequest;
if (debug === true) {
console.log('string to sign: ' + stringToSign + '\n');
}
var signingKey = getSignatureKey(secretKey, today, region, serviceName);
if (debug === true) {
console.log('signing key: ' + signingKey + '\n');
}
var signature = hmacSHA256(stringToSign, signingKey, {
asBytes: true
});
if (debug === true) {
console.log('signature: ' + signature + '\n');
}
var finalParams = queryParams + '&X-Amz-Signature=' + signature;
if (!isUndefined(awsSTSToken)) {
finalParams += '&X-Amz-Security-Token=' + encodeURIComponent(awsSTSToken);
}
var url = scheme + hostname + path + '?' + finalParams;
if (debug === true) {
console.log('url: ' + url + '\n');
}
return url;
}
function prepareWebSocketUrl(options, awsAccessId, awsSecretKey, awsSTSToken) {
var now = getDateTimeString();
var today = getDateString(now);
var path = '/mqtt';
var awsServiceName = 'iotdevicegateway';
var queryParams = 'X-Amz-Algorithm=AWS4-HMAC-SHA256' +
'&X-Amz-Credential=' + awsAccessId + '%2F' + today + '%2F' + options.region + '%2F' + awsServiceName + '%2Faws4_request' +
'&X-Amz-Date=' + now +
'&X-Amz-SignedHeaders=host';
var hostName = options.host;
// Include the port number in the hostname if it's not
// the standard wss port (443).
//
if (!isUndefined(options.port) && options.port !== 443) {
hostName = options.host + ':' + options.port;
}
return signUrl('GET', 'wss://', hostName, path, queryParams,
awsAccessId, awsSecretKey, options.region, awsServiceName, '', today, now, options.debug, awsSTSToken);
}
function prepareWebSocketCustomAuthUrl(options) {
var path = '/mqtt';
var hostName = options.host;
// Include the port number in the hostname if it's not
// the standard wss port (443).
//
if (!isUndefined(options.port) && options.port !== 443) {
hostName = options.host + ':' + options.port;
}
return 'wss://' + hostName + path + (options.customAuthQueryString || '');
}
function arrayEach(array, iterFunction) {
for (var idx in array) {
if (Object.prototype.hasOwnProperty.call(array, idx)) {
iterFunction.call(this, array[idx], parseInt(idx, 10));
}
}
}
function getCredentials(ini) {
//Get shared credential function from AWS SDK.
var map = {};
var currentSection ={};
arrayEach(ini.split(/\r?\n/), function(line) {
line = line.split(/(^|\s)[;#]/)[0]; // remove comments
var section = line.match(/^\s*\[([^\[\]]+)\]\s*$/);
if (section) {
currentSection = section[1];
} else if (currentSection) {
var item = line.match(/^\s*(.+?)\s*=\s*(.+?)\s*$/);
if (item) {
map[currentSection] = map[currentSection] || {};
map[currentSection][item[1]] = item[2];
}
}
});
return map;
}
//
// This method is the exposed module; it validates the mqtt options,
// creates a secure mqtt connection via TLS, and returns the mqtt
// connection instance.
//
function DeviceClient(options) {
//
// Force instantiation using the 'new' operator; this will cause inherited
// constructors (e.g. the 'events' class) to be called.
//
if (!(this instanceof DeviceClient)) {
return new DeviceClient(options);
}
//
// A copy of 'this' for use inside of closures
//
var that = this;
//
// Offline Operation
//
// The connection to AWS IoT can be in one of three states:
//
// 1) Inactive
// 2) Established
// 3) Stable
//
// During state 1), publish operations are placed in a queue
// ("filling")
//
// During states 2) and 3), any operations present in the queue
// are sent to the mqtt client for completion ("draining").
//
// In all states, subscriptions are tracked in a cache
//
// A "draining interval" is used to specify the rate at which
// which operations are drained from the queue.
//
// +- - - - - - - - - - - - - - - - - - - - - - - - +
// | |
//
// | FILLING |
//
// | |
// +-----------------------------+
// | | | |
// | |
// | v | |
// +- - Established Inactive - -+
// | | ^ |
// | |
// | | | |
// +----------> Stable ----------+
// | |
//
// | DRAINING |
//
// | |
// +- - - - - - - - - - - - - - - - - - - - - - - - +
//
//
// Draining Operation
//
// During draining, existing subscriptions are re-sent,
// followed by any publishes which occurred while offline.
//
//
// Publish cache used during filling
//
var offlinePublishQueue = [];
var offlineQueueing = true;
var offlineQueueMaxSize = 0;
var offlineQueueDropBehavior = 'oldest'; // oldest or newest
offlinePublishQueue.length = 0;
//
// Subscription queue for subscribe/unsubscribe requests received when offline
// We do not want an unbounded queue so for now limit to current max subs in AWS IoT
//
var offlineSubscriptionQueue = [];
var offlineSubscriptionQueueMaxSize = 50;
offlineSubscriptionQueue.length = 0;
//
// Subscription cache; active if autoResubscribe === true
//
var activeSubscriptions = [];
var autoResubscribe = true;
activeSubscriptions.length = 0;
//
// Cloned subscription cache; active during initial draining.
//
var clonedSubscriptions = [];
clonedSubscriptions.length = 0;
//
// Contains the operational state of the connection
//
var connectionState = 'inactive';
//
// Used to time draining operations; active during draining.
//
var drainingTimer = null;
var drainTimeMs = 250;
//Default keep alive time interval in seconds.
var defaultKeepalive = 300;
//
// These properties control the reconnect behavior of the MQTT Client. If
// the MQTT client becomes disconnected, it will attempt to reconnect after
// a quiet period; this quiet period doubles with each reconnection attempt,
// e.g. 1 seconds, 2 seconds, 2, 8, 16, 32, etc... up until a maximum
// reconnection time is reached.
//
// If a connection is active for the minimum connection time, the quiet
// period is reset to the initial value.
//
// baseReconnectTime: the time in seconds to wait before the first
// reconnect attempt
//
// minimumConnectionTime: the time in seconds that a connection must be
// active before resetting the current reconnection time to the base
// reconnection time
//
// maximumReconnectTime: the maximum time in seconds to wait between
// reconnect attempts
//
// The defaults for these values are:
//
// baseReconnectTime: 1 seconds
// minimumConnectionTime: 20 seconds
// maximumReconnectTime: 128 seconds
//
var baseReconnectTimeMs = 1000;
var minimumConnectionTimeMs = 20000;
var maximumReconnectTimeMs = 128000;
var currentReconnectTimeMs;
//
// Used to measure the length of time the connection has been active to
// know if it's stable or not. Active beginning from receipt of a 'connect'
// event (e.g. received CONNACK) until 'minimumConnectionTimeMs' has elapsed.
//
var connectionTimer = null;
//
// Credentials when authenticating via WebSocket/SigV4
//
var awsAccessId;
var awsSecretKey;
var awsSTSToken;
//
// Validate options, set default reconnect period if not specified.
//
var metricPrefix = "?SDK=JavaScript&Version=";
var pjson = require('../package.json');
var sdkVersion = pjson.version;
var defaultUsername = metricPrefix + sdkVersion;
if (isUndefined(options) ||
Object.keys(options).length === 0) {
throw new Error(exceptions.INVALID_CONNECT_OPTIONS);
}
if (isUndefined(options.keepalive)) {
options.keepalive = defaultKeepalive;
}
//
// Metrics will be enabled by default unless the user explicitly disables it
//
if (isUndefined(options.enableMetrics) || options.enableMetrics === true){
if (isUndefined(options.username)) {
options.username = defaultUsername;
} else {
options.username += defaultUsername;
}
}
if (!isUndefined(options.baseReconnectTimeMs)) {
baseReconnectTimeMs = options.baseReconnectTimeMs;
}
if (!isUndefined(options.minimumConnectionTimeMs)) {
minimumConnectionTimeMs = options.minimumConnectionTimeMs;
}
if (!isUndefined(options.maximumReconnectTimeMs)) {
maximumReconnectTimeMs = options.maximumReconnectTimeMs;
}
if (!isUndefined(options.drainTimeMs)) {
drainTimeMs = options.drainTimeMs;
}
if (!isUndefined(options.autoResubscribe)) {
autoResubscribe = options.autoResubscribe;
}
if (!isUndefined(options.offlineQueueing)) {
offlineQueueing = options.offlineQueueing;
}
if (!isUndefined(options.offlineQueueMaxSize)) {
offlineQueueMaxSize = options.offlineQueueMaxSize;
}
if (!isUndefined(options.offlineQueueDropBehavior)) {
offlineQueueDropBehavior = options.offlineQueueDropBehavior;
}
currentReconnectTimeMs = baseReconnectTimeMs;
options.reconnectPeriod = currentReconnectTimeMs;
options.fastDisconnectDetection = true;
//
//SDK has its own logic to deal with auto resubscribe
//
options.resubscribe = false;
//
// Verify that the reconnection timing parameters make sense.
//
if (options.baseReconnectTimeMs <= 0) {
throw new Error(exceptions.INVALID_RECONNECT_TIMING);
}
if (maximumReconnectTimeMs < baseReconnectTimeMs) {
throw new Error(exceptions.INVALID_RECONNECT_TIMING);
}
if (minimumConnectionTimeMs < baseReconnectTimeMs) {
throw new Error(exceptions.INVALID_RECONNECT_TIMING);
}
//
// Verify that the other optional parameters make sense.
//
if (offlineQueueDropBehavior !== 'newest' &&
offlineQueueDropBehavior !== 'oldest') {
throw new Error(exceptions.INVALID_OFFLINE_QUEUEING_PARAMETERS);
}
if (offlineQueueMaxSize < 0) {
throw new Error(exceptions.INVALID_OFFLINE_QUEUEING_PARAMETERS);
}
// set protocol, do not override existing definitions if available
if (isUndefined(options.protocol)) {
options.protocol = 'mqtts';
}
if (isUndefined(options.host)) {
throw new Error(exceptions.INVALID_CONNECT_OPTIONS);
}
// set SNI, do not override existing definitions if available
if (isUndefined(options.servername)) {
options.servername = options.host.split(':')[0]; // Stripping out port if it exists along with host name
}
if (options.protocol === 'mqtts') {
// set port, do not override existing definitions if available
if (isUndefined(options.port)) {
options.port = 8883;
}
//read and map certificates
tlsReader(options);
} else if (options.protocol === 'wss' || options.protocol === 'wss-custom-auth') {
if (options.protocol === 'wss') {
//
// AWS access id and secret key
// It first check Input options and Environment variables
// If that not available, it will try to load credentials from default credential file
if (!isUndefined(options.accessKeyId)) {
awsAccessId = options.accessKeyId;
} else {
awsAccessId = process.env.AWS_ACCESS_KEY_ID;
}
if (!isUndefined(options.secretKey)) {
awsSecretKey = options.secretKey;
} else {
awsSecretKey = process.env.AWS_SECRET_ACCESS_KEY;
}
if (!isUndefined(options.sessionToken)) {
awsSTSToken = options.sessionToken;
} else {
awsSTSToken = process.env.AWS_SESSION_TOKEN;
}
if (isUndefined(awsAccessId) || isUndefined(awsSecretKey)) {
var filename;
var user_profile = options.profile || process.env.AWS_PROFILE || 'default';
try {
if (!isUndefined(options.filename)) {
filename = options.filename;
} else {
filename = _loadDefaultFilename();
}
var creds = getCredentials(fs.readFileSync(filename, 'utf-8'));
var profile = creds[user_profile];
awsAccessId = profile.aws_access_key_id;
awsSecretKey = profile.aws_secret_access_key;
awsSTSToken = profile.aws_session_token;
} catch (e) {
console.log(e);
console.log('Failed to read credentials for AWS_PROFILE ' + user_profile + ' from ' + filename);
}
}
// AWS Access Key ID and AWS Secret Key must be defined
if (isUndefined(awsAccessId) || (isUndefined(awsSecretKey))) {
console.log('To connect via WebSocket/SigV4, AWS Access Key ID and AWS Secret Key must be passed either in options or as environment variables; see README.md');
throw new Error(exceptions.INVALID_CONNECT_OPTIONS);
}
} else {
if (isUndefined(options.customAuthHeaders) && isUndefined(options.customAuthQueryString)) {
console.log('To authenticate with a custom authorizer, you must provide the required HTTP headers or queryString; see README.md');
throw new Error(exceptions.INVALID_CONNECT_OPTIONS);
}
}
if (!isUndefined(options.host) && isUndefined(options.region)) {
// extract anything in between "iot" and "amazonaws" as region
var pattern = /[a-zA-Z0-9]+\.iot\.([^\.]+)\.amazonaws\..+/;
var region = pattern.exec(options.host);
if (region === null) {
console.log('Host endpoint is not valid');
throw new Error(exceptions.INVALID_CONNECT_OPTIONS);
} else {
options.region = region[1];
}
}
// set port, do not override existing definitions if available
if (isUndefined(options.port)) {
options.port = 443;
}
// check websocketOptions and ensure that the protocol is defined
if (isUndefined(options.websocketOptions)) {
options.websocketOptions = {
protocol: 'mqttv3.1'
};
} else {
options.websocketOptions.protocol = 'mqttv3.1';
}
if (options.protocol === 'wss-custom-auth') {
options.websocketOptions.headers = options.customAuthHeaders;
}
}
if ((!isUndefined(options)) && (options.debug === true)) {
console.log(options);
console.log('attempting new mqtt connection...');
}
//connect and return the client instance to map all mqttjs apis
var protocols = {};
protocols.mqtts = require('./lib/tls');
protocols.wss = require('./lib/ws');
function _loadDefaultFilename() {
var home = process.env.HOME ||
process.env.USERPROFILE ||
(process.env.HOMEPATH ? ((process.env.HOMEDRIVE || 'C:/') + process.env.HOMEPATH) : null);
return path.join(home, '.aws', 'credentials');
}
function _addToSubscriptionCache(topic, options) {
var matches = activeSubscriptions.filter(function(element) {
return element.topic === topic;
});
//
// Add the element only if it doesn't already exist.
//
if (matches.length === 0) {
activeSubscriptions.push({
topic: topic,
options: options
});
}
}
function _deleteFromSubscriptionCache(topic, options) {
var remaining = activeSubscriptions.filter(function(element) {
return element.topic !== topic;
});
activeSubscriptions = remaining;
}
function _updateSubscriptionCache(operation, topics, options) {
var opFunc = null;
//
// Don't cache subscriptions if auto-resubscribe is disabled
//
if (autoResubscribe === false) {
return;
}
if (operation === 'subscribe') {
opFunc = _addToSubscriptionCache;
} else if (operation === 'unsubscribe') {
opFunc = _deleteFromSubscriptionCache;
}
//
// Test to see if 'topics' is an array and if so, iterate.
//
if (Object.prototype.toString.call(topics) === '[object Array]') {
topics.forEach(function(item, index, array) {
opFunc(item, options);
});
} else {
opFunc(topics, options);
}
}
//
// Return true if the connection is currently in a 'filling'
// state
//
function _filling() {
return connectionState === 'inactive';
}
function _wrapper(client) {
var protocol = options.protocol;
if (protocol === 'wss') {
var url;
//
// If the access id and secret key are available, prepare the URL.
// Otherwise, set the url to an invalid value.
//
if (awsAccessId === '' || awsSecretKey === '') {
url = 'wss://no-credentials-available';
} else {
url = prepareWebSocketUrl(options, awsAccessId, awsSecretKey, awsSTSToken);
}
if (options.debug === true) {
console.log('using websockets, will connect to \'' + url + '\'...');
}
options.url = url;
} else if (protocol === 'wss-custom-auth') {
options.url = prepareWebSocketCustomAuthUrl(options);
if (options.debug === true) {
console.log('using websockets custom auth, will connect to \'' + options.url + '\'...');
}
// Treat the request as a standard websocket request from here onwards
protocol = 'wss';
}
return protocols[protocol](client, options);
}
var device = new mqtt.MqttClient(_wrapper, options);
//handle events from the mqtt client
//
// Timeout expiry function for the connection timer; once a connection
// is stable, reset the current reconnection time to the base value.
//
function _markConnectionStable() {
currentReconnectTimeMs = baseReconnectTimeMs;
device.options.reconnectPeriod = currentReconnectTimeMs;
//
// Mark this timeout as expired
//
connectionTimer = null;
connectionState = 'stable';
}
//
// Trim the offline queue if required; returns true if another
// element can be placed in the queue
//
function _trimOfflinePublishQueueIfNecessary() {
var rc = true;
if ((offlineQueueMaxSize > 0) &&
(offlinePublishQueue.length >= offlineQueueMaxSize)) {
//
// The queue has reached its maximum size, trim it
// according to the defined drop behavior.
//
if (offlineQueueDropBehavior === 'oldest') {
offlinePublishQueue.shift();
} else {
rc = false;
}
}
return rc;
}
//
// Timeout expiry function for the drain timer; once a connection
// has been established, begin draining cached transactions.
//
function _drainOperationQueue() {
//
// Handle our active subscriptions first, using a cloned
// copy of the array. We shift them out one-by-one until
// all have been processed, leaving the official record
// of active subscriptions untouched.
//
var subscription = clonedSubscriptions.shift();
if (!isUndefined(subscription)) {
//
// If the 3rd argument (namely callback) is not present, we will
// use two-argument form to call mqtt.Client#subscribe(), which
// supports both subscribe(topics, options) and subscribe(topics, callback).
//
if (!isUndefined(subscription.callback)) {
device.subscribe(subscription.topic, subscription.options, subscription.callback);
} else {
device.subscribe(subscription.topic, subscription.options);
}
} else {
//
// If no remaining active subscriptions to process,
// then handle subscription requests queued while offline.
//
var req = offlineSubscriptionQueue.shift();
if (!isUndefined(req)) {
_updateSubscriptionCache(req.type, req.topics, req.options);
if (req.type === 'subscribe') {
if (!isUndefined(req.callback)) {
device.subscribe(req.topics, req.options, req.callback);
} else {
device.subscribe(req.topics, req.options);
}
} else if (req.type === 'unsubscribe') {
device.unsubscribe(req.topics, req.callback);
}
} else {
//
// If no active or queued subscriptions remaining to process,
// then handle queued publish operations.
//
var offlinePublishMessage = offlinePublishQueue.shift();
if (!isUndefined(offlinePublishMessage)) {
device.publish(offlinePublishMessage.topic,
offlinePublishMessage.message,
offlinePublishMessage.options,
offlinePublishMessage.callback);
}
if (offlinePublishQueue.length === 0) {
//
// The subscription and offlinePublishQueue queues are fully drained,
// cancel the draining timer.
//
clearInterval(drainingTimer);
drainingTimer = null;
}
}
}
}
//
// Event handling - *all* events generated by the mqtt.js client must be
// handled here, *and* propagated upwards.
//
device.on('connect', function(connack) {
//
// If not already running, start the connection timer.
//
if (connectionTimer === null) {
connectionTimer = setTimeout(_markConnectionStable,
minimumConnectionTimeMs);
}
connectionState = 'established';
//
// If not already running, start the draining timer and
// clone the active subscriptions.
//
if (drainingTimer === null) {
clonedSubscriptions = activeSubscriptions.slice(0);
drainingTimer = setInterval(_drainOperationQueue,
drainTimeMs);
}
that.emit('connect', connack);
});
device.on('close', function(err) {
if (!isUndefined(err)) {
that.emit('error', err);
}
if ((!isUndefined(options)) && (options.debug === true)) {
console.log('connection lost - will attempt reconnection in ' +
device.options.reconnectPeriod / 1000 + ' seconds...');
}
//
// Clear the connection and drain timers
//
clearTimeout(connectionTimer);
connectionTimer = null;
clearInterval(drainingTimer);
drainingTimer = null;
//
// Mark the connection state as inactive
//
connectionState = 'inactive';
that.emit('close');
});
device.on('reconnect', function() {
//
// Update the current reconnect timeout; this will be the
// next timeout value used if this connect attempt fails.
//
currentReconnectTimeMs = currentReconnectTimeMs * 2;
currentReconnectTimeMs = Math.min(maximumReconnectTimeMs, currentReconnectTimeMs);
device.options.reconnectPeriod = currentReconnectTimeMs;
that.emit('reconnect');
});
device.on('end', function() {
that.emit('end');
});
device.on('offline', function() {
that.emit('offline');
});
device.on('error', function(error) {
that.emit('error', error);
});
device.on('packetsend', function(packet) {
that.emit('packetsend', packet);
});
device.on('packetreceive', function(packet) {
that.emit('packetreceive', packet);
});
device.on('message', function(topic, message, packet) {
that.emit('message', topic, message, packet);
});
//
// The signatures of these methods *must* match those of the mqtt.js
// client.
//
this.publish = function(topic, message, options, callback) {
//
// If filling or still draining, push this publish operation
// into the offline operations queue; otherwise, perform it
// immediately.
//
if (offlineQueueing === true && (_filling() || drainingTimer !== null)) {
if (_trimOfflinePublishQueueIfNecessary()) {
offlinePublishQueue.push({
topic: topic,
message: message,
options: options,
callback: callback
});
}
} else {
if (offlineQueueing === true || !_filling()) {
device.publish(topic, message, options, callback);
}
}
};
this.subscribe = function(topics, options, callback) {
if (!_filling() || autoResubscribe === false) {
_updateSubscriptionCache('subscribe', topics, options); // we do not store callback in active cache
//
// If the 3rd argument (namely callback) is not present, we will
// use two-argument form to call mqtt.Client#subscribe(), which
// supports both subscribe(topics, options) and subscribe(topics, callback).
//
if (!isUndefined(callback)) {
device.subscribe(topics, options, callback);
} else {
device.subscribe(topics, options);
}
} else {
// we're offline - queue this subscription request
if (offlineSubscriptionQueue.length < offlineSubscriptionQueueMaxSize) {
offlineSubscriptionQueue.push({
type: 'subscribe',
topics: topics,
options: options,
callback: callback
});
} else {
that.emit('error', new Error('Maximum queued offline subscription reached'));
}
}
};
this.unsubscribe = function(topics, callback) {
if (!_filling() || autoResubscribe === false) {
_updateSubscriptionCache('unsubscribe', topics);
device.unsubscribe(topics, callback);
} else {
// we're offline - queue this unsubscribe request
if (offlineSubscriptionQueue.length < offlineSubscriptionQueueMaxSize) {
offlineSubscriptionQueue.push({
type: 'unsubscribe',
topics: topics,
options: options,
callback: callback
});
}
}
};
this.end = function(force, callback) {
device.end(force, callback);
};
this.handleMessage = device.handleMessage.bind(device);
device.handleMessage = function(packet, callback) {
that.handleMessage(packet, callback);
};
this.updateWebSocketCredentials = function(accessKeyId, secretKey, sessionToken, expiration) {
awsAccessId = accessKeyId;
awsSecretKey = secretKey;
awsSTSToken = sessionToken;
};
this.getWebsocketHeaders = function() {
return options.websocketOptions.headers;
};
//
// Call this function to update the custom auth headers
//
this.updateCustomAuthHeaders = function(newHeaders) {
options.websocketOptions.headers = newHeaders;
};
//
// Used for integration testing only
//
this.simulateNetworkFailure = function() {
device.stream.emit('error', new Error('simulated connection error'));
device.stream.end();
};
}
//
// Allow instances to listen in on events that we produce for them
//
inherits(DeviceClient, events.EventEmitter);
module.exports = DeviceClient;
module.exports.DeviceClient = DeviceClient;
//
// Exported for unit testing only
//
module.exports.prepareWebSocketUrl = prepareWebSocketUrl;
module.exports.prepareWebSocketCustomAuthUrl = prepareWebSocketCustomAuthUrl;