| 1 | 'use strict';
|
|---|
| 2 |
|
|---|
| 3 | const http = require('http');
|
|---|
| 4 | const https = require('https');
|
|---|
| 5 | const urllib = require('url');
|
|---|
| 6 | const zlib = require('zlib');
|
|---|
| 7 | const PassThrough = require('stream').PassThrough;
|
|---|
| 8 | const Cookies = require('./cookies');
|
|---|
| 9 | const packageData = require('../../package.json');
|
|---|
| 10 | const net = require('net');
|
|---|
| 11 |
|
|---|
| 12 | const MAX_REDIRECTS = 5;
|
|---|
| 13 |
|
|---|
| 14 | module.exports = function (url, options) {
|
|---|
| 15 | return nmfetch(url, options);
|
|---|
| 16 | };
|
|---|
| 17 |
|
|---|
| 18 | module.exports.Cookies = Cookies;
|
|---|
| 19 |
|
|---|
| 20 | function nmfetch(url, options) {
|
|---|
| 21 | options = options || {};
|
|---|
| 22 |
|
|---|
| 23 | options.fetchRes = options.fetchRes || new PassThrough();
|
|---|
| 24 | options.cookies = options.cookies || new Cookies();
|
|---|
| 25 | options.redirects = options.redirects || 0;
|
|---|
| 26 | options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
|
|---|
| 27 |
|
|---|
| 28 | if (options.cookie) {
|
|---|
| 29 | [].concat(options.cookie || []).forEach(cookie => {
|
|---|
| 30 | options.cookies.set(cookie, url);
|
|---|
| 31 | });
|
|---|
| 32 | options.cookie = false;
|
|---|
| 33 | }
|
|---|
| 34 |
|
|---|
| 35 | let fetchRes = options.fetchRes;
|
|---|
| 36 | let parsed = urllib.parse(url);
|
|---|
| 37 | let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
|
|---|
| 38 | let finished = false;
|
|---|
| 39 | let cookies;
|
|---|
| 40 | let body;
|
|---|
| 41 |
|
|---|
| 42 | let handler = parsed.protocol === 'https:' ? https : http;
|
|---|
| 43 |
|
|---|
| 44 | let headers = {
|
|---|
| 45 | 'accept-encoding': 'gzip,deflate',
|
|---|
| 46 | 'user-agent': 'nodemailer/' + packageData.version
|
|---|
| 47 | };
|
|---|
| 48 |
|
|---|
| 49 | Object.keys(options.headers || {}).forEach(key => {
|
|---|
| 50 | headers[key.toLowerCase().trim()] = options.headers[key];
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | if (options.userAgent) {
|
|---|
| 54 | headers['user-agent'] = options.userAgent;
|
|---|
| 55 | }
|
|---|
| 56 |
|
|---|
| 57 | if (parsed.auth) {
|
|---|
| 58 | headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64');
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | if ((cookies = options.cookies.get(url))) {
|
|---|
| 62 | headers.cookie = cookies;
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | if (options.body) {
|
|---|
| 66 | if (options.contentType !== false) {
|
|---|
| 67 | headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
|
|---|
| 68 | }
|
|---|
| 69 |
|
|---|
| 70 | if (typeof options.body.pipe === 'function') {
|
|---|
| 71 | // it's a stream
|
|---|
| 72 | headers['Transfer-Encoding'] = 'chunked';
|
|---|
| 73 | body = options.body;
|
|---|
| 74 | body.on('error', err => {
|
|---|
| 75 | if (finished) {
|
|---|
| 76 | return;
|
|---|
| 77 | }
|
|---|
| 78 | finished = true;
|
|---|
| 79 | err.type = 'FETCH';
|
|---|
| 80 | err.sourceUrl = url;
|
|---|
| 81 | fetchRes.emit('error', err);
|
|---|
| 82 | });
|
|---|
| 83 | } else {
|
|---|
| 84 | if (options.body instanceof Buffer) {
|
|---|
| 85 | body = options.body;
|
|---|
| 86 | } else if (typeof options.body === 'object') {
|
|---|
| 87 | try {
|
|---|
| 88 | // encodeURIComponent can fail on invalid input (partial emoji etc.)
|
|---|
| 89 | body = Buffer.from(
|
|---|
| 90 | Object.keys(options.body)
|
|---|
| 91 | .map(key => {
|
|---|
| 92 | let value = options.body[key].toString().trim();
|
|---|
| 93 | return encodeURIComponent(key) + '=' + encodeURIComponent(value);
|
|---|
| 94 | })
|
|---|
| 95 | .join('&')
|
|---|
| 96 | );
|
|---|
| 97 | } catch (E) {
|
|---|
| 98 | if (finished) {
|
|---|
| 99 | return;
|
|---|
| 100 | }
|
|---|
| 101 | finished = true;
|
|---|
| 102 | E.type = 'FETCH';
|
|---|
| 103 | E.sourceUrl = url;
|
|---|
| 104 | fetchRes.emit('error', E);
|
|---|
| 105 | return;
|
|---|
| 106 | }
|
|---|
| 107 | } else {
|
|---|
| 108 | body = Buffer.from(options.body.toString().trim());
|
|---|
| 109 | }
|
|---|
| 110 |
|
|---|
| 111 | headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
|
|---|
| 112 | headers['Content-Length'] = body.length;
|
|---|
| 113 | }
|
|---|
| 114 | // if method is not provided, use POST instead of GET
|
|---|
| 115 | method = (options.method || '').toString().trim().toUpperCase() || 'POST';
|
|---|
| 116 | }
|
|---|
| 117 |
|
|---|
| 118 | let req;
|
|---|
| 119 | let reqOptions = {
|
|---|
| 120 | method,
|
|---|
| 121 | host: parsed.hostname,
|
|---|
| 122 | path: parsed.path,
|
|---|
| 123 | port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
|---|
| 124 | headers,
|
|---|
| 125 | rejectUnauthorized: false,
|
|---|
| 126 | agent: false
|
|---|
| 127 | };
|
|---|
| 128 |
|
|---|
| 129 | if (options.tls) {
|
|---|
| 130 | Object.keys(options.tls).forEach(key => {
|
|---|
| 131 | reqOptions[key] = options.tls[key];
|
|---|
| 132 | });
|
|---|
| 133 | }
|
|---|
| 134 |
|
|---|
| 135 | if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
|
|---|
| 136 | reqOptions.servername = parsed.hostname;
|
|---|
| 137 | }
|
|---|
| 138 |
|
|---|
| 139 | try {
|
|---|
| 140 | req = handler.request(reqOptions);
|
|---|
| 141 | } catch (E) {
|
|---|
| 142 | finished = true;
|
|---|
| 143 | setImmediate(() => {
|
|---|
| 144 | E.type = 'FETCH';
|
|---|
| 145 | E.sourceUrl = url;
|
|---|
| 146 | fetchRes.emit('error', E);
|
|---|
| 147 | });
|
|---|
| 148 | return fetchRes;
|
|---|
| 149 | }
|
|---|
| 150 |
|
|---|
| 151 | if (options.timeout) {
|
|---|
| 152 | req.setTimeout(options.timeout, () => {
|
|---|
| 153 | if (finished) {
|
|---|
| 154 | return;
|
|---|
| 155 | }
|
|---|
| 156 | finished = true;
|
|---|
| 157 | req.abort();
|
|---|
| 158 | let err = new Error('Request Timeout');
|
|---|
| 159 | err.type = 'FETCH';
|
|---|
| 160 | err.sourceUrl = url;
|
|---|
| 161 | fetchRes.emit('error', err);
|
|---|
| 162 | });
|
|---|
| 163 | }
|
|---|
| 164 |
|
|---|
| 165 | req.on('error', err => {
|
|---|
| 166 | if (finished) {
|
|---|
| 167 | return;
|
|---|
| 168 | }
|
|---|
| 169 | finished = true;
|
|---|
| 170 | err.type = 'FETCH';
|
|---|
| 171 | err.sourceUrl = url;
|
|---|
| 172 | fetchRes.emit('error', err);
|
|---|
| 173 | });
|
|---|
| 174 |
|
|---|
| 175 | req.on('response', res => {
|
|---|
| 176 | let inflate;
|
|---|
| 177 |
|
|---|
| 178 | if (finished) {
|
|---|
| 179 | return;
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | switch (res.headers['content-encoding']) {
|
|---|
| 183 | case 'gzip':
|
|---|
| 184 | case 'deflate':
|
|---|
| 185 | inflate = zlib.createUnzip();
|
|---|
| 186 | break;
|
|---|
| 187 | }
|
|---|
| 188 |
|
|---|
| 189 | if (res.headers['set-cookie']) {
|
|---|
| 190 | [].concat(res.headers['set-cookie'] || []).forEach(cookie => {
|
|---|
| 191 | options.cookies.set(cookie, url);
|
|---|
| 192 | });
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|---|
| 196 | // redirect
|
|---|
| 197 | options.redirects++;
|
|---|
| 198 | if (options.redirects > options.maxRedirects) {
|
|---|
| 199 | finished = true;
|
|---|
| 200 | let err = new Error('Maximum redirect count exceeded');
|
|---|
| 201 | err.type = 'FETCH';
|
|---|
| 202 | err.sourceUrl = url;
|
|---|
| 203 | fetchRes.emit('error', err);
|
|---|
| 204 | req.abort();
|
|---|
| 205 | return;
|
|---|
| 206 | }
|
|---|
| 207 | // redirect does not include POST body
|
|---|
| 208 | options.method = 'GET';
|
|---|
| 209 | options.body = false;
|
|---|
| 210 | return nmfetch(urllib.resolve(url, res.headers.location), options);
|
|---|
| 211 | }
|
|---|
| 212 |
|
|---|
| 213 | fetchRes.statusCode = res.statusCode;
|
|---|
| 214 | fetchRes.headers = res.headers;
|
|---|
| 215 |
|
|---|
| 216 | if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
|---|
| 217 | finished = true;
|
|---|
| 218 | let err = new Error('Invalid status code ' + res.statusCode);
|
|---|
| 219 | err.type = 'FETCH';
|
|---|
| 220 | err.sourceUrl = url;
|
|---|
| 221 | fetchRes.emit('error', err);
|
|---|
| 222 | req.abort();
|
|---|
| 223 | return;
|
|---|
| 224 | }
|
|---|
| 225 |
|
|---|
| 226 | res.on('error', err => {
|
|---|
| 227 | if (finished) {
|
|---|
| 228 | return;
|
|---|
| 229 | }
|
|---|
| 230 | finished = true;
|
|---|
| 231 | err.type = 'FETCH';
|
|---|
| 232 | err.sourceUrl = url;
|
|---|
| 233 | fetchRes.emit('error', err);
|
|---|
| 234 | req.abort();
|
|---|
| 235 | });
|
|---|
| 236 |
|
|---|
| 237 | if (inflate) {
|
|---|
| 238 | res.pipe(inflate).pipe(fetchRes);
|
|---|
| 239 | inflate.on('error', err => {
|
|---|
| 240 | if (finished) {
|
|---|
| 241 | return;
|
|---|
| 242 | }
|
|---|
| 243 | finished = true;
|
|---|
| 244 | err.type = 'FETCH';
|
|---|
| 245 | err.sourceUrl = url;
|
|---|
| 246 | fetchRes.emit('error', err);
|
|---|
| 247 | req.abort();
|
|---|
| 248 | });
|
|---|
| 249 | } else {
|
|---|
| 250 | res.pipe(fetchRes);
|
|---|
| 251 | }
|
|---|
| 252 | });
|
|---|
| 253 |
|
|---|
| 254 | setImmediate(() => {
|
|---|
| 255 | if (body) {
|
|---|
| 256 | try {
|
|---|
| 257 | if (typeof body.pipe === 'function') {
|
|---|
| 258 | return body.pipe(req);
|
|---|
| 259 | } else {
|
|---|
| 260 | req.write(body);
|
|---|
| 261 | }
|
|---|
| 262 | } catch (err) {
|
|---|
| 263 | finished = true;
|
|---|
| 264 | err.type = 'FETCH';
|
|---|
| 265 | err.sourceUrl = url;
|
|---|
| 266 | fetchRes.emit('error', err);
|
|---|
| 267 | return;
|
|---|
| 268 | }
|
|---|
| 269 | }
|
|---|
| 270 | req.end();
|
|---|
| 271 | });
|
|---|
| 272 |
|
|---|
| 273 | return fetchRes;
|
|---|
| 274 | }
|
|---|