Files

629 lines
22 KiB
JavaScript
Raw Permalink Normal View History

2020-02-29 23:01:03 +01:00
/* eslint no-console: 0 */
2026-09-15 13:00:32 +02:00
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';
2026-09-07 07:05:39 +02:00
// re-exported for the callers that already depend on this module, see ./objects
2026-09-15 13:00:32 +02:00
export { isProtoKey, copyOwnKeys };
2020-02-29 23:01:03 +01:00
const DNS_TTL = 5 * 60 * 1000;
2025-12-25 10:58:28 +01:00
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;
2026-09-15 13:00:32 +02:00
/** @internal */
export const _lastCacheCleanup = () => lastCacheCleanup;
/** @internal */
export const _resetCacheCleanup = () => {
2025-12-25 10:58:28 +01:00
lastCacheCleanup = 0;
};
2026-09-15 13:00:32 +02:00
export let networkInterfaces;
2022-10-21 16:52:45 +02:00
try {
networkInterfaces = os.networkInterfaces();
2026-09-15 13:00:32 +02:00
}
catch (_err) {
2022-10-21 16:52:45 +02:00
// fails on some systems
}
const isFamilySupported = (family, allowInternal) => {
2026-09-15 13:00:32 +02:00
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
2022-10-21 16:52:45 +02:00
return true;
}
2026-09-15 13:00:32 +02:00
return addresses.filter(i => !i.internal || allowInternal).some(i => i.family === 'IPv' + family || i.family === family);
2022-10-21 16:52:45 +02:00
};
2026-04-28 12:50:45 +02:00
const resolve = (family, hostname, options, callback) => {
2022-10-21 16:52:45 +02:00
options = options || {};
2026-04-28 12:50:45 +02:00
if (!isFamilySupported(family, options.allowInternalNetworkInterfaces)) {
2022-10-21 16:52:45 +02:00
return callback(null, []);
}
2026-04-28 12:50:45 +02:00
const dnsResolver = dns.Resolver ? new dns.Resolver(options) : dns;
dnsResolver['resolve' + family](hostname, (err, addresses) => {
2020-02-29 23:01:03 +01:00
if (err) {
switch (err.code) {
case dns.NODATA:
case dns.NOTFOUND:
case dns.NOTIMP:
case dns.SERVFAIL:
case dns.CONNREFUSED:
2022-10-21 16:52:45 +02:00
case dns.REFUSED:
2020-02-29 23:01:03 +01:00
case 'EAI_AGAIN':
return callback(null, []);
}
return callback(err);
}
return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
});
};
2026-09-15 13:00:32 +02:00
export const dnsCache = new Map();
2022-10-21 16:52:45 +02:00
const formatDNSValue = (value, extra) => {
if (!value) {
return Object.assign({}, extra || {});
}
2026-04-28 12:50:45 +02:00
const addresses = value.addresses || [];
2026-02-05 08:49:11 +01:00
// Select a random address from available addresses, or null if none
2026-04-28 12:50:45 +02:00
const host = addresses.length > 0 ? addresses[Math.floor(Math.random() * addresses.length)] : null;
2026-09-15 13:00:32 +02:00
return Object.assign({
host,
// Include all addresses for connection fallback support
_addresses: addresses
}, extra || {});
2022-10-21 16:52:45 +02:00
};
2026-09-15 13:00:32 +02:00
export const resolveHostname = (options, callback) => {
2020-02-29 23:01:03 +01:00
options = options || {};
2022-10-21 16:52:45 +02:00
if (!options.host && options.servername) {
options.host = options.servername;
}
2020-02-29 23:01:03 +01:00
if (!options.host || net.isIP(options.host)) {
// nothing to do here
2026-04-28 12:50:45 +02:00
const value = {
2026-09-15 13:00:32 +02:00
addresses: [options.host]
2020-02-29 23:01:03 +01:00
};
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(value, {
servername: options.servername || false,
cached: false
}));
2020-02-29 23:01:03 +01:00
}
2026-09-15 13:00:32 +02:00
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;
2020-02-29 23:01:03 +01:00
let cached;
if (dnsCache.has(options.host)) {
cached = dnsCache.get(options.host);
2025-12-25 10:58:28 +01:00
// 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) {
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(cached.value, {
servername,
cached: true
}));
2020-02-29 23:01:03 +01:00
}
}
2026-02-05 08:49:11 +01:00
// Resolve both IPv4 and IPv6 addresses for fallback support
let ipv4Addresses = [];
let ipv6Addresses = [];
let ipv4Error = null;
let ipv6Error = null;
2026-04-28 12:50:45 +02:00
resolve(4, options.host, options, (err, addresses) => {
2020-02-29 23:01:03 +01:00
if (err) {
2026-02-05 08:49:11 +01:00
ipv4Error = err;
2026-09-15 13:00:32 +02:00
}
else {
2026-02-05 08:49:11 +01:00
ipv4Addresses = addresses || [];
}
2026-09-15 13:00:32 +02:00
resolve(6, host, options, (err, addresses) => {
2026-02-05 08:49:11 +01:00
if (err) {
ipv6Error = err;
2026-09-15 13:00:32 +02:00
}
else {
2026-02-05 08:49:11 +01:00
ipv6Addresses = addresses || [];
}
// Combine addresses: IPv4 first, then IPv6
2026-04-28 12:50:45 +02:00
const allAddresses = ipv4Addresses.concat(ipv6Addresses);
2026-02-05 08:49:11 +01:00
if (allAddresses.length) {
2026-04-28 12:50:45 +02:00
const value = {
2026-09-15 13:00:32 +02:00
addresses: allAddresses
2026-02-05 08:49:11 +01:00
};
2026-09-15 13:00:32 +02:00
dnsCache.set(host, {
2026-02-05 08:49:11 +01:00
value,
2025-12-25 10:58:28 +01:00
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(value, {
servername,
cached: false
}));
2020-02-29 23:01:03 +01:00
}
2026-02-05 08:49:11 +01:00
// No addresses from resolve4/resolve6, try dns.lookup as fallback
if (ipv4Error && ipv6Error) {
// Both resolvers had errors
2020-02-29 23:01:03 +01:00
if (cached) {
2026-09-15 13:00:32 +02:00
dnsCache.set(host, {
2025-12-25 10:58:28 +01:00
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(cached.value, {
servername,
cached: true,
error: ipv4Error
}));
2020-02-29 23:01:03 +01:00
}
}
try {
2026-09-15 13:00:32 +02:00
dns.lookup(host, { all: true }, (err, addresses) => {
2020-02-29 23:01:03 +01:00
if (err) {
if (cached) {
2026-09-15 13:00:32 +02:00
dnsCache.set(host, {
2025-12-25 10:58:28 +01:00
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(cached.value, {
servername,
cached: true,
error: err
}));
2020-02-29 23:01:03 +01:00
}
return callback(err);
}
2026-02-05 08:49:11 +01:00
// Get all supported addresses from dns.lookup
2026-04-28 12:50:45 +02:00
const supportedAddresses = addresses
2026-02-05 08:49:11 +01:00
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
: [];
if (addresses && addresses.length && !supportedAddresses.length) {
2022-10-21 16:52:45 +02:00
// there are addresses but none can be used
2025-06-13 19:44:16 +02:00
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
2022-10-21 16:52:45 +02:00
}
2026-02-05 08:49:11 +01:00
if (!supportedAddresses.length && cached) {
2020-02-29 23:01:03 +01:00
// nothing was found, fallback to cached value
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(cached.value, {
servername,
cached: true
}));
2020-02-29 23:01:03 +01:00
}
2026-04-28 12:50:45 +02:00
const value = {
2026-09-15 13:00:32 +02:00
addresses: supportedAddresses.length ? supportedAddresses : [host]
2020-02-29 23:01:03 +01:00
};
2026-09-15 13:00:32 +02:00
dnsCache.set(host, {
2020-02-29 23:01:03 +01:00
value,
2022-10-21 16:52:45 +02:00
expires: Date.now() + (options.dnsTtl || DNS_TTL)
2020-02-29 23:01:03 +01:00
});
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(value, {
servername,
cached: false
}));
2020-02-29 23:01:03 +01:00
});
2026-09-15 13:00:32 +02:00
}
catch (lookupErr) {
2020-02-29 23:01:03 +01:00
if (cached) {
2026-09-15 13:00:32 +02:00
dnsCache.set(host, {
2025-12-25 10:58:28 +01:00
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
2026-09-15 13:00:32 +02:00
return callback(null, formatDNSValue(cached.value, {
servername,
cached: true,
error: lookupErr
}));
2020-02-29 23:01:03 +01:00
}
2026-02-05 08:49:11 +01:00
return callback(ipv4Error || ipv6Error || lookupErr);
2020-02-29 23:01:03 +01:00
}
});
});
};
/**
* Parses connection url to a structured configuration object
*
2026-09-15 13:00:32 +02:00
* @param str Connection url
* @return Configuration object
2020-02-29 23:01:03 +01:00
*/
2026-09-15 13:00:32 +02:00
export const parseConnectionUrl = (str) => {
2020-02-29 23:01:03 +01:00
str = str || '';
2026-04-28 12:50:45 +02:00
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;
}
2026-09-15 13:00:32 +02:00
if (url.username || url.password) {
2026-04-28 12:50:45 +02:00
options.auth = {
2026-09-15 13:00:32 +02:00
user: url.username || '',
pass: url.password || ''
2026-04-28 12:50:45 +02:00
};
}
Object.keys(url.query || {}).forEach(key => {
let obj = options;
let lKey = key;
let value = url.query[key];
if (!isNaN(value)) {
value = Number(value);
2020-02-29 23:01:03 +01:00
}
2026-04-28 12:50:45 +02:00
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 = {};
2020-02-29 23:01:03 +01:00
}
2026-04-28 12:50:45 +02:00
obj = options.tls;
2026-09-15 13:00:32 +02:00
}
else if (key.indexOf('.') >= 0) {
2026-04-28 12:50:45 +02:00
// ignore nested properties besides tls
return;
}
2026-09-07 07:05:39 +02:00
// `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)) {
2026-04-28 12:50:45 +02:00
obj[lKey] = value;
}
2020-02-29 23:01:03 +01:00
});
return options;
};
2026-09-15 13:00:32 +02:00
/** @internal */
export const _logFunc = (logger, level, defaults, data, message, ...args) => {
2026-04-28 12:50:45 +02:00
const entry = Object.assign({}, defaults || {}, data || {});
delete entry.level;
2026-06-15 07:32:52 +02:00
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);
}
2020-02-29 23:01:03 +01:00
};
/**
* Returns a bunyan-compatible logger interface. Uses either provided logger or
* creates a default console logger
*
2026-09-15 13:00:32 +02:00
* @param [options] Options object that might include 'logger' value
* @return bunyan compatible logger
2020-02-29 23:01:03 +01:00
*/
2026-09-15 13:00:32 +02:00
export const getLogger = (options, defaults) => {
2020-02-29 23:01:03 +01:00
options = options || {};
2026-04-28 12:50:45 +02:00
const response = {};
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
2020-02-29 23:01:03 +01:00
if (!options.logger) {
// use vanity logger
levels.forEach(level => {
response[level] = () => false;
});
return response;
}
2026-04-28 12:50:45 +02:00
const logger = options.logger === true ? createDefaultLogger(levels) : options.logger;
2020-02-29 23:01:03 +01:00
levels.forEach(level => {
response[level] = (data, message, ...args) => {
2026-09-15 13:00:32 +02:00
_logFunc(logger, level, defaults, data, message, ...args);
2020-02-29 23:01:03 +01:00
};
});
return response;
};
/**
* Wrapper for creating a callback that either resolves or rejects a promise
* based on input
*
2026-09-15 13:00:32 +02:00
* @param resolve Function to run if callback is called
* @param reject Function to run if callback ends with an error
2020-02-29 23:01:03 +01:00
*/
2026-09-15 13:00:32 +02:00
export const callbackPromise = (resolve, reject) => function (...args) {
const err = args.shift();
if (err) {
reject(err);
}
else {
resolve(...args);
}
};
export const parseDataURI = (uri) => {
2025-12-25 10:58:28 +01:00
if (typeof uri !== 'string') {
return null;
2025-06-13 19:44:16 +02:00
}
2025-12-25 10:58:28 +01:00
// 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();
}
2025-06-13 19:44:16 +02:00
}
2025-12-25 10:58:28 +01:00
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();
2026-09-07 07:05:39 +02:00
if (key && !isProtoKey(key)) {
2025-12-25 10:58:28 +01:00
params[key] = value;
}
2025-06-13 19:44:16 +02:00
}
}
2025-12-25 10:58:28 +01:00
// Decode data based on encoding with proper error handling
let bufferData;
try {
if (encoding === 'base64') {
bufferData = Buffer.from(data, 'base64');
2026-09-15 13:00:32 +02:00
}
else {
2025-06-13 19:44:16 +02:00
try {
2025-12-25 10:58:28 +01:00
bufferData = Buffer.from(decodeURIComponent(data));
2026-09-15 13:00:32 +02:00
}
catch (_decodeError) {
2025-12-25 10:58:28 +01:00
bufferData = Buffer.from(data);
2025-06-13 19:44:16 +02:00
}
2025-12-25 10:58:28 +01:00
}
2026-09-15 13:00:32 +02:00
}
catch (_bufferError) {
2025-12-25 10:58:28 +01:00
bufferData = Buffer.alloc(0);
2025-06-13 19:44:16 +02:00
}
2025-12-25 10:58:28 +01:00
return {
data: bufferData,
encoding: encoding || null,
contentType: contentType || 'application/octet-stream',
params
};
2025-06-13 19:44:16 +02:00
};
2026-09-15 13:00:32 +02:00
export function resolveContent(data, key, options, callback) {
2026-06-15 07:32:52 +02:00
// options is optional; support the legacy resolveContent(data, key, callback) signature
if (!callback && typeof options === 'function') {
callback = options;
options = false;
}
options = options || {};
2020-02-29 23:01:03 +01:00
let promise;
if (!callback) {
promise = new Promise((resolve, reject) => {
2026-09-15 13:00:32 +02:00
callback = callbackPromise(resolve, reject);
2020-02-29 23:01:03 +01:00
});
}
2026-06-15 07:32:52 +02:00
resolveContentValue(data, key, options, callback);
return promise;
2026-09-15 13:00:32 +02:00
}
2026-06-15 07:32:52 +02:00
function resolveContentValue(data, key, options, callback) {
2020-02-29 23:01:03 +01:00
let content = (data && data[key] && data[key].content) || data[key];
2026-04-28 12:50:45 +02:00
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
2020-02-29 23:01:03 +01:00
.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
2021-07-14 08:08:23 +02:00
if (data[key].content) {
data[key].content = value;
2026-09-15 13:00:32 +02:00
}
else {
2021-07-14 08:08:23 +02:00
data[key] = value;
}
2020-02-29 23:01:03 +01:00
callback(null, value);
});
2026-09-15 13:00:32 +02:00
}
else if (/^data:/i.test(content.path || content.href)) {
const parsedDataUri = parseDataURI(content.path || content.href);
2026-09-07 07:05:39 +02:00
return callback(null, parsedDataUri && parsedDataUri.data ? parsedDataUri.data : Buffer.alloc(0));
2026-09-15 13:00:32 +02:00
}
else if (content.href || /^https?:\/\//i.test(content.path)) {
2026-09-07 07:05:39 +02:00
// 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;
2026-06-15 07:32:52 +02:00
if (options.disableUrlAccess) {
2026-09-15 13:00:32 +02:00
setImmediate(() => {
2026-09-07 07:05:39 +02:00
const err = new Error('Url access rejected for ' + url);
2026-06-15 07:32:52 +02:00
err.code = errors.EURLACCESS;
callback(err);
});
2026-09-15 13:00:32 +02:00
return;
2026-06-15 07:32:52 +02:00
}
2026-09-07 07:05:39 +02:00
return resolveStream(nmfetch(url, { headers: content.httpHeaders, tls: content.tls }), callback);
2026-09-15 13:00:32 +02:00
}
else if (content.path) {
2026-06-15 07:32:52 +02:00
if (options.disableFileAccess) {
2026-09-15 13:00:32 +02:00
setImmediate(() => {
2026-06-15 07:32:52 +02:00
const err = new Error('File access rejected for ' + content.path);
err.code = errors.EFILEACCESS;
callback(err);
});
2026-09-15 13:00:32 +02:00
return;
2026-06-15 07:32:52 +02:00
}
2020-02-29 23:01:03 +01:00
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));
2026-06-15 07:32:52 +02:00
}
2020-02-29 23:01:03 +01:00
/**
* Copies properties from source objects to target objects
*/
2026-09-15 13:00:32 +02:00
export const assign = function (...args) {
2026-04-28 12:50:45 +02:00
const target = args.shift() || {};
2020-02-29 23:01:03 +01:00
args.forEach(source => {
Object.keys(source || {}).forEach(key => {
2026-09-07 07:05:39 +02:00
if (isProtoKey(key)) {
return;
}
2026-09-15 13:00:32 +02:00
if (['tls', 'auth'].includes(key) &&
source[key] &&
typeof source[key] === 'object') {
2020-02-29 23:01:03 +01:00
// tls and auth are special keys that need to be enumerated separately
2026-09-07 07:05:39 +02:00
// 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
2026-09-15 13:00:32 +02:00
target[key] = copyOwnKeys(target[key] || {}, source[key]);
}
else {
2020-02-29 23:01:03 +01:00
target[key] = source[key];
}
});
});
return target;
};
2026-09-15 13:00:32 +02:00
export const encodeXText = (str) => {
2020-02-29 23:01:03 +01:00
// ! 0x21
// + 0x2B
// = 0x3D
// ~ 0x7E
if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
return str;
}
2026-04-28 12:50:45 +02:00
const buf = Buffer.from(str);
2020-02-29 23:01:03 +01:00
let result = '';
for (let i = 0, len = buf.length; i < len; i++) {
2026-04-28 12:50:45 +02:00
const c = buf[i];
2020-02-29 23:01:03 +01:00
if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
2026-09-15 13:00:32 +02:00
}
else {
2020-02-29 23:01:03 +01:00
result += String.fromCharCode(c);
}
}
return result;
};
/**
* Streams a stream value into a Buffer
*
2026-09-15 13:00:32 +02:00
* @param stream Readable stream
* @param callback Callback function with (err, value)
2020-02-29 23:01:03 +01:00
*/
function resolveStream(stream, callback) {
let responded = false;
2026-04-28 12:50:45 +02:00
const chunks = [];
2020-02-29 23:01:03 +01:00
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);
2026-09-15 13:00:32 +02:00
}
catch (E) {
2020-02-29 23:01:03 +01:00
return callback(E);
}
callback(null, value);
});
}
/**
* Generates a bunyan-like logger that prints to console
*
2026-09-15 13:00:32 +02:00
* @returns Bunyan logger instance
2020-02-29 23:01:03 +01:00
*/
function createDefaultLogger(levels) {
2026-04-28 12:50:45 +02:00
const levelMaxLen = levels.reduce((max, level) => Math.max(max, level.length), 0);
const levelNames = new Map();
2020-02-29 23:01:03 +01:00
levels.forEach(level => {
let levelName = level.toUpperCase();
if (levelName.length < levelMaxLen) {
levelName += ' '.repeat(levelMaxLen - levelName.length);
}
levelNames.set(level, levelName);
});
2026-04-28 12:50:45 +02:00
const print = (level, entry, message, ...args) => {
2020-02-29 23:01:03 +01:00
let prefix = '';
if (entry) {
if (entry.tnx === 'server') {
prefix = 'S: ';
2026-09-15 13:00:32 +02:00
}
else if (entry.tnx === 'client') {
2020-02-29 23:01:03 +01:00
prefix = 'C: ';
}
if (entry.sid) {
prefix = '[' + entry.sid + '] ' + prefix;
}
if (entry.cid) {
prefix = '[#' + entry.cid + '] ' + prefix;
}
}
message = util.format(message, ...args);
2026-09-15 13:00:32 +02:00
message.split(/\r?\n/).forEach((line) => {
2020-11-12 16:31:20 +01:00
console.log('[%s] %s %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), levelNames.get(level), prefix + line);
2020-02-29 23:01:03 +01:00
});
};
2026-04-28 12:50:45 +02:00
const logger = {};
2020-02-29 23:01:03 +01:00
levels.forEach(level => {
logger[level] = print.bind(null, level);
});
return logger;
}