123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- 'use strict';
- const http = require('http');
- const https = require('https');
- const urllib = require('url');
- const zlib = require('zlib');
- const PassThrough = require('stream').PassThrough;
- const Cookies = require('./cookies');
- const packageData = require('../../package.json');
- const MAX_REDIRECTS = 5;
- module.exports = function(url, options) {
- return fetch(url, options);
- };
- module.exports.Cookies = Cookies;
- function fetch(url, options) {
- options = options || {};
- options.fetchRes = options.fetchRes || new PassThrough();
- options.cookies = options.cookies || new Cookies();
- options.redirects = options.redirects || 0;
- options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
- if (options.cookie) {
- [].concat(options.cookie || []).forEach(cookie => {
- options.cookies.set(cookie, url);
- });
- options.cookie = false;
- }
- let fetchRes = options.fetchRes;
- let parsed = urllib.parse(url);
- let method =
- (options.method || '')
- .toString()
- .trim()
- .toUpperCase() || 'GET';
- let finished = false;
- let cookies;
- let body;
- let handler = parsed.protocol === 'https:' ? https : http;
- let headers = {
- 'accept-encoding': 'gzip,deflate',
- 'user-agent': 'nodemailer/' + packageData.version
- };
- Object.keys(options.headers || {}).forEach(key => {
- headers[key.toLowerCase().trim()] = options.headers[key];
- });
- if (options.userAgent) {
- headers['user-agent'] = options.userAgent;
- }
- if (parsed.auth) {
- headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64');
- }
- if ((cookies = options.cookies.get(url))) {
- headers.cookie = cookies;
- }
- if (options.body) {
- if (options.contentType !== false) {
- headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
- }
- if (typeof options.body.pipe === 'function') {
- // it's a stream
- headers['Transfer-Encoding'] = 'chunked';
- body = options.body;
- body.on('error', err => {
- if (finished) {
- return;
- }
- finished = true;
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- });
- } else {
- if (options.body instanceof Buffer) {
- body = options.body;
- } else if (typeof options.body === 'object') {
- try {
- // encodeURIComponent can fail on invalid input (partial emoji etc.)
- body = Buffer.from(
- Object.keys(options.body)
- .map(key => {
- let value = options.body[key].toString().trim();
- return encodeURIComponent(key) + '=' + encodeURIComponent(value);
- })
- .join('&')
- );
- } catch (E) {
- if (finished) {
- return;
- }
- finished = true;
- E.type = 'FETCH';
- E.sourceUrl = url;
- fetchRes.emit('error', E);
- return;
- }
- } else {
- body = Buffer.from(options.body.toString().trim());
- }
- headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
- headers['Content-Length'] = body.length;
- }
- // if method is not provided, use POST instead of GET
- method =
- (options.method || '')
- .toString()
- .trim()
- .toUpperCase() || 'POST';
- }
- let req;
- let reqOptions = {
- method,
- host: parsed.hostname,
- path: parsed.path,
- port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
- headers,
- rejectUnauthorized: false,
- agent: false
- };
- if (options.tls) {
- Object.keys(options.tls).forEach(key => {
- reqOptions[key] = options.tls[key];
- });
- }
- try {
- req = handler.request(reqOptions);
- } catch (E) {
- finished = true;
- setImmediate(() => {
- E.type = 'FETCH';
- E.sourceUrl = url;
- fetchRes.emit('error', E);
- });
- return fetchRes;
- }
- if (options.timeout) {
- req.setTimeout(options.timeout, () => {
- if (finished) {
- return;
- }
- finished = true;
- req.abort();
- let err = new Error('Request Timeout');
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- });
- }
- req.on('error', err => {
- if (finished) {
- return;
- }
- finished = true;
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- });
- req.on('response', res => {
- let inflate;
- if (finished) {
- return;
- }
- switch (res.headers['content-encoding']) {
- case 'gzip':
- case 'deflate':
- inflate = zlib.createUnzip();
- break;
- }
- if (res.headers['set-cookie']) {
- [].concat(res.headers['set-cookie'] || []).forEach(cookie => {
- options.cookies.set(cookie, url);
- });
- }
- if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
- // redirect
- options.redirects++;
- if (options.redirects > options.maxRedirects) {
- finished = true;
- let err = new Error('Maximum redirect count exceeded');
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- req.abort();
- return;
- }
- // redirect does not include POST body
- options.method = 'GET';
- options.body = false;
- return fetch(urllib.resolve(url, res.headers.location), options);
- }
- fetchRes.statusCode = res.statusCode;
- fetchRes.headers = res.headers;
- if (res.statusCode >= 300 && !options.allowErrorResponse) {
- finished = true;
- let err = new Error('Invalid status code ' + res.statusCode);
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- req.abort();
- return;
- }
- res.on('error', err => {
- if (finished) {
- return;
- }
- finished = true;
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- req.abort();
- });
- if (inflate) {
- res.pipe(inflate).pipe(fetchRes);
- inflate.on('error', err => {
- if (finished) {
- return;
- }
- finished = true;
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- req.abort();
- });
- } else {
- res.pipe(fetchRes);
- }
- });
- setImmediate(() => {
- if (body) {
- try {
- if (typeof body.pipe === 'function') {
- return body.pipe(req);
- } else {
- req.write(body);
- }
- } catch (err) {
- finished = true;
- err.type = 'FETCH';
- err.sourceUrl = url;
- fetchRes.emit('error', err);
- return;
- }
- }
- req.end();
- });
- return fetchRes;
- }
|