mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-09-17 17:09:30 +07:00
node_modules: update (#322)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Options for the Cookies jar
|
||||
*/
|
||||
export interface CookiesOptions {
|
||||
/** Lifetime in seconds for cookies that do not set their own expiration (default 1800) */
|
||||
sessionTimeout?: number | string | undefined;
|
||||
}
|
||||
/**
|
||||
* A cookie as parsed from a 'Set-Cookie:' header and kept in the jar
|
||||
*/
|
||||
export interface Cookie {
|
||||
name?: string | undefined;
|
||||
value?: string | undefined;
|
||||
domain?: string | undefined;
|
||||
path?: string | undefined;
|
||||
expires?: Date | undefined;
|
||||
secure?: boolean | undefined;
|
||||
httponly?: boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* Creates a biskviit cookie jar for managing cookie values in memory
|
||||
*
|
||||
* @constructor
|
||||
* @param [options] Optional options object
|
||||
*/
|
||||
export default class Cookies {
|
||||
options: CookiesOptions;
|
||||
cookies: Cookie[];
|
||||
constructor(options?: CookiesOptions);
|
||||
/**
|
||||
* Stores a cookie string to the cookie storage
|
||||
*
|
||||
* @param cookieStr Value from the 'Set-Cookie:' header
|
||||
* @param url Current URL
|
||||
*/
|
||||
set(cookieStr: string, url?: string): boolean;
|
||||
/**
|
||||
* Returns cookie string for the 'Cookie:' header.
|
||||
*
|
||||
* @param url URL to check for
|
||||
* @returns Cookie header or empty string if no matches were found
|
||||
*/
|
||||
get(url?: string): string;
|
||||
/**
|
||||
* Lists all valied cookie objects for the specified URL
|
||||
*
|
||||
* @param url URL to check for
|
||||
* @returns An array of cookie objects
|
||||
*/
|
||||
list(url?: string): Cookie[];
|
||||
/**
|
||||
* Parses cookie string from the 'Set-Cookie:' header
|
||||
*
|
||||
* @param cookieStr String from the 'Set-Cookie:' header
|
||||
* @returns Cookie object
|
||||
*/
|
||||
parse(cookieStr?: string): Cookie;
|
||||
/**
|
||||
* Checks if a cookie object is valid for a specified URL
|
||||
*
|
||||
* @param cookie Cookie object
|
||||
* @param url URL to check for
|
||||
* @returns true if cookie is valid for specifiec URL
|
||||
*/
|
||||
match(cookie: Cookie, url?: string): boolean;
|
||||
/**
|
||||
* Adds (or updates/removes if needed) a cookie object to the cookie storage
|
||||
*
|
||||
* @param cookie Cookie value to be stored
|
||||
*/
|
||||
add(cookie: Cookie): boolean;
|
||||
/**
|
||||
* Checks if two cookie objects are the same
|
||||
*
|
||||
* @param a Cookie to check against
|
||||
* @param b Cookie to check against
|
||||
* @returns True, if the cookies are the same
|
||||
*/
|
||||
compare(a: Cookie, b: Cookie): boolean;
|
||||
/**
|
||||
* Checks if a cookie is expired
|
||||
*
|
||||
* @param cookie Cookie object to check against
|
||||
* @returns True, if the cookie is expired
|
||||
*/
|
||||
isExpired(cookie: Cookie): boolean;
|
||||
/**
|
||||
* Returns the default path for an URL path argument, the default-path of
|
||||
* RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
|
||||
* to the directory of the URL it was set from
|
||||
*
|
||||
* @param pathname
|
||||
* @returns Default path
|
||||
*/
|
||||
getPath(pathname?: string | null): string;
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
// module to handle cookies
|
||||
import net from 'node:net';
|
||||
import * as urllib from '../shared/url.js';
|
||||
const SESSION_TIMEOUT = 1800; // 30 min
|
||||
/**
|
||||
* Creates a biskviit cookie jar for managing cookie values in memory
|
||||
*
|
||||
* @constructor
|
||||
* @param [options] Optional options object
|
||||
*/
|
||||
export default class Cookies {
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.cookies = [];
|
||||
}
|
||||
/**
|
||||
* Stores a cookie string to the cookie storage
|
||||
*
|
||||
* @param cookieStr Value from the 'Set-Cookie:' header
|
||||
* @param url Current URL
|
||||
*/
|
||||
set(cookieStr, url) {
|
||||
const urlparts = urllib.parse(url || '');
|
||||
const cookie = this.parse(cookieStr);
|
||||
let domain;
|
||||
if (cookie.domain) {
|
||||
domain = cookie.domain.replace(/^\./, '');
|
||||
// do not allow cross origin cookies. There is no public suffix list here, so a
|
||||
// multi-label suffix like 'co.uk' can not be told apart from a registrable domain
|
||||
if (
|
||||
// can't be valid if the requested domain is shorter than current hostname
|
||||
urlparts.hostname.length < domain.length ||
|
||||
// a top level domain is not a valid scope, 'Domain=com' would otherwise be
|
||||
// sent to every .com host. A trailing dot does not make 'com.' any better
|
||||
domain.indexOf('.') < 0 ||
|
||||
domain.endsWith('.') ||
|
||||
// an IP address has no subdomains, so cookies set on it stay host-only
|
||||
net.isIP(urlparts.hostname) ||
|
||||
// prefix domains with dot to be sure that partial matches are not used
|
||||
!('.' + urlparts.hostname).endsWith('.' + domain)) {
|
||||
cookie.domain = urlparts.hostname;
|
||||
}
|
||||
}
|
||||
else {
|
||||
cookie.domain = urlparts.hostname;
|
||||
}
|
||||
if (!cookie.path) {
|
||||
cookie.path = this.getPath(urlparts.pathname);
|
||||
}
|
||||
// if no expire date, then use sessionTimeout value
|
||||
if (!cookie.expires) {
|
||||
cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
|
||||
}
|
||||
return this.add(cookie);
|
||||
}
|
||||
/**
|
||||
* Returns cookie string for the 'Cookie:' header.
|
||||
*
|
||||
* @param url URL to check for
|
||||
* @returns Cookie header or empty string if no matches were found
|
||||
*/
|
||||
get(url) {
|
||||
return this.list(url)
|
||||
.map(cookie => cookie.name + '=' + cookie.value)
|
||||
.join('; ');
|
||||
}
|
||||
/**
|
||||
* Lists all valied cookie objects for the specified URL
|
||||
*
|
||||
* @param url URL to check for
|
||||
* @returns An array of cookie objects
|
||||
*/
|
||||
list(url) {
|
||||
const result = [];
|
||||
for (let i = this.cookies.length - 1; i >= 0; i--) {
|
||||
const cookie = this.cookies[i];
|
||||
if (this.isExpired(cookie)) {
|
||||
this.cookies.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
if (this.match(cookie, url)) {
|
||||
result.unshift(cookie);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Parses cookie string from the 'Set-Cookie:' header
|
||||
*
|
||||
* @param cookieStr String from the 'Set-Cookie:' header
|
||||
* @returns Cookie object
|
||||
*/
|
||||
parse(cookieStr) {
|
||||
const cookie = {};
|
||||
(cookieStr || '')
|
||||
.toString()
|
||||
.split(';')
|
||||
.forEach(cookiePart => {
|
||||
const valueParts = cookiePart.split('=');
|
||||
const key = valueParts.shift().trim().toLowerCase();
|
||||
let value = valueParts.join('=').trim();
|
||||
let domain;
|
||||
if (!key) {
|
||||
// skip empty parts
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case 'expires': {
|
||||
const expires = new Date(value);
|
||||
// ignore date if can not parse it
|
||||
if (expires.toString() !== 'Invalid Date') {
|
||||
cookie.expires = expires;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'path':
|
||||
cookie.path = value;
|
||||
break;
|
||||
case 'domain':
|
||||
domain = value.toLowerCase();
|
||||
if (domain.length && domain.charAt(0) !== '.') {
|
||||
domain = '.' + domain; // ensure preceeding dot for user set domains
|
||||
}
|
||||
cookie.domain = domain;
|
||||
break;
|
||||
case 'max-age':
|
||||
cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
|
||||
break;
|
||||
case 'secure':
|
||||
cookie.secure = true;
|
||||
break;
|
||||
case 'httponly':
|
||||
cookie.httponly = true;
|
||||
break;
|
||||
default:
|
||||
if (!cookie.name) {
|
||||
cookie.name = key;
|
||||
cookie.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return cookie;
|
||||
}
|
||||
/**
|
||||
* Checks if a cookie object is valid for a specified URL
|
||||
*
|
||||
* @param cookie Cookie object
|
||||
* @param url URL to check for
|
||||
* @returns true if cookie is valid for specifiec URL
|
||||
*/
|
||||
match(cookie, url) {
|
||||
const urlparts = urllib.parse(url || '');
|
||||
// check if hostname matches
|
||||
// .foo.com also matches subdomains, foo.com does not
|
||||
if (urlparts.hostname !== cookie.domain &&
|
||||
(cookie.domain.charAt(0) !== '.' ||
|
||||
('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)) {
|
||||
return false;
|
||||
}
|
||||
// check if the request path path-matches the cookie path (RFC 6265 section 5.1.4):
|
||||
// identical paths match, otherwise the cookie path must be a directory prefix
|
||||
const pathname = urlparts.pathname || '/';
|
||||
const cookiePath = cookie.path;
|
||||
const pathMatches = pathname === cookiePath ||
|
||||
(pathname.startsWith(cookiePath) && (cookiePath.endsWith('/') || pathname.charAt(cookiePath.length) === '/'));
|
||||
if (!pathMatches) {
|
||||
return false;
|
||||
}
|
||||
// check secure argument
|
||||
if (cookie.secure && urlparts.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Adds (or updates/removes if needed) a cookie object to the cookie storage
|
||||
*
|
||||
* @param cookie Cookie value to be stored
|
||||
*/
|
||||
add(cookie) {
|
||||
// nothing to do here
|
||||
if (!cookie || !cookie.name) {
|
||||
return false;
|
||||
}
|
||||
// overwrite if has same params
|
||||
for (let i = 0, len = this.cookies.length; i < len; i++) {
|
||||
if (this.compare(this.cookies[i], cookie)) {
|
||||
// check if the cookie needs to be removed instead
|
||||
if (this.isExpired(cookie)) {
|
||||
this.cookies.splice(i, 1); // remove expired/unset cookie
|
||||
return false;
|
||||
}
|
||||
this.cookies[i] = cookie;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// add as new if not already expired
|
||||
if (!this.isExpired(cookie)) {
|
||||
this.cookies.push(cookie);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Checks if two cookie objects are the same
|
||||
*
|
||||
* @param a Cookie to check against
|
||||
* @param b Cookie to check against
|
||||
* @returns True, if the cookies are the same
|
||||
*/
|
||||
compare(a, b) {
|
||||
return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === b.httponly;
|
||||
}
|
||||
/**
|
||||
* Checks if a cookie is expired
|
||||
*
|
||||
* @param cookie Cookie object to check against
|
||||
* @returns True, if the cookie is expired
|
||||
*/
|
||||
isExpired(cookie) {
|
||||
return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
|
||||
}
|
||||
/**
|
||||
* Returns the default path for an URL path argument, the default-path of
|
||||
* RFC 6265 section 5.1.4. A cookie that carries no Path attribute is scoped
|
||||
* to the directory of the URL it was set from
|
||||
*
|
||||
* @param pathname
|
||||
* @returns Default path
|
||||
*/
|
||||
getPath(pathname) {
|
||||
const pathParts = (pathname || '/').split('/');
|
||||
pathParts.pop(); // remove filename part
|
||||
const path = pathParts.join('/').trim();
|
||||
// a path that holds no more than one '/' is scoped to the root path, and so
|
||||
// is one that does not start with '/' at all
|
||||
if (path.charAt(0) !== '/') {
|
||||
return '/';
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import http from 'node:http';
|
||||
import { PassThrough, type Readable } from 'node:stream';
|
||||
import Cookies from './cookies.js';
|
||||
/**
|
||||
* Options for nmfetch
|
||||
*/
|
||||
export interface FetchOptions {
|
||||
/** HTTP method, defaults to GET, or to POST when a body is given */
|
||||
method?: string | undefined;
|
||||
/** Request headers, keys are lowercased before use */
|
||||
headers?: http.OutgoingHttpHeaders | undefined;
|
||||
/** Overrides the default User-Agent header */
|
||||
userAgent?: string | undefined;
|
||||
/** Cookie string(s) to seed the cookie jar with for this URL */
|
||||
cookie?: string | string[] | false | undefined;
|
||||
/** Cookie jar shared across redirects, created when missing */
|
||||
cookies?: Cookies | undefined;
|
||||
/** Request body: a readable stream, a Buffer, a form object or a string */
|
||||
body?: Readable | Buffer | {
|
||||
[key: string]: any;
|
||||
} | string | false | undefined;
|
||||
/** Content-Type header for the body, false leaves it out for a stream body */
|
||||
contentType?: string | false | undefined;
|
||||
/** TLS settings, only the keys listed in TLS_OPTION_KEYS are used */
|
||||
tls?: {
|
||||
[key: string]: any;
|
||||
} | undefined;
|
||||
/** Request timeout in milliseconds */
|
||||
timeout?: number | undefined;
|
||||
/** Maximum number of redirects to follow (default 5) */
|
||||
maxRedirects?: number | undefined;
|
||||
/** Resolve responses with a status code of 300 or above instead of emitting an error */
|
||||
allowErrorResponse?: boolean | undefined;
|
||||
/** Redirects followed so far, set by nmfetch itself */
|
||||
redirects?: number | undefined;
|
||||
/** Response stream shared across redirects, set by nmfetch itself */
|
||||
fetchRes?: FetchResponse | undefined;
|
||||
}
|
||||
/**
|
||||
* The stream nmfetch returns. The response body is piped into it, the status code and
|
||||
* headers of the final response are attached once they arrive
|
||||
*/
|
||||
export interface FetchResponse extends PassThrough {
|
||||
statusCode?: number | undefined;
|
||||
headers?: http.IncomingHttpHeaders | undefined;
|
||||
}
|
||||
declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
|
||||
declare namespace nmfetch {
|
||||
var Cookies: typeof import("./cookies.js").default;
|
||||
}
|
||||
export default nmfetch;
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import * as urllib from '../shared/url.js';
|
||||
import zlib from 'node:zlib';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import Cookies from './cookies.js';
|
||||
import * as packageData from '../package-info.js';
|
||||
import net from 'node:net';
|
||||
import * as errors from '../errors.js';
|
||||
import { isProtoKey } from '../shared/objects.js';
|
||||
const MAX_REDIRECTS = 5;
|
||||
// Only genuine TLS settings are taken from options.tls. That object reaches us straight
|
||||
// from a user supplied attachment (content.tls), so keys like host, port, path, socketPath
|
||||
// or lookup would otherwise repoint the request at a destination that never went through
|
||||
// the URL checks below.
|
||||
//
|
||||
// The source of truth is the tls.connect() option list in the Node docs. A key missing
|
||||
// here is dropped silently, so extend this list rather than working around it.
|
||||
const TLS_OPTION_KEYS = [
|
||||
'ALPNProtocols',
|
||||
'ca',
|
||||
'cert',
|
||||
'checkServerIdentity',
|
||||
'ciphers',
|
||||
'crl',
|
||||
'dhparam',
|
||||
'ecdhCurve',
|
||||
'honorCipherOrder',
|
||||
'key',
|
||||
'maxVersion',
|
||||
'minVersion',
|
||||
'passphrase',
|
||||
'pfx',
|
||||
'rejectUnauthorized',
|
||||
'secureContext',
|
||||
'secureOptions',
|
||||
'secureProtocol',
|
||||
'servername',
|
||||
'sessionIdContext',
|
||||
'sigalgs'
|
||||
];
|
||||
/**
|
||||
* Resolves a URL only if it is one this module is willing to request.
|
||||
*
|
||||
* urllib.parse throws for a host that contains forbidden bytes, and it is called for
|
||||
* every URL that reaches nmfetch, including ones that arrive from a message attachment
|
||||
* or from a redirect Location header. An uncaught throw here takes the process down,
|
||||
* so a URL that does not parse is reported the same way as one with a scheme we refuse.
|
||||
*
|
||||
* @param url URL to parse
|
||||
* @returns Parsed URL, or false if it is not a usable http(s) URL
|
||||
*/
|
||||
function parseFetchUrl(url) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = urllib.parse(url);
|
||||
}
|
||||
catch (_err) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
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;
|
||||
const fetchRes = options.fetchRes;
|
||||
const parsed = parseFetchUrl(url);
|
||||
if (!parsed) {
|
||||
// Only http(s) URLs can be fetched. Any other scheme (file:, gopher:, a
|
||||
// protocol-relative redirect target etc.) would otherwise be silently served over
|
||||
// plain HTTP, possibly against an unintended host. Bail out before the cookie jar
|
||||
// is touched so a refused URL can not seed it, and release a caller supplied body:
|
||||
// this is the one exit that runs before the error handler below is attached to it,
|
||||
// so an error on that stream would have nowhere to go and the fd or socket behind
|
||||
// it would never be released.
|
||||
if (options.body && typeof options.body.destroy === 'function') {
|
||||
options.body.on('error', () => false);
|
||||
options.body.destroy();
|
||||
}
|
||||
setImmediate(() => {
|
||||
const err = new Error('Unsupported protocol for URL ' + url);
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
if (options.cookie) {
|
||||
[].concat(options.cookie || []).forEach(cookie => {
|
||||
options.cookies.set(cookie, url);
|
||||
});
|
||||
options.cookie = false;
|
||||
}
|
||||
let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
|
||||
let finished = false;
|
||||
let cookies;
|
||||
let body;
|
||||
const handler = parsed.protocol === 'https:' ? https : http;
|
||||
const headers = {
|
||||
'accept-encoding': 'gzip,deflate',
|
||||
'user-agent': 'nodemailer/' + packageData.version
|
||||
};
|
||||
Object.keys(options.headers || {}).forEach(key => {
|
||||
// options.headers is the caller's httpHeaders, straight off an attachment
|
||||
if (isProtoKey(key.toLowerCase().trim())) {
|
||||
return;
|
||||
}
|
||||
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.code = errors.EFETCH;
|
||||
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 => {
|
||||
const value = options.body[key].toString().trim();
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
|
||||
})
|
||||
.join('&'));
|
||||
}
|
||||
catch (E) {
|
||||
if (finished) {
|
||||
return undefined;
|
||||
}
|
||||
finished = true;
|
||||
E.code = errors.EFETCH;
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
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;
|
||||
const reqOptions = {
|
||||
method,
|
||||
host: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
||||
headers,
|
||||
// Validate TLS certificates by default. Callers that genuinely need to
|
||||
// reach a self-signed/internal host opt out explicitly with
|
||||
// options.tls = { rejectUnauthorized: false }.
|
||||
rejectUnauthorized: true,
|
||||
agent: false
|
||||
};
|
||||
if (options.tls) {
|
||||
// see TLS_OPTION_KEYS
|
||||
Object.keys(options.tls).forEach(key => {
|
||||
if (TLS_OPTION_KEYS.includes(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.code = errors.EFETCH;
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
if (options.timeout) {
|
||||
req.setTimeout(options.timeout, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
req.abort();
|
||||
const err = new Error('Request Timeout');
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
}
|
||||
req.on('error', (err) => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
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;
|
||||
const err = new Error('Maximum redirect count exceeded');
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
// redirect does not include POST body
|
||||
options.method = 'GET';
|
||||
options.body = false;
|
||||
let redirectUrl;
|
||||
try {
|
||||
redirectUrl = urllib.resolve(url, res.headers.location);
|
||||
}
|
||||
catch (_err) {
|
||||
// the legacy resolver throws on a Location the WHATWG parser also refused,
|
||||
// so fall through to the check below with what the server actually sent
|
||||
redirectUrl = res.headers.location;
|
||||
}
|
||||
const redirectParsed = parseFetchUrl(redirectUrl);
|
||||
if (!redirectParsed) {
|
||||
// Refuse the redirect target here rather than leaving it to the recursive
|
||||
// call: that call gets its own `finished` flag and no handle on this
|
||||
// request, so this one would stay open and could emit a second error on
|
||||
// the shared fetchRes once it times out. Callers listen with req.once().
|
||||
finished = true;
|
||||
const err = new Error('Unsupported protocol for URL ' + redirectUrl);
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = redirectUrl;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
// Do not forward credentials when the redirect leaves the original
|
||||
// security context: a different host, or a downgrade from https to
|
||||
// http (which would otherwise put them on the wire in cleartext).
|
||||
// Strip sensitive request headers so an attacker who controls the
|
||||
// redirect target cannot harvest them.
|
||||
const crossHost = redirectParsed.hostname !== parsed.hostname;
|
||||
const downgrade = parsed.protocol === 'https:' && redirectParsed.protocol === 'http:';
|
||||
if (options.headers && (crossHost || downgrade)) {
|
||||
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||
Object.keys(options.headers).forEach(key => {
|
||||
if (sensitive.includes(key.toLowerCase())) {
|
||||
delete options.headers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
return nmfetch(redirectUrl, options);
|
||||
}
|
||||
fetchRes.statusCode = res.statusCode;
|
||||
fetchRes.headers = res.headers;
|
||||
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
||||
finished = true;
|
||||
const err = new Error('Invalid status code ' + res.statusCode);
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
res.on('error', (err) => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
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.code = errors.EFETCH;
|
||||
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);
|
||||
}
|
||||
req.write(body);
|
||||
}
|
||||
catch (err) {
|
||||
finished = true;
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
nmfetch.Cookies = Cookies;
|
||||
export default nmfetch;
|
||||
Reference in New Issue
Block a user