274 lines
8 KiB
JavaScript
274 lines
8 KiB
JavaScript
'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 net = require('net');
|
|
|
|
const MAX_REDIRECTS = 5;
|
|
|
|
module.exports = function (url, options) {
|
|
return nmfetch(url, options);
|
|
};
|
|
|
|
module.exports.Cookies = Cookies;
|
|
|
|
function nmfetch(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];
|
|
});
|
|
}
|
|
|
|
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
|
|
reqOptions.servername = parsed.hostname;
|
|
}
|
|
|
|
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 nmfetch(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;
|
|
}
|