relu-core
Version:
669 lines (526 loc) • 19.1 kB
JavaScript
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var EventEmitter = require('events').EventEmitter;
var fs = require('fs');
var http = require('http');
var https = require('https');
var os = require('os');
var path = require('path');
var querystring = require('querystring');
var url = require('url');
var util = require('util');
var assert = require('assert-plus');
var backoff = require('backoff');
var mime = require('mime');
var once = require('once');
var tunnelAgent = require('tunnel-agent');
var dtrace = require('../dtrace');
var errors = require('../errors');
var bunyan = require('../bunyan_helper');
// Use native KeepAlive in Node as of 0.11.6
var semver = require('semver');
var nodeVersion = process.version;
var nativeKeepAlive = semver.satisfies(nodeVersion, '>=0.11.6');
var KeepAliveAgent;
var KeepAliveAgentSecure;
var httpMaxSockets = http.globalAgent.maxSockets;
var httpsMaxSockets = https.globalAgent.maxSockets;
if (!nativeKeepAlive) {
KeepAliveAgent = require('keep-alive-agent');
KeepAliveAgentSecure = KeepAliveAgent.Secure;
} else {
KeepAliveAgent = http.Agent;
KeepAliveAgentSecure = https.Agent;
// maxSockets defaults to Infinity, but that doesn't
// lend itself well to KeepAlive, since sockets will
// never be reused.
httpMaxSockets = Math.min(httpMaxSockets, 1024);
httpsMaxSockets = Math.min(httpsMaxSockets, 1024);
}
///--- Globals
// jscs:disable maximumLineLength
var VERSION = JSON.parse(fs.readFileSync(path.normalize(__dirname + '/../../package.json'), 'utf8')).version;
// jscs:enable maximumLineLength
///--- Helpers
function cloneRetryOptions(options, defaults) {
if (options === false) {
return (false);
}
assert.optionalObject(options, 'options.retry');
var r = options || {};
assert.optionalNumber(r.minTimeout, 'options.retry.minTimeout');
assert.optionalNumber(r.maxTimeout, 'options.retry.maxTimeout');
assert.optionalNumber(r.retries, 'options.retry.retries');
assert.optionalObject(defaults, 'defaults');
defaults = defaults || {};
return ({
minTimeout: r.minTimeout || defaults.minTimeout || 1000,
maxTimeout: r.maxTimeout || defaults.maxTimeout || Infinity,
retries: r.retries || defaults.retries || 4
});
}
function defaultUserAgent() {
var UA = 'restify/' + VERSION +
' (' + os.arch() + '-' + os.platform() + '; ' +
'v8/' + process.versions.v8 + '; ' +
'OpenSSL/' + process.versions.openssl + ') ' +
'node/' + process.versions.node;
return (UA);
}
function ConnectTimeoutError(ms) {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ConnectTimeoutError);
}
this.message = 'connect timeout after ' + ms + 'ms';
this.name = 'ConnectTimeoutError';
}
util.inherits(ConnectTimeoutError, Error);
function RequestTimeoutError(ms) {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RequestTimeoutError);
}
this.message = 'request timeout after ' + ms + 'ms';
this.name = 'RequestTimeoutError';
}
util.inherits(RequestTimeoutError, Error);
function rawRequest(opts, cb) {
assert.object(opts, 'options');
assert.object(opts.log, 'options.log');
assert.func(cb, 'callback');
cb = once(cb);
var id = dtrace.nextId();
var log = opts.log;
var proto = opts.protocol === 'https:' ? https : http;
var connectionTimer;
var requestTimer;
var req;
if (opts.cert && opts.key) {
opts.agent = false;
}
if (opts.connectTimeout) {
connectionTimer = setTimeout(function connectTimeout() {
connectionTimer = null;
if (req) {
req.abort();
}
var err = new ConnectTimeoutError(opts.connectTimeout);
dtrace._rstfy_probes['client-error'].fire(function () {
return ([id, err.toString()]);
});
cb(err, req);
}, opts.connectTimeout);
}
dtrace._rstfy_probes['client-request'].fire(function () {
return ([
opts.method,
opts.path,
opts.headers,
id
]);
});
var emit_result = once(function _emit_result(_err, _req, _res) {
_req.emit('result', _err, _res);
});
req = proto.request(opts, function onResponse(res) {
clearTimeout(connectionTimer);
clearTimeout(requestTimer);
dtrace._rstfy_probes['client-response'].fire(function () {
return ([ id, res.statusCode, res.headers ]);
});
log.trace({client_res: res}, 'Response received');
res.log = log;
var err;
if (res.statusCode >= 400) {
err = errors.codeToHttpError(res.statusCode);
}
req.removeAllListeners('socket');
emit_result((err || null), req, res);
});
req.log = log;
req.on('error', function onError(err) {
dtrace._rstfy_probes['client-error'].fire(function () {
return ([id, (err || {}).toString()]);
});
log.trace({err: err}, 'Request failed');
clearTimeout(connectionTimer);
clearTimeout(requestTimer);
cb(err, req);
if (req) {
process.nextTick(function () {
emit_result(err, req, null);
});
}
});
req.once('upgrade', function onUpgrade(res, socket, _head) {
clearTimeout(connectionTimer);
clearTimeout(requestTimer);
dtrace._rstfy_probes['client-response'].fire(function () {
return ([ id, res.statusCode, res.headers ]);
});
log.trace({client_res: res}, 'upgrade response received');
res.log = log;
var err;
if (res.statusCode >= 400) {
err = errors.codeToHttpError(res.statusCode);
}
req.removeAllListeners('error');
req.removeAllListeners('socket');
req.emit('upgradeResult', (err || null), res, socket, _head);
});
req.once('socket', function onSocket(socket) {
var _socket = socket;
if (opts.protocol === 'https:' && socket.socket) {
_socket = socket.socket;
}
if (_socket.writable && !_socket._connecting) {
clearTimeout(connectionTimer);
cb(null, req);
return;
}
_socket.once('connect', function onConnect() {
clearTimeout(connectionTimer);
if (opts._keep_alive) {
_socket.setKeepAlive(true);
socket.setKeepAlive(true);
}
if (opts.requestTimeout) {
requestTimer = setTimeout(function requestTimeout() {
requestTimer = null;
var err = new RequestTimeoutError(opts.requestTimeout);
dtrace._rstfy_probes['client-error'].fire(function () {
return ([id, err.toString()]);
});
cb(err, req);
if (req) {
req.abort();
process.nextTick(function () {
req.emit('result', err, null);
});
}
}, opts.requestTimeout);
}
cb(null, req);
});
});
if (opts.signRequest) {
opts.signRequest(req);
}
if (log.trace()) {
log.trace({client_req: opts}, 'request sent');
}
} // end `rawRequest`
// Check if url is excluded by the no_proxy environment variable
function isProxyForURL(address) {
var noProxy = process.env.NO_PROXY || process.env.no_proxy || null;
// wildcard
if (noProxy === '*') {
return (null);
}
// otherwise, parse the noProxy value to see if it applies to the URL
if (noProxy !== null) {
var noProxyItem, hostname, port, noProxyItemParts,
noProxyHost, noProxyPort, noProxyList;
// canonicalize the hostname
/* JSSTYLED */
hostname = address.hostname.replace(/^\.*/, '.').toLowerCase();
noProxyList = noProxy.split(',');
for (var i = 0, len = noProxyList.length; i < len; i++) {
noProxyItem = noProxyList[i].trim().toLowerCase();
// no_proxy can be granular at the port level
if (noProxyItem.indexOf(':') > -1) {
noProxyItemParts = noProxyItem.split(':', 2);
/* JSSTYLED */
noProxyHost = noProxyItemParts[0].replace(/^\.*/, '.');
noProxyPort = noProxyItemParts[1];
port = address.port ||
(address.protocol === 'https:' ? '443' : '80');
// match - ports are same and host ends with no_proxy entry.
if (port === noProxyPort &&
hostname.indexOf(noProxyHost) ===
hostname.length - noProxyHost.length) {
return (null);
}
} else {
/* JSSTYLED */
noProxyItem = noProxyItem.replace(/^\.*/, '.');
var isMatchedAt = hostname.indexOf(noProxyItem);
if (isMatchedAt > -1 &&
isMatchedAt === hostname.length - noProxyItem.length) {
return (null);
}
}
}
}
return (true);
}
///--- API
function HttpClient(options) {
assert.object(options, 'options');
assert.optionalObject(options.headers, 'options.headers');
assert.object(options.log, 'options.log');
assert.optionalFunc(options.signRequest, 'options.signRequest');
assert.optionalString(options.socketPath, 'options.socketPath');
assert.optionalString(options.url, 'options.url');
EventEmitter.call(this);
var self = this;
this.agent = options.agent;
this.ca = options.ca;
this.cert = options.cert;
this.ciphers = options.ciphers;
this.connectTimeout = options.connectTimeout || false;
this.requestTimeout = options.requestTimeout || false;
this.headers = options.headers || {};
this.log = options.log;
if (!this.log.serializers) {
// Ensure logger has a reasonable serializer for `client_res`
// and `client_req` logged in this module.
this.log = this.log.child({serializers: bunyan.serializers});
}
this.key = options.key;
this.name = options.name || 'HttpClient';
this.passphrase = options.passphrase;
this.pfx = options.pfx;
if (options.rejectUnauthorized !== undefined) {
this.rejectUnauthorized = options.rejectUnauthorized;
} else {
this.rejectUnauthorized = true;
}
this.retry = cloneRetryOptions(options.retry);
this.signRequest = options.signRequest || false;
this.socketPath = options.socketPath || false;
this.url = options.url ? url.parse(options.url) : {};
if (process.env.https_proxy) {
this.proxy = url.parse(process.env.https_proxy);
} else if (process.env.http_proxy) {
this.proxy = url.parse(process.env.http_proxy);
} else if (options.proxy) {
this.proxy = options.proxy;
} else {
this.proxy = false;
}
if (this.proxy && !isProxyForURL(self.url)) {
this.proxy = false;
}
if (options.accept) {
if (options.accept.indexOf('/') === -1) {
options.accept = mime.lookup(options.accept);
}
this.headers.accept = options.accept;
}
if (options.contentType) {
if (options.contentType.indexOf('/') === -1) {
options.type = mime.lookup(options.contentType);
}
this.headers['content-type'] = options.contentType;
}
if (options.userAgent !== false) {
this.headers['user-agent'] = options.userAgent ||
defaultUserAgent();
}
if (options.version) {
this.headers['accept-version'] = options.version;
}
if (this.agent === undefined) {
var Agent;
var maxSockets;
if (this.proxy) {
if (this.url.protocol === 'https:') {
if (this.proxy.protocol === 'https:') {
Agent = tunnelAgent.httpsOverHttps;
} else {
Agent = tunnelAgent.httpsOverHttp;
}
} else {
if (this.proxy.protocol === 'https:') {
Agent = tunnelAgent.httpOverHttps;
} else {
Agent = tunnelAgent.httpOverHttp;
}
}
} else if (this.url.protocol === 'https:') {
Agent = KeepAliveAgentSecure;
maxSockets = httpsMaxSockets;
} else {
Agent = KeepAliveAgent;
maxSockets = httpMaxSockets;
}
if (this.proxy) {
this.agent = new Agent({
proxy: self.proxy,
rejectUnauthorized: self.rejectUnauthorized,
ca: self.ca
});
} else {
this.agent = new Agent({
cert: self.cert,
ca: self.ca,
ciphers: self.ciphers,
key: self.key,
maxSockets: maxSockets,
// require('keep-alive-agent')
maxKeepAliveRequests: 0,
maxKeepAliveTime: 0,
// native keepalive
keepAliveMsecs: 1000,
keepAlive: true,
passphrase: self.passphrase,
pfx: self.pfx,
rejectUnauthorized: self.rejectUnauthorized
});
this._keep_alive = true;
}
}
}
util.inherits(HttpClient, EventEmitter);
module.exports = HttpClient;
HttpClient.prototype.close = function close() {
var sockets = this.agent.sockets;
Object.keys((sockets || {})).forEach(function (k) {
if (Array.isArray(sockets[k])) {
sockets[k].forEach(function (s) {
s.end();
});
}
});
sockets = this.agent.idleSockets || this.agent.freeSockets;
Object.keys((sockets || {})).forEach(function (k) {
sockets[k].forEach(function (s) {
s.end();
});
});
};
HttpClient.prototype.del = function del(options, callback) {
var opts = this._options('DELETE', options);
return (this.read(opts, callback));
};
HttpClient.prototype.get = function get(options, callback) {
var opts = this._options('GET', options);
return (this.read(opts, callback));
};
HttpClient.prototype.head = function head(options, callback) {
var opts = this._options('HEAD', options);
return (this.read(opts, callback));
};
HttpClient.prototype.opts = function http_options(options, callback) {
var _opts = this._options('OPTIONS', options);
return (this.read(_opts, callback));
};
HttpClient.prototype.post = function post(options, callback) {
var opts = this._options('POST', options);
return (this.request(opts, callback));
};
HttpClient.prototype.put = function put(options, callback) {
var opts = this._options('PUT', options);
return (this.request(opts, callback));
};
HttpClient.prototype.patch = function patch(options, callback) {
var opts = this._options('PATCH', options);
return (this.request(opts, callback));
};
HttpClient.prototype.read = function read(options, callback) {
var r = this.request(options, function readRequestCallback(err, req) {
if (!err) {
req.end();
}
return (callback(err, req));
});
return (r);
};
HttpClient.prototype.basicAuth = function basicAuth(username, password) {
if (username === false) {
delete this.headers.authorization;
} else {
assert.string(username, 'username');
assert.string(password, 'password');
var buffer = new Buffer(username + ':' + password, 'utf8');
this.headers.authorization = 'Basic ' +
buffer.toString('base64');
}
return (this);
};
HttpClient.prototype.request = function request(opts, cb) {
assert.object(opts, 'options');
assert.func(cb, 'callback');
cb = once(cb);
if (opts.retry === false) {
rawRequest(opts, cb);
return;
}
var call;
var retry = cloneRetryOptions(opts.retry);
opts._keep_alive = this._keep_alive;
call = backoff.call(rawRequest, opts, cb);
call.setStrategy(new backoff.ExponentialStrategy({
initialDelay: retry.minTimeout,
maxDelay: retry.maxTimeout
}));
call.failAfter(retry.retries);
call.on('backoff', this.emit.bind(this, 'attempt'));
call.start();
};
HttpClient.prototype._options = function (method, options) {
if (typeof (options) !== 'object') {
options = { path: options };
}
var self = this;
var opts = {
agent: options.agent !== undefined ? options.agent : self.agent,
ca: options.ca || self.ca,
cert: options.cert || self.cert,
ciphers: options.ciphers || self.ciphers,
connectTimeout: options.connectTimeout || self.connectTimeout,
requestTimeout: options.requestTimeout || self.requestTimeout,
headers: options.headers || {},
key: options.key || self.key,
log: options.log || self.log,
method: method,
passphrase: options.passphrase || self.passphrase,
path: options.path || self.path,
pfx: options.pfx || self.pfx,
rejectUnauthorized: options.rejectUnauthorized ||
self.rejectUnauthorized,
retry: options.retry !== false ? options.retry : false,
signRequest: options.signRequest || self.signRequest
};
if (!opts.retry && opts.retry !== false) {
opts.retry = self.retry;
}
// Backwards compatibility with restify < 1.0
if (options.query &&
Object.keys(options.query).length &&
opts.path.indexOf('?') === -1) {
opts.path += '?' + querystring.stringify(options.query);
}
if (this.socketPath) {
opts.socketPath = this.socketPath;
}
Object.keys(this.url).forEach(function (k) {
if (!opts[k]) {
opts[k] = self.url[k];
}
});
Object.keys(self.headers).forEach(function (k) {
if (!opts.headers[k]) {
opts.headers[k] = self.headers[k];
}
});
if (!opts.headers.date) {
opts.headers.date = new Date().toUTCString();
}
if (method === 'GET' || method === 'HEAD' || method === 'DELETE') {
if (opts.headers['content-type']) {
delete opts.headers['content-type'];
}
if (opts.headers['content-md5']) {
delete opts.headers['content-md5'];
}
if (opts.headers['content-length'] && method !== 'DELETE') {
delete opts.headers['content-length'];
}
if (opts.headers['transfer-encoding']) {
delete opts.headers['transfer-encoding'];
}
}
return (opts);
};
// vim: set ts=4 sts=4 sw=4 et: