rackspace-shared-utils
Version:
Shared Rackspace Node.js utility modules and functions.
347 lines (288 loc) • 9.53 kB
JavaScript
/**
* Copyright 2012 Rackspace
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.
*
*/
var parse = require('url').parse;
var http = require('http');
var https = require('https');
var constants = require('constants');
var util = require('util');
var _ = require('underscore');
var async = require('async');
var sprintf = require('sprintf').sprintf;
var misc = require('./misc');
var UnexpectedStatusCodeError = require('./errors').UnexpectedStatusCodeError;
var PersistentAgent = require('./persistent_agent').PersistentAgent;
var PersistentAgentSSL = require('./persistent_agent').PersistentAgentSSL;
/**
* A reference to the global agent instance which used for all the persistent
* requests.
*/
exports.GLOBAL_AGENT = null;
/**
* Check a status code versus a variant array.
*
* @param {Integer, String, RegExp} statusCode the status code to check.
* @param {Array<Integer, String, RegExp>} expectedStatusCodes list of list of mixed status Codes.
* @return {Boolean} true/false depending on a match.
*/
exports.checkStatusCodes = function(statusCode, expectedStatusCodes) {
var i,
comparator;
for (i = 0; i < expectedStatusCodes.length; i++) {
comparator = expectedStatusCodes[i];
if (comparator instanceof RegExp) {
if (statusCode.toString().match(comparator)) {
return true;
}
} else if (typeof comparator === 'string' || comparator instanceof String) {
if (comparator === statusCode.toString()) {
return true;
}
} else {
if (comparator === statusCode) {
return true;
}
}
}
return false;
};
/** Generate Authentication Header
* @param {String} username The Username.
* @param {String} password The Password.
* @return {String} The header string for authentication.
*/
exports.getAuthHeader = function(username, password) {
var auth;
if (!username || !password) {
throw new Error('Missing username or password');
}
auth = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
return auth;
};
/**
* Build a cURL command with the provided options.
*
* @param {String} url target url.
* @param {String} method HTTP methpd (get, post, put, etc.).
* @param {Object} headers Request headers.
* @param {?String} body Optional body.
* @return {String} cURL command.
*/
exports.buildCurlCommand = function(url, method, headers, body) {
var key, value, parts = ['curl', '-i'];
parts.push('-X');
parts.push(misc.shellQuote(method.toUpperCase()));
for (key in headers) {
if (headers.hasOwnProperty(key)) {
value = headers[key];
if (key.toLowerCase() === 'content-length' && parseInt(value, 10) === 0) {
continue;
}
parts.push('-H');
parts.push(misc.shellQuote(sprintf('%s: %s', key, value)));
}
}
if (body) {
parts.push('--data-binary');
parts.push(misc.shellQuote(body));
}
parts.push(misc.shellQuote(url));
return parts.join(' ');
};
/**
* Perform an HTTP request to the specified URL.
*
* @param {String} url target url.
* @param {String} method HTTP methpd (get, post, put, etc.).
* @param {?String} body Optional body.
* @param {Object} options Different request options.
* @param {Function} callback Callback called with (err, result).
*/
function request(url, method, body, options, callback) {
body = body || '';
var defaultOptions = {
'parse_json': false, // Parse body as JSON
'expected_status_codes': [], // Array of expected status codes, each element can be either
// a _string, number or regexp_
'username': null, // Optional username for basic auth
'password': null, // Optional password for basic auth
'headers': {}, // Optional request headers.
'timeout': 20000, // request timeout in milliseconds,
'return_response': false, // wait for body and return response object even if an unexpected status code is returned
'persistent': false, // true if the same connection should be reused for all the requests
'use_agent': true, // true to preserve backwards compatiblity, disables the agent otherwise
'hooks': {},
'key': null,
'cert': null,
'keep_alive': null
},
reqOptions, reqFunc, auth, req, customAgent = false,
agent = false, AgentCls, ssl = false, parsed = parse(url);
options = misc.merge(defaultOptions, options);
if (parsed.protocol === 'https:') {
ssl = true;
reqFunc = https.request.bind(https);
}
else {
ssl = false;
reqFunc = http.request.bind(http);
}
if (options.persistent) {
customAgent = true;
AgentCls = (ssl) ? PersistentAgentSSL : PersistentAgent;
}
reqOptions = {
host: parsed.hostname || parsed.host,
path: parsed.pathname + (parsed.search || ''),
method: method,
headers: options.headers
};
if (parsed.port) {
reqOptions.port = parseInt(parsed.port, 10);
}
else {
reqOptions.port = (parsed.protocol === 'https:') ? 443 : 80;
}
if (options.key) {
customAgent = true;
reqOptions.key = options.key;
}
if (options.cert) {
customAgent = true;
reqOptions.cert = options.cert;
}
if (options.ca) {
customAgent = true;
reqOptions.ca = options.ca;
}
if (options.servername) {
customAgent = true;
reqOptions.servername = options.servername;
}
if (options.rejectUnauthorized === false) {
customAgent = true;
reqOptions.rejectUnauthorized = options.rejectUnauthorized;
}
// Add content-length (if body is provided)
if (body.length > 0) {
if (body instanceof Buffer) {
reqOptions.headers['Content-Length'] = body.length;
} else {
reqOptions.headers['Content-Length'] = Buffer.byteLength(body, 'utf8');
}
}
// Add authorization headers
if (options.username && options.password) {
auth = 'Basic ' + new Buffer(options.username + ':' + options.password).toString('base64');
options.headers.authorization = auth;
}
function reqTimeoutHandler(req) {
var err = new Error('ETIMEDOUT, Operation timed out via reqTimeoutHandler');
err.errno = constants.ETIMEDOUT;
err.code = 'ETIMEDOUT';
try {
req.socket.destroy(err);
}
catch (e) {}
}
function setReqTimeout(req, timeout) {
// TODO: Requires a hack to work on freebsd.
var timeoutId = setTimeout(reqTimeoutHandler.bind(null, req), timeout);
req.on('response', clearTimeout.bind(null, timeoutId));
req.on('continue', clearTimeout.bind(null, timeoutId));
req.on('error', clearTimeout.bind(null, timeoutId));
}
/* TODO: cache reqOptions.agent */
if (customAgent) {
if (exports.GLOBAL_AGENT) {
agent = exports.GLOBAL_AGENT;
}
else if (AgentCls) {
agent = new AgentCls(reqOptions);
exports.GLOBAL_AGENT = agent;
}
else if (!agent) {
agent = (ssl) ? new https.Agent(reqOptions) : new http.Agent(reqOptions);
}
reqOptions.agent = agent;
}
// Disable the agent if we have been requested to.
if (!options.use_agent) {
reqOptions.agent = false;
}
async.waterfall([
function preRequestHook(callback) {
if (!options.hooks.hasOwnProperty('pre_request')) {
callback(null, reqOptions);
return;
}
// Each hooks gets passed in request options and must pass
// (err, modifiedRequestOptions) to its callback
options.hooks.pre_request(reqOptions, callback);
},
function performRequest(reqOptions, callback) {
// Perform the request
callback = _.once(callback);
try {
req = reqFunc(reqOptions);
setReqTimeout(req, options.timeout);
}
catch (err) {
callback(err);
return;
}
req.on('error', callback);
req.on('response', function(res) {
var data = '', statusCode, err = null;
res.setEncoding('utf8');
res.on('data', function(chunk) {
data += chunk;
});
if (!exports.checkStatusCodes(res.statusCode, options.expected_status_codes)) {
err = new UnexpectedStatusCodeError(options.expected_status_codes, res.statusCode);
err.statusCode = res.statusCode;
if (!options.return_response) {
res.removeAllListeners('data');
callback(err);
return;
}
}
res.on('end', function() {
var result = {};
if (options.parse_json && data.length > 0) {
try {
data = JSON.parse(data);
}
catch (err) {
err.originalData = data;
callback(err);
return;
}
}
result.headers = res.headers;
result.statusCode = res.statusCode;
result.body = data;
if (err && options.return_response) {
err.response = result;
}
callback(err, result);
});
});
req.end(body);
}], callback);
}
/** request function */
exports.request = request;