index.js 11 KB

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