/*jslint node:true*/
module.exports = AirWatchService;
var fs = require('fs');
var _ = require('lodash');
var request = require('request');
var winston = require('winston');
var chunkingStreams = require('chunking-streams');
var SizeChunker = chunkingStreams.SizeChunker;
var deviceIdentifiers = ['macaddress', 'serialnumber', 'UDID'];
winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, {'timestamp': true, 'colorize': true});
require('util').inherits(AirWatchService, require('events').EventEmitter);
/**
* Returns a filesize in bytes of a file.
* @param {String} filename - Path to file.
* @returns {Number} bytes - Total file size in bytes.
*/
var getFilesizeInBytes = function getFilesizeInBytes(filename) {
var stats = fs.statSync(filename);
var fileSizeInBytes = stats.size;
return fileSizeInBytes;
};
/**
* Creates an object of the AirWatchService
* @param {Object} config - Service configurations object
* @param {String} config.username - AirWatch API Username.
* @param {String} config.password - AirWatch API Password.
* @param {String} config.groupid - Group ID.
* @param {String} config.apicode - aw-tenant-code.
* @param {String} config.host - IP Address or domain name of the AirWatch server.
* @returns {AirWatchService}
* @class
* @classdesc This is a description of the AirWatchService class.
*/
var AirWatchService = function AirWatchService(config) {
if (!(this instanceof AirWatchService)) {
return new AirWatchService(config);
}
require('events').EventEmitter.call(this);
this.version = '0.0.1';
this.logger = winston;
this.config = config; //meh
this.username = config.username;
this.password = config.password;
this.groupid = config.groupid;
this.apicode = config.apicode;
this.apiVersion = config.apiVersion || 'v1';
this.path = 'https://' + config.host + '/api/' + this.apiVersion + '/';
winston.log('info', 'AirWatchService Loaded');
};
/**
* Make an request to the AirWatch API
* @param {String} url - Endpoint to make the request, do not include hostname.
* @param {Object} post - A json object with data to post. Optional.
* @param {function} callback - The callback that handles the response.
* @param {Object} callback.error
* @param {Object} callback.response
* @param {Object} callback.body
*/
AirWatchService.prototype.makeRequest = function (url, post, callback) {
var self, options;
self = this;
options = {
method: post ? 'POST' : 'GET',
url: self.path + url,
strictSSL: false,
followRedirect: true,
followAllRedirects: true,
json: true,
headers: {
'Host': self.config.host,
'Accept': 'application/json',
'User-Agent': 'node-airwatch',
'aw-tenant-code': self.apicode,
'Date': new Date().toUTCString(),
'content-type': 'application/json',
'Authorization': 'Basic ' + new Buffer(self.username + ':' + self.password).toString('base64')
}
};
if (post) {
options.body = post;
}
request(options, callback);
};
/**
* Request the current version of an App and update it via semantic versioning. This is currently designed for iOS
* apps, must have agvtool installed.
*
* @param {Object} options
* @param {String} options.app - Bundle ID of App.
* @param {String} [options.status=active] - Status of App.
* @param {String} [options.release=PATCH] - Type of release.
* @param callback
*/
AirWatchService.prototype.updateVersion = function (options, callback) {
var self, newVersion, currentVersion, versionSplit, operate, reqUrl;
self = this;
if (!options.status) {
options.status = 'active';
}
if (!options.release) {
options.release = 'PATCH';
}
reqUrl = 'mam/apps/search?bundleid=' + options.app + '&status=' + options.status;
self.makeRequest(reqUrl, null, function (error, request, body) {
if (request.statusCode !== 200) {
return callback(error);
}
currentVersion = body.Application[0].ActualFileVersion;
versionSplit = currentVersion.split('.');
switch (options.release) {
case ('MAJOR'):
operate = parseInt(versionSplit[0]);
operate += 1;
versionSplit[0] = operate;
break;
case ('MINOR'):
operate = parseInt(versionSplit[1]);
operate += 1;
versionSplit[1] = operate;
break;
case ('PATCH'):
operate = parseInt(versionSplit[2]);
operate += 1;
versionSplit[2] = operate;
break;
default:
break;
}
newVersion = versionSplit.join('.');
winston.log('info', 'Updating version ' + currentVersion + ' -> ' + newVersion);
return callback(null);
//TODO update ios with new version
//os.system('agvtool new-marketing-version ' + newVer)
});
};
/**
* Installs an Application on a device
* @param {Object} options
* @param {String} options.TransactionId -
* @param {function} callback
*/
AirWatchService.prototype.installApp = function (options, callback) {
var self;
self = this;
self.makeRequest('mam/apps/internal/begininstall', options, function (error, response, body) {
if (error || response.statusCode !== 200) {
self.emit('debug', 'Install App Failed ' + (error || response.statusCode));
return callback(error || new Error(response.statusCode));
}
if (!body) {
self.emit('debug', 'Install App Failed');
return callback(new Error('Invalid Response'));
}
callback(null, body);
});
};
/**
* Upload an App to the AirWatch API as a single Blob
* @param {String} filename - Filename of app. Must be in the same directory (for now).
* @param {function} callback - The callback that handles the response.
* @param {Object} callback.error
* @param {Object} callback.response
* @param {Object} callback.body
*/
AirWatchService.prototype.uploadAppBlob = function (filename, callback) {
var self, endpoint, options;
self = this;
endpoint = 'mam/blobs/uploadblob?filename=' + filename + '&organizationgroupid=' + self.groupid;
options = {
method: 'POST',
url: self.path + endpoint,
json: true,
strictSSL: false,
followRedirect: true,
followAllRedirects: true,
headers: {
'Host': self.config.host,
'Accept': 'application/json',
'aw-tenant-code': self.apicode,
'Date': new Date().toUTCString(),
'Authorization': 'Basic ' + new Buffer(self.username + ':' + self.password).toString('base64')
}
};
winston.log('info', options.url);
fs.createReadStream(filename).pipe(request(options, callback));
};
/**
* Upload Application Chunks (iOS and Android).
* Uploads application chunks into the AirWatch database for internal application install. This function must be used
* prior to the 'Begin Install API' for uploading an internal application.
* @param {String} filename - Name of the file you want to upload.
* @param {String} callback - Returns a function with the uploaded transactionId.
*/
AirWatchService.prototype.uploadAppChunks = function (filename, callback) {
var self, chunker, input, totalUploaded, totalAppSize, tmp, endpoint, chunkData, output, transactionId, chunkDataSize;
self = this;
tmp = './tmp/';
endpoint = 'mam/apps/internal/uploadchunk';
totalAppSize = getFilesizeInBytes(filename);
totalUploaded = 0;
input = fs.createReadStream(filename);
chunker = new SizeChunker({chunkSize: 1024 * 35, flushTail: true});
chunker.on('chunkStart', function (id, done) {
chunkData = [];
chunkDataSize = 0;
done();
});
chunker.on('chunkEnd', function (id, done) {
var fullChunkToUpload = Buffer.concat(chunkData, chunkDataSize);
var post = {
'ChunkData': fullChunkToUpload.toString('base64'),
'ChunkSequenceNumber': id + 1,
'TotalApplicationSize': totalAppSize,
'ChunkSize': fullChunkToUpload.length
};
if (transactionId) {
post.TransactionId = transactionId;
}
self.makeRequest(endpoint, post, function (error, response, body) {
if (!body.UploadSuccess) {
self.emit('debug', 'problem ' + (error || response.statusCode));
return callback(error || new Error(response.statusCode));
}
transactionId = body.TranscationId;
totalUploaded += fullChunkToUpload.length;
if (totalUploaded === totalAppSize) {
return callback(null, transactionId);
}
done();
});
});
chunker.on('data', function (chunk) {
chunkData.push(chunk.data);
chunkDataSize += chunk.data.length;
});
input.pipe(chunker);
};
/**
* Returns an object containing an array of applications installed on the device.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param callback
*/
AirWatchService.prototype.getDeviceApps = function (options, callback) {
var self = this;
self._makeDeviceRequest(options, 'apps', callback);
};
/**
* Retrieves details of the device identified by its numeric ID.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param {function} callback
*/
AirWatchService.prototype.getDeviceInfo = function (options, callback) {
var self = this;
self._makeDeviceRequest(options, null, callback);
};
/**
* Retrieves the details of the certificates that are present on the device.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param {function} callback
*/
AirWatchService.prototype.getDeviceCertificates = function (options, callback) {
var self = this;
self._makeDeviceRequest(options, 'certificates', callback);
};
/**
* Retrieves the details of the compliance policies that are present on a device.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param {function} callback
*/
AirWatchService.prototype.getDeviceCompliance = function (options, callback) {
var self = this;
self._makeDeviceRequest(options, 'compliance', callback);
};
/**
* Retrieves the details of the content that is present on a device.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param {function} callback
*/
AirWatchService.prototype.getDeviceContent = function (options, callback) {
var self = this;
self._makeDeviceRequest(options, 'content', callback);
};
/**
* Retrieves the profile related information of the device.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param {function} callback
*/
AirWatchService.prototype.getDeviceProfiles = function (options, callback) {
var self = this;
self._makeDeviceRequest(options, 'profiles', callback);
};
/**
* Retrieves the event log details of the device.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param {function} callback
*/
AirWatchService.prototype.getDeviceEventLog = function (options, callback) {
var self = this;
self._makeDeviceRequest(options, 'eventlog', callback);
};
/**
* Make a request to the API for a device specific action.
* @param {Object} options - A json object containing items needed to make the request.
* @param {String} options.deviceId - Type of identification. Can be either 'macaddress', 'serialnumber' or 'UDID'.
* @param {String} options.uid - Unique ID of the device. Can be either a valid MAC address, serial number or UDID.
* @param {function} callback
* @private
*/
AirWatchService.prototype._makeDeviceRequest = function (options, action, callback) {
var self, endpoint, suffix;
self = this;
if (!_.includes(deviceIdentifiers, options.deviceId)) {
winston.log('warn', 'Bad ID Type');
//TODO: handle error
}
suffix = action ? '/' + action : '';
endpoint = 'mdm/devices/' + options.deviceId + '/' + options.uid + suffix;
self.makeRequest(endpoint, null, function (error, response, body) {
if (error || response.statusCode !== 200) {
self.emit('debug', 'problem ' + (error || response.statusCode));
return callback(error || new Error(response.statusCode));
}
if (!body) {
self.emit('debug', 'no body');
return callback(new Error('Invalid Response'));
}
callback(null, body);
});
};