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:
+224
@@ -0,0 +1,224 @@
|
||||
import { isProtoKey, copyOwnKeys } from './objects.js';
|
||||
import os from 'node:os';
|
||||
import type { Readable } from 'node:stream';
|
||||
import type { OutgoingHttpHeaders } from 'node:http';
|
||||
export { isProtoKey, copyOwnKeys };
|
||||
/**
|
||||
* Options for resolveHostname. The object is also handed to dns.Resolver, so its
|
||||
* `timeout` and `tries` settings apply to the lookups
|
||||
*/
|
||||
export interface ResolveHostnameOptions {
|
||||
/** Hostname or IP address to resolve */
|
||||
host?: string | undefined;
|
||||
/** Server name for TLS, used as the host when no host is set */
|
||||
servername?: string | undefined;
|
||||
/** Count loopback interfaces when checking which address families are usable */
|
||||
allowInternalNetworkInterfaces?: boolean | undefined;
|
||||
/** How long a resolved value stays cached, in milliseconds (default 5 minutes) */
|
||||
dnsTtl?: number | undefined;
|
||||
/** Query timeout in milliseconds, passed to dns.Resolver */
|
||||
timeout?: number | undefined;
|
||||
/** Number of query attempts, passed to dns.Resolver */
|
||||
tries?: number | undefined;
|
||||
}
|
||||
/**
|
||||
* Resolved value handed to the resolveHostname callback
|
||||
*/
|
||||
export interface ResolvedHostname {
|
||||
/** Server name to use for TLS, false when an IP literal was given without one */
|
||||
servername?: string | false | undefined;
|
||||
/** Address to connect to, picked at random from the resolved addresses */
|
||||
host?: string | null | undefined;
|
||||
/** Whether the value came from the DNS cache */
|
||||
cached?: boolean | undefined;
|
||||
/** The resolver error when a cached value was used because of it */
|
||||
error?: Error | undefined;
|
||||
}
|
||||
/**
|
||||
* Resolved addresses as stored in the DNS cache
|
||||
*/
|
||||
export interface DnsCacheValue {
|
||||
addresses: string[];
|
||||
}
|
||||
/**
|
||||
* A DNS cache entry
|
||||
*/
|
||||
export interface DnsCacheEntry {
|
||||
value: DnsCacheValue;
|
||||
/** Expiration time as a timestamp, entries without one never expire */
|
||||
expires?: number | undefined;
|
||||
}
|
||||
/**
|
||||
* Configuration object parsed from a connection url. Query parameters become
|
||||
* top level keys, `tls.*` parameters go into `tls`
|
||||
*/
|
||||
export interface ConnectionUrlOptions {
|
||||
secure?: boolean | undefined;
|
||||
direct?: boolean | undefined;
|
||||
port?: number | undefined;
|
||||
host?: string | undefined;
|
||||
/** Well-known service name from the ?service= query parameter */
|
||||
service?: string | undefined;
|
||||
auth?: {
|
||||
user: string;
|
||||
pass: string;
|
||||
} | undefined;
|
||||
tls?: {
|
||||
[key: string]: unknown;
|
||||
} | undefined;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
/**
|
||||
* Structured data attached to a log line. tnx, sid and cid drive the line prefix
|
||||
* of the default console logger
|
||||
*/
|
||||
export interface LogEntry {
|
||||
/** 'server' or 'client' for SMTP transaction lines */
|
||||
tnx?: string | undefined;
|
||||
/** Session id */
|
||||
sid?: string | undefined;
|
||||
/** Connection id */
|
||||
cid?: string | number | undefined;
|
||||
level?: string | undefined;
|
||||
[key: string]: any;
|
||||
}
|
||||
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
/**
|
||||
* A logger supplied by the caller, bunyan style. Any object works, a level it does not
|
||||
* implement is routed to one it does, see _logFunc
|
||||
*/
|
||||
export interface ExternalLogger {
|
||||
trace?(...args: any[]): any;
|
||||
debug?(...args: any[]): any;
|
||||
info?(...args: any[]): any;
|
||||
warn?(...args: any[]): any;
|
||||
error?(...args: any[]): any;
|
||||
fatal?(...args: any[]): any;
|
||||
log?(...args: any[]): any;
|
||||
[level: string]: any;
|
||||
}
|
||||
/**
|
||||
* Options for getLogger
|
||||
*/
|
||||
export interface GetLoggerOptions {
|
||||
/** A bunyan compatible logger, true for the default console logger, false or unset for no logging */
|
||||
logger?: ExternalLogger | boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* The bunyan compatible logger interface returned by getLogger
|
||||
*/
|
||||
export interface Logger {
|
||||
trace(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
debug(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
info(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
warn(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
error(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
fatal(data?: LogEntry, message?: string, ...args: any[]): void;
|
||||
}
|
||||
/**
|
||||
* A parsed data URI
|
||||
*/
|
||||
export interface ParsedDataURI {
|
||||
/** Decoded payload */
|
||||
data: Buffer;
|
||||
/** 'base64', 'utf8' or 'utf-8' when the URI declared one, null otherwise */
|
||||
encoding: string | null;
|
||||
contentType: string;
|
||||
/** Further `key=value` parameters from the metadata section */
|
||||
params: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Access policy for resolveContent
|
||||
*/
|
||||
export interface ResolveContentOptions {
|
||||
/** Reject content that points to a file path */
|
||||
disableFileAccess?: boolean | undefined;
|
||||
/** Reject content that points to a URL */
|
||||
disableUrlAccess?: boolean | undefined;
|
||||
}
|
||||
/**
|
||||
* An object value resolveContent understands. A plain string or Buffer value is returned as
|
||||
* is and a readable stream is read into a Buffer, an object carries the content in `content`
|
||||
* or points to it with `path` or `href`
|
||||
*/
|
||||
export interface ContentDescriptor {
|
||||
/** The content itself, a string, a Buffer or a readable stream */
|
||||
content?: string | Buffer | Readable | undefined;
|
||||
/** Encoding of a string `content`, it is decoded into a Buffer unless it is utf8 or ascii */
|
||||
encoding?: string | undefined;
|
||||
/** File path, http(s) URL or data URI to read the content from */
|
||||
path?: string | undefined;
|
||||
/** URL to fetch the content from */
|
||||
href?: string | undefined;
|
||||
/** Request headers for a URL fetch */
|
||||
httpHeaders?: OutgoingHttpHeaders | undefined;
|
||||
/** TLS settings for a URL fetch, see nmfetch */
|
||||
tls?: {
|
||||
[key: string]: any;
|
||||
} | undefined;
|
||||
}
|
||||
export type ResolveContentCallback = (err: Error | null, value?: any) => void;
|
||||
export declare let networkInterfaces: NodeJS.Dict<os.NetworkInterfaceInfo[]> | undefined;
|
||||
export declare const dnsCache: Map<string, DnsCacheEntry>;
|
||||
export declare const resolveHostname: (options: ResolveHostnameOptions | undefined, callback: (err: Error | null, result?: ResolvedHostname) => void) => void;
|
||||
/**
|
||||
* Parses connection url to a structured configuration object
|
||||
*
|
||||
* @param str Connection url
|
||||
* @return Configuration object
|
||||
*/
|
||||
export declare const parseConnectionUrl: (str?: string | null) => ConnectionUrlOptions;
|
||||
/**
|
||||
* Returns a bunyan-compatible logger interface. Uses either provided logger or
|
||||
* creates a default console logger
|
||||
*
|
||||
* @param [options] Options object that might include 'logger' value
|
||||
* @return bunyan compatible logger
|
||||
*/
|
||||
export declare const getLogger: (options?: GetLoggerOptions, defaults?: LogEntry) => Logger;
|
||||
/**
|
||||
* Wrapper for creating a callback that either resolves or rejects a promise
|
||||
* based on input
|
||||
*
|
||||
* @param resolve Function to run if callback is called
|
||||
* @param reject Function to run if callback ends with an error
|
||||
*/
|
||||
export declare const callbackPromise: (resolve: (...args: any[]) => void, reject: (reason?: any) => void) => (...args: any[]) => void;
|
||||
export declare const parseDataURI: (uri: unknown) => ParsedDataURI | null;
|
||||
/**
|
||||
* Resolves a String or a Buffer value for content value. Useful if the value
|
||||
* is a Stream or a file or an URL. If the value is a Stream, overwrites
|
||||
* the stream object with the resolved value (you can't stream a value twice).
|
||||
*
|
||||
* This is useful when you want to create a plugin that needs a content value,
|
||||
* for example the `html` or `text` value as a String or a Buffer but not as
|
||||
* a file path or an URL.
|
||||
*
|
||||
* @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands
|
||||
* @param key Property name or an Array index
|
||||
* @param [options] Optional access policy: { disableFileAccess, disableUrlAccess }
|
||||
* @param callback Callback function with (err, value)
|
||||
*/
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, callback: ResolveContentCallback): void;
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback): void;
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, options?: ResolveContentOptions | false): Promise<any>;
|
||||
export declare function resolveContent(data: {
|
||||
[key: string]: any;
|
||||
}, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback | undefined): Promise<any> | void;
|
||||
/**
|
||||
* Copies properties from source objects to target objects
|
||||
*/
|
||||
export declare const assign: (...args: ({
|
||||
[key: string]: any;
|
||||
} | false | null | undefined)[]) => {
|
||||
[key: string]: any;
|
||||
};
|
||||
export declare const encodeXText: (str: string) => string;
|
||||
+628
@@ -0,0 +1,628 @@
|
||||
/* eslint no-console: 0 */
|
||||
import * as urllib from './url.js';
|
||||
import util from 'node:util';
|
||||
import fs from 'node:fs';
|
||||
import nmfetch from '../fetch/index.js';
|
||||
import * as errors from '../errors.js';
|
||||
import { isProtoKey, copyOwnKeys } from './objects.js';
|
||||
import dns from 'node:dns';
|
||||
import net from 'node:net';
|
||||
import os from 'node:os';
|
||||
// re-exported for the callers that already depend on this module, see ./objects
|
||||
export { isProtoKey, copyOwnKeys };
|
||||
const DNS_TTL = 5 * 60 * 1000;
|
||||
const CACHE_CLEANUP_INTERVAL = 30 * 1000; // Minimum 30 seconds between cleanups
|
||||
const MAX_CACHE_SIZE = 1000; // Maximum number of entries in cache
|
||||
let lastCacheCleanup = 0;
|
||||
/** @internal */
|
||||
export const _lastCacheCleanup = () => lastCacheCleanup;
|
||||
/** @internal */
|
||||
export const _resetCacheCleanup = () => {
|
||||
lastCacheCleanup = 0;
|
||||
};
|
||||
export let networkInterfaces;
|
||||
try {
|
||||
networkInterfaces = os.networkInterfaces();
|
||||
}
|
||||
catch (_err) {
|
||||
// fails on some systems
|
||||
}
|
||||
const isFamilySupported = (family, allowInternal) => {
|
||||
const addresses = Object.values(networkInterfaces || {}).flat();
|
||||
if (!addresses.length) {
|
||||
// hope for the best. Runtimes without an interface table (Cloudflare
|
||||
// Workers) report an empty object rather than throwing
|
||||
return true;
|
||||
}
|
||||
return addresses.filter(i => !i.internal || allowInternal).some(i => i.family === 'IPv' + family || i.family === family);
|
||||
};
|
||||
const resolve = (family, hostname, options, callback) => {
|
||||
options = options || {};
|
||||
if (!isFamilySupported(family, options.allowInternalNetworkInterfaces)) {
|
||||
return callback(null, []);
|
||||
}
|
||||
const dnsResolver = dns.Resolver ? new dns.Resolver(options) : dns;
|
||||
dnsResolver['resolve' + family](hostname, (err, addresses) => {
|
||||
if (err) {
|
||||
switch (err.code) {
|
||||
case dns.NODATA:
|
||||
case dns.NOTFOUND:
|
||||
case dns.NOTIMP:
|
||||
case dns.SERVFAIL:
|
||||
case dns.CONNREFUSED:
|
||||
case dns.REFUSED:
|
||||
case 'EAI_AGAIN':
|
||||
return callback(null, []);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
|
||||
});
|
||||
};
|
||||
export const dnsCache = new Map();
|
||||
const formatDNSValue = (value, extra) => {
|
||||
if (!value) {
|
||||
return Object.assign({}, extra || {});
|
||||
}
|
||||
const addresses = value.addresses || [];
|
||||
// Select a random address from available addresses, or null if none
|
||||
const host = addresses.length > 0 ? addresses[Math.floor(Math.random() * addresses.length)] : null;
|
||||
return Object.assign({
|
||||
host,
|
||||
// Include all addresses for connection fallback support
|
||||
_addresses: addresses
|
||||
}, extra || {});
|
||||
};
|
||||
export const resolveHostname = (options, callback) => {
|
||||
options = options || {};
|
||||
if (!options.host && options.servername) {
|
||||
options.host = options.servername;
|
||||
}
|
||||
if (!options.host || net.isIP(options.host)) {
|
||||
// nothing to do here
|
||||
const value = {
|
||||
addresses: [options.host]
|
||||
};
|
||||
return callback(null, formatDNSValue(value, {
|
||||
servername: options.servername || false,
|
||||
cached: false
|
||||
}));
|
||||
}
|
||||
const host = options.host;
|
||||
// The TLS server name belongs to the connection asking, not to the host it resolves. The
|
||||
// cache is shared by every transport of the process and keyed by host alone, so a server
|
||||
// name stored in it would be the one of whichever transport resolved the host first, and a
|
||||
// later transport with its own tls.servername would present and verify that name instead
|
||||
const servername = options.servername || host;
|
||||
let cached;
|
||||
if (dnsCache.has(options.host)) {
|
||||
cached = dnsCache.get(options.host);
|
||||
// Lazy cleanup with time throttling
|
||||
const now = Date.now();
|
||||
if (now - lastCacheCleanup > CACHE_CLEANUP_INTERVAL) {
|
||||
lastCacheCleanup = now;
|
||||
// Clean up expired entries
|
||||
for (const [host, entry] of dnsCache.entries()) {
|
||||
if (entry.expires && entry.expires < now) {
|
||||
dnsCache.delete(host);
|
||||
}
|
||||
}
|
||||
// If cache is still too large, remove oldest entries
|
||||
if (dnsCache.size > MAX_CACHE_SIZE) {
|
||||
const toDelete = Math.floor(MAX_CACHE_SIZE * 0.1); // Remove 10% of entries
|
||||
const keys = Array.from(dnsCache.keys()).slice(0, toDelete);
|
||||
keys.forEach(key => dnsCache.delete(key));
|
||||
}
|
||||
}
|
||||
if (!cached.expires || cached.expires >= now) {
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Resolve both IPv4 and IPv6 addresses for fallback support
|
||||
let ipv4Addresses = [];
|
||||
let ipv6Addresses = [];
|
||||
let ipv4Error = null;
|
||||
let ipv6Error = null;
|
||||
resolve(4, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
ipv4Error = err;
|
||||
}
|
||||
else {
|
||||
ipv4Addresses = addresses || [];
|
||||
}
|
||||
resolve(6, host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
ipv6Error = err;
|
||||
}
|
||||
else {
|
||||
ipv6Addresses = addresses || [];
|
||||
}
|
||||
// Combine addresses: IPv4 first, then IPv6
|
||||
const allAddresses = ipv4Addresses.concat(ipv6Addresses);
|
||||
if (allAddresses.length) {
|
||||
const value = {
|
||||
addresses: allAddresses
|
||||
};
|
||||
dnsCache.set(host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(value, {
|
||||
servername,
|
||||
cached: false
|
||||
}));
|
||||
}
|
||||
// No addresses from resolve4/resolve6, try dns.lookup as fallback
|
||||
if (ipv4Error && ipv6Error) {
|
||||
// Both resolvers had errors
|
||||
if (cached) {
|
||||
dnsCache.set(host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true,
|
||||
error: ipv4Error
|
||||
}));
|
||||
}
|
||||
}
|
||||
try {
|
||||
dns.lookup(host, { all: true }, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
dnsCache.set(host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true,
|
||||
error: err
|
||||
}));
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
// Get all supported addresses from dns.lookup
|
||||
const supportedAddresses = addresses
|
||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
||||
: [];
|
||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||
// there are addresses but none can be used
|
||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||
}
|
||||
if (!supportedAddresses.length && cached) {
|
||||
// nothing was found, fallback to cached value
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true
|
||||
}));
|
||||
}
|
||||
const value = {
|
||||
addresses: supportedAddresses.length ? supportedAddresses : [host]
|
||||
};
|
||||
dnsCache.set(host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(value, {
|
||||
servername,
|
||||
cached: false
|
||||
}));
|
||||
});
|
||||
}
|
||||
catch (lookupErr) {
|
||||
if (cached) {
|
||||
dnsCache.set(host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
return callback(null, formatDNSValue(cached.value, {
|
||||
servername,
|
||||
cached: true,
|
||||
error: lookupErr
|
||||
}));
|
||||
}
|
||||
return callback(ipv4Error || ipv6Error || lookupErr);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Parses connection url to a structured configuration object
|
||||
*
|
||||
* @param str Connection url
|
||||
* @return Configuration object
|
||||
*/
|
||||
export const parseConnectionUrl = (str) => {
|
||||
str = str || '';
|
||||
const options = {};
|
||||
const url = urllib.parse(str, true);
|
||||
switch (url.protocol) {
|
||||
case 'smtp:':
|
||||
options.secure = false;
|
||||
break;
|
||||
case 'smtps:':
|
||||
options.secure = true;
|
||||
break;
|
||||
case 'direct:':
|
||||
options.direct = true;
|
||||
break;
|
||||
}
|
||||
if (!isNaN(url.port) && Number(url.port)) {
|
||||
options.port = Number(url.port);
|
||||
}
|
||||
if (url.hostname) {
|
||||
options.host = url.hostname;
|
||||
}
|
||||
if (url.username || url.password) {
|
||||
options.auth = {
|
||||
user: url.username || '',
|
||||
pass: url.password || ''
|
||||
};
|
||||
}
|
||||
Object.keys(url.query || {}).forEach(key => {
|
||||
let obj = options;
|
||||
let lKey = key;
|
||||
let value = url.query[key];
|
||||
if (!isNaN(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
switch (value) {
|
||||
case 'true':
|
||||
value = true;
|
||||
break;
|
||||
case 'false':
|
||||
value = false;
|
||||
break;
|
||||
}
|
||||
// tls is nested object
|
||||
if (key.indexOf('tls.') === 0) {
|
||||
lKey = key.substr(4);
|
||||
if (!options.tls) {
|
||||
options.tls = {};
|
||||
}
|
||||
obj = options.tls;
|
||||
}
|
||||
else if (key.indexOf('.') >= 0) {
|
||||
// ignore nested properties besides tls
|
||||
return;
|
||||
}
|
||||
// `in` already keeps "__proto__" out, but only as a side effect of it being an
|
||||
// Object.prototype member. Say it, so the protection survives a change to the check
|
||||
if (!isProtoKey(lKey) && !(lKey in obj)) {
|
||||
obj[lKey] = value;
|
||||
}
|
||||
});
|
||||
return options;
|
||||
};
|
||||
/** @internal */
|
||||
export const _logFunc = (logger, level, defaults, data, message, ...args) => {
|
||||
const entry = Object.assign({}, defaults || {}, data || {});
|
||||
delete entry.level;
|
||||
let logLevel = level;
|
||||
if (typeof logger[logLevel] !== 'function') {
|
||||
// Provided logger does not implement this level. Fall back to a
|
||||
// lower-severity handler instead of throwing.
|
||||
logLevel = ['info', 'debug', 'log', 'trace', 'warn', 'error'].find(name => typeof logger[name] === 'function');
|
||||
}
|
||||
if (logLevel) {
|
||||
logger[logLevel](entry, message, ...args);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Returns a bunyan-compatible logger interface. Uses either provided logger or
|
||||
* creates a default console logger
|
||||
*
|
||||
* @param [options] Options object that might include 'logger' value
|
||||
* @return bunyan compatible logger
|
||||
*/
|
||||
export const getLogger = (options, defaults) => {
|
||||
options = options || {};
|
||||
const response = {};
|
||||
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
||||
if (!options.logger) {
|
||||
// use vanity logger
|
||||
levels.forEach(level => {
|
||||
response[level] = () => false;
|
||||
});
|
||||
return response;
|
||||
}
|
||||
const logger = options.logger === true ? createDefaultLogger(levels) : options.logger;
|
||||
levels.forEach(level => {
|
||||
response[level] = (data, message, ...args) => {
|
||||
_logFunc(logger, level, defaults, data, message, ...args);
|
||||
};
|
||||
});
|
||||
return response;
|
||||
};
|
||||
/**
|
||||
* Wrapper for creating a callback that either resolves or rejects a promise
|
||||
* based on input
|
||||
*
|
||||
* @param resolve Function to run if callback is called
|
||||
* @param reject Function to run if callback ends with an error
|
||||
*/
|
||||
export const callbackPromise = (resolve, reject) => function (...args) {
|
||||
const err = args.shift();
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve(...args);
|
||||
}
|
||||
};
|
||||
export const parseDataURI = (uri) => {
|
||||
if (typeof uri !== 'string') {
|
||||
return null;
|
||||
}
|
||||
// Early return for non-data URIs to avoid unnecessary processing
|
||||
if (!uri.startsWith('data:')) {
|
||||
return null;
|
||||
}
|
||||
// Find the first comma safely - this prevents ReDoS
|
||||
const commaPos = uri.indexOf(',');
|
||||
if (commaPos === -1) {
|
||||
return null;
|
||||
}
|
||||
const data = uri.substring(commaPos + 1);
|
||||
const metaStr = uri.substring('data:'.length, commaPos);
|
||||
let encoding;
|
||||
const metaEntries = metaStr.split(';');
|
||||
if (metaEntries.length > 0) {
|
||||
const lastEntry = metaEntries[metaEntries.length - 1].toLowerCase().trim();
|
||||
// Only recognize valid encoding types to prevent manipulation
|
||||
if (['base64', 'utf8', 'utf-8'].includes(lastEntry) && lastEntry.indexOf('=') === -1) {
|
||||
encoding = lastEntry;
|
||||
metaEntries.pop();
|
||||
}
|
||||
}
|
||||
const contentType = metaEntries.length > 0 ? metaEntries.shift() : 'application/octet-stream';
|
||||
const params = {};
|
||||
for (let i = 0; i < metaEntries.length; i++) {
|
||||
const entry = metaEntries[i];
|
||||
const sepPos = entry.indexOf('=');
|
||||
if (sepPos > 0) {
|
||||
// Ensure there's a key before the '='
|
||||
const key = entry.substring(0, sepPos).trim();
|
||||
const value = entry.substring(sepPos + 1).trim();
|
||||
if (key && !isProtoKey(key)) {
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Decode data based on encoding with proper error handling
|
||||
let bufferData;
|
||||
try {
|
||||
if (encoding === 'base64') {
|
||||
bufferData = Buffer.from(data, 'base64');
|
||||
}
|
||||
else {
|
||||
try {
|
||||
bufferData = Buffer.from(decodeURIComponent(data));
|
||||
}
|
||||
catch (_decodeError) {
|
||||
bufferData = Buffer.from(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (_bufferError) {
|
||||
bufferData = Buffer.alloc(0);
|
||||
}
|
||||
return {
|
||||
data: bufferData,
|
||||
encoding: encoding || null,
|
||||
contentType: contentType || 'application/octet-stream',
|
||||
params
|
||||
};
|
||||
};
|
||||
export function resolveContent(data, key, options, callback) {
|
||||
// options is optional; support the legacy resolveContent(data, key, callback) signature
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = false;
|
||||
}
|
||||
options = options || {};
|
||||
let promise;
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
resolveContentValue(data, key, options, callback);
|
||||
return promise;
|
||||
}
|
||||
function resolveContentValue(data, key, options, callback) {
|
||||
let content = (data && data[key] && data[key].content) || data[key];
|
||||
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/[-_\s]/g, '');
|
||||
if (!content) {
|
||||
return callback(null, content);
|
||||
}
|
||||
if (typeof content === 'object') {
|
||||
if (typeof content.pipe === 'function') {
|
||||
return resolveStream(content, (err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
// we can't stream twice the same content, so we need
|
||||
// to replace the stream object with the streaming result
|
||||
if (data[key].content) {
|
||||
data[key].content = value;
|
||||
}
|
||||
else {
|
||||
data[key] = value;
|
||||
}
|
||||
callback(null, value);
|
||||
});
|
||||
}
|
||||
else if (/^data:/i.test(content.path || content.href)) {
|
||||
const parsedDataUri = parseDataURI(content.path || content.href);
|
||||
return callback(null, parsedDataUri && parsedDataUri.data ? parsedDataUri.data : Buffer.alloc(0));
|
||||
}
|
||||
else if (content.href || /^https?:\/\//i.test(content.path)) {
|
||||
// An href is always a URL, and so is a path that looks like one. Let nmfetch
|
||||
// decide whether it is fetchable, it validates the parsed URL. Testing the raw
|
||||
// string here instead would let a file: href fall through to the "return as is"
|
||||
// default below and travel on inside the resolved message.
|
||||
const url = content.href || content.path;
|
||||
if (options.disableUrlAccess) {
|
||||
setImmediate(() => {
|
||||
const err = new Error('Url access rejected for ' + url);
|
||||
err.code = errors.EURLACCESS;
|
||||
callback(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
return resolveStream(nmfetch(url, { headers: content.httpHeaders, tls: content.tls }), callback);
|
||||
}
|
||||
else if (content.path) {
|
||||
if (options.disableFileAccess) {
|
||||
setImmediate(() => {
|
||||
const err = new Error('File access rejected for ' + content.path);
|
||||
err.code = errors.EFILEACCESS;
|
||||
callback(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
return resolveStream(fs.createReadStream(content.path), callback);
|
||||
}
|
||||
}
|
||||
if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
|
||||
content = Buffer.from(data[key].content, encoding);
|
||||
}
|
||||
// default action, return as is
|
||||
setImmediate(() => callback(null, content));
|
||||
}
|
||||
/**
|
||||
* Copies properties from source objects to target objects
|
||||
*/
|
||||
export const assign = function (...args) {
|
||||
const target = args.shift() || {};
|
||||
args.forEach(source => {
|
||||
Object.keys(source || {}).forEach(key => {
|
||||
if (isProtoKey(key)) {
|
||||
return;
|
||||
}
|
||||
if (['tls', 'auth'].includes(key) &&
|
||||
source[key] &&
|
||||
typeof source[key] === 'object') {
|
||||
// tls and auth are special keys that need to be enumerated separately
|
||||
// other objects are passed as is. Enumerating is a copy of user supplied
|
||||
// keys just like the loop above, so it gets the same treatment
|
||||
target[key] = copyOwnKeys(target[key] || {}, source[key]);
|
||||
}
|
||||
else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
return target;
|
||||
};
|
||||
export const encodeXText = (str) => {
|
||||
// ! 0x21
|
||||
// + 0x2B
|
||||
// = 0x3D
|
||||
// ~ 0x7E
|
||||
if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
const buf = Buffer.from(str);
|
||||
let result = '';
|
||||
for (let i = 0, len = buf.length; i < len; i++) {
|
||||
const c = buf[i];
|
||||
if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
|
||||
result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
|
||||
}
|
||||
else {
|
||||
result += String.fromCharCode(c);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
/**
|
||||
* Streams a stream value into a Buffer
|
||||
*
|
||||
* @param stream Readable stream
|
||||
* @param callback Callback function with (err, value)
|
||||
*/
|
||||
function resolveStream(stream, callback) {
|
||||
let responded = false;
|
||||
const chunks = [];
|
||||
let chunklen = 0;
|
||||
stream.on('error', err => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
callback(err);
|
||||
});
|
||||
stream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = stream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
stream.on('end', () => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
let value;
|
||||
try {
|
||||
value = Buffer.concat(chunks, chunklen);
|
||||
}
|
||||
catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
callback(null, value);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Generates a bunyan-like logger that prints to console
|
||||
*
|
||||
* @returns Bunyan logger instance
|
||||
*/
|
||||
function createDefaultLogger(levels) {
|
||||
const levelMaxLen = levels.reduce((max, level) => Math.max(max, level.length), 0);
|
||||
const levelNames = new Map();
|
||||
levels.forEach(level => {
|
||||
let levelName = level.toUpperCase();
|
||||
if (levelName.length < levelMaxLen) {
|
||||
levelName += ' '.repeat(levelMaxLen - levelName.length);
|
||||
}
|
||||
levelNames.set(level, levelName);
|
||||
});
|
||||
const print = (level, entry, message, ...args) => {
|
||||
let prefix = '';
|
||||
if (entry) {
|
||||
if (entry.tnx === 'server') {
|
||||
prefix = 'S: ';
|
||||
}
|
||||
else if (entry.tnx === 'client') {
|
||||
prefix = 'C: ';
|
||||
}
|
||||
if (entry.sid) {
|
||||
prefix = '[' + entry.sid + '] ' + prefix;
|
||||
}
|
||||
if (entry.cid) {
|
||||
prefix = '[#' + entry.cid + '] ' + prefix;
|
||||
}
|
||||
}
|
||||
message = util.format(message, ...args);
|
||||
message.split(/\r?\n/).forEach((line) => {
|
||||
console.log('[%s] %s %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), levelNames.get(level), prefix + line);
|
||||
});
|
||||
};
|
||||
const logger = {};
|
||||
levels.forEach(level => {
|
||||
logger[level] = print.bind(null, level);
|
||||
});
|
||||
return logger;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Detects a key that can not be copied onto a plain object with `target[key] = value`.
|
||||
*
|
||||
* "__proto__" is the only one: assigning it runs the inherited setter and replaces the
|
||||
* prototype of the target instead of adding a property to it, so a caller can smuggle
|
||||
* values past validation that only inspects own keys. JSON.parse produces such a key
|
||||
* where an object literal can not. "constructor" and "prototype" have no such setter and
|
||||
* become ordinary own properties, so dropping them would only discard legitimate values.
|
||||
*
|
||||
* @param key Key to check
|
||||
* @returns true if the key must not be copied
|
||||
*/
|
||||
export declare const isProtoKey: (key: string) => boolean;
|
||||
/**
|
||||
* Copies own enumerable keys from a source object to a target object. Every copy that
|
||||
* walks the keys of user supplied data goes through here, see isProtoKey.
|
||||
*
|
||||
* @param target Object to copy the keys to
|
||||
* @param source Object to copy the keys from
|
||||
* @param [skip] Optional predicate, return true to leave a key out
|
||||
* @returns The target object
|
||||
*/
|
||||
export declare const copyOwnKeys: <T extends object>(target: T, source: object | null | undefined, skip?: (key: string) => boolean) => T;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Safe copying of objects whose keys come from the caller.
|
||||
//
|
||||
// This lives in its own leaf module, like ./url.ts, so that every layer can reach it.
|
||||
// src/shared/index.ts imports src/fetch, so src/fetch can not import src/shared back,
|
||||
// and src/mime-funcs is a leaf that would otherwise pull in dns/net/os/fs for a string
|
||||
// comparison. src/shared/index.ts re-exports both functions for the callers that already
|
||||
// depend on it.
|
||||
/**
|
||||
* Detects a key that can not be copied onto a plain object with `target[key] = value`.
|
||||
*
|
||||
* "__proto__" is the only one: assigning it runs the inherited setter and replaces the
|
||||
* prototype of the target instead of adding a property to it, so a caller can smuggle
|
||||
* values past validation that only inspects own keys. JSON.parse produces such a key
|
||||
* where an object literal can not. "constructor" and "prototype" have no such setter and
|
||||
* become ordinary own properties, so dropping them would only discard legitimate values.
|
||||
*
|
||||
* @param key Key to check
|
||||
* @returns true if the key must not be copied
|
||||
*/
|
||||
export const isProtoKey = (key) => key === '__proto__';
|
||||
/**
|
||||
* Copies own enumerable keys from a source object to a target object. Every copy that
|
||||
* walks the keys of user supplied data goes through here, see isProtoKey.
|
||||
*
|
||||
* @param target Object to copy the keys to
|
||||
* @param source Object to copy the keys from
|
||||
* @param [skip] Optional predicate, return true to leave a key out
|
||||
* @returns The target object
|
||||
*/
|
||||
export const copyOwnKeys = (target, source, skip) => {
|
||||
Object.keys(source || {}).forEach(key => {
|
||||
if (isProtoKey(key) || (skip && skip(key))) {
|
||||
return;
|
||||
}
|
||||
target[key] = source[key];
|
||||
});
|
||||
return target;
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Parsed URL in the shape of the legacy `url.parse()` result
|
||||
*/
|
||||
export interface ParsedUrl {
|
||||
protocol: string | null;
|
||||
host: string | null;
|
||||
hostname: string | null;
|
||||
port: string | null;
|
||||
pathname: string | null;
|
||||
search: string | null;
|
||||
path: string | null;
|
||||
href: string;
|
||||
auth: string | null;
|
||||
/** Decoded user name, null when the URL carries no credentials */
|
||||
username: string | null;
|
||||
/** Decoded password, null when the URL carries none */
|
||||
password: string | null;
|
||||
query: string | null | Record<string, string | string[]>;
|
||||
}
|
||||
export declare const parse: (input?: string | null, parseQueryString?: boolean) => ParsedUrl;
|
||||
export declare const resolve: (from: string, to: string) => string;
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
// URL parsing wrapper around the WHATWG `URL` class. It only falls back to the
|
||||
// legacy, deprecation-warning-emitting `url.parse()` / `url.resolve()` for input
|
||||
// the WHATWG parser rejects.
|
||||
//
|
||||
// The WHATWG `URL` exposes a different shape than the legacy parser, so results
|
||||
// are normalized back into the legacy field names the rest of the codebase reads
|
||||
// (`protocol`, `hostname`, `port`, `pathname`, `path`, `search`, `auth`, `query`,
|
||||
// `href`). This keeps every existing call site unchanged.
|
||||
//
|
||||
// Known, accepted divergences from the legacy parser:
|
||||
// - non-special schemes (smtp:/smtps:/direct:) are not host-lowercased by
|
||||
// WHATWG; cosmetic only, SMTP/DNS hosts are case-insensitive. (IDNA mapping
|
||||
// and IPv6 brackets are normalized back by normalizeHostname below.)
|
||||
// - a literal unescaped ':' inside a password is percent-encoded by WHATWG;
|
||||
// such passwords should be percent-encoded by the caller anyway.
|
||||
import net from 'node:net';
|
||||
import urllib from 'node:url';
|
||||
import * as punycode from '../punycode/index.js';
|
||||
// Matches a "scheme:" not followed by "//" (and with something after it), used
|
||||
// to re-insert the authority separator the legacy parser did not require.
|
||||
const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)([\s\S]+)$/;
|
||||
// Leading and trailing C0 controls and spaces, which the WHATWG parser strips before it
|
||||
// looks at the input. Stripped up front so that the slash-less form is recognized in a
|
||||
// value read from a file with a trailing newline as well.
|
||||
const SURROUNDING_WHITESPACE = /^[\x00-\x20]+|[\x00-\x20]+$/g;
|
||||
// Leading characters legacy url.parse() skips before it reads the scheme
|
||||
const LEGACY_TRIM = /^[\x00-\x20\u00a0\ufeff]+/;
|
||||
// The authority of a "scheme://authority/..." or a scheme-relative "//authority/..."
|
||||
// string: the scheme, if any (anything the legacy parser takes for one, it is less strict
|
||||
// than WHATWG about the first character), and what the legacy parser has to report as the
|
||||
// host once the userinfo is removed, see legacyParse. The legacy parser treats a backslash
|
||||
// as a slash.
|
||||
const AUTHORITY = /^([a-zA-Z0-9+.-]+:)?[\\/]{2}([^\\/?#]*)/;
|
||||
// The WHATWG forbidden domain code points, except '%' which an opaque host may carry: the
|
||||
// C0 control characters, space, DEL and the URL delimiters. CONTROL_CHARS is the C0 and DEL
|
||||
// subset, checked on its own in legacyParse where the host string still carries the port
|
||||
// and IPv6 brackets.
|
||||
const FORBIDDEN_HOST_CHARS = /[\x00-\x20#/:<>?@[\\\]^|\x7f]/;
|
||||
const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
|
||||
// The error the WHATWG parser throws, for a host that only fails the checks in this module
|
||||
function invalidUrl(input) {
|
||||
const err = new TypeError('Invalid URL');
|
||||
err.code = 'ERR_INVALID_URL';
|
||||
err.input = input;
|
||||
return err;
|
||||
}
|
||||
// Legacy url.parse() for input the WHATWG parser refused. The legacy parser does not
|
||||
// reject a host it can not represent: a NUL byte, a percent-encoded byte, a space or a
|
||||
// '<' inside the host ends the host early and the rest becomes the path, so
|
||||
// 'localhost%00.example.com' silently turns into a request to 'localhost'. When the
|
||||
// input has an authority that the legacy parser reads as the host (always for
|
||||
// "scheme://", for "//host" only when slashesDenoteHost is set, as url.resolve() does
|
||||
// for its target), the legacy result is accepted only if that host is the whole written
|
||||
// authority, lowercased and IDNA mapped the way the legacy parser does it, and carries
|
||||
// no control character. A legacy result with a host (an empty one included, 'http:///x'
|
||||
// is a request to localhost) that no such authority accounts for is refused as well.
|
||||
// Otherwise the WHATWG error is reported. Relative input has no authority to check and
|
||||
// keeps the legacy behavior.
|
||||
function legacyParse(input, parseQueryString, whatwgError, slashesDenoteHost) {
|
||||
const parsed = urllib.parse(input, parseQueryString, slashesDenoteHost);
|
||||
const authority = AUTHORITY.exec(input.replace(LEGACY_TRIM, ''));
|
||||
if (authority && (authority[1] || parsed.hostname !== null)) {
|
||||
const written = authority[2].slice(authority[2].lastIndexOf('@') + 1);
|
||||
if (!written || CONTROL_CHARS.test(written) || (parsed.host || '').toLowerCase() !== punycode.toASCII(written.toLowerCase())) {
|
||||
throw whatwgError;
|
||||
}
|
||||
// the legacy parser takes any bracketed value for an IPv6 literal
|
||||
if (written.charAt(0) === '[' && !net.isIPv6(written.slice(1, written.indexOf(']')))) {
|
||||
throw whatwgError;
|
||||
}
|
||||
}
|
||||
else if (parsed.hostname !== null) {
|
||||
throw whatwgError;
|
||||
}
|
||||
// the legacy parser only offers the joined form, split it on the first colon
|
||||
const legacyAuth = parsed.auth === null || parsed.auth === undefined ? null : parsed.auth.split(':');
|
||||
const result = parsed;
|
||||
result.username = legacyAuth ? legacyAuth.shift() : null;
|
||||
result.password = legacyAuth && legacyAuth.length ? legacyAuth.join(':') : null;
|
||||
return result;
|
||||
}
|
||||
// decodeURIComponent that never throws. Legacy url.parse() decodes the auth
|
||||
// component but tolerates malformed percent sequences, so mirror that.
|
||||
function safeDecode(str) {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
}
|
||||
catch (_err) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
// Derives the legacy-shaped bare hostname from a WHATWG URL. WHATWG keeps IPv6
|
||||
// literals bracketed ('[::1]') and, for non-special schemes (smtp:/smtps:/socks:),
|
||||
// percent-encodes a non-ASCII host instead of IDNA-mapping it. Both forms are
|
||||
// un-resolvable when handed to net/dns/http.request, which is what every call
|
||||
// site does, so map them back to what legacy url.parse() returned: the bare
|
||||
// address and the IDNA mapped (lowercased, punycode) form. Idempotent on plain
|
||||
// ASCII and already-punycode hosts, so special-scheme hosts (already IDNA-mapped
|
||||
// by WHATWG) pass through.
|
||||
function normalizeHostname(raw, href) {
|
||||
const hostname = raw || '';
|
||||
if (!hostname) {
|
||||
// Host-less URL (e.g. 'direct:'): legacy returned '' here, not null;
|
||||
// consumers do `hostname.length` / `'.' + hostname`, so keep it a string.
|
||||
return '';
|
||||
}
|
||||
if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
|
||||
return hostname.slice(1, -1);
|
||||
}
|
||||
const decoded = safeDecode(hostname);
|
||||
// domainToASCII applies the WHATWG host rules (IDNA mapping included) and returns an
|
||||
// empty string for a host it refuses, the forbidden characters among them
|
||||
const mapped = FORBIDDEN_HOST_CHARS.test(decoded) ? '' : urllib.domainToASCII(decoded);
|
||||
if (!mapped) {
|
||||
throw invalidUrl(href);
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
export const parse = (input, parseQueryString) => {
|
||||
input = (input || '').replace(SURROUNDING_WHITESPACE, '');
|
||||
// Legacy url.parse() parses a "user:pass@host:port" authority that follows
|
||||
// the scheme even without the "//" separator, for schemes outside its
|
||||
// built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
|
||||
// parser instead treats a scheme not followed by "//" as an opaque path.
|
||||
// Re-insert the "//" so slash-less connection/proxy URLs keep resolving to
|
||||
// an authority, as they did before. This assumes a slash-authority scheme,
|
||||
// which every consumer here uses (http/https/smtp/smtps/socks/direct); an
|
||||
// opaque scheme like mailto:/data:/tel: would be mis-split, but none reach
|
||||
// this module.
|
||||
const slashless = SLASHLESS_AUTHORITY.exec(input);
|
||||
const normalized = slashless ? slashless[1] + '//' + slashless[2] : input;
|
||||
let u;
|
||||
try {
|
||||
u = new URL(normalized);
|
||||
}
|
||||
catch (err) {
|
||||
// WHATWG rejects some input the legacy parser tolerated (empty/relative
|
||||
// strings, scheme-relative '//host/path', out-of-range ports, ...). Fall
|
||||
// back to the legacy parser so behavior, including the downstream errors
|
||||
// callers rely on, is preserved. This is the only path that can still
|
||||
// emit a deprecation warning; it fires for anything WHATWG cannot
|
||||
// represent, including legitimate relative URLs, not just malformed input.
|
||||
return legacyParse(normalized, parseQueryString, err);
|
||||
}
|
||||
const hostname = normalizeHostname(u.hostname, u.href);
|
||||
const port = u.port || null;
|
||||
const pathname = u.pathname || null;
|
||||
const search = u.search || null;
|
||||
// Legacy `.auth` is the decoded "user[:pass]" string; WHATWG keeps the
|
||||
// username/password percent-encoded, so decode to stay byte-compatible with
|
||||
// existing consumers (parseConnectionUrl, Basic/Proxy-Authorization headers).
|
||||
let auth = null;
|
||||
let username = null;
|
||||
let password = null;
|
||||
if (u.username || u.password) {
|
||||
// Gate on password too: legacy url.parse('smtps://:pass@host').auth was
|
||||
// ':pass'. Dropping it would silently connect unauthenticated.
|
||||
username = safeDecode(u.username);
|
||||
password = u.password ? safeDecode(u.password) : null;
|
||||
// the joined form is ambiguous once the user name contains a colon, so
|
||||
// consumers that need the parts read username and password instead
|
||||
auth = username + (password !== null ? ':' + password : '');
|
||||
}
|
||||
let query;
|
||||
if (parseQueryString) {
|
||||
// Mirror querystring.parse(): null-prototype object, repeated keys become an array.
|
||||
const parsed = Object.create(null);
|
||||
u.searchParams.forEach((value, key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
|
||||
const existing = parsed[key];
|
||||
if (Array.isArray(existing)) {
|
||||
existing.push(value);
|
||||
}
|
||||
else {
|
||||
parsed[key] = [existing, value];
|
||||
}
|
||||
}
|
||||
else {
|
||||
parsed[key] = value;
|
||||
}
|
||||
});
|
||||
query = parsed;
|
||||
}
|
||||
else {
|
||||
query = search ? search.slice(1) : null;
|
||||
}
|
||||
return {
|
||||
protocol: u.protocol || null,
|
||||
host: u.host || null,
|
||||
hostname,
|
||||
port,
|
||||
pathname,
|
||||
search,
|
||||
path: (pathname || '') + (search || '') || null,
|
||||
href: u.href,
|
||||
auth,
|
||||
username,
|
||||
password,
|
||||
query
|
||||
};
|
||||
};
|
||||
export const resolve = (from, to) => {
|
||||
try {
|
||||
return new URL(to, from).href;
|
||||
}
|
||||
catch (err) {
|
||||
// Malformed target, fall back to the legacy resolver, but only when the legacy
|
||||
// parser reads the same host out of both inputs that was written. The target
|
||||
// decides the host when it is absolute or scheme-relative, the base otherwise
|
||||
legacyParse(from, false, err, true);
|
||||
legacyParse(to, false, err, true);
|
||||
return urllib.resolve(from, to);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user