lib/akamai.js
/**
* Licensed under the MIT License
*
* @author Kanstantsin A Kamkou (2ka.by), Alexander Günther
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link https://github.com/kkamkou/node-akamai-http-api
*/
// required stuff
var request = require('request'),
crypto = require('crypto'),
xml2js = require('xml2js'),
qs = require('querystring'),
_ = require('lodash');
// the class itself
var akamai = Object.create(null, {
config: {
writable: true,
value: {
key: null,
keyName: null,
host: null,
ssl: false,
verbose: false,
request: {timeout: 20000}
}
}
});
/**
* Updates the config object
*
* @param {Object} conf
* @returns {akamai}
*/
akamai.setConfig = function (conf) {
this.config = _.merge({}, this.config, conf);
return this;
};
/**
* Generates a random number
*
* @returns {String}
*/
akamai.getUniqueId = function () {
var str = '';
for (var i = 0, r; i < 6; i++) {
if ((i & 0x03) === 0) {
r = Math.random() * 0x100000000;
}
str += r >>> ((i & 0x03) << 3) & 0xff;
}
return str + process.pid;
};
/**
* Returns the config object
*
* @returns {Object}
*/
akamai.getConfig = function () {
return this.config;
};
/**
* Returns a set of headers for the authentication process
*
* @param {String} path
* @param {Object} queryObj
* @returns {Object}
*/
akamai.getHeaders = function (path, queryObj) {
var authData, authSign, query;
query = qs.stringify(_.merge({version: 1, action: 'du', format: 'xml'}, queryObj || {}));
authData = [
5, '0.0.0.0', '0.0.0.0', parseInt(Date.now() / 1000, 10), this.getUniqueId(),
this.getConfig().keyName
].join(', ');
authSign = crypto.createHmac('sha256', this.getConfig().key)
.update([authData + path.replace(/\/$/, ''), 'x-akamai-acs-action:' + query, null].join("\n"))
.digest('base64');
return {
'X-Akamai-ACS-Action': query,
'X-Akamai-ACS-Auth-Data': authData,
'X-Akamai-ACS-Auth-Sign': authSign
};
};
/**
* Adds http or https to the host
*
* @param {String} path
* @returns {String}
*/
akamai.getUri = function (path) {
var host = ['http', this.getConfig().ssl ? 's' : '', '://', this.getConfig().host].join('');
return [host, path.replace(/(^\/|\/$)/g, '')].join('/');
};
/**
* Converts a xml string to an object
*
* @param {String} data
* @param {Function} cb
*/
akamai.getObjectFromXml = function (data, cb) {
(new xml2js.Parser())
.parseString(data, function (err, result) {
/* istanbul ignore if */
if (err) {
return cb(err);
}
cb(null, _.mergeWith({}, result, function (a, b) {
var obj = {};
Object.keys(b).forEach(function (key) {
if (key === '$') {
obj.attribs = b[key];
} else if (_.isArray(b[key])) {
obj[key] = _.map(b[key], '$');
}
});
return obj;
}));
});
};
/**
* Returns a request object for streaming
*
* @param {String} path
* @param {Object} params
* @param {Function} cb
* @returns {request}
*/
akamai.getRequestObject = function (path, params, cb) {
var self = this,
callback = function () {},
options = _.merge(
{url: this.getUri(path), headers: this.getHeaders(path, params.headers)},
params.request, this.getConfig().request
);
if (typeof(cb) === 'function') {
callback = function (err, resp, body) {
if (err) {
return cb(err);
}
// wrong response code
if (resp.statusCode >= 300) {
var exception = new Error();
exception.code = resp.statusCode;
exception.message = 'The server sent us the ' + resp.statusCode + ' code';
if (self.getConfig().verbose && body) {
exception.message += '. Body: ' + body;
}
return cb(exception);
}
if (!body.match(/^<\?xml\s+/)) {
return cb(null, {status: resp.statusCode});
}
self.getObjectFromXml(body, cb);
};
}
return request(options, callback);
};
/**
* Sugar function to simplify calls
* @param {string} path
* @param {string} method
* @param {object} headers
* @param {function} cb
* @returns {request}
* @private
*/
akamai._simpleRequestObject = function (path, method, headers, cb) {
return this.getRequestObject(path, {request: {method: method}, headers: headers}, cb);
};
/** Custom helpers **/
akamai.fileExists = function (path, cb) {
this.stat(path, function (err, data) {
if (err && err.message.indexOf('404 code') !== -1) {
return cb(null, false);
}
if (data && data.stat && data.stat.file) {
return cb(null, true);
}
/* istanbul ignore next */
return cb(err);
});
return this;
};
/** Api functions **/
_.each(
{stat: 'get', du: 'get', dir: 'get', 'delete': 'put', mkdir: 'put', rmdir: 'put'},
function (method, fnc) {
akamai[fnc] = function (path, cb) {
this._simpleRequestObject(path, method, {action: fnc}, cb);
return this;
};
}
);
akamai.upload = function (stream, path, cb) {
stream
.pipe(this._simpleRequestObject(path, 'put', {action: 'upload', 'upload-type': 'binary'}, cb));
return this;
};
akamai.download = function (path, stream, cb) {
this.getRequestObject(path, {headers: {action: 'download'}}, cb).pipe(stream);
return this;
};
akamai.rename = function (pathFrom, pathTo, cb) {
this._simpleRequestObject(pathFrom, 'put', {action: 'rename', destination: pathTo}, cb);
return this;
};
akamai.symlink = function (pathFileTo, pathSymlink, cb) {
this._simpleRequestObject(pathSymlink, 'put', {action: 'symlink', target: pathFileTo}, cb);
return this;
};
akamai.mtime = function (path, date, cb) {
if (!(date instanceof Date)) {
cb(new TypeError('The date has to be an instance of Date'));
return this;
}
this._simpleRequestObject(
path, 'put', {action: 'mtime', mtime: parseInt(date.getTime() / 1000, 10)},
cb
);
return this;
};
// exporting outside
module.exports = akamai;