index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. /* eslint no-console: 0 */
  2. 'use strict';
  3. const urllib = require('url');
  4. const util = require('util');
  5. const fs = require('fs');
  6. const fetch = require('../fetch');
  7. const dns = require('dns');
  8. const net = require('net');
  9. const DNS_TTL = 5 * 60 * 1000;
  10. const resolver = (family, hostname, callback) => {
  11. dns['resolve' + family](hostname, (err, addresses) => {
  12. if (err) {
  13. switch (err.code) {
  14. case dns.NODATA:
  15. case dns.NOTFOUND:
  16. case dns.NOTIMP:
  17. return callback(null, []);
  18. }
  19. return callback(err);
  20. }
  21. return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
  22. });
  23. };
  24. const dnsCache = (module.exports.dnsCache = new Map());
  25. module.exports.resolveHostname = (options, callback) => {
  26. options = options || {};
  27. if (!options.host || net.isIP(options.host)) {
  28. // nothing to do here
  29. let value = {
  30. host: options.host,
  31. servername: options.servername || false
  32. };
  33. return callback(null, value);
  34. }
  35. let cached;
  36. if (dnsCache.has(options.host)) {
  37. cached = dnsCache.get(options.host);
  38. if (!cached.expires || cached.expires >= Date.now()) {
  39. return callback(null, {
  40. host: cached.value.host,
  41. servername: cached.value.servername,
  42. _cached: true
  43. });
  44. }
  45. }
  46. resolver(4, options.host, (err, addresses) => {
  47. if (err) {
  48. if (cached) {
  49. // ignore error, use expired value
  50. return callback(null, cached.value);
  51. }
  52. return callback(err);
  53. }
  54. if (addresses && addresses.length) {
  55. let value = {
  56. host: addresses[0] || options.host,
  57. servername: options.servername || options.host
  58. };
  59. dnsCache.set(options.host, {
  60. value,
  61. expires: Date.now() + DNS_TTL
  62. });
  63. return callback(null, value);
  64. }
  65. resolver(6, options.host, (err, addresses) => {
  66. if (err) {
  67. if (cached) {
  68. // ignore error, use expired value
  69. return callback(null, cached.value);
  70. }
  71. return callback(err);
  72. }
  73. if (addresses && addresses.length) {
  74. let value = {
  75. host: addresses[0] || options.host,
  76. servername: options.servername || options.host
  77. };
  78. dnsCache.set(options.host, {
  79. value,
  80. expires: Date.now() + DNS_TTL
  81. });
  82. return callback(null, value);
  83. }
  84. try {
  85. dns.lookup(options.host, {}, (err, address) => {
  86. if (err) {
  87. if (cached) {
  88. // ignore error, use expired value
  89. return callback(null, cached.value);
  90. }
  91. return callback(err);
  92. }
  93. if (!address && cached) {
  94. // nothing was found, fallback to cached value
  95. return callback(null, cached.value);
  96. }
  97. let value = {
  98. host: address || options.host,
  99. servername: options.servername || options.host
  100. };
  101. dnsCache.set(options.host, {
  102. value,
  103. expires: Date.now() + DNS_TTL
  104. });
  105. return callback(null, value);
  106. });
  107. } catch (err) {
  108. if (cached) {
  109. // ignore error, use expired value
  110. return callback(null, cached.value);
  111. }
  112. return callback(err);
  113. }
  114. });
  115. });
  116. };
  117. /**
  118. * Parses connection url to a structured configuration object
  119. *
  120. * @param {String} str Connection url
  121. * @return {Object} Configuration object
  122. */
  123. module.exports.parseConnectionUrl = str => {
  124. str = str || '';
  125. let options = {};
  126. [urllib.parse(str, true)].forEach(url => {
  127. let auth;
  128. switch (url.protocol) {
  129. case 'smtp:':
  130. options.secure = false;
  131. break;
  132. case 'smtps:':
  133. options.secure = true;
  134. break;
  135. case 'direct:':
  136. options.direct = true;
  137. break;
  138. }
  139. if (!isNaN(url.port) && Number(url.port)) {
  140. options.port = Number(url.port);
  141. }
  142. if (url.hostname) {
  143. options.host = url.hostname;
  144. }
  145. if (url.auth) {
  146. auth = url.auth.split(':');
  147. if (!options.auth) {
  148. options.auth = {};
  149. }
  150. options.auth.user = auth.shift();
  151. options.auth.pass = auth.join(':');
  152. }
  153. Object.keys(url.query || {}).forEach(key => {
  154. let obj = options;
  155. let lKey = key;
  156. let value = url.query[key];
  157. if (!isNaN(value)) {
  158. value = Number(value);
  159. }
  160. switch (value) {
  161. case 'true':
  162. value = true;
  163. break;
  164. case 'false':
  165. value = false;
  166. break;
  167. }
  168. // tls is nested object
  169. if (key.indexOf('tls.') === 0) {
  170. lKey = key.substr(4);
  171. if (!options.tls) {
  172. options.tls = {};
  173. }
  174. obj = options.tls;
  175. } else if (key.indexOf('.') >= 0) {
  176. // ignore nested properties besides tls
  177. return;
  178. }
  179. if (!(lKey in obj)) {
  180. obj[lKey] = value;
  181. }
  182. });
  183. });
  184. return options;
  185. };
  186. module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
  187. let entry = {};
  188. Object.keys(defaults || {}).forEach(key => {
  189. if (key !== 'level') {
  190. entry[key] = defaults[key];
  191. }
  192. });
  193. Object.keys(data || {}).forEach(key => {
  194. if (key !== 'level') {
  195. entry[key] = data[key];
  196. }
  197. });
  198. logger[level](entry, message, ...args);
  199. };
  200. /**
  201. * Returns a bunyan-compatible logger interface. Uses either provided logger or
  202. * creates a default console logger
  203. *
  204. * @param {Object} [options] Options object that might include 'logger' value
  205. * @return {Object} bunyan compatible logger
  206. */
  207. module.exports.getLogger = (options, defaults) => {
  208. options = options || {};
  209. let response = {};
  210. let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
  211. if (!options.logger) {
  212. // use vanity logger
  213. levels.forEach(level => {
  214. response[level] = () => false;
  215. });
  216. return response;
  217. }
  218. let logger = options.logger;
  219. if (options.logger === true) {
  220. // create console logger
  221. logger = createDefaultLogger(levels);
  222. }
  223. levels.forEach(level => {
  224. response[level] = (data, message, ...args) => {
  225. module.exports._logFunc(logger, level, defaults, data, message, ...args);
  226. };
  227. });
  228. return response;
  229. };
  230. /**
  231. * Wrapper for creating a callback that either resolves or rejects a promise
  232. * based on input
  233. *
  234. * @param {Function} resolve Function to run if callback is called
  235. * @param {Function} reject Function to run if callback ends with an error
  236. */
  237. module.exports.callbackPromise = (resolve, reject) =>
  238. function() {
  239. let args = Array.from(arguments);
  240. let err = args.shift();
  241. if (err) {
  242. reject(err);
  243. } else {
  244. resolve(...args);
  245. }
  246. };
  247. /**
  248. * Resolves a String or a Buffer value for content value. Useful if the value
  249. * is a Stream or a file or an URL. If the value is a Stream, overwrites
  250. * the stream object with the resolved value (you can't stream a value twice).
  251. *
  252. * This is useful when you want to create a plugin that needs a content value,
  253. * for example the `html` or `text` value as a String or a Buffer but not as
  254. * a file path or an URL.
  255. *
  256. * @param {Object} data An object or an Array you want to resolve an element for
  257. * @param {String|Number} key Property name or an Array index
  258. * @param {Function} callback Callback function with (err, value)
  259. */
  260. module.exports.resolveContent = (data, key, callback) => {
  261. let promise;
  262. if (!callback) {
  263. promise = new Promise((resolve, reject) => {
  264. callback = module.exports.callbackPromise(resolve, reject);
  265. });
  266. }
  267. let content = (data && data[key] && data[key].content) || data[key];
  268. let contentStream;
  269. let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
  270. .toString()
  271. .toLowerCase()
  272. .replace(/[-_\s]/g, '');
  273. if (!content) {
  274. return callback(null, content);
  275. }
  276. if (typeof content === 'object') {
  277. if (typeof content.pipe === 'function') {
  278. return resolveStream(content, (err, value) => {
  279. if (err) {
  280. return callback(err);
  281. }
  282. // we can't stream twice the same content, so we need
  283. // to replace the stream object with the streaming result
  284. data[key] = value;
  285. callback(null, value);
  286. });
  287. } else if (/^https?:\/\//i.test(content.path || content.href)) {
  288. contentStream = fetch(content.path || content.href);
  289. return resolveStream(contentStream, callback);
  290. } else if (/^data:/i.test(content.path || content.href)) {
  291. let parts = (content.path || content.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i);
  292. if (!parts) {
  293. return callback(null, Buffer.from(0));
  294. }
  295. return callback(null, /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2])));
  296. } else if (content.path) {
  297. return resolveStream(fs.createReadStream(content.path), callback);
  298. }
  299. }
  300. if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
  301. content = Buffer.from(data[key].content, encoding);
  302. }
  303. // default action, return as is
  304. setImmediate(() => callback(null, content));
  305. return promise;
  306. };
  307. /**
  308. * Copies properties from source objects to target objects
  309. */
  310. module.exports.assign = function(/* target, ... sources */) {
  311. let args = Array.from(arguments);
  312. let target = args.shift() || {};
  313. args.forEach(source => {
  314. Object.keys(source || {}).forEach(key => {
  315. if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
  316. // tls and auth are special keys that need to be enumerated separately
  317. // other objects are passed as is
  318. if (!target[key]) {
  319. // ensure that target has this key
  320. target[key] = {};
  321. }
  322. Object.keys(source[key]).forEach(subKey => {
  323. target[key][subKey] = source[key][subKey];
  324. });
  325. } else {
  326. target[key] = source[key];
  327. }
  328. });
  329. });
  330. return target;
  331. };
  332. module.exports.encodeXText = str => {
  333. // ! 0x21
  334. // + 0x2B
  335. // = 0x3D
  336. // ~ 0x7E
  337. if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
  338. return str;
  339. }
  340. let buf = Buffer.from(str);
  341. let result = '';
  342. for (let i = 0, len = buf.length; i < len; i++) {
  343. let c = buf[i];
  344. if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
  345. result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
  346. } else {
  347. result += String.fromCharCode(c);
  348. }
  349. }
  350. return result;
  351. };
  352. /**
  353. * Streams a stream value into a Buffer
  354. *
  355. * @param {Object} stream Readable stream
  356. * @param {Function} callback Callback function with (err, value)
  357. */
  358. function resolveStream(stream, callback) {
  359. let responded = false;
  360. let chunks = [];
  361. let chunklen = 0;
  362. stream.on('error', err => {
  363. if (responded) {
  364. return;
  365. }
  366. responded = true;
  367. callback(err);
  368. });
  369. stream.on('readable', () => {
  370. let chunk;
  371. while ((chunk = stream.read()) !== null) {
  372. chunks.push(chunk);
  373. chunklen += chunk.length;
  374. }
  375. });
  376. stream.on('end', () => {
  377. if (responded) {
  378. return;
  379. }
  380. responded = true;
  381. let value;
  382. try {
  383. value = Buffer.concat(chunks, chunklen);
  384. } catch (E) {
  385. return callback(E);
  386. }
  387. callback(null, value);
  388. });
  389. }
  390. /**
  391. * Generates a bunyan-like logger that prints to console
  392. *
  393. * @returns {Object} Bunyan logger instance
  394. */
  395. function createDefaultLogger(levels) {
  396. let levelMaxLen = 0;
  397. let levelNames = new Map();
  398. levels.forEach(level => {
  399. if (level.length > levelMaxLen) {
  400. levelMaxLen = level.length;
  401. }
  402. });
  403. levels.forEach(level => {
  404. let levelName = level.toUpperCase();
  405. if (levelName.length < levelMaxLen) {
  406. levelName += ' '.repeat(levelMaxLen - levelName.length);
  407. }
  408. levelNames.set(level, levelName);
  409. });
  410. let print = (level, entry, message, ...args) => {
  411. let prefix = '';
  412. if (entry) {
  413. if (entry.tnx === 'server') {
  414. prefix = 'S: ';
  415. } else if (entry.tnx === 'client') {
  416. prefix = 'C: ';
  417. }
  418. if (entry.sid) {
  419. prefix = '[' + entry.sid + '] ' + prefix;
  420. }
  421. if (entry.cid) {
  422. prefix = '[#' + entry.cid + '] ' + prefix;
  423. }
  424. }
  425. message = util.format(message, ...args);
  426. message.split(/\r?\n/).forEach(line => {
  427. console.log(
  428. '[%s] %s %s',
  429. new Date()
  430. .toISOString()
  431. .substr(0, 19)
  432. .replace(/T/, ' '),
  433. levelNames.get(level),
  434. prefix + line
  435. );
  436. });
  437. };
  438. let logger = {};
  439. levels.forEach(level => {
  440. logger[level] = print.bind(null, level);
  441. });
  442. return logger;
  443. }