node_modules: update (#322)

Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
Dawid Dziurla
2026-09-15 13:00:32 +02:00
committed by GitHub
parent 4a3d0f9ca3
commit 67ce3558d6
174 changed files with 25661 additions and 5540 deletions
+96
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;