Name: js-handler/node_modules/restify/lib/clients/http_client.js
| 1: | // Copyright 2012 Mark Cavage, Inc. All rights reserved. |
| 2: | |
| 3: | var crypto = require('crypto'); |
| 4: | var EventEmitter = require('events').EventEmitter; |
| 5: | var fs = require('fs'); |
| 6: | var http = require('http'); |
| 7: | var https = require('https'); |
| 8: | var os = require('os'); |
| 9: | var querystring = require('querystring'); |
| 10: | var url = require('url'); |
| 11: | var util = require('util'); |
| 12: | var zlib = require('zlib'); |
| 13: | |
| 14: | var assert = require('assert-plus'); |
| 15: | var backoff = require('backoff'); |
| 16: | var KeepAliveAgent = require('keep-alive-agent'); |
| 17: | var mime = require('mime'); |
| 18: | var once = require('once'); |
| 19: | var uuid = require('node-uuid'); |
| 20: | |
| 21: | var dtrace = require('../dtrace'); |
| 22: | var errors = require('../errors'); |
| 23: | var bunyan = require('../bunyan_helper'); |
| 24: | |
| 25: | |
| 26: | |
| 27: | ///--- Globals |
| 28: | |
| 29: | /* JSSTYLED */ |
| 30: | var VERSION = JSON.parse(fs.readFileSync(require('path').normalize(__dirname + '/../../package.json'), 'utf8')).version; |
| 31: | |
| 32: | |
| 33: | |
| 34: | ///--- Helpers |
| 35: | |
| 36: | function cloneRetryOptions(options, defaults) { |
| 37: | if (options === false) { |
| 38: | return ({ |
| 39: | minTimeout: 1, |
| 40: | maxTimeout: 2, |
| 41: | retries: 1 |
| 42: | }); |
| 43: | } |
| 44: | |
| 45: | assert.optionalObject(options, 'options.retry'); |
| 46: | var r = options || {}; |
| 47: | assert.optionalNumber(r.minTimeout, 'options.retry.minTimeout'); |
| 48: | assert.optionalNumber(r.maxTimeout, 'options.retry.maxTimeout'); |
| 49: | assert.optionalNumber(r.retries, 'options.retry.retries'); |
| 50: | assert.optionalObject(defaults, 'defaults'); |
| 51: | defaults = defaults || {}; |
| 52: | |
| 53: | return ({ |
| 54: | minTimeout: r.minTimeout || defaults.minTimeout || 1000, |
| 55: | maxTimeout: r.maxTimeout || defaults.maxTimeout || Infinity, |
| 56: | retries: r.retries || defaults.retries || 4 |
| 57: | }); |
| 58: | } |
| 59: | |
| 60: | |
| 61: | function defaultUserAgent() { |
| 62: | var UA = 'restify/' + VERSION + |
| 63: | ' (' + os.arch() + '-' + os.platform() + '; ' + |
| 64: | 'v8/' + process.versions.v8 + '; ' + |
| 65: | 'OpenSSL/' + process.versions.openssl + ') ' + |
| 66: | 'node/' + process.versions.node; |
| 67: | |
| 68: | return (UA); |
| 69: | } |
| 70: | |
| 71: | |
| 72: | function ConnectTimeoutError(ms) { |
| 73: | if (Error.captureStackTrace) |
| 74: | Error.captureStackTrace(this, ConnectTimeoutError); |
| 75: | |
| 76: | this.message = 'connect timeout after ' + ms + 'ms'; |
| 77: | this.name = 'ConnectTimeoutError'; |
| 78: | } |
| 79: | util.inherits(ConnectTimeoutError, Error); |
| 80: | |
| 81: | |
| 82: | function rawRequest(opts, cb) { |
| 83: | assert.object(opts, 'options'); |
| 84: | assert.object(opts.log, 'options.log'); |
| 85: | assert.func(cb, 'callback'); |
| 86: | |
| 87: | cb = once(cb); |
| 88: | |
| 89: | var id = dtrace.nextId(); |
| 90: | var log = opts.log; |
| 91: | var proto = opts.protocol === 'https:' ? https : http; |
| 92: | var timer; |
| 93: | |
| 94: | if (opts.cert && opts.key) |
| 95: | opts.agent = false; |
| 96: | |
| 97: | if (opts.connectTimeout) { |
| 98: | timer = setTimeout(function connectTimeout() { |
| 99: | timer = null; |
| 100: | if (req) { |
| 101: | req.abort(); |
| 102: | } |
| 103: | |
| 104: | var err = new ConnectTimeoutError(opts.connectTimeout); |
| 105: | dtrace._rstfy_probes['client-error'].fire(function () { |
| 106: | return ([id, err.toString()]); |
| 107: | }); |
| 108: | cb(err, req); |
| 109: | }, opts.connectTimeout); |
| 110: | } |
| 111: | |
| 112: | dtrace._rstfy_probes['client-request'].fire(function () { |
| 113: | return ([ |
| 114: | opts.method, |
| 115: | opts.path, |
| 116: | opts.headers, |
| 117: | id |
| 118: | ]); |
| 119: | }); |
| 120: | |
| 121: | var req = proto.request(opts, function onResponse(res) { |
| 122: | clearTimeout(timer); |
| 123: | dtrace._rstfy_probes['client-response'].fire(function () { |
| 124: | return ([ id, res.statusCode, res.headers ]); |
| 125: | }); |
| 126: | log.trace({client_res: res}, 'Response received'); |
| 127: | |
| 128: | res.log = log; |
| 129: | |
| 130: | var err; |
| 131: | if (res.statusCode >= 400) |
| 132: | err = errors.codeToHttpError(res.statusCode); |
| 133: | |
| 134: | req.removeAllListeners('error'); |
| 135: | req.removeAllListeners('socket'); |
| 136: | req.emit('result', (err || null), res); |
| 137: | }); |
| 138: | req.log = log; |
| 139: | |
| 140: | req.on('error', function onError(err) { |
| 141: | dtrace._rstfy_probes['client-error'].fire(function () { |
| 142: | return ([id, (err || {}).toString()]); |
| 143: | }); |
| 144: | log.trace({err: err}, 'Request failed'); |
| 145: | clearTimeout(timer); |
| 146: | |
| 147: | cb(err, req); |
| 148: | if (req) { |
| 149: | process.nextTick(function () { |
| 150: | req.emit('result', err, null); |
| 151: | }); |
| 152: | } |
| 153: | }); |
| 154: | |
| 155: | req.once('upgrade', function onUpgrade(res, socket, _head) { |
| 156: | clearTimeout(timer); |
| 157: | dtrace._rstfy_probes['client-response'].fire(function () { |
| 158: | return ([ id, res.statusCode, res.headers ]); |
| 159: | }); |
| 160: | log.trace({client_res: res}, 'Upgrade Response received'); |
| 161: | |
| 162: | res.log = log; |
| 163: | |
| 164: | var err; |
| 165: | if (res.statusCode >= 400) |
| 166: | err = errors.codeToHttpError(res.statusCode); |
| 167: | |
| 168: | req.removeAllListeners('error'); |
| 169: | req.removeAllListeners('socket'); |
| 170: | req.emit('upgradeResult', (err || null), res, socket, _head); |
| 171: | }); |
| 172: | |
| 173: | req.once('socket', function onSocket(socket) { |
| 174: | if (socket.writable && !socket._connecting) { |
| 175: | clearTimeout(timer); |
| 176: | cb(null, req); |
| 177: | return; |
| 178: | } |
| 179: | |
| 180: | socket.once('connect', function onConnect() { |
| 181: | clearTimeout(timer); |
| 182: | cb(null, req); |
| 183: | }); |
| 184: | }); |
| 185: | |
| 186: | if (opts.signRequest) |
| 187: | opts.signRequest(req); |
| 188: | |
| 189: | if (log.trace()) |
| 190: | log.trace({client_req: opts}, 'request sent'); |
| 191: | } // end `rawRequest` |
| 192: | |
| 193: | |
| 194: | |
| 195: | ///--- API |
| 196: | |
| 197: | function HttpClient(options) { |
| 198: | assert.object(options, 'options'); |
| 199: | assert.optionalObject(options.headers, 'options.headers'); |
| 200: | assert.object(options.log, 'options.log'); |
| 201: | assert.optionalFunc(options.signRequest, 'options.signRequest'); |
| 202: | assert.optionalString(options.socketPath, 'options.socketPath'); |
| 203: | assert.optionalString(options.url, 'options.url'); |
| 204: | |
| 205: | EventEmitter.call(this); |
| 206: | |
| 207: | var self = this; |
| 208: | |
| 209: | this.agent = options.agent; |
| 210: | this.ca = options.ca; |
| 211: | this.cert = options.cert; |
| 212: | this.ciphers = options.ciphers; |
| 213: | this.connectTimeout = options.connectTimeout || false; |
| 214: | this.headers = options.headers || {}; |
| 215: | this.log = options.log; |
| 216: | if (!this.log.serializers.client_res) { |
| 217: | // Ensure logger has a reasonable serializer for `client_res` |
| 218: | // logged in this module. |
| 219: | this.log = this.log.child({ |
| 220: | serializers: {client_res: bunyan.serializers.client_res} |
| 221: | }); |
| 222: | } |
| 223: | this.key = options.key; |
| 224: | this.name = options.name || 'HttpClient'; |
| 225: | this.passphrase = options.passphrase; |
| 226: | this.pfx = options.pfx; |
| 227: | if (options.rejectUnauthorized !== undefined) { |
| 228: | this.rejectUnauthorized = options.rejectUnauthorized; |
| 229: | } else { |
| 230: | this.rejectUnauthorized = true; |
| 231: | } |
| 232: | |
| 233: | this.retry = cloneRetryOptions(options.retry); |
| 234: | this.signRequest = options.signRequest || false; |
| 235: | this.socketPath = options.socketPath || false; |
| 236: | this.url = options.url ? url.parse(options.url) : {}; |
| 237: | |
| 238: | if (options.accept) { |
| 239: | if (options.accept.indexOf('/') === -1) |
| 240: | options.accept = mime.lookup(options.accept); |
| 241: | |
| 242: | this.headers.accept = options.accept; |
| 243: | } |
| 244: | |
| 245: | if (options.contentType) { |
| 246: | if (options.contentType.indexOf('/') === -1) |
| 247: | options.type = mime.lookup(options.contentType); |
| 248: | |
| 249: | this.headers['content-type'] = options.contentType; |
| 250: | } |
| 251: | |
| 252: | if (options.userAgent !== false) { |
| 253: | this.headers['user-agent'] = options.userAgent || |
| 254: | defaultUserAgent(); |
| 255: | } |
| 256: | |
| 257: | if (options.version) |
| 258: | this.headers['accept-version'] = options.version; |
| 259: | |
| 260: | if (this.agent === undefined) { |
| 261: | var Agent; |
| 262: | var maxSockets; |
| 263: | |
| 264: | if (this.url.protocol === 'https:') { |
| 265: | Agent = KeepAliveAgent.Secure; |
| 266: | maxSockets = https.globalAgent.maxSockets; |
| 267: | } else { |
| 268: | Agent = KeepAliveAgent; |
| 269: | maxSockets = http.globalAgent.maxSockets; |
| 270: | } |
| 271: | |
| 272: | this.agent = new Agent({ |
| 273: | cert: self.cert, |
| 274: | ca: self.ca, |
| 275: | ciphers: self.ciphers, |
| 276: | key: self.key, |
| 277: | maxSockets: maxSockets, |
| 278: | maxKeepAliveRequests: 0, |
| 279: | maxKeepAliveTime: 0, |
| 280: | passphrase: self.passphrase, |
| 281: | pfx: self.pfx, |
| 282: | rejectUnauthorized: self.rejectUnauthorized |
| 283: | }); |
| 284: | } |
| 285: | } |
| 286: | util.inherits(HttpClient, EventEmitter); |
| 287: | module.exports = HttpClient; |
| 288: | |
| 289: | |
| 290: | HttpClient.prototype.close = function close() { |
| 291: | var sockets = this.agent.sockets; |
| 292: | Object.keys((sockets || {})).forEach(function (k) { |
| 293: | sockets[k].forEach(function (s) { |
| 294: | s.end(); |
| 295: | }); |
| 296: | }); |
| 297: | |
| 298: | sockets = this.agent.idleSockets; |
| 299: | Object.keys((sockets || {})).forEach(function (k) { |
| 300: | sockets[k].forEach(function (s) { |
| 301: | s.end(); |
| 302: | }); |
| 303: | }); |
| 304: | }; |
| 305: | |
| 306: | |
| 307: | HttpClient.prototype.del = function del(options, callback) { |
| 308: | var opts = this._options('DELETE', options); |
| 309: | |
| 310: | return (this.read(opts, callback)); |
| 311: | }; |
| 312: | |
| 313: | |
| 314: | HttpClient.prototype.get = function get(options, callback) { |
| 315: | var opts = this._options('GET', options); |
| 316: | |
| 317: | return (this.read(opts, callback)); |
| 318: | }; |
| 319: | |
| 320: | |
| 321: | HttpClient.prototype.head = function head(options, callback) { |
| 322: | var opts = this._options('HEAD', options); |
| 323: | |
| 324: | return (this.read(opts, callback)); |
| 325: | }; |
| 326: | |
| 327: | HttpClient.prototype.opts = function http_options(options, callback) { |
| 328: | var _opts = this._options('OPTIONS', options); |
| 329: | |
| 330: | return (this.read(_opts, callback)); |
| 331: | }; |
| 332: | |
| 333: | |
| 334: | HttpClient.prototype.post = function post(options, callback) { |
| 335: | var opts = this._options('POST', options); |
| 336: | |
| 337: | return (this.request(opts, callback)); |
| 338: | }; |
| 339: | |
| 340: | |
| 341: | HttpClient.prototype.put = function put(options, callback) { |
| 342: | var opts = this._options('PUT', options); |
| 343: | |
| 344: | return (this.request(opts, callback)); |
| 345: | }; |
| 346: | |
| 347: | |
| 348: | HttpClient.prototype.patch = function patch(options, callback) { |
| 349: | var opts = this._options('PATCH', options); |
| 350: | |
| 351: | |
| 352: | return (this.request(opts, callback)); |
| 353: | }; |
| 354: | |
| 355: | |
| 356: | HttpClient.prototype.read = function read(options, callback) { |
| 357: | var r = this.request(options, function readRequestCallback(err, req) { |
| 358: | if (!err) |
| 359: | req.end(); |
| 360: | |
| 361: | return (callback(err, req)); |
| 362: | }); |
| 363: | return (r); |
| 364: | }; |
| 365: | |
| 366: | |
| 367: | HttpClient.prototype.basicAuth = function basicAuth(username, password) { |
| 368: | if (username === false) { |
| 369: | delete this.headers.authorization; |
| 370: | } else { |
| 371: | assert.string(username, 'username'); |
| 372: | assert.string(password, 'password'); |
| 373: | |
| 374: | var buffer = new Buffer(username + ':' + password, 'utf8'); |
| 375: | this.headers.authorization = 'Basic ' + |
| 376: | buffer.toString('base64'); |
| 377: | } |
| 378: | |
| 379: | return (this); |
| 380: | }; |
| 381: | |
| 382: | |
| 383: | HttpClient.prototype.request = function request(opts, cb) { |
| 384: | assert.object(opts, 'options'); |
| 385: | assert.func(cb, 'callback'); |
| 386: | |
| 387: | cb = once(cb); |
| 388: | |
| 389: | var call; |
| 390: | var retry = cloneRetryOptions(opts.retry); |
| 391: | |
| 392: | call = backoff.call(rawRequest, opts, cb); |
| 393: | call.setStrategy(new backoff.ExponentialStrategy({ |
| 394: | initialDelay: retry.minTimeout, |
| 395: | maxDelay: retry.maxTimeout |
| 396: | })); |
| 397: | call.failAfter(retry.retries); |
| 398: | call.on('backoff', this.emit.bind(this, 'attempt')); |
| 399: | |
| 400: | call.start(); |
| 401: | }; |
| 402: | |
| 403: | |
| 404: | HttpClient.prototype._options = function (method, options) { |
| 405: | if (typeof (options) !== 'object') |
| 406: | options = { path: options }; |
| 407: | |
| 408: | var self = this; |
| 409: | var opts = { |
| 410: | agent: options.agent || self.agent, |
| 411: | ca: options.ca || self.ca, |
| 412: | cert: options.cert || self.cert, |
| 413: | ciphers: options.ciphers || self.ciphers, |
| 414: | connectTimeout: options.connectTimeout || self.connectTimeout, |
| 415: | headers: options.headers || {}, |
| 416: | key: options.key || self.key, |
| 417: | log: options.log || self.log, |
| 418: | method: method, |
| 419: | passphrase: options.passphrase || self.passphrase, |
| 420: | path: options.path || self.path, |
| 421: | pfx: options.pfx || self.pfx, |
| 422: | rejectUnauthorized: options.rejectUnauthorized || |
| 423: | self.rejectUnauthorized, |
| 424: | retry: options.retry || self.retry, |
| 425: | signRequest: options.signRequest || self.signRequest |
| 426: | }; |
| 427: | |
| 428: | // Backwards compatibility with restify < 1.0 |
| 429: | if (options.query && |
| 430: | Object.keys(options.query).length && |
| 431: | opts.path.indexOf('?') === -1) { |
| 432: | opts.path += '?' + querystring.stringify(options.query); |
| 433: | } |
| 434: | |
| 435: | if (this.socketPath) |
| 436: | opts.socketPath = this.socketPath; |
| 437: | |
| 438: | Object.keys(self.url).forEach(function (k) { |
| 439: | if (!opts[k]) |
| 440: | opts[k] = self.url[k]; |
| 441: | }); |
| 442: | |
| 443: | Object.keys(self.headers).forEach(function (k) { |
| 444: | if (!opts.headers[k]) |
| 445: | opts.headers[k] = self.headers[k]; |
| 446: | }); |
| 447: | |
| 448: | if (!opts.headers.date) |
| 449: | opts.headers.date = new Date().toUTCString(); |
| 450: | |
| 451: | if (method === 'GET' || method === 'HEAD' || method === 'DELETE') { |
| 452: | if (opts.headers['content-type']) |
| 453: | delete opts.headers['content-type']; |
| 454: | if (opts.headers['content-md5']) |
| 455: | delete opts.headers['content-md5']; |
| 456: | if (opts.headers['content-length']) |
| 457: | delete opts.headers['content-length']; |
| 458: | if (opts.headers['transfer-encoding']) |
| 459: | delete opts.headers['transfer-encoding']; |
| 460: | } |
| 461: | |
| 462: | return (opts); |
| 463: | }; |
| 464: | // vim: set ts=8 sts=8 sw=8 et: |
