-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathworkers.js
608 lines (566 loc) · 27.9 KB
/
workers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
/***
* @module workers.js
* This modules provides high-level complex methods and declarations specific to applications
* where workers are characterized as generally having dependencies, multiple actions, or logic.
* (c) 2020 Enchanted Engineering, MIT license
* @example
* const workers = require('./workers');
*
* TBD...
* JSDOCS
*/
///*************************************************************
/// Dependencies...
require('./Extensions2JS');
const frmt = require('util').format;
const FS = require('fs').constants;
const fsp = require('fs').promises;
const path = require('path');
const qs = require('querystring');
const https = require('https');
const { asList, asStyle, asTimeStr, base64:x64, hmac, jxTo, pad, print, uniqueID } = require('./helpers');
const bcrypt = require('bcryptjs');
const nodemailer = require('nodemailer');
const { resolve } = require('dns');
const { ExportConfigurationInstance } = require('twilio/lib/rest/bulkexports/v1/exportConfiguration');
///*************************************************************
/// worker definitions...
var workers = {}; // container variable
var unsupported = async ()=>{ throw(501); };
///*************************************************************
/// Process error handling for graceful exit...
let cleanup = {
callback: (code)=>{
let internals = workers.internals();
scribe.write('','dump',[JSON.stringify(internals,null,2)]);
scribe.write('','flush',[`Graceful exit[${code}]...`]); // default callback
},
delay: 400,
called: false // flag to prevent circular calls.
};
let gracefulExit = function (code=1) { // graceful exit call...
if (!cleanup.called) {
cleanup.called = true;
cleanup.callback(code); // do app specific cleaning once before exiting
setTimeout(process.exit,cleanup.delay,code); // no stopping!
};
};
// catch clean exit ...
process.on('beforeExit', function (code) { gracefulExit(code); });
// catch forced exit ...
process.on('exit', function (code) { gracefulExit(code); });
// catch ctrl+c event and exit gracefully
process.on('SIGINT', function () { gracefulExit(2); });
//catch uncaught exceptions, trace, then exit gracefully...
process.on('uncaughtException', function(e) { console.error('Uncaught Exception...\n',e.stack||e); gracefulExit(99); });
/**
* @function cleanupProcess manages process exit operations for graceful exit; optional call for override
* @param {} options - object for override of cleanup defaults
* @param {function} [options.callback] - callback called on gracefull exit
* @param {number} [options.delay] - callback called on gracefull exit
* @return internal cleanup object for confirmation
*/
workers.cleanupProcess = (options={}) => { cleanup.mergekeys(options); return cleanup; };
///*************************************************************
/// Authentication code routines...
/**
* @class auth provides routines to generate and check authentication codes and passwords
* @function checkCode validates a challenge code and returns a true/false result
* @param {string} challengeCode - code to be tested
* @param {object} credentials - object with validation parameters: code, iat, expiration
* @returns {boolean} - validation check state: true = valid code
* @function checkPW validates a password against a hash
* @param {string} pw - clear text password being tested
* @param {object} hash - valid password bcrypt hash
* @returns {boolean} - validation check state: true = valid password
* @function genHashPW encrypts a password for storing
* @param {string} pw - clear text password being encrypted
* @returns {string} - encrypted password
* @function genCode returns a unique code formatted per given parameters
* @param {number} size - length of result, default 7
* @param {number} base - modulo of the result: 10, 16, 36, default 10
* @param {number} exp - expiration time in minutes, default 10
* @return {object} - code object containing code, iat, and exp
* @function getActivationCode returns an activation code formatted per configuration
* @function getLoginCode returns an activation code formatted per configuration
*/
let cfgAuth = { activation: {size: 6, base: 10, expiration: 10}, login: {size: 7, base: 36, expiration: 10}, bcrypt_iterations: 11}
let auth = {
checkCode: (challengeCode,passcode) => {
if (!passcode) return false;
let expires = new Date((passcode.iat+passcode.exp)*1000);
if (expires<new Date()) return false;
return challengeCode===passcode.code; },
checkPW: async (pw,hash) => await bcrypt.compare(pw,hash),
genCode: (size=7,base=10,exp=10) => ({code: uniqueID(size,base), iat: new Date().valueOf()/1000|0, exp: exp*60}),
genHashPW: async (pw) => await bcrypt.hash(pw,cfgAuth.bcrypt_iterations),
getActivationCode: function() {
let { size, base, expiration } = cfgAuth.activation;
return auth.genCode(size, base, expiration); },
getLoginCode: function() {
let { size, base, expiration } = cfgAuth.login;
return auth.genCode(size, base, expiration); }
};
workers.auth = auth;
///*************************************************************
/// HTTP Error Messaging Service...
const httpCodes = {
'200': "OK", // success messages
'201': "Created",
'202': "Accepted",
'304': "Not Modified", // Redirection messages
'400': "Bad Request",
'401': "NOT Authorized!", // client errors
'403': "Forbidden",
'404': "File NOT found!",
'405': "Method Not Allowed",
'413': "Payload Too Large",
'500': "Internal Server Error", // server errors
'501': "Not Supported",
'503': "Service Unavailable"
};
/**
* @function httpStatusMsg implements unified (JSON) error message formating for http server responses
* @param {string|number|object} error - input error code
* @return {{}} - object suitable for delivery as JSON
*/
workers.httpStatusMsg = error => {
const validCode = (c) => Object.keys(httpCodes).includes(String(c));
if (typeof error == 'object') { // internal error or standard error with detail msg
let ext = { code: (('code' in error) && validCode(error.code)) ? Number(error.code) : 500 };
ext.msg = httpCodes[String(ext.code)] + ('msg' in error ? ` (${error.msg})` : '');
ext.detail = ('detail' in error) ? print(error.detail,40) : !('msg' in error) ? print(error,60) : '';
return ext;
} else { // some system responses or http error
let c = validCode(error) ? parseInt(error) : 500;
let e = { error: c>399, code: c, msg: httpCodes[String(error)]||'UNKNOWN ERROR'};
e.detail = e.msg=='UNKNOWN ERROR' ? print(error) : '';
return e
};
};
///*************************************************************
/// JSON Web Token handling...
/**
* @class jwt provides JSON Web Token (JWT) functions
* @function create defines a new JWT
* @param {object} data - token data
* @param {string} secret - encryption secret key, defaults to configured value of 256-bit unique value at startup
* @param {number} expiration - time in seconds until JWT expires
* @returns {object} a new JWT
* @function expired checks if a JWT has expired
* @param {number} expiration - time in seconds until JWT expires
* @returns {boolean} true if expired
* @function extract checks if a JWT has expired
* @param {object} jwt - JWT string
* @returns {object} JWT fields: header, payload, signature
* @function verify checks validity of a JWT
* @param {object} data - token data, accepts jwt string or jwt object (fields)
* @param {string} secret - encryption secret key, defaults to configured value of 256-bit unique value at startup
* @returns {object} JWT payload if valid, null if invalid
*/
// set defaults; override on initial workers call w/cfg
let cfgJWT = {expiration: 60*24, secret: uniqueID(64,16), apiKey: uniqueID(64,16), renewal: false};
workers.jwt = {
create: (data,secret,expiration) => {
// payload always includes 'initiated at' (iat) and expiration in minutes (exp), plus data
let exp = expiration===null ? 0 : expiration*60 || data.exp || cfgJWT.expiration*60; // expiration in seconds
let payload = Object.assign({},data,{ iat: new Date().valueOf()/1000|0, exp: exp, ext: cfgJWT.renewal});
let encHeader = x64.j64u({alg: 'HS256',typ: 'JWT'}); // only support for HS256
let encPayload = x64.j64u(payload);
let signature = x64.b64u(hmac(encHeader+'.'+encPayload,secret||cfgJWT.secret));
return [encHeader,encPayload,signature].join('.');
},
expired: (payload) => { // accepts decoded payload object; returns true if expired
let { exp, iat } = payload; // initiated at and expiration times in seconds
if (!exp) return false;
let expDate = new Date(1000*(iat + exp));
return expDate < new Date(); // exp < now == false if not expired
},
extract: (jwt) => {
let fields = (jwt+"..").split('.',3);
return { header: x64.u64j(fields[0]), payload: x64.u64j(fields[1]), signature: fields[2] };
},
verify: (jwt,secret) => { // accepts jwt token string; returns true if valid
let [ header, payload, signature ] = (jwt+"..").split('.',3); // encoded fields
let chkSignature = x64.b64u(hmac(header+'.'+payload,secret||cfgJWT.secret));
let payloadData = x64.u64j(payload);
let expired = workers.jwt.expired(payloadData);
return (signature===chkSignature) && !expired ? payloadData : null;
}
};
///*************************************************************
/// Directory/folder/file (i.e. files system objects, FSO) listing function
/**
* @function safeAccess safely stats a file system object without throwing an error (null)
* @param {string} spec - folder or file to stat
* @param {object} [lnks] - follows links as files and directories if true, else as links (ignored)
* @return {object} stats for the given file system object
*/
async function safeAccess(spec,flgs) {
let flags = {f:FS.F_OK, r:FS.R_OK, w:FS.W_OK, x:FS.X_OK };
let fx = (flgs||'').split('').map(f=>flags[f]).reduce((a,c)=>a|c,flags.f);
try { return await fsp.access(spec,fx) } catch(e) { return null; }; };
/**
* @function safeStat safely stats a file system object without throwing an error (null)
* @param {string} spec - folder or file to stat
* @param {object} [lnks] - follows links as files and directories if true, else as links (ignored)
* @return {object} stats for the given file system object
*/
async function safeStat(spec,lnks) { try { return await (lnks?fsp.stat(spec):fsp.lstat(spec)) } catch(e) { return null; }; };
/**
* @function listFolder recursively scans a directory folder and lists files and folders and their basic stats
* @param {string} dir - folder to scan
* @param {object} [options] - listing options
* @info options include, location: prefix for listing location, default '/', flat: flat listing flag (files only when true),
* links: flag to follow links when true
* @return {object} hierarchical or flat folder listing of files and subfolders contents and their details (i.e. stats)
*/
async function listFolder(dir, options={}) {
let listing = [];
let location = options.location===undefined ? '/' : options.location;
let recursive = options.recursive===undefined ? true : !!options.recursive;
try {
let fsoListing = await fsp.readdir(dir);
for (let f in fsoListing) {
let name = fsoListing[f]
let spec = path.resolve(dir,name);
let stats = await safeStat(spec,options.links);
let fso = !stats || stats.isSymbolicLink() ? null :
{ location: location+name, name: name, size:stats.size, time: stats.mtime,
type: stats.isFile()?'file':stats.isDirectory()?'dir':stats.isSymbolicLink()?'link':'unknown' };
if (fso) {
if (fso.type == 'dir' && !recursive) continue;
if (fso.type == 'dir') {
fso.listing = await listFolder(spec,options.mergekeys({location: fso.location+'/', recursive: recursive}));
if (options.flat) { listing = [...listing, ...fso.listing]; continue; };
};
if (fso.type!=='unknown' || options.unknown) listing.push(fso);
};
};
return listing;
} catch (e) { return e; };
};
workers.safeAccess = safeAccess;
workers.safeStat = safeStat;
workers.listFolder = listFolder;
///*************************************************************
/// eMail Service
let cfgMail = null;
/**
* @function mail sends a mail message via nodemailer, throws an error if nodemailer module not configured
* @param {object} msg - email message object containing addresses and body
* @param {string} msg.to - optional address list (string or array), at least one must be defined
* @param {string} msg.cc - optional address list
* @param {string} msg.bcc - optional address list
* @param {string} msg.from - optional from address, defaults to configuration
* @param {string} msg.subject - optional subject line, defaults to configuration
* @param {string} msg.text - plain text message,
* @param {string} msg.html - alternate email HTML formatted text message,
* @param {object} msg.body - alternate email content object {type: ..., value: ...},
* @return {} - object containing a summary report and transcript of action
*/
// email service wrapper assumes msg provides valid 'to,cc,&bcc addresses' and a 'body/text/html'
workers.mail = async function mail(msg) {
if (!cfgMail) throw 503;
let emsg = {from: cfgMail.from };
if (msg.from) emsg.replyTo = msg.from;
emsg.subject = msg.subject || msg.subj || cfgMail.subject;
let who = {to: asList(msg.to), cc: asList(msg.cc), bcc: asList(msg.bcc)}.filterByKey(lst=>lst.length);
if (Object.keys(who).length===0) who.to = asList(cfgMail.to);
emsg.mergekeys(who);
let toWhom = Object.keys(who).reduce((a,k)=>a.concat(who[k]),[]).join(', ');
if (msg.text||msg.body) emsg.text = msg.text||msg.body;
if (msg.html) emsg.html = msg.html;
if (!msg.text && !msg.html) emsg.text = cfgMail.text;
let note = { msg: `MAIL[${emsg.subject}] sent to: ${toWhom}` };
try {
let info = await cfgMail.transporter.sendMail(emsg);
return { response: msg.verbose ? info:info.response, msg: emsg, summary: {msg: note.msg, error: ''} };
} catch(e) {
return { error: e, msg: emsg, summary: {msg: note.msg, error: e.toString()||'REASON UNKNOWN'} };
};
};
///*************************************************************
/// mime-types lookup ...
let mimes = { // define most common mimeTypes, extend/override with configuration
'bin': 'application/octet-stream',
'css': 'text/css',
'csv': 'text/csv',
'gz': 'application/gzip',
'gif': 'image/gif',
'htm': 'text/html',
'html': 'text/html',
'ico': 'image/vnd.microsoft.icon',
'jpg': 'image/jpeg',
'js': 'text/javascript',
'json': 'application/json',
'md': 'text/markdown',
'mpg': 'video/mpeg',
'png': 'image/png',
'pdf': 'application/pdf',
'txt': 'text/plain',
'xml': 'application/xml'
};
let mimeTypesExtend = (mimeDefs={}) => {
mimes.mergekeys(mimeDefs);
Object.keys(mimes).map(e=>mimes[mimes[e]]=e); // add keys for applications for reverse lookup of extensions
return mimes;
};
/**
* @function mimeType returns the mime-type for a given extension or vice versa
* @param {string} mime - lookup key
* @param {*} fallback - default lookup
* @return {string} - mime-type for extension or extension for mime-type
*/
workers.mimeType = (mime) => mimes[mime.replace('.','')] || mimes['bin']; // application/octet-stream fallback
///*************************************************************
// scribe i.e. application logger, singleton object (worker)...
var scribe = {
tag: 'scribe',
transcript: {
file: 'scribe.log',
bsize: 10000,
fsize: 100000
},
buffer: '',
busy: false,
verbose: false,
mask: lvl => { if (scribe.levels.includes(lvl)) scribe.level = scribe.rank(lvl); return scribe.levels[scribe.level]; },
label: 'SCRIBE ', // tag formatted for output
level: 3, // rank equivalent for defined mask
// note levels, styles, and text must track
levels: ['dump', 'extra', 'trace', 'debug', 'log', 'info', 'warn', 'error', 'fatal', 'note', 'flush'],
rank: lvl => scribe.levels.indexOf(lvl),
styles: [['gray','dim'], ['magenta'], ['magenta','bold'], ['cyan','bold'], ['white'], ['green'],
['yellow','bold'], ['red'], ['bgRed','white','bold'], ['brightBlue'], ['bgCyan','black']],
text: ['DUMP ', 'EXTRA', 'TRACE', 'DEBUG', 'LOG ', 'INFO ', 'WARN ', 'ERROR', 'FATAL', 'NOTE ', 'FLUSH'],
toTranscript: function(text,flush) {
scribe.buffer += text + (flush ? '\n\n' : '\n'); // extra linefeed for "page-break" when flushing
if ((scribe.buffer.length>scribe.transcript.bsize) || flush)
scribe.saveTranscript().catch(e=>{ console.error(`Transcripting ERROR: ${e.message||e.toString()}`); });
},
saveTranscript: async function() {
if (scribe.busy) return; // already in process of saving transcript, just buffer new input
scribe.busy = true;
let tmp = scribe.buffer;
scribe.buffer = '';
let stat = {};
try { stat = await fsp.stat(scribe.transcript.file); } catch(e) {}; // undefined if not found
if ((stat.size+tmp.length)>scribe.transcript.fsize) { // roll the transcript log on overflow
let dx = new Date().style('stamp','local');
let parts = path.parse(scribe.transcript.file);
let bak = path.normalize(parts.dir + '/' + parts.name +'-' + dx + parts.ext);
await fsp.rename(scribe.transcript.file,bak); // rename log to backup
scribe.write(scribe.label,'trace',[`Rolled log: ${bak} [${stat.size}]`]);
};
await fsp.writeFile(scribe.transcript.file,tmp,{encoding:'utf8',flag:'a'}); // write tmp buffer to transcript file
scribe.busy=false;
},
write: function(label,level,args) {
let stamp = new Date().style('iso','local');
let rank = scribe.rank(level);
let msg = frmt.apply(this,args);
let prefix = [stamp,scribe.text[rank],label||scribe.label].join(' ') + ' ';
let lines = frmt.apply(this,args).replace(/\n/g,'\n'+' '.repeat(prefix.length)); // break msg lines and add blank prefix
if (rank >= scribe.level || level=='note') console.log(asStyle(scribe.styles[rank],prefix + lines));
if (level!='note') scribe.toTranscript(prefix + msg.replace(/\n/g,'|'), level=='fatal'||level=='flush');
}
};
scribe.mask('log'); // default level
// scribe instance object prototype
const scribePrototype = {
mask: scribe.mask,
dump: function(...args) { if (scribe.verbose) scribe.write(this.label,'dump',args) }, // always transcript only, no console output
extra: function(...args) { scribe.write(this.label,'extra',args) }, // verbose trace
trace: function(...args) { scribe.write(this.label,'trace',args) }, // verbose debug
debug: function(...args) { scribe.write(this.label,'debug',args) },
log: function(...args) { scribe.write(this.label,'log',args) },
info: function(...args) { scribe.write(this.label,'info',args) },
warn: function(...args) { scribe.write(this.label,'warn',args) },
error: function(...args) { scribe.write(this.label,'error',args) },
fatal: function(...args) { scribe.write(this.label,'fatal',args); process.exit(100); }, // always halts program!
note: function(...args) { scribe.write(this.label,'note',args) }, // always to console (only), no transcript output
flush: function(...args) { scribe.write(this.label,'flush',args) } // flush, always writes transcript to empty the buffer
};
/**
* @function scribe creates transcripting instances from scribe prototype
* @param {object} config - main configuration, overrides defaults
* @param {string} config - tag name reference for scribe instances, (8 character max)
*/
workers.Scribe = function Scribe(config={}) {
if (typeof config !== 'string') { // then override any defaults with defined values of object
scribe.tag = config.tag || scribe.tag;
scribe.verbose = config.verbose || scribe.verbose;
scribe.mask(config.mask);
scribe.transcript = ({}).mergekeys(scribe.transport).mergekeys(config.transcript||{});
};
let tag = (typeof config == 'string') ? config : scribe.tag;
return Object.create(scribePrototype).mergekeys({
tag: tag,
file: scribe.transcript.file,
label: pad(tag.toUpperCase(),8)
});
};
///*************************************************************
// define external services...
/// SMS Text Messaging service...
let cfgTwilio = null;
const prefix = (n)=>n && String(n).replace(/^\+{0,1}1{0,1}/,'+1'); // phone number formatting helper to prefix numbers with +1
/**
* @function sms sends a text message via Twilio, throws an error if Twilio module not installed
* @param {{}} msg - message object containing numbers list and text
* @param {[]} [msg.numbers] - optional list of numbers,array or comma delimited string, default to cfgTwilio.admin
* @param {string} msg.body - required message text, alternate msg.text
* @return {{}} - object containing a summary report and queue of action
*/
const smsPrefix = (n)=>n && String(n).replace(/^\+{0,1}1{0,1}/,'+1'); // prefix phone numbers with +1
const smsRequest = (payload) =>({
protocol: 'https:',
hostname: 'api.twilio.com',
method: 'POST',
path: '/2010-04-01/Accounts/'+cfgTwilio.accountSID+'/Messages.json',
auth: cfgTwilio.accountSID+':'+cfgTwilio.authToken,
headers: {
'Content-type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.from(payload).byteLength
}
});
const smsSend = function(msg) {
let payload = qs.stringify(msg);
let rqst = smsRequest(payload);
return new Promise((resolve,reject)=>{
let req = https.request(rqst,res=>{
let body = '';
res.on('data',d=>{body +=d});
res.on('end', ()=>resolve(jxTo(body,{})));
});
req.on('error',(e)=>reject(e));
req.end(payload);
});
};
// msg: { contact (for callback), numbers||to, [callback], body||text } => queue of reports and summary msg
workers.sms = async function sms(msg) {
if (!cfgTwilio) throw 503;
// convert numbers to list, prefix, filter invalid and duplicates
let contact = (cfgTwilio.callbackContacts[msg.contact]||[]);
let numbers = asList(msg.numbers||msg.to||contact||cfgTwilio.admin).map(p=>smsPrefix(p)).filter((v,i,a)=>v && (a.indexOf(v)==i));
const cb = msg.callback || cfgTwilio.callback || null; // optional server acknowledgement
let queue = await Promise.all(numbers.map(n => {
let txt = {To: n, From: cfgTwilio.number, Body: msg.body||msg.text, statusCallback:cb};
return new Promise(resolve => {
smsSend(txt)
.then(mr=>{ resolve({ report: mr, msg: txt, error: null, summary: { id:mr.sid, msg:`Text message queued to: ${n} as ${mr.sid}`, error:'' }}); })
.catch(e=>{ resolve({ report: null, msg: txt, error: e, summary: { msg:`Text message to: ${n} failed`, error: e.toString()||'REASON UNKNOWN' }}); });
});
}));
return queue;
};
class Internals {
constructor() {
this.data = {};
}
/**
* @function get retrieves internal data by tag and key
* @param {string} [tag] - first level identifier; may be undefined to retrieve all data under
* @param {string} [key] - second level identifier; may be undefined to retrieve a whole branch
* @return {*} - data as stored, which may be undefined as specified
*/
get(tag, key) {
if (tag===undefined) return this.data;
if (tag in this.data) {
if (key===undefined) return this.data[tag];
if (key in this.data[tag]) return this.data[tag][key];
};
return undefined;
}
/**
* @function set assigns a give value to a tag and key
* @param {string} tag - first level identifier
* @param {string} key - second level identifier; may be undefined to assign a whole branch
* @param {*} value - data assigned to internal
* @return {*} - internal value
*/
set(tag, key, value) {
this.data[tag] = (tag in this.data) ? this.data[tag] : {}; // verify existance of tag object or create
if (key===undefined || key===null) { // value may be an object (i.e. branch) to store directly
this.data[tag] = value;
return this.data[tag];
};
this.data[tag][key] = value;
return this.data[tag][key];
}
/**
* @function inc increments a statistic, or defines it if it does not exist
* @param {string} tag - first level identifier; required
* @param {string} key - second level identifier; required
* @return {*} - updated data value
*/
inc(tag, key) {
let value = this.get(tag,key);
this.set(tag, key, value ? value+1 : 1);
return this.get(tag,key);
}
/**
* @function refs retrieves a list of tags or keys
* @param {string} [tag] - undefined retrieves all data tags; or all keys for a defined tag
* @return {[]} - list of tags or keys
*/
refs(tag) { return tag ? Object.keys(this.data[tag]) : Object.keys(this.data) }
/**
* @function clear a statistic specified by tag and key or a branch specified by tag
* @param {string} [tag] - first level identifier; may be undefined to clear all object data
* @param {string} [key] - second level identifier; may be undefined to clear a whole branch
* @return {*} - undefined
*/
clear(tag, key) { tag ? (key ? delete this.data[tag][key] : delete this.data[tag]) : Object.keys(this.data).forEach(k=>delete this.data(k)) }
}
// internal server data...
workers.analytics = new Internals();
workers.blacklists = new Internals();
workers.logins = new Internals();
workers.statistics = new Internals();
workers.logins.log = function log(usr, tag, err) {
usr = usr || 'unknown';
workers.logins.set(usr,tag,new Date().toISOString());
if (tag.startsWith('fail')) {
let mark = workers.logins.get(usr, 'mark');
if (mark==undefined) {
workers.logins.set(usr, 'mark', new Date().toISOString());
mark = workers.logins.get(usr, 'mark');
};
if (+new Date() < (+new Date(mark) + 10*60*1000)) {
if (workers.logins.inc(usr, 'count') > 3) {
workers.logins.set(usr, 'mark', new Date().toISOString());
throw {code: 401, msg: 'Too many login attempts; Account locked for 10 minutes!'};
};
} else {
workers.logins.set(usr, 'mark', new Date().toISOString());
workers.logins.set(usr, 'count', 1);
};
if (err) throw err;
} else {
workers.logins.clear(usr,'count');
workers.logins.clear(usr,'mark');
};
};
workers.internals = ()=>{
if (!workers.statistics.get('$','start')) workers.statistics.set('$','start',new Date().toISOString() );
workers.statistics.set('$','uptime',asTimeStr(new Date() - new Date(workers.statistics.get('$','start'))));
let internals = { statistics: workers.statistics.get(), analytics: workers.analytics.get(),
logins: workers.logins.get(), blacklists: workers.blacklists.get() };
return internals;
}
module.exports = function configure(cfg={}) {
if ((typeof cfg=='object') && cfg!==null) {
if (cfg.auth) cfgAuth.mergekeys(cfg.auth);
if (cfg.jwt) cfgJWT.mergekeys(cfg.jwt);
mimeTypesExtend(cfg.mimeTypes); // must be called with or w/o configuration
if (cfg.mail) { // set configuration and create transport
cfgMail = cfg.mail;
cfgMail.transporter = nodemailer.createTransport(cfgMail.smtp);
};
if (cfg.twilio || cfg.text) cfgTwilio = cfg.twilio || cfg.text;
}
module.exports = workers;
return workers;
}