main.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. // Copyright 2010-2011 Mikeal Rogers
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. var http = require('http')
  15. , https = false
  16. , tls = false
  17. , url = require('url')
  18. , util = require('util')
  19. , stream = require('stream')
  20. , qs = require('querystring')
  21. , mimetypes = require('./mimetypes')
  22. ;
  23. try {
  24. https = require('https');
  25. } catch (e) {}
  26. try {
  27. tls = require('tls');
  28. } catch (e) {}
  29. var toBase64 = function(str) {
  30. return (new Buffer(str || "", "ascii")).toString("base64");
  31. };
  32. // Hacky fix for pre-0.4.4 https
  33. if (https && !https.Agent) {
  34. https.Agent = function (options) {
  35. http.Agent.call(this, options);
  36. }
  37. util.inherits(https.Agent, http.Agent);
  38. https.Agent.prototype._getConnection = function(host, port, cb) {
  39. var s = tls.connect(port, host, this.options, function() {
  40. // do other checks here?
  41. if (cb) cb();
  42. });
  43. return s;
  44. };
  45. }
  46. var isReadStream = function (rs) {
  47. if (rs.readable && rs.path && rs.mode) {
  48. return true;
  49. }
  50. }
  51. var isUrl = /^https?:/;
  52. var globalPool = {};
  53. var Request = function (options) {
  54. stream.Stream.call(this);
  55. this.readable = true;
  56. this.writable = true;
  57. if (typeof options === 'string') {
  58. options = {uri:options};
  59. }
  60. for (i in options) {
  61. this[i] = options[i];
  62. }
  63. if (!this.pool) this.pool = globalPool;
  64. this.dests = [];
  65. this.__isRequestRequest = true;
  66. }
  67. util.inherits(Request, stream.Stream);
  68. Request.prototype.getAgent = function (host, port) {
  69. if (!this.pool[host+':'+port]) {
  70. this.pool[host+':'+port] = new this.httpModule.Agent({host:host, port:port});
  71. }
  72. return this.pool[host+':'+port];
  73. }
  74. Request.prototype.request = function () {
  75. var options = this;
  76. if (options.url) {
  77. // People use this property instead all the time so why not just support it.
  78. options.uri = options.url;
  79. delete options.url;
  80. }
  81. if (!options.uri) {
  82. throw new Error("options.uri is a required argument");
  83. } else {
  84. if (typeof options.uri == "string") options.uri = url.parse(options.uri);
  85. }
  86. if (options.proxy) {
  87. if (typeof options.proxy == 'string') options.proxy = url.parse(options.proxy);
  88. }
  89. options._redirectsFollowed = options._redirectsFollowed || 0;
  90. options.maxRedirects = (options.maxRedirects !== undefined) ? options.maxRedirects : 10;
  91. options.followRedirect = (options.followRedirect !== undefined) ? options.followRedirect : true;
  92. options.headers = options.headers || {};
  93. var setHost = false;
  94. if (!options.headers.host) {
  95. options.headers.host = options.uri.hostname;
  96. if (options.uri.port) {
  97. if ( !(options.uri.port === 80 && options.uri.protocol === 'http:') &&
  98. !(options.uri.port === 443 && options.uri.protocol === 'https:') )
  99. options.headers.host += (':'+options.uri.port);
  100. }
  101. setHost = true;
  102. }
  103. if (!options.uri.pathname) {options.uri.pathname = '/';}
  104. if (!options.uri.port) {
  105. if (options.uri.protocol == 'http:') {options.uri.port = 80;}
  106. else if (options.uri.protocol == 'https:') {options.uri.port = 443;}
  107. }
  108. if (options.bodyStream || options.responseBodyStream) {
  109. console.error('options.bodyStream and options.responseBodyStream is deprecated. You should now send the request object to stream.pipe()');
  110. this.pipe(options.responseBodyStream || options.bodyStream)
  111. }
  112. if (options.proxy) {
  113. options.port = options.proxy.port;
  114. options.host = options.proxy.hostname;
  115. } else {
  116. options.port = options.uri.port;
  117. options.host = options.uri.hostname;
  118. }
  119. if (options.onResponse === true) {
  120. options.onResponse = options.callback;
  121. delete options.callback;
  122. }
  123. var clientErrorHandler = function (error) {
  124. if (setHost) delete options.headers.host;
  125. options.emit('error', error);
  126. };
  127. if (options.onResponse) options.on('error', function (e) {options.onResponse(e)});
  128. if (options.callback) options.on('error', function (e) {options.callback(e)});
  129. if (options.uri.auth && !options.headers.authorization) {
  130. options.headers.authorization = "Basic " + toBase64(options.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'));
  131. }
  132. if (options.proxy && options.proxy.auth && !options.headers['proxy-authorization']) {
  133. options.headers.authorization = "Basic " + toBase64(options.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'));
  134. }
  135. options.path = options.uri.href.replace(options.uri.protocol + '//' + options.uri.host, '');
  136. if (options.path.length === 0) options.path = '/';
  137. if (options.proxy) options.path = (options.uri.protocol + '//' + options.uri.host + options.path);
  138. if (options.json) {
  139. options.headers['content-type'] = 'application/json';
  140. options.body = JSON.stringify(options.json);
  141. } else if (options.multipart) {
  142. options.body = '';
  143. options.headers['content-type'] = 'multipart/related;boundary="frontier"';
  144. if (!options.multipart.forEach) throw new Error('Argument error, options.multipart.');
  145. options.multipart.forEach(function (part) {
  146. var body = part.body;
  147. if(!body) throw Error('Body attribute missing in multipart.');
  148. delete part.body;
  149. options.body += '--frontier\r\n';
  150. Object.keys(part).forEach(function(key){
  151. options.body += key + ': ' + part[key] + '\r\n'
  152. })
  153. options.body += '\r\n' + body + '\r\n';
  154. })
  155. options.body += '--frontier--'
  156. }
  157. if (options.body) {
  158. if (!Buffer.isBuffer(options.body)) {
  159. options.body = new Buffer(options.body);
  160. }
  161. if (options.body.length) {
  162. options.headers['content-length'] = options.body.length;
  163. } else {
  164. throw new Error('Argument error, options.body.');
  165. }
  166. }
  167. options.httpModule =
  168. {"http:":http, "https:":https}[options.proxy ? options.proxy.protocol : options.uri.protocol]
  169. if (!options.httpModule) throw new Error("Invalid protocol");
  170. if (options.pool === false) {
  171. options.agent = false;
  172. } else {
  173. if (options.maxSockets) {
  174. // Don't use our pooling if node has the refactored client
  175. options.agent = options.httpModule.globalAgent || options.getAgent(options.host, options.port);
  176. options.agent.maxSockets = options.maxSockets;
  177. }
  178. if (options.pool.maxSockets) {
  179. // Don't use our pooling if node has the refactored client
  180. options.agent = options.httpModule.globalAgent || options.getAgent(options.host, options.port);
  181. options.agent.maxSockets = options.pool.maxSockets;
  182. }
  183. }
  184. options.start = function () {
  185. options._started = true;
  186. options.method = options.method || 'GET';
  187. options.req = options.httpModule.request(options, function (response) {
  188. options.response = response;
  189. response.request = options;
  190. if (setHost) delete options.headers.host;
  191. if (options.timeout && options.timeoutTimer) clearTimeout(options.timeoutTimer);
  192. if (response.statusCode >= 300 &&
  193. response.statusCode < 400 &&
  194. options.followRedirect &&
  195. options.method !== 'PUT' &&
  196. options.method !== 'POST' &&
  197. response.headers.location) {
  198. if (options._redirectsFollowed >= options.maxRedirects) {
  199. options.emit('error', new Error("Exceeded maxRedirects. Probably stuck in a redirect loop."));
  200. return;
  201. }
  202. options._redirectsFollowed += 1;
  203. if (!isUrl.test(response.headers.location)) {
  204. response.headers.location = url.resolve(options.uri.href, response.headers.location);
  205. }
  206. options.uri = response.headers.location;
  207. delete options.req;
  208. delete options.agent;
  209. delete options._started;
  210. if (options.headers) {
  211. delete options.headers.host;
  212. }
  213. request(options, options.callback);
  214. return; // Ignore the rest of the response
  215. } else {
  216. options._redirectsFollowed = 0;
  217. // Be a good stream and emit end when the response is finished.
  218. // Hack to emit end on close because of a core bug that never fires end
  219. response.on('close', function () {options.response.emit('end')})
  220. if (options.encoding) {
  221. if (options.dests.length !== 0) {
  222. console.error("Ingoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.");
  223. } else {
  224. response.setEncoding(options.encoding);
  225. }
  226. }
  227. options.dests.forEach(function (dest) {
  228. if (dest.headers) {
  229. dest.headers['content-type'] = response.headers['content-type'];
  230. if (response.headers['content-length']) {
  231. dest.headers['content-length'] = response.headers['content-length'];
  232. }
  233. }
  234. if (dest.setHeader) {
  235. for (i in response.headers) {
  236. dest.setHeader(i, response.headers[i])
  237. }
  238. dest.statusCode = response.statusCode;
  239. }
  240. })
  241. response.on("data", function (chunk) {options.emit("data", chunk)});
  242. response.on("end", function (chunk) {options.emit("end", chunk)});
  243. response.on("close", function () {options.emit("close")});
  244. if (options.onResponse) {
  245. options.onResponse(null, response);
  246. }
  247. if (options.callback) {
  248. var buffer = '';
  249. options.on("data", function (chunk) {
  250. buffer += chunk;
  251. })
  252. options.on("end", function () {
  253. response.body = buffer;
  254. options.callback(null, response, buffer);
  255. })
  256. ;
  257. }
  258. }
  259. })
  260. if (options.timeout) {
  261. options.timeoutTimer = setTimeout(function() {
  262. options.req.abort();
  263. options.emit("error", "ETIMEDOUT");
  264. }, options.timeout);
  265. }
  266. options.req.on('error', clientErrorHandler);
  267. }
  268. options.once('pipe', function (src) {
  269. if (options.ntick) throw new Error("You cannot pipe to this stream after the first nextTick() after creation of the request stream.")
  270. options.src = src;
  271. if (isReadStream(src)) {
  272. options.headers['content-type'] = mimetypes.lookup(src.path.slice(src.path.lastIndexOf('.')+1))
  273. } else {
  274. if (src.headers) {
  275. for (i in src.headers) {
  276. if (!options.headers[i]) {
  277. options.headers[i] = src.headers[i]
  278. }
  279. }
  280. }
  281. if (src.method && !options.method) {
  282. options.method = src.method;
  283. }
  284. }
  285. options.on('pipe', function () {
  286. console.error("You have already piped to this stream. Pipeing twice is likely to break the request.")
  287. })
  288. })
  289. process.nextTick(function () {
  290. if (options.body) {
  291. options.write(options.body);
  292. options.end();
  293. } else if (options.requestBodyStream) {
  294. console.warn("options.requestBodyStream is deprecated, please pass the request object to stream.pipe.")
  295. options.requestBodyStream.pipe(options);
  296. } else if (!options.src) {
  297. options.end();
  298. }
  299. options.ntick = true;
  300. })
  301. }
  302. Request.prototype.pipe = function (dest) {
  303. if (this.response) throw new Error("You cannot pipe after the response event.")
  304. this.dests.push(dest);
  305. stream.Stream.prototype.pipe.call(this, dest)
  306. return dest
  307. }
  308. Request.prototype.write = function () {
  309. if (!this._started) this.start();
  310. if (!this.req) throw new Error("This request has been piped before http.request() was called.");
  311. this.req.write.apply(this.req, arguments);
  312. }
  313. Request.prototype.end = function () {
  314. if (!this._started) this.start();
  315. if (!this.req) throw new Error("This request has been piped before http.request() was called.");
  316. this.req.end.apply(this.req, arguments);
  317. }
  318. Request.prototype.pause = function () {
  319. if (!this.response) throw new Error("This request has been piped before http.request() was called.");
  320. this.response.pause.apply(this.response, arguments);
  321. }
  322. Request.prototype.resume = function () {
  323. if (!this.response) throw new Error("This request has been piped before http.request() was called.");
  324. this.response.resume.apply(this.response, arguments);
  325. }
  326. function request (options, callback) {
  327. if (typeof options === 'string') options = {uri:options};
  328. if (callback) options.callback = callback;
  329. var r = new Request(options);
  330. r.request();
  331. return r;
  332. }
  333. module.exports = request;
  334. request.defaults = function (options) {
  335. var def = function (method) {
  336. var d = function (opts, callback) {
  337. for (i in options) {
  338. if (opts[i] === undefined) opts[i] = options[i];
  339. }
  340. return method(opts, callback);
  341. }
  342. return d;
  343. }
  344. de = def(request);
  345. de.get = def(request.get);
  346. de.post = def(request.post);
  347. de.put = def(request.put);
  348. de.head = def(request.head);
  349. de.del = def(request.del);
  350. return de;
  351. }
  352. request.get = request;
  353. request.post = function (options, callback) {
  354. if (typeof options === 'string') options = {uri:options};
  355. options.method = 'POST';
  356. return request(options, callback);
  357. };
  358. request.put = function (options, callback) {
  359. if (typeof options === 'string') options = {uri:options};
  360. options.method = 'PUT';
  361. return request(options, callback);
  362. };
  363. request.head = function (options, callback) {
  364. if (typeof options === 'string') options = {uri:options};
  365. options.method = 'HEAD';
  366. if (options.body || options.requestBodyStream || options.json || options.multipart) {
  367. throw new Error("HTTP HEAD requests MUST NOT include a request body.");
  368. }
  369. return request(options, callback);
  370. };
  371. request.del = function (options, callback) {
  372. if (typeof options === 'string') options = {uri:options};
  373. options.method = 'DELETE';
  374. return request(options, callback);
  375. }