| 1 | /* eslint no-console: 0 */
|
|---|
| 2 |
|
|---|
| 3 | 'use strict';
|
|---|
| 4 |
|
|---|
| 5 | const urllib = require('url');
|
|---|
| 6 | const util = require('util');
|
|---|
| 7 | const fs = require('fs');
|
|---|
| 8 | const nmfetch = require('../fetch');
|
|---|
| 9 | const dns = require('dns');
|
|---|
| 10 | const net = require('net');
|
|---|
| 11 | const os = require('os');
|
|---|
| 12 |
|
|---|
| 13 | const DNS_TTL = 5 * 60 * 1000;
|
|---|
| 14 |
|
|---|
| 15 | let networkInterfaces;
|
|---|
| 16 | try {
|
|---|
| 17 | networkInterfaces = os.networkInterfaces();
|
|---|
| 18 | } catch (err) {
|
|---|
| 19 | // fails on some systems
|
|---|
| 20 | }
|
|---|
| 21 |
|
|---|
| 22 | module.exports.networkInterfaces = networkInterfaces;
|
|---|
| 23 |
|
|---|
| 24 | const isFamilySupported = (family, allowInternal) => {
|
|---|
| 25 | let networkInterfaces = module.exports.networkInterfaces;
|
|---|
| 26 | if (!networkInterfaces) {
|
|---|
| 27 | // hope for the best
|
|---|
| 28 | return true;
|
|---|
| 29 | }
|
|---|
| 30 |
|
|---|
| 31 | const familySupported =
|
|---|
| 32 | // crux that replaces Object.values(networkInterfaces) as Object.values is not supported in nodejs v6
|
|---|
| 33 | Object.keys(networkInterfaces)
|
|---|
| 34 | .map(key => networkInterfaces[key])
|
|---|
| 35 | // crux that replaces .flat() as it is not supported in older Node versions (v10 and older)
|
|---|
| 36 | .reduce((acc, val) => acc.concat(val), [])
|
|---|
| 37 | .filter(i => !i.internal || allowInternal)
|
|---|
| 38 | .filter(i => i.family === 'IPv' + family || i.family === family).length > 0;
|
|---|
| 39 |
|
|---|
| 40 | return familySupported;
|
|---|
| 41 | };
|
|---|
| 42 |
|
|---|
| 43 | const resolver = (family, hostname, options, callback) => {
|
|---|
| 44 | options = options || {};
|
|---|
| 45 | const familySupported = isFamilySupported(family, options.allowInternalNetworkInterfaces);
|
|---|
| 46 |
|
|---|
| 47 | if (!familySupported) {
|
|---|
| 48 | return callback(null, []);
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | const resolver = dns.Resolver ? new dns.Resolver(options) : dns;
|
|---|
| 52 | resolver['resolve' + family](hostname, (err, addresses) => {
|
|---|
| 53 | if (err) {
|
|---|
| 54 | switch (err.code) {
|
|---|
| 55 | case dns.NODATA:
|
|---|
| 56 | case dns.NOTFOUND:
|
|---|
| 57 | case dns.NOTIMP:
|
|---|
| 58 | case dns.SERVFAIL:
|
|---|
| 59 | case dns.CONNREFUSED:
|
|---|
| 60 | case dns.REFUSED:
|
|---|
| 61 | case 'EAI_AGAIN':
|
|---|
| 62 | return callback(null, []);
|
|---|
| 63 | }
|
|---|
| 64 | return callback(err);
|
|---|
| 65 | }
|
|---|
| 66 | return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
|
|---|
| 67 | });
|
|---|
| 68 | };
|
|---|
| 69 |
|
|---|
| 70 | const dnsCache = (module.exports.dnsCache = new Map());
|
|---|
| 71 |
|
|---|
| 72 | const formatDNSValue = (value, extra) => {
|
|---|
| 73 | if (!value) {
|
|---|
| 74 | return Object.assign({}, extra || {});
|
|---|
| 75 | }
|
|---|
| 76 |
|
|---|
| 77 | return Object.assign(
|
|---|
| 78 | {
|
|---|
| 79 | servername: value.servername,
|
|---|
| 80 | host:
|
|---|
| 81 | !value.addresses || !value.addresses.length
|
|---|
| 82 | ? null
|
|---|
| 83 | : value.addresses.length === 1
|
|---|
| 84 | ? value.addresses[0]
|
|---|
| 85 | : value.addresses[Math.floor(Math.random() * value.addresses.length)]
|
|---|
| 86 | },
|
|---|
| 87 | extra || {}
|
|---|
| 88 | );
|
|---|
| 89 | };
|
|---|
| 90 |
|
|---|
| 91 | module.exports.resolveHostname = (options, callback) => {
|
|---|
| 92 | options = options || {};
|
|---|
| 93 |
|
|---|
| 94 | if (!options.host && options.servername) {
|
|---|
| 95 | options.host = options.servername;
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | if (!options.host || net.isIP(options.host)) {
|
|---|
| 99 | // nothing to do here
|
|---|
| 100 | let value = {
|
|---|
| 101 | addresses: [options.host],
|
|---|
| 102 | servername: options.servername || false
|
|---|
| 103 | };
|
|---|
| 104 | return callback(
|
|---|
| 105 | null,
|
|---|
| 106 | formatDNSValue(value, {
|
|---|
| 107 | cached: false
|
|---|
| 108 | })
|
|---|
| 109 | );
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | let cached;
|
|---|
| 113 | if (dnsCache.has(options.host)) {
|
|---|
| 114 | cached = dnsCache.get(options.host);
|
|---|
| 115 |
|
|---|
| 116 | if (!cached.expires || cached.expires >= Date.now()) {
|
|---|
| 117 | return callback(
|
|---|
| 118 | null,
|
|---|
| 119 | formatDNSValue(cached.value, {
|
|---|
| 120 | cached: true
|
|---|
| 121 | })
|
|---|
| 122 | );
|
|---|
| 123 | }
|
|---|
| 124 | }
|
|---|
| 125 |
|
|---|
| 126 | resolver(4, options.host, options, (err, addresses) => {
|
|---|
| 127 | if (err) {
|
|---|
| 128 | if (cached) {
|
|---|
| 129 | // ignore error, use expired value
|
|---|
| 130 | return callback(
|
|---|
| 131 | null,
|
|---|
| 132 | formatDNSValue(cached.value, {
|
|---|
| 133 | cached: true,
|
|---|
| 134 | error: err
|
|---|
| 135 | })
|
|---|
| 136 | );
|
|---|
| 137 | }
|
|---|
| 138 | return callback(err);
|
|---|
| 139 | }
|
|---|
| 140 |
|
|---|
| 141 | if (addresses && addresses.length) {
|
|---|
| 142 | let value = {
|
|---|
| 143 | addresses,
|
|---|
| 144 | servername: options.servername || options.host
|
|---|
| 145 | };
|
|---|
| 146 |
|
|---|
| 147 | dnsCache.set(options.host, {
|
|---|
| 148 | value,
|
|---|
| 149 | expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
|---|
| 150 | });
|
|---|
| 151 |
|
|---|
| 152 | return callback(
|
|---|
| 153 | null,
|
|---|
| 154 | formatDNSValue(value, {
|
|---|
| 155 | cached: false
|
|---|
| 156 | })
|
|---|
| 157 | );
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | resolver(6, options.host, options, (err, addresses) => {
|
|---|
| 161 | if (err) {
|
|---|
| 162 | if (cached) {
|
|---|
| 163 | // ignore error, use expired value
|
|---|
| 164 | return callback(
|
|---|
| 165 | null,
|
|---|
| 166 | formatDNSValue(cached.value, {
|
|---|
| 167 | cached: true,
|
|---|
| 168 | error: err
|
|---|
| 169 | })
|
|---|
| 170 | );
|
|---|
| 171 | }
|
|---|
| 172 | return callback(err);
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | if (addresses && addresses.length) {
|
|---|
| 176 | let value = {
|
|---|
| 177 | addresses,
|
|---|
| 178 | servername: options.servername || options.host
|
|---|
| 179 | };
|
|---|
| 180 |
|
|---|
| 181 | dnsCache.set(options.host, {
|
|---|
| 182 | value,
|
|---|
| 183 | expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
|---|
| 184 | });
|
|---|
| 185 |
|
|---|
| 186 | return callback(
|
|---|
| 187 | null,
|
|---|
| 188 | formatDNSValue(value, {
|
|---|
| 189 | cached: false
|
|---|
| 190 | })
|
|---|
| 191 | );
|
|---|
| 192 | }
|
|---|
| 193 |
|
|---|
| 194 | try {
|
|---|
| 195 | dns.lookup(options.host, { all: true }, (err, addresses) => {
|
|---|
| 196 | if (err) {
|
|---|
| 197 | if (cached) {
|
|---|
| 198 | // ignore error, use expired value
|
|---|
| 199 | return callback(
|
|---|
| 200 | null,
|
|---|
| 201 | formatDNSValue(cached.value, {
|
|---|
| 202 | cached: true,
|
|---|
| 203 | error: err
|
|---|
| 204 | })
|
|---|
| 205 | );
|
|---|
| 206 | }
|
|---|
| 207 | return callback(err);
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | let address = addresses
|
|---|
| 211 | ? addresses
|
|---|
| 212 | .filter(addr => isFamilySupported(addr.family))
|
|---|
| 213 | .map(addr => addr.address)
|
|---|
| 214 | .shift()
|
|---|
| 215 | : false;
|
|---|
| 216 |
|
|---|
| 217 | if (addresses && addresses.length && !address) {
|
|---|
| 218 | // there are addresses but none can be used
|
|---|
| 219 | console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
|---|
| 220 | }
|
|---|
| 221 |
|
|---|
| 222 | if (!address && cached) {
|
|---|
| 223 | // nothing was found, fallback to cached value
|
|---|
| 224 | return callback(
|
|---|
| 225 | null,
|
|---|
| 226 | formatDNSValue(cached.value, {
|
|---|
| 227 | cached: true
|
|---|
| 228 | })
|
|---|
| 229 | );
|
|---|
| 230 | }
|
|---|
| 231 |
|
|---|
| 232 | let value = {
|
|---|
| 233 | addresses: address ? [address] : [options.host],
|
|---|
| 234 | servername: options.servername || options.host
|
|---|
| 235 | };
|
|---|
| 236 |
|
|---|
| 237 | dnsCache.set(options.host, {
|
|---|
| 238 | value,
|
|---|
| 239 | expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
|---|
| 240 | });
|
|---|
| 241 |
|
|---|
| 242 | return callback(
|
|---|
| 243 | null,
|
|---|
| 244 | formatDNSValue(value, {
|
|---|
| 245 | cached: false
|
|---|
| 246 | })
|
|---|
| 247 | );
|
|---|
| 248 | });
|
|---|
| 249 | } catch (err) {
|
|---|
| 250 | if (cached) {
|
|---|
| 251 | // ignore error, use expired value
|
|---|
| 252 | return callback(
|
|---|
| 253 | null,
|
|---|
| 254 | formatDNSValue(cached.value, {
|
|---|
| 255 | cached: true,
|
|---|
| 256 | error: err
|
|---|
| 257 | })
|
|---|
| 258 | );
|
|---|
| 259 | }
|
|---|
| 260 | return callback(err);
|
|---|
| 261 | }
|
|---|
| 262 | });
|
|---|
| 263 | });
|
|---|
| 264 | };
|
|---|
| 265 | /**
|
|---|
| 266 | * Parses connection url to a structured configuration object
|
|---|
| 267 | *
|
|---|
| 268 | * @param {String} str Connection url
|
|---|
| 269 | * @return {Object} Configuration object
|
|---|
| 270 | */
|
|---|
| 271 | module.exports.parseConnectionUrl = str => {
|
|---|
| 272 | str = str || '';
|
|---|
| 273 | let options = {};
|
|---|
| 274 |
|
|---|
| 275 | [urllib.parse(str, true)].forEach(url => {
|
|---|
| 276 | let auth;
|
|---|
| 277 |
|
|---|
| 278 | switch (url.protocol) {
|
|---|
| 279 | case 'smtp:':
|
|---|
| 280 | options.secure = false;
|
|---|
| 281 | break;
|
|---|
| 282 | case 'smtps:':
|
|---|
| 283 | options.secure = true;
|
|---|
| 284 | break;
|
|---|
| 285 | case 'direct:':
|
|---|
| 286 | options.direct = true;
|
|---|
| 287 | break;
|
|---|
| 288 | }
|
|---|
| 289 |
|
|---|
| 290 | if (!isNaN(url.port) && Number(url.port)) {
|
|---|
| 291 | options.port = Number(url.port);
|
|---|
| 292 | }
|
|---|
| 293 |
|
|---|
| 294 | if (url.hostname) {
|
|---|
| 295 | options.host = url.hostname;
|
|---|
| 296 | }
|
|---|
| 297 |
|
|---|
| 298 | if (url.auth) {
|
|---|
| 299 | auth = url.auth.split(':');
|
|---|
| 300 |
|
|---|
| 301 | if (!options.auth) {
|
|---|
| 302 | options.auth = {};
|
|---|
| 303 | }
|
|---|
| 304 |
|
|---|
| 305 | options.auth.user = auth.shift();
|
|---|
| 306 | options.auth.pass = auth.join(':');
|
|---|
| 307 | }
|
|---|
| 308 |
|
|---|
| 309 | Object.keys(url.query || {}).forEach(key => {
|
|---|
| 310 | let obj = options;
|
|---|
| 311 | let lKey = key;
|
|---|
| 312 | let value = url.query[key];
|
|---|
| 313 |
|
|---|
| 314 | if (!isNaN(value)) {
|
|---|
| 315 | value = Number(value);
|
|---|
| 316 | }
|
|---|
| 317 |
|
|---|
| 318 | switch (value) {
|
|---|
| 319 | case 'true':
|
|---|
| 320 | value = true;
|
|---|
| 321 | break;
|
|---|
| 322 | case 'false':
|
|---|
| 323 | value = false;
|
|---|
| 324 | break;
|
|---|
| 325 | }
|
|---|
| 326 |
|
|---|
| 327 | // tls is nested object
|
|---|
| 328 | if (key.indexOf('tls.') === 0) {
|
|---|
| 329 | lKey = key.substr(4);
|
|---|
| 330 | if (!options.tls) {
|
|---|
| 331 | options.tls = {};
|
|---|
| 332 | }
|
|---|
| 333 | obj = options.tls;
|
|---|
| 334 | } else if (key.indexOf('.') >= 0) {
|
|---|
| 335 | // ignore nested properties besides tls
|
|---|
| 336 | return;
|
|---|
| 337 | }
|
|---|
| 338 |
|
|---|
| 339 | if (!(lKey in obj)) {
|
|---|
| 340 | obj[lKey] = value;
|
|---|
| 341 | }
|
|---|
| 342 | });
|
|---|
| 343 | });
|
|---|
| 344 |
|
|---|
| 345 | return options;
|
|---|
| 346 | };
|
|---|
| 347 |
|
|---|
| 348 | module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
|
|---|
| 349 | let entry = {};
|
|---|
| 350 |
|
|---|
| 351 | Object.keys(defaults || {}).forEach(key => {
|
|---|
| 352 | if (key !== 'level') {
|
|---|
| 353 | entry[key] = defaults[key];
|
|---|
| 354 | }
|
|---|
| 355 | });
|
|---|
| 356 |
|
|---|
| 357 | Object.keys(data || {}).forEach(key => {
|
|---|
| 358 | if (key !== 'level') {
|
|---|
| 359 | entry[key] = data[key];
|
|---|
| 360 | }
|
|---|
| 361 | });
|
|---|
| 362 |
|
|---|
| 363 | logger[level](entry, message, ...args);
|
|---|
| 364 | };
|
|---|
| 365 |
|
|---|
| 366 | /**
|
|---|
| 367 | * Returns a bunyan-compatible logger interface. Uses either provided logger or
|
|---|
| 368 | * creates a default console logger
|
|---|
| 369 | *
|
|---|
| 370 | * @param {Object} [options] Options object that might include 'logger' value
|
|---|
| 371 | * @return {Object} bunyan compatible logger
|
|---|
| 372 | */
|
|---|
| 373 | module.exports.getLogger = (options, defaults) => {
|
|---|
| 374 | options = options || {};
|
|---|
| 375 |
|
|---|
| 376 | let response = {};
|
|---|
| 377 | let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|---|
| 378 |
|
|---|
| 379 | if (!options.logger) {
|
|---|
| 380 | // use vanity logger
|
|---|
| 381 | levels.forEach(level => {
|
|---|
| 382 | response[level] = () => false;
|
|---|
| 383 | });
|
|---|
| 384 | return response;
|
|---|
| 385 | }
|
|---|
| 386 |
|
|---|
| 387 | let logger = options.logger;
|
|---|
| 388 |
|
|---|
| 389 | if (options.logger === true) {
|
|---|
| 390 | // create console logger
|
|---|
| 391 | logger = createDefaultLogger(levels);
|
|---|
| 392 | }
|
|---|
| 393 |
|
|---|
| 394 | levels.forEach(level => {
|
|---|
| 395 | response[level] = (data, message, ...args) => {
|
|---|
| 396 | module.exports._logFunc(logger, level, defaults, data, message, ...args);
|
|---|
| 397 | };
|
|---|
| 398 | });
|
|---|
| 399 |
|
|---|
| 400 | return response;
|
|---|
| 401 | };
|
|---|
| 402 |
|
|---|
| 403 | /**
|
|---|
| 404 | * Wrapper for creating a callback that either resolves or rejects a promise
|
|---|
| 405 | * based on input
|
|---|
| 406 | *
|
|---|
| 407 | * @param {Function} resolve Function to run if callback is called
|
|---|
| 408 | * @param {Function} reject Function to run if callback ends with an error
|
|---|
| 409 | */
|
|---|
| 410 | module.exports.callbackPromise = (resolve, reject) =>
|
|---|
| 411 | function () {
|
|---|
| 412 | let args = Array.from(arguments);
|
|---|
| 413 | let err = args.shift();
|
|---|
| 414 | if (err) {
|
|---|
| 415 | reject(err);
|
|---|
| 416 | } else {
|
|---|
| 417 | resolve(...args);
|
|---|
| 418 | }
|
|---|
| 419 | };
|
|---|
| 420 |
|
|---|
| 421 | module.exports.parseDataURI = uri => {
|
|---|
| 422 | let input = uri;
|
|---|
| 423 | let commaPos = input.indexOf(',');
|
|---|
| 424 | if (!commaPos) {
|
|---|
| 425 | return uri;
|
|---|
| 426 | }
|
|---|
| 427 |
|
|---|
| 428 | let data = input.substring(commaPos + 1);
|
|---|
| 429 | let metaStr = input.substring('data:'.length, commaPos);
|
|---|
| 430 |
|
|---|
| 431 | let encoding;
|
|---|
| 432 |
|
|---|
| 433 | let metaEntries = metaStr.split(';');
|
|---|
| 434 | let lastMetaEntry = metaEntries.length > 1 ? metaEntries[metaEntries.length - 1] : false;
|
|---|
| 435 | if (lastMetaEntry && lastMetaEntry.indexOf('=') < 0) {
|
|---|
| 436 | encoding = lastMetaEntry.toLowerCase();
|
|---|
| 437 | metaEntries.pop();
|
|---|
| 438 | }
|
|---|
| 439 |
|
|---|
| 440 | let contentType = metaEntries.shift() || 'application/octet-stream';
|
|---|
| 441 | let params = {};
|
|---|
| 442 | for (let entry of metaEntries) {
|
|---|
| 443 | let sep = entry.indexOf('=');
|
|---|
| 444 | if (sep >= 0) {
|
|---|
| 445 | let key = entry.substring(0, sep);
|
|---|
| 446 | let value = entry.substring(sep + 1);
|
|---|
| 447 | params[key] = value;
|
|---|
| 448 | }
|
|---|
| 449 | }
|
|---|
| 450 |
|
|---|
| 451 | switch (encoding) {
|
|---|
| 452 | case 'base64':
|
|---|
| 453 | data = Buffer.from(data, 'base64');
|
|---|
| 454 | break;
|
|---|
| 455 | case 'utf8':
|
|---|
| 456 | data = Buffer.from(data);
|
|---|
| 457 | break;
|
|---|
| 458 | default:
|
|---|
| 459 | try {
|
|---|
| 460 | data = Buffer.from(decodeURIComponent(data));
|
|---|
| 461 | } catch (err) {
|
|---|
| 462 | data = Buffer.from(data);
|
|---|
| 463 | }
|
|---|
| 464 | data = Buffer.from(data);
|
|---|
| 465 | }
|
|---|
| 466 |
|
|---|
| 467 | return { data, encoding, contentType, params };
|
|---|
| 468 | };
|
|---|
| 469 |
|
|---|
| 470 | /**
|
|---|
| 471 | * Resolves a String or a Buffer value for content value. Useful if the value
|
|---|
| 472 | * is a Stream or a file or an URL. If the value is a Stream, overwrites
|
|---|
| 473 | * the stream object with the resolved value (you can't stream a value twice).
|
|---|
| 474 | *
|
|---|
| 475 | * This is useful when you want to create a plugin that needs a content value,
|
|---|
| 476 | * for example the `html` or `text` value as a String or a Buffer but not as
|
|---|
| 477 | * a file path or an URL.
|
|---|
| 478 | *
|
|---|
| 479 | * @param {Object} data An object or an Array you want to resolve an element for
|
|---|
| 480 | * @param {String|Number} key Property name or an Array index
|
|---|
| 481 | * @param {Function} callback Callback function with (err, value)
|
|---|
| 482 | */
|
|---|
| 483 | module.exports.resolveContent = (data, key, callback) => {
|
|---|
| 484 | let promise;
|
|---|
| 485 |
|
|---|
| 486 | if (!callback) {
|
|---|
| 487 | promise = new Promise((resolve, reject) => {
|
|---|
| 488 | callback = module.exports.callbackPromise(resolve, reject);
|
|---|
| 489 | });
|
|---|
| 490 | }
|
|---|
| 491 |
|
|---|
| 492 | let content = (data && data[key] && data[key].content) || data[key];
|
|---|
| 493 | let contentStream;
|
|---|
| 494 | let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
|---|
| 495 | .toString()
|
|---|
| 496 | .toLowerCase()
|
|---|
| 497 | .replace(/[-_\s]/g, '');
|
|---|
| 498 |
|
|---|
| 499 | if (!content) {
|
|---|
| 500 | return callback(null, content);
|
|---|
| 501 | }
|
|---|
| 502 |
|
|---|
| 503 | if (typeof content === 'object') {
|
|---|
| 504 | if (typeof content.pipe === 'function') {
|
|---|
| 505 | return resolveStream(content, (err, value) => {
|
|---|
| 506 | if (err) {
|
|---|
| 507 | return callback(err);
|
|---|
| 508 | }
|
|---|
| 509 | // we can't stream twice the same content, so we need
|
|---|
| 510 | // to replace the stream object with the streaming result
|
|---|
| 511 | if (data[key].content) {
|
|---|
| 512 | data[key].content = value;
|
|---|
| 513 | } else {
|
|---|
| 514 | data[key] = value;
|
|---|
| 515 | }
|
|---|
| 516 | callback(null, value);
|
|---|
| 517 | });
|
|---|
| 518 | } else if (/^https?:\/\//i.test(content.path || content.href)) {
|
|---|
| 519 | contentStream = nmfetch(content.path || content.href);
|
|---|
| 520 | return resolveStream(contentStream, callback);
|
|---|
| 521 | } else if (/^data:/i.test(content.path || content.href)) {
|
|---|
| 522 | let parsedDataUri = module.exports.parseDataURI(content.path || content.href);
|
|---|
| 523 |
|
|---|
| 524 | if (!parsedDataUri || !parsedDataUri.data) {
|
|---|
| 525 | return callback(null, Buffer.from(0));
|
|---|
| 526 | }
|
|---|
| 527 | return callback(null, parsedDataUri.data);
|
|---|
| 528 | } else if (content.path) {
|
|---|
| 529 | return resolveStream(fs.createReadStream(content.path), callback);
|
|---|
| 530 | }
|
|---|
| 531 | }
|
|---|
| 532 |
|
|---|
| 533 | if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
|
|---|
| 534 | content = Buffer.from(data[key].content, encoding);
|
|---|
| 535 | }
|
|---|
| 536 |
|
|---|
| 537 | // default action, return as is
|
|---|
| 538 | setImmediate(() => callback(null, content));
|
|---|
| 539 |
|
|---|
| 540 | return promise;
|
|---|
| 541 | };
|
|---|
| 542 |
|
|---|
| 543 | /**
|
|---|
| 544 | * Copies properties from source objects to target objects
|
|---|
| 545 | */
|
|---|
| 546 | module.exports.assign = function (/* target, ... sources */) {
|
|---|
| 547 | let args = Array.from(arguments);
|
|---|
| 548 | let target = args.shift() || {};
|
|---|
| 549 |
|
|---|
| 550 | args.forEach(source => {
|
|---|
| 551 | Object.keys(source || {}).forEach(key => {
|
|---|
| 552 | if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
|---|
| 553 | // tls and auth are special keys that need to be enumerated separately
|
|---|
| 554 | // other objects are passed as is
|
|---|
| 555 | if (!target[key]) {
|
|---|
| 556 | // ensure that target has this key
|
|---|
| 557 | target[key] = {};
|
|---|
| 558 | }
|
|---|
| 559 | Object.keys(source[key]).forEach(subKey => {
|
|---|
| 560 | target[key][subKey] = source[key][subKey];
|
|---|
| 561 | });
|
|---|
| 562 | } else {
|
|---|
| 563 | target[key] = source[key];
|
|---|
| 564 | }
|
|---|
| 565 | });
|
|---|
| 566 | });
|
|---|
| 567 | return target;
|
|---|
| 568 | };
|
|---|
| 569 |
|
|---|
| 570 | module.exports.encodeXText = str => {
|
|---|
| 571 | // ! 0x21
|
|---|
| 572 | // + 0x2B
|
|---|
| 573 | // = 0x3D
|
|---|
| 574 | // ~ 0x7E
|
|---|
| 575 | if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
|
|---|
| 576 | return str;
|
|---|
| 577 | }
|
|---|
| 578 | let buf = Buffer.from(str);
|
|---|
| 579 | let result = '';
|
|---|
| 580 | for (let i = 0, len = buf.length; i < len; i++) {
|
|---|
| 581 | let c = buf[i];
|
|---|
| 582 | if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
|
|---|
| 583 | result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
|
|---|
| 584 | } else {
|
|---|
| 585 | result += String.fromCharCode(c);
|
|---|
| 586 | }
|
|---|
| 587 | }
|
|---|
| 588 | return result;
|
|---|
| 589 | };
|
|---|
| 590 |
|
|---|
| 591 | /**
|
|---|
| 592 | * Streams a stream value into a Buffer
|
|---|
| 593 | *
|
|---|
| 594 | * @param {Object} stream Readable stream
|
|---|
| 595 | * @param {Function} callback Callback function with (err, value)
|
|---|
| 596 | */
|
|---|
| 597 | function resolveStream(stream, callback) {
|
|---|
| 598 | let responded = false;
|
|---|
| 599 | let chunks = [];
|
|---|
| 600 | let chunklen = 0;
|
|---|
| 601 |
|
|---|
| 602 | stream.on('error', err => {
|
|---|
| 603 | if (responded) {
|
|---|
| 604 | return;
|
|---|
| 605 | }
|
|---|
| 606 |
|
|---|
| 607 | responded = true;
|
|---|
| 608 | callback(err);
|
|---|
| 609 | });
|
|---|
| 610 |
|
|---|
| 611 | stream.on('readable', () => {
|
|---|
| 612 | let chunk;
|
|---|
| 613 | while ((chunk = stream.read()) !== null) {
|
|---|
| 614 | chunks.push(chunk);
|
|---|
| 615 | chunklen += chunk.length;
|
|---|
| 616 | }
|
|---|
| 617 | });
|
|---|
| 618 |
|
|---|
| 619 | stream.on('end', () => {
|
|---|
| 620 | if (responded) {
|
|---|
| 621 | return;
|
|---|
| 622 | }
|
|---|
| 623 | responded = true;
|
|---|
| 624 |
|
|---|
| 625 | let value;
|
|---|
| 626 |
|
|---|
| 627 | try {
|
|---|
| 628 | value = Buffer.concat(chunks, chunklen);
|
|---|
| 629 | } catch (E) {
|
|---|
| 630 | return callback(E);
|
|---|
| 631 | }
|
|---|
| 632 | callback(null, value);
|
|---|
| 633 | });
|
|---|
| 634 | }
|
|---|
| 635 |
|
|---|
| 636 | /**
|
|---|
| 637 | * Generates a bunyan-like logger that prints to console
|
|---|
| 638 | *
|
|---|
| 639 | * @returns {Object} Bunyan logger instance
|
|---|
| 640 | */
|
|---|
| 641 | function createDefaultLogger(levels) {
|
|---|
| 642 | let levelMaxLen = 0;
|
|---|
| 643 | let levelNames = new Map();
|
|---|
| 644 | levels.forEach(level => {
|
|---|
| 645 | if (level.length > levelMaxLen) {
|
|---|
| 646 | levelMaxLen = level.length;
|
|---|
| 647 | }
|
|---|
| 648 | });
|
|---|
| 649 |
|
|---|
| 650 | levels.forEach(level => {
|
|---|
| 651 | let levelName = level.toUpperCase();
|
|---|
| 652 | if (levelName.length < levelMaxLen) {
|
|---|
| 653 | levelName += ' '.repeat(levelMaxLen - levelName.length);
|
|---|
| 654 | }
|
|---|
| 655 | levelNames.set(level, levelName);
|
|---|
| 656 | });
|
|---|
| 657 |
|
|---|
| 658 | let print = (level, entry, message, ...args) => {
|
|---|
| 659 | let prefix = '';
|
|---|
| 660 | if (entry) {
|
|---|
| 661 | if (entry.tnx === 'server') {
|
|---|
| 662 | prefix = 'S: ';
|
|---|
| 663 | } else if (entry.tnx === 'client') {
|
|---|
| 664 | prefix = 'C: ';
|
|---|
| 665 | }
|
|---|
| 666 |
|
|---|
| 667 | if (entry.sid) {
|
|---|
| 668 | prefix = '[' + entry.sid + '] ' + prefix;
|
|---|
| 669 | }
|
|---|
| 670 |
|
|---|
| 671 | if (entry.cid) {
|
|---|
| 672 | prefix = '[#' + entry.cid + '] ' + prefix;
|
|---|
| 673 | }
|
|---|
| 674 | }
|
|---|
| 675 |
|
|---|
| 676 | message = util.format(message, ...args);
|
|---|
| 677 | message.split(/\r?\n/).forEach(line => {
|
|---|
| 678 | console.log('[%s] %s %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), levelNames.get(level), prefix + line);
|
|---|
| 679 | });
|
|---|
| 680 | };
|
|---|
| 681 |
|
|---|
| 682 | let logger = {};
|
|---|
| 683 | levels.forEach(level => {
|
|---|
| 684 | logger[level] = print.bind(null, level);
|
|---|
| 685 | });
|
|---|
| 686 |
|
|---|
| 687 | return logger;
|
|---|
| 688 | }
|
|---|