@radatek/microserver 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3001 @@
1
+ /**
2
+ * MicroServer
3
+ * @version 2.0.0
4
+ * @package @radatek/microserver
5
+ * @copyright Darius Kisonas 2022
6
+ * @license MIT
7
+ */
8
+ import http from 'http';
9
+ import https from 'https';
10
+ import net from 'net';
11
+ import tls from 'tls';
12
+ import querystring from 'querystring';
13
+ import { Readable } from 'stream';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import crypto from 'crypto';
17
+ import zlib from 'zlib';
18
+ import { EventEmitter } from 'events';
19
+ const defaultToken = 'wx)>:ZUqVc+E,u0EmkPz%ZW@TFDY^3vm';
20
+ const defaultExpire = 24 * 60 * 60;
21
+ const defaultMaxBodySize = 5 * 1024 * 1024;
22
+ const defaultMethods = 'HEAD,GET,POST,PUT,PATCH,DELETE';
23
+ function NOOP(...args) { }
24
+ export class Warning extends Error {
25
+ constructor(text) {
26
+ super(text);
27
+ }
28
+ }
29
+ const commonCodes = { 404: 'Not found', 403: 'Access denied', 422: 'Invalid data' };
30
+ const commonTexts = { 'Not found': 404, 'Access denied': 403, 'Permission denied': 422, 'Invalid data': 422, InvalidData: 422, AccessDenied: 403, NotFound: 404, Failed: 422, OK: 200 };
31
+ export class ResponseError extends Error {
32
+ static getStatusCode(text) { return typeof text === 'number' ? text : text && commonTexts[text] || 500; }
33
+ static getStatusText(text) { return typeof text === 'number' ? commonCodes[text] : text?.toString() || 'Error'; }
34
+ constructor(text, statusCode) {
35
+ super(ResponseError.getStatusText(text || statusCode || 500));
36
+ this.statusCode = ResponseError.getStatusCode(statusCode || text) || 500;
37
+ }
38
+ }
39
+ export class AccessDenied extends ResponseError {
40
+ constructor(text) { super(text, 403); }
41
+ }
42
+ export class InvalidData extends ResponseError {
43
+ constructor(text, type) {
44
+ super(type ? text ? `Invalid ${type}: ${text}` : `Invalid ${type}` : text, 422);
45
+ }
46
+ }
47
+ export class NotFound extends ResponseError {
48
+ constructor(text) { super(text, 404); }
49
+ }
50
+ export class WebSocketError extends Error {
51
+ constructor(text, code) {
52
+ super(text);
53
+ this.statusCode = code || 1002;
54
+ }
55
+ }
56
+ export class Plugin {
57
+ constructor(router, ...args) { }
58
+ }
59
+ /** Extended http.IncomingMessage */
60
+ export class ServerRequest extends http.IncomingMessage {
61
+ constructor(router) {
62
+ super(new net.Socket());
63
+ /** Request whole path */
64
+ this.path = '/';
65
+ /** Request pathname */
66
+ this.pathname = '/';
67
+ /** Base url */
68
+ this.baseUrl = '/';
69
+ this._init(router);
70
+ }
71
+ _init(router) {
72
+ Object.assign(this, {
73
+ router,
74
+ protocol: 'encrypted' in this.socket && this.socket.encrypted ? 'https' : 'http',
75
+ get: {},
76
+ params: {},
77
+ paramsList: [],
78
+ path: '/',
79
+ pathname: '/',
80
+ baseUrl: '/',
81
+ rawBody: [],
82
+ rawBodySize: 0
83
+ });
84
+ this.updateUrl(this.url || '/');
85
+ }
86
+ /** Update request url */
87
+ updateUrl(url) {
88
+ this.url = url;
89
+ if (!this.originalUrl)
90
+ this.originalUrl = url;
91
+ const parsedUrl = new URL(url || '/', 'body:/'), pathname = parsedUrl.pathname;
92
+ this.pathname = pathname;
93
+ this.path = pathname.slice(pathname.lastIndexOf('/'));
94
+ this.baseUrl = pathname.slice(0, pathname.length - this.path.length);
95
+ this.get = {};
96
+ parsedUrl.searchParams.forEach((v, k) => this.get[k] = v);
97
+ }
98
+ /** Rewrite request url */
99
+ rewrite(url) {
100
+ throw new Error('Internal error');
101
+ }
102
+ /** Request body: JSON or POST parameters */
103
+ get body() {
104
+ if (!this._body) {
105
+ if (this.method === 'GET')
106
+ this._body = {};
107
+ else {
108
+ const contentType = this.headers['content-type'] || '', charset = contentType.match(/charset=(\S+)/);
109
+ let bodyString = Buffer.concat(this.rawBody).toString((charset ? charset[1] : 'utf8'));
110
+ this._body = {};
111
+ if (bodyString.startsWith('{') || bodyString.startsWith('[')) {
112
+ try {
113
+ this._body = JSON.parse(bodyString);
114
+ }
115
+ catch {
116
+ throw new Error('Invalid request format');
117
+ }
118
+ }
119
+ else if (contentType.startsWith('application/x-www-form-urlencoded')) {
120
+ this._body = querystring.parse(bodyString);
121
+ }
122
+ }
123
+ }
124
+ return this._body;
125
+ }
126
+ /** Alias to body */
127
+ get post() {
128
+ return this.body;
129
+ }
130
+ /** Get websocket */
131
+ get websocket() {
132
+ if (!this._websocket) {
133
+ if (!this.headers.upgrade)
134
+ throw new Error('Invalid WebSocket request');
135
+ this._websocket = new WebSocket(this, {
136
+ permessageDeflate: this.router.server.config.websocketCompress,
137
+ maxPayload: this.router.server.config.websocketMaxPayload || 1024 * 1024,
138
+ maxWindowBits: this.router.server.config.websocketMaxWindowBits || 10
139
+ });
140
+ }
141
+ if (!this._websocket.ready)
142
+ throw new Error('Invalid WebSocket request');
143
+ return this._websocket;
144
+ }
145
+ /** get files list in request */
146
+ async files() {
147
+ this.resume();
148
+ delete this.headers.connection;
149
+ const files = this._files;
150
+ if (files) {
151
+ if (files.resolve !== NOOP)
152
+ throw new Error('Invalid request files usage');
153
+ return new Promise((resolve, reject) => {
154
+ files.resolve = err => {
155
+ files.done = true;
156
+ files.resolve = NOOP;
157
+ if (err)
158
+ reject(err);
159
+ else
160
+ resolve(files.list);
161
+ };
162
+ if (files.done)
163
+ files.resolve();
164
+ });
165
+ }
166
+ }
167
+ /** Decode request body */
168
+ bodyDecode(res, options, next) {
169
+ const contentType = (this.headers['content-type'] || '').split(';');
170
+ const maxSize = options.maxBodySize || defaultMaxBodySize;
171
+ if (contentType.includes('multipart/form-data')) {
172
+ const chunkParse = (chunk) => {
173
+ const files = this._files;
174
+ if (!files || files.done)
175
+ return;
176
+ chunk = files.chunk = files.chunk ? Buffer.concat([files.chunk, chunk]) : chunk;
177
+ const p = chunk.indexOf(files.boundary) || -1;
178
+ if (p >= 0 && chunk.length - p >= 2) {
179
+ if (files.last) {
180
+ if (p > 0)
181
+ files.last.write(chunk.subarray(0, p));
182
+ files.last.srtream.close();
183
+ delete files.last.srtream;
184
+ files.last = undefined;
185
+ }
186
+ let pe = p + files.boundary.length;
187
+ if (chunk[pe] === 13 && chunk[pe + 1] === 10) {
188
+ chunk = files.chunk = chunk.subarray(p);
189
+ // next header
190
+ pe = chunk.indexOf('\r\n\r\n');
191
+ if (pe > 0) { // whole header
192
+ const header = chunk.toString('utf8', files.boundary.length + 2, pe);
193
+ chunk = chunk.subarray(pe + 4);
194
+ const fileInfo = header.match(/content-disposition: ([^\r\n]+)/i);
195
+ const contentType = header.match(/content-type: ([^\r\n;]+)/i);
196
+ let fieldName = '', fileName = '';
197
+ if (fileInfo)
198
+ fileInfo[1].replace(/(\w+)="?([^";]+)"?/, (_, n, v) => {
199
+ if (n === 'name')
200
+ fieldName = v;
201
+ if (n === 'filename')
202
+ fileName = v;
203
+ return _;
204
+ });
205
+ if (fileName) {
206
+ let file;
207
+ do {
208
+ file = path.resolve(path.join(files.uploadDir, crypto.randomBytes(16).toString('hex') + '.tmp'));
209
+ } while (fs.existsSync(file));
210
+ files.last = {
211
+ name: fieldName,
212
+ fileName: fileName,
213
+ contentType: contentType && contentType[1],
214
+ file: file,
215
+ stream: fs.createWriteStream(file)
216
+ };
217
+ files.list.push(files.last);
218
+ }
219
+ else if (fieldName) {
220
+ files.last = {
221
+ name: fieldName,
222
+ stream: {
223
+ write: (chunk) => {
224
+ if (!this._body)
225
+ this._body = {};
226
+ this._body[fieldName] = (this._body[fieldName] || '') + chunk.toString();
227
+ },
228
+ close() { }
229
+ }
230
+ };
231
+ }
232
+ }
233
+ }
234
+ else {
235
+ files.chunk = undefined;
236
+ files.done = true;
237
+ }
238
+ }
239
+ else {
240
+ if (chunk.length > 8096) {
241
+ if (files.last)
242
+ files.last.stream.write(chunk.subarray(0, files.boundary.length - 1));
243
+ chunk = files.chunk = chunk.subarray(files.boundary.length - 1);
244
+ }
245
+ }
246
+ };
247
+ this.pause();
248
+ //res.setHeader('Connection', 'close') // TODO: check if this is needed
249
+ this._body = {};
250
+ const files = this._files = {
251
+ list: [],
252
+ uploadDir: path.resolve(options.uploadDir || 'upload'),
253
+ resolve: NOOP,
254
+ boundary: ''
255
+ };
256
+ if (!contentType.find(l => {
257
+ const p = l.indexOf('boundary=');
258
+ if (p >= 0) {
259
+ files.boundary = '\r\n--' + l.slice(p + 9).trim();
260
+ return true;
261
+ }
262
+ }))
263
+ return res.error(400);
264
+ next();
265
+ this.once('error', () => files.resolve(new ResponseError('Request error')))
266
+ .on('data', chunk => chunkParse(chunk))
267
+ .once('end', () => files.resolve(new Error('Request error')));
268
+ res.on('finish', () => this._removeTempFiles());
269
+ res.on('error', () => this._removeTempFiles());
270
+ res.on('close', () => this._removeTempFiles());
271
+ }
272
+ else {
273
+ this.once('error', err => console.error(err))
274
+ .on('data', chunk => {
275
+ this.rawBodySize += chunk.length;
276
+ if (this.rawBodySize >= maxSize) {
277
+ this.pause();
278
+ res.setHeader('Connection', 'close');
279
+ res.error(413);
280
+ }
281
+ else
282
+ this.rawBody.push(chunk);
283
+ })
284
+ .once('end', next);
285
+ }
286
+ }
287
+ _removeTempFiles() {
288
+ if (this._files) {
289
+ if (!this._files.done) {
290
+ this.pause();
291
+ this._files.resolve(new Error('Invalid request files usage'));
292
+ }
293
+ this._files.list.forEach(f => {
294
+ if (f.stream)
295
+ f.stream.close();
296
+ if (f.file)
297
+ fs.unlink(f.file, NOOP);
298
+ });
299
+ this._files = undefined;
300
+ }
301
+ }
302
+ }
303
+ /** Extends http.ServerResponse */
304
+ export class ServerResponse extends http.ServerResponse {
305
+ constructor(router) {
306
+ super(new http.IncomingMessage(new net.Socket()));
307
+ this._init(router);
308
+ }
309
+ _init(router) {
310
+ this.router = router;
311
+ this.isJson = false;
312
+ this.headersOnly = false;
313
+ this.statusCode = 200;
314
+ }
315
+ /** Send error reponse */
316
+ error(error, text) {
317
+ let code = 0;
318
+ if (error instanceof Error) {
319
+ if ('statusCode' in error)
320
+ code = error.statusCode;
321
+ text = error.message;
322
+ }
323
+ else if (typeof error === 'number') {
324
+ code = error;
325
+ text = text || commonCodes[code] || 'Error';
326
+ }
327
+ else
328
+ text = error.toString();
329
+ if (!code && text) {
330
+ code = ResponseError.getStatusCode(text);
331
+ if (!code) {
332
+ const m = text.match(/^(Error|Exception)?([\w ]+)(Error|Exception)?:\s*(.+)/i);
333
+ if (m) {
334
+ const errorId = m[2].toLowerCase();
335
+ code = ResponseError.getStatusCode(m[1]);
336
+ text = m[2];
337
+ if (!code) {
338
+ if (errorId.includes('access'))
339
+ code = 403;
340
+ else if (errorId.includes('valid') || errorId.includes('case') || errorId.includes('param') || errorId.includes('permission'))
341
+ code = 422;
342
+ else if (errorId.includes('busy') || errorId.includes('timeout'))
343
+ code = 408;
344
+ }
345
+ }
346
+ }
347
+ }
348
+ code = code || 500;
349
+ try {
350
+ if (code === 400 || code === 413)
351
+ this.setHeader('Connection', 'close');
352
+ this.statusCode = code || 200;
353
+ if (code < 200 || code === 204 || (code >= 300 && code <= 399))
354
+ return this.send();
355
+ if (this.isJson && (code < 300 || code >= 400))
356
+ this.send({ success: false, error: text ?? (commonCodes[this.statusCode] || http.STATUS_CODES[this.statusCode]) });
357
+ else
358
+ this.send(text != null ? text : (this.statusCode + ' ' + (commonCodes[this.statusCode] || http.STATUS_CODES[this.statusCode])));
359
+ }
360
+ catch (e) {
361
+ this.statusCode = 500;
362
+ this.send('Internal error');
363
+ console.error(e);
364
+ }
365
+ }
366
+ /** Sets Content-Type acording to data and sends response */
367
+ send(data = '') {
368
+ if (data instanceof Readable)
369
+ return (data.pipe(this, { end: true }), void 0);
370
+ if (!this.getHeader('Content-Type') && !(data instanceof Buffer)) {
371
+ if (data instanceof Error)
372
+ return this.error(data);
373
+ if (this.isJson || typeof data === 'object') {
374
+ data = JSON.stringify(typeof data === 'string' ? { message: data } : data);
375
+ this.setHeader('Content-Type', 'application/json');
376
+ }
377
+ else {
378
+ data = data.toString();
379
+ if (data[0] === '{' || data[1] === '[')
380
+ this.setHeader('Content-Type', 'application/json');
381
+ else if (data[0] === '<' && (data.startsWith('<!DOCTYPE') || data.startsWith('<html')))
382
+ this.setHeader('Content-Type', 'text/html');
383
+ else
384
+ this.setHeader('Content-Type', 'text/plain');
385
+ }
386
+ }
387
+ data = data.toString();
388
+ this.setHeader('Content-Length', Buffer.byteLength(data, 'utf8'));
389
+ if (this.headersOnly)
390
+ this.end();
391
+ else
392
+ this.end(data, 'utf8');
393
+ }
394
+ /** Send json response */
395
+ json(data) {
396
+ this.isJson = true;
397
+ if (data instanceof Error)
398
+ return this.error(data);
399
+ this.send(data);
400
+ }
401
+ /** Send json response in form { success: false, error: err } */
402
+ jsonError(error, code) {
403
+ this.isJson = true;
404
+ this.statusCode = code || 200;
405
+ if (typeof error === 'number')
406
+ [code, error] = [error, http.STATUS_CODES[error] || 'Error'];
407
+ if (error instanceof Error)
408
+ return this.json(error);
409
+ this.json(typeof error === 'string' ? { success: false, error } : { success: false, ...error });
410
+ }
411
+ /** Send json response in form { success: true, ... } */
412
+ jsonSuccess(data, code) {
413
+ this.isJson = true;
414
+ this.statusCode = code || 200;
415
+ if (data instanceof Error)
416
+ return this.json(data);
417
+ this.json(typeof data === 'string' ? { success: true, message: data } : { success: true, ...data });
418
+ }
419
+ /** Send redirect response to specified URL with optional status code (default: 302) */
420
+ redirect(code, url) {
421
+ if (typeof code === 'string') {
422
+ url = code;
423
+ code = 302;
424
+ }
425
+ this.setHeader('Location', url || '/');
426
+ this.setHeader('Content-Length', 0);
427
+ this.statusCode = code || 302;
428
+ this.end();
429
+ }
430
+ }
431
+ const EMPTY_BUFFER = Buffer.alloc(0);
432
+ const DEFLATE_TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
433
+ /** WebSocket class */
434
+ export class WebSocket extends EventEmitter {
435
+ constructor(req, options) {
436
+ super();
437
+ this._buffers = [EMPTY_BUFFER];
438
+ this._buffersLength = 0;
439
+ this.ready = false;
440
+ this._socket = req.socket;
441
+ this._options = {
442
+ maxPayload: 1024 * 1024,
443
+ permessageDeflate: false,
444
+ maxWindowBits: 15,
445
+ ...options
446
+ };
447
+ const key = req.headers['sec-websocket-key'];
448
+ const upgrade = req.headers.upgrade;
449
+ const version = +(req.headers['sec-websocket-version'] || 0);
450
+ const extensions = req.headers['sec-websocket-extensions'];
451
+ const headers = [];
452
+ if (!key || !upgrade || upgrade.toLocaleLowerCase() !== 'websocket' || version !== 13 || req.method !== 'GET') {
453
+ this._abort('Invalid WebSocket request', 400);
454
+ return;
455
+ }
456
+ if (this._options.permessageDeflate && extensions?.includes('permessage-deflate')) {
457
+ let header = 'Sec-WebSocket-Extensions: permessage-deflate';
458
+ if ((this._options.maxWindowBits || 15) < 15 && extensions.includes('client_max_window_bits'))
459
+ header += `; client_max_window_bits=${this._options.maxWindowBits}`;
460
+ headers.push(header);
461
+ this._options.deflate = true;
462
+ }
463
+ this.ready = true;
464
+ this._upgrade(key, headers);
465
+ }
466
+ _upgrade(key, headers = []) {
467
+ const digest = crypto.createHash('sha1')
468
+ .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
469
+ .digest('base64');
470
+ headers = [
471
+ 'HTTP/1.1 101 Switching Protocols',
472
+ 'Upgrade: websocket',
473
+ 'Connection: Upgrade',
474
+ `Sec-WebSocket-Accept: ${digest}`,
475
+ ...headers,
476
+ '',
477
+ ''
478
+ ];
479
+ this._socket.write(headers.join('\r\n'));
480
+ this._socket.on('error', this._errorHandler.bind(this));
481
+ this._socket.on('data', this._dataHandler.bind(this));
482
+ this._socket.on('close', () => this.emit('close'));
483
+ this._socket.on('end', () => this.emit('end'));
484
+ }
485
+ /** Close connection */
486
+ close(reason, data) {
487
+ if (reason !== undefined) {
488
+ const buffer = Buffer.alloc(2 + (data ? data.length : 0));
489
+ buffer.writeUInt16BE(reason, 0);
490
+ if (data)
491
+ data.copy(buffer, 2);
492
+ data = buffer;
493
+ }
494
+ return this._sendFrame(0x88, data || EMPTY_BUFFER, () => this._socket.destroy());
495
+ }
496
+ /** Generate WebSocket frame from data */
497
+ static getFrame(data, options) {
498
+ let msgType = 8;
499
+ let dataLength = 0;
500
+ if (typeof data === 'string') {
501
+ msgType = 1;
502
+ dataLength = Buffer.byteLength(data, 'utf8');
503
+ }
504
+ else if (data instanceof Buffer) {
505
+ msgType = 2;
506
+ dataLength = data.length;
507
+ }
508
+ else if (typeof data === 'number') {
509
+ msgType = data;
510
+ }
511
+ const headerSize = 2 + (dataLength < 126 ? 0 : dataLength < 65536 ? 2 : 8) + (dataLength && options?.mask ? 4 : 0);
512
+ const frame = Buffer.allocUnsafe(headerSize + dataLength);
513
+ frame[0] = 0x80 | msgType;
514
+ frame[1] = dataLength > 65535 ? 127 : dataLength > 125 ? 126 : dataLength;
515
+ if (dataLength > 65535)
516
+ frame.writeBigUInt64BE(dataLength, 2);
517
+ else if (dataLength > 125)
518
+ frame.writeUInt16BE(dataLength, 2);
519
+ if (dataLength && frame.length > dataLength) {
520
+ if (typeof data === 'string')
521
+ frame.write(data, headerSize, 'utf8');
522
+ else
523
+ data.copy(frame, headerSize);
524
+ }
525
+ if (dataLength && options?.mask) {
526
+ let i = headerSize, h = headerSize - 4;
527
+ for (let i = 0; i < 4; i++)
528
+ frame[h + i] = Math.floor(Math.random() * 256);
529
+ for (let j = 0; j < dataLength; j++, i++) {
530
+ frame[i] ^= frame[h + (j & 3)];
531
+ }
532
+ }
533
+ return frame;
534
+ }
535
+ /** Send data */
536
+ send(data) {
537
+ let msgType = typeof data === 'string' ? 1 : 2;
538
+ if (typeof data === 'string')
539
+ data = Buffer.from(data, 'utf8');
540
+ if (this._options.deflate && data.length > 256) {
541
+ const output = [];
542
+ const deflate = zlib.createDeflateRaw({
543
+ windowBits: this._options.maxWindowBits
544
+ });
545
+ deflate.write(data);
546
+ deflate.on('data', (chunk) => output.push(chunk));
547
+ deflate.flush(() => {
548
+ if (output.length > 0 && output[output.length - 1].length > 4)
549
+ output[output.length - 1] = output[output.length - 1].subarray(0, output[output.length - 1].length - 4);
550
+ this._sendFrame(0xC0 | msgType, Buffer.concat(output));
551
+ });
552
+ }
553
+ else
554
+ return this._sendFrame(0x80 | msgType, data);
555
+ }
556
+ _errorHandler(error) {
557
+ this.emit('error', error);
558
+ if (this.ready)
559
+ this.close(error instanceof WebSocketError && error.statusCode || 1002);
560
+ else
561
+ this._socket.destroy();
562
+ this.ready = false;
563
+ }
564
+ _headerLength(buffer) {
565
+ if (this._frame)
566
+ return 0;
567
+ if (!buffer || buffer.length < 2)
568
+ return 2;
569
+ let hederInfo = buffer[1];
570
+ return 2 + (hederInfo & 0x80 ? 4 : 0) + ((hederInfo & 0x7F) === 126 ? 2 : 0) + ((hederInfo & 0x7F) === 127 ? 8 : 0);
571
+ }
572
+ _dataHandler(data) {
573
+ while (data.length) {
574
+ let frame = this._frame;
575
+ if (!frame) {
576
+ let lastBuffer = this._buffers[this._buffers.length - 1];
577
+ this._buffers[this._buffers.length - 1] = lastBuffer = Buffer.concat([lastBuffer, data]);
578
+ let headerLength = this._headerLength(lastBuffer);
579
+ if (lastBuffer.length < headerLength)
580
+ return;
581
+ const headerBits = lastBuffer[0];
582
+ const lengthBits = lastBuffer[1] & 0x7F;
583
+ this._buffers.pop();
584
+ data = lastBuffer.subarray(headerLength);
585
+ // parse header
586
+ frame = this._frame = {
587
+ fin: (headerBits & 0x80) !== 0,
588
+ rsv1: (headerBits & 0x40) !== 0,
589
+ opcode: headerBits & 0x0F,
590
+ mask: (lastBuffer[1] & 0x80) ? lastBuffer.subarray(headerLength - 4, headerLength) : EMPTY_BUFFER,
591
+ length: lengthBits === 126 ? lastBuffer.readUInt16BE(2) : lengthBits === 127 ? lastBuffer.readBigUInt64BE(2) : lengthBits,
592
+ lengthReceived: 0,
593
+ index: this._buffers.length
594
+ };
595
+ }
596
+ let toRead = frame.length - frame.lengthReceived;
597
+ if (toRead > data.length)
598
+ toRead = data.length;
599
+ if (this._options.maxPayload && this._options.maxPayload < this._buffersLength + frame.length) {
600
+ this._errorHandler(new WebSocketError('Payload too big', 1009));
601
+ return;
602
+ }
603
+ // unmask
604
+ for (let i = 0, j = frame.lengthReceived; i < toRead; i++, j++)
605
+ data[i] ^= frame.mask[j & 3];
606
+ frame.lengthReceived += toRead;
607
+ if (frame.lengthReceived < frame.length) {
608
+ this._buffers.push(data);
609
+ return;
610
+ }
611
+ this._buffers.push(data.subarray(0, toRead));
612
+ this._buffersLength += toRead;
613
+ data = data.subarray(toRead);
614
+ if (frame.opcode >= 8) {
615
+ const message = Buffer.concat(this._buffers.splice(frame.index));
616
+ switch (frame.opcode) {
617
+ case 8:
618
+ if (!frame.length)
619
+ this.emit('close');
620
+ else {
621
+ const code = message.readInt16BE(0);
622
+ if (frame.length === 2)
623
+ this.emit('close', code);
624
+ else
625
+ this.emit('close', code, message.subarray(2));
626
+ }
627
+ this._socket.destroy();
628
+ return;
629
+ case 9:
630
+ if (message.length)
631
+ this.emit('ping', message);
632
+ else
633
+ this.emit('ping');
634
+ if (this._options.autoPong)
635
+ this.pong(message);
636
+ break;
637
+ case 10:
638
+ if (message.length)
639
+ this.emit('pong', message);
640
+ else
641
+ this.emit('pong');
642
+ break;
643
+ default:
644
+ return this._errorHandler(new WebSocketError('Invalid WebSocket frame'));
645
+ }
646
+ }
647
+ else if (frame.fin) {
648
+ if (!frame.opcode)
649
+ return this._errorHandler(new WebSocketError('Invalid WebSocket frame'));
650
+ if (this._options.deflate && frame.rsv1) {
651
+ const output = [];
652
+ const inflate = zlib.createInflateRaw({
653
+ windowBits: this._options.maxWindowBits
654
+ });
655
+ inflate.on('data', (chunk) => output.push(chunk));
656
+ inflate.on('error', (err) => this._errorHandler(err));
657
+ for (const buffer of this._buffers)
658
+ inflate.write(buffer);
659
+ inflate.write(DEFLATE_TRAILER);
660
+ inflate.flush(() => {
661
+ if (this.ready) {
662
+ const message = Buffer.concat(output);
663
+ this.emit('message', frame.opcode === 1 ? message.toString('utf8') : message);
664
+ }
665
+ });
666
+ }
667
+ else {
668
+ const message = Buffer.concat(this._buffers);
669
+ this.emit('message', frame.opcode === 1 ? message.toString('utf8') : message);
670
+ }
671
+ this._buffers = [];
672
+ this._buffersLength = 0;
673
+ }
674
+ this._frame = undefined;
675
+ this._buffers.push(EMPTY_BUFFER);
676
+ }
677
+ }
678
+ _abort(message, code, headers) {
679
+ code = code || 400;
680
+ message = message || http.STATUS_CODES[code] || 'Closed';
681
+ headers = [
682
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
683
+ 'Connection: close',
684
+ 'Content-Type: text/html',
685
+ `Content-Length: ${Buffer.byteLength(message)}`,
686
+ '',
687
+ message
688
+ ];
689
+ this._socket.once('finish', () => {
690
+ this._socket.destroy();
691
+ this.emit('close');
692
+ });
693
+ this._socket.end(headers.join('\r\n'));
694
+ this.emit('error', new Error(message));
695
+ }
696
+ /** Send ping frame */
697
+ ping(buffer) {
698
+ this._sendFrame(0x89, buffer || EMPTY_BUFFER);
699
+ }
700
+ /** Send pong frame */
701
+ pong(buffer) {
702
+ this._sendFrame(0x8A, buffer || EMPTY_BUFFER);
703
+ }
704
+ _sendFrame(opcode, data, cb) {
705
+ if (!this.ready)
706
+ return;
707
+ const dataLength = data.length;
708
+ const headerSize = 2 + (dataLength < 126 ? 0 : dataLength < 65536 ? 2 : 8);
709
+ const frame = Buffer.allocUnsafe(headerSize + (dataLength < 4096 ? dataLength : 0));
710
+ frame[0] = opcode;
711
+ frame[1] = dataLength > 65535 ? 127 : dataLength > 125 ? 126 : dataLength;
712
+ if (dataLength > 65535)
713
+ frame.writeBigUInt64BE(dataLength, 2);
714
+ else if (dataLength > 125)
715
+ frame.writeUInt16BE(dataLength, 2);
716
+ if (dataLength && frame.length > dataLength) {
717
+ data.copy(frame, headerSize);
718
+ this._socket.write(frame, cb);
719
+ }
720
+ else
721
+ this._socket.write(frame, () => this._socket.write(data, cb));
722
+ }
723
+ }
724
+ const server = {};
725
+ /**
726
+ * Controller for dynamic routes
727
+ *
728
+ * @example
729
+ * ```js
730
+ * class MyController extends Controller {
731
+ * static model = MyModel;
732
+ * static acl = 'auth';
733
+ *
734
+ * static 'acl:index' = '';
735
+ * static 'url:index' = 'GET /index';
736
+ * async index (req, res) {
737
+ * res.send('Hello World')
738
+ * }
739
+ *
740
+ * //function name prefixes translated to HTTP methods:
741
+ * // all => GET, get => GET, insert => POST, post => POST,
742
+ * // update => PUT, put => PUT, delete => DELETE,
743
+ * // modify => PATCH, patch => PATCH,
744
+ * // websocket => internal WebSocket
745
+ * // automatic acl will be: class_name + '/' + function_name_prefix
746
+ * // automatic url will be: method + ' /' + class_name + '/' + function_name_without_prefix
747
+ *
748
+ * //static 'acl:allUsers' = 'MyController/all';
749
+ * //static 'url:allUsers' = 'GET /MyController/Users';
750
+ * async allUsers () {
751
+ * return ['usr1', 'usr2', 'usr3']
752
+ * }
753
+ *
754
+ * //static 'acl:getOrder' = 'MyController/get';
755
+ * //static 'url:getOrder' = 'GET /Users/:id/:id1';
756
+ * static 'group:getOrder' = 'orders';
757
+ * static 'model:getOrder' = OrderModel;
758
+ * async getOrder (id: string, id1: string) {
759
+ * return {id, extras: id1, type: 'order'}
760
+ * }
761
+ *
762
+ * //static 'acl:insertOrder' = 'MyController/insert';
763
+ * //static 'url:insertOrder' = 'POST /Users/:id';
764
+ * static 'model:insertOrder' = OrderModel;
765
+ * async insertOrder (id: string, id1: string) {
766
+ * return {id, extras: id1, type: 'order'}
767
+ * }
768
+ *
769
+ * static 'acl:POST /login' = '';
770
+ * async 'POST /login' () {
771
+ * return {id, extras: id1, type: 'order'}
772
+ * }
773
+ * }
774
+ * ```
775
+ */
776
+ export class Controller {
777
+ constructor(req, res) {
778
+ this.req = req;
779
+ this.res = res;
780
+ res.isJson = true;
781
+ }
782
+ /** Generate routes for this controller */
783
+ static routes() {
784
+ const routes = [];
785
+ const prefix = Object.getOwnPropertyDescriptor(this, 'name')?.enumerable ? this.name + '/' : '';
786
+ // iterate throught decorators
787
+ Object.getOwnPropertyNames(this.prototype).forEach(key => {
788
+ if (key === 'constructor' || key.startsWith('_'))
789
+ return;
790
+ const func = this.prototype[key];
791
+ if (typeof func !== 'function')
792
+ return;
793
+ const thisStatic = this;
794
+ let url = thisStatic['url:' + key];
795
+ let acl = thisStatic['acl:' + key] ?? thisStatic['acl'];
796
+ const user = thisStatic['user:' + key] ?? thisStatic['user'];
797
+ const group = thisStatic['group:' + key] ?? thisStatic['group'];
798
+ const model = thisStatic['model:' + key] ?? thisStatic['model'];
799
+ let method = '';
800
+ if (!url)
801
+ key = key.replaceAll('$', '/');
802
+ if (!url && key.startsWith('/')) {
803
+ method = '*';
804
+ url = key;
805
+ }
806
+ let keyMatch = !url && key.match(/^(all|get|put|post|patch|insert|update|modify|delete|websocket)[/_]?([\w_/-]*)$/i);
807
+ if (keyMatch) {
808
+ method = keyMatch[1];
809
+ url = '/' + prefix + keyMatch[2];
810
+ }
811
+ keyMatch = !url && key.match(/^([*\w]+) (.+)$/);
812
+ if (keyMatch) {
813
+ method = keyMatch[1];
814
+ url = keyMatch[2].startsWith('/') ? keyMatch[2] : ('/' + prefix + keyMatch[1]);
815
+ }
816
+ keyMatch = !method && url?.match(/^([*\w]+) (.+)$/);
817
+ if (keyMatch) {
818
+ method = keyMatch[1];
819
+ url = keyMatch[2].startsWith('/') ? keyMatch[2] : ('/' + prefix + keyMatch[2]);
820
+ }
821
+ if (!method)
822
+ return;
823
+ let autoAcl = method.toLowerCase();
824
+ switch (autoAcl) {
825
+ case '*':
826
+ autoAcl = '';
827
+ break;
828
+ case 'post':
829
+ autoAcl = 'insert';
830
+ break;
831
+ case 'put':
832
+ autoAcl = 'update';
833
+ break;
834
+ case 'patch':
835
+ autoAcl = 'modify';
836
+ break;
837
+ }
838
+ method = method.toUpperCase();
839
+ switch (method) {
840
+ case '*':
841
+ break;
842
+ case 'GET':
843
+ case 'POST':
844
+ case 'PUT':
845
+ case 'PATCH':
846
+ case 'DELETE':
847
+ case 'WEBSOCKET':
848
+ break;
849
+ case 'ALL':
850
+ method = 'GET';
851
+ break;
852
+ case 'INSERT':
853
+ method = 'POST';
854
+ break;
855
+ case 'UPDATE':
856
+ method = 'PUT';
857
+ break;
858
+ case 'MODIFY':
859
+ method = 'PATCH';
860
+ break;
861
+ default:
862
+ throw new Error('Invalid url method for: ' + key);
863
+ }
864
+ if (user === undefined && group === undefined && acl === undefined)
865
+ acl = prefix + autoAcl;
866
+ // add params if not available in url
867
+ if (func.length && !url.includes(':')) {
868
+ let args = ['/:id'];
869
+ for (let i = 1; i < func.length; i++)
870
+ args.push('/:id' + i);
871
+ url += args.join('');
872
+ }
873
+ const list = [method + ' ' + url.replace(/\/\//g, '/')];
874
+ if (acl)
875
+ list.push('acl:' + acl);
876
+ if (user)
877
+ list.push('user:' + user);
878
+ if (group)
879
+ list.push('group:' + group);
880
+ list.push((req, res) => {
881
+ res.isJson = true;
882
+ const obj = new this(req, res);
883
+ if (model) {
884
+ req.model = obj.model = model instanceof Model ? model : Model.models[model];
885
+ if (!obj.model)
886
+ throw new InvalidData(model, 'model');
887
+ }
888
+ return func.apply(obj, req.paramsList);
889
+ });
890
+ routes.push(list);
891
+ });
892
+ return routes;
893
+ }
894
+ }
895
+ /** Router */
896
+ export class Router extends EventEmitter {
897
+ /** @param {MicroServer} server */
898
+ constructor(server) {
899
+ super();
900
+ this._stack = [];
901
+ this._stackAfter = [];
902
+ this._tree = {};
903
+ this.server = server;
904
+ }
905
+ /** Handler */
906
+ handler(req, res, next, method) {
907
+ const nextAfter = next;
908
+ next = () => this._walkStack(this._stackAfter, req, res, nextAfter);
909
+ if (method)
910
+ return !this._walkTree(this._tree[method], req, res, next) && next();
911
+ const walk = () => {
912
+ if (!this._walkTree(this._tree[req.method || 'GET'], req, res, next) &&
913
+ !this._walkTree(this._tree['*'], req, res, next))
914
+ next();
915
+ };
916
+ req.rewrite = (url) => {
917
+ if (req.originalUrl)
918
+ res.error(508);
919
+ req.updateUrl(url);
920
+ walk();
921
+ };
922
+ this._walkStack(this._stack, req, res, walk);
923
+ }
924
+ _walkStack(rstack, req, res, next) {
925
+ let rnexti = 0;
926
+ const sendData = (data) => {
927
+ if (!res.headersSent && data !== undefined) {
928
+ if ((data === null || typeof data === 'string') && !res.isJson)
929
+ return res.send(data);
930
+ if (typeof data === 'object' &&
931
+ (data instanceof Buffer || data instanceof Readable || (data instanceof Error && !res.isJson)))
932
+ return res.send(data);
933
+ return res.jsonSuccess(data);
934
+ }
935
+ };
936
+ const rnext = () => {
937
+ const cb = rstack[rnexti++];
938
+ if (cb) {
939
+ try {
940
+ req.router = this;
941
+ const p = cb(req, res, rnext);
942
+ if (p instanceof Promise)
943
+ p.catch(e => e).then(sendData);
944
+ else
945
+ sendData(p);
946
+ }
947
+ catch (e) {
948
+ sendData(e);
949
+ }
950
+ }
951
+ else
952
+ return next();
953
+ };
954
+ return rnext();
955
+ }
956
+ _walkTree(item, req, res, next) {
957
+ req.params = {};
958
+ req.paramsList = [];
959
+ const rstack = [];
960
+ const reg = /\/([^/]*)/g;
961
+ let m;
962
+ let lastItem, done;
963
+ while (m = reg.exec(req.pathname)) {
964
+ const name = m[1];
965
+ if (!item || done) {
966
+ item = undefined;
967
+ break;
968
+ }
969
+ if (lastItem !== item) {
970
+ lastItem = item;
971
+ item.hook?.forEach((hook) => rstack.push(hook.bind(item)));
972
+ }
973
+ if (!item.tree) { // last
974
+ if (item.name) {
975
+ req.params[item.name] += '/' + name;
976
+ req.paramsList[req.paramsList.length - 1] = req.params[item.name];
977
+ }
978
+ else
979
+ done = true;
980
+ }
981
+ else {
982
+ item = item.last || item.tree[name] || item.param;
983
+ if (item && item.name) {
984
+ req.params[item.name] = name;
985
+ req.paramsList.push(name);
986
+ }
987
+ }
988
+ }
989
+ if (lastItem !== item)
990
+ item?.hook?.forEach((hook) => rstack.push(hook.bind(item)));
991
+ item?.next?.forEach((cb) => rstack.push(cb));
992
+ if (!rstack.length)
993
+ return;
994
+ this._walkStack(rstack, req, res, next);
995
+ return true;
996
+ }
997
+ _add(method, url, key, middlewares) {
998
+ if (key === 'next')
999
+ this.server.emit('route', {
1000
+ method,
1001
+ url,
1002
+ middlewares
1003
+ });
1004
+ middlewares = middlewares.map(i => this.server.bind(i));
1005
+ let item = this._tree[method];
1006
+ if (!item)
1007
+ item = this._tree[method] = { tree: {} };
1008
+ if (!url.startsWith('/')) {
1009
+ if (method === '*' && url === '') {
1010
+ this._stack.push(...middlewares);
1011
+ return this;
1012
+ }
1013
+ url = '/' + url;
1014
+ }
1015
+ const reg = /\/(:?)([^/*]+)(\*?)/g;
1016
+ let m;
1017
+ while (m = reg.exec(url)) {
1018
+ const param = m[1], name = m[2], last = m[3];
1019
+ if (last) {
1020
+ item.last = { name: name };
1021
+ item = item.last;
1022
+ }
1023
+ else {
1024
+ if (!item.tree)
1025
+ throw new Error('Invalid route path');
1026
+ if (param) {
1027
+ item = item.param = item.param || { tree: {}, name: name };
1028
+ }
1029
+ else {
1030
+ let subitem = item.tree[name];
1031
+ if (!subitem)
1032
+ subitem = item.tree[name] = { tree: {} };
1033
+ item = subitem;
1034
+ }
1035
+ }
1036
+ }
1037
+ if (!item[key])
1038
+ item[key] = [];
1039
+ item[key].push(...middlewares);
1040
+ return this;
1041
+ }
1042
+ /** Clear routes and middlewares */
1043
+ clear() {
1044
+ this._tree = {};
1045
+ this._stack = [];
1046
+ return this;
1047
+ }
1048
+ /**
1049
+ * Add middleware route.
1050
+ * Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
1051
+ *
1052
+ * @signature add(plugin: Plugin)
1053
+ * @param {Plugin} plugin plugin module instance
1054
+ * @return {Router} current router
1055
+ *
1056
+ * @signature add(pluginid: string, ...args: any)
1057
+ * @param {string} pluginid pluginid module
1058
+ * @param {...any} args arguments passed to constructor
1059
+ * @return {Router} current router
1060
+ *
1061
+ * @signature add(pluginClass: typeof Plugin, ...args: any)
1062
+ * @param {typeof Plugin} pluginClass plugin class
1063
+ * @param {...any} args arguments passed to constructor
1064
+ * @return {Router} current router
1065
+ *
1066
+ * @signature add(middleware: Middleware)
1067
+ * @param {Middleware} middleware
1068
+ * @return {Router} current router
1069
+ *
1070
+ * @signature add(methodUrl: string, ...middlewares: any)
1071
+ * @param {string} methodUrl 'METHOD /url' or '/url'
1072
+ * @param {...any} middlewares
1073
+ * @return {Router} current router
1074
+ *
1075
+ * @signature add(methodUrl: string, controllerClass: typeof Controller)
1076
+ * @param {string} methodUrl 'METHOD /url' or '/url'
1077
+ * @param {typeof Controller} controllerClass
1078
+ * @return {Router} current router
1079
+ *
1080
+ * @signature add(methodUrl: string, routes: Array<Array<any>>)
1081
+ * @param {string} methodUrl 'METHOD /url' or '/url'
1082
+ * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1083
+ * @return {Router} current router
1084
+ *
1085
+ * @signature add(methodUrl: string, routes: Array<Array<any>>)
1086
+ * @param {string} methodUrl 'METHOD /url' or '/url'
1087
+ * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1088
+ * @return {Router} current router
1089
+ *
1090
+ * @signature add(routes: { [key: string]: Array<any> })
1091
+ * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1092
+ * @return {Router} current router
1093
+ *
1094
+ * @signature add(methodUrl: string, routes: { [key: string]: Array<any> })
1095
+ * @param {string} methodUrl 'METHOD /url' or '/url'
1096
+ * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1097
+ * @return {Router} current router
1098
+ */
1099
+ add(...args) {
1100
+ if (!args[0])
1101
+ return this;
1102
+ // add(plugin)
1103
+ if (args[0] instanceof Plugin)
1104
+ return this._plugin(args[0]);
1105
+ // add(pluginid, ...args)
1106
+ if (typeof args[0] === 'string' && MicroServer.plugins[args[0]]) {
1107
+ const constructor = MicroServer.plugins[args[0]];
1108
+ const plugin = new constructor(this, ...args.slice(1));
1109
+ return this._plugin(plugin);
1110
+ }
1111
+ if (args[0].prototype instanceof Plugin) {
1112
+ const plugin = new args[0](this, ...args.slice(1));
1113
+ return this._plugin(plugin);
1114
+ }
1115
+ if (typeof args[0] === 'function') {
1116
+ class Middleware extends Plugin {
1117
+ constructor() {
1118
+ super(...arguments);
1119
+ this.priority = args[0].priority;
1120
+ }
1121
+ }
1122
+ const middleware = new Middleware(this);
1123
+ middleware.handler = args[0];
1124
+ return this._plugin(middleware);
1125
+ }
1126
+ let method = '*', url = '/';
1127
+ if (typeof args[0] === 'string') {
1128
+ const m = args[0].match(/^([A-Z]+) (.*)/);
1129
+ if (m)
1130
+ [method, url] = [m[1], m[2]];
1131
+ else
1132
+ url = args[0];
1133
+ if (!url.startsWith('/'))
1134
+ throw new Error(`Invalid url ${url}`);
1135
+ args = args.slice(1);
1136
+ }
1137
+ // add('/url', ControllerClass)
1138
+ if (typeof args[0] === 'function' && args[0].prototype instanceof Controller) {
1139
+ const routes = args[0].routes();
1140
+ if (routes)
1141
+ args[0] = routes;
1142
+ }
1143
+ // add('/url', [ ['METHOD /url', ...], {'METHOD } ])
1144
+ if (Array.isArray(args[0])) {
1145
+ if (method !== '*')
1146
+ throw new Error('Invalid router usage');
1147
+ args[0].forEach(item => {
1148
+ if (Array.isArray(item)) {
1149
+ // [methodUrl, ...middlewares]
1150
+ if (typeof item[0] !== 'string' || !item[0].match(/^(\w+ )?\//))
1151
+ throw new Error('Url expected');
1152
+ return this.add(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1153
+ }
1154
+ else
1155
+ throw new Error('Invalid param');
1156
+ });
1157
+ return this;
1158
+ }
1159
+ // add('/url', {'METHOD /url': [...middlewares], ... } ])
1160
+ if (typeof args[0] === 'object' && args[0].constructor === Object) {
1161
+ if (method !== '*')
1162
+ throw new Error('Invalid router usage');
1163
+ for (const [subUrl, subArgs] of Object.entries(args[0])) {
1164
+ if (!subUrl.match(/^(\w+ )?\//))
1165
+ throw new Error('Url expected');
1166
+ this.add(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1167
+ }
1168
+ return this;
1169
+ }
1170
+ // add('/url', ...middleware)
1171
+ return this._add(method, url, 'next', args.filter((o) => o));
1172
+ }
1173
+ _middleware(middleware) {
1174
+ if (!middleware)
1175
+ return this;
1176
+ const priority = (middleware?.priority || 0) - 1;
1177
+ const stack = priority < -1 ? this._stackAfter : this._stack;
1178
+ const idx = stack.findIndex(f => 'priority' in f
1179
+ && priority > (f.priority || 0));
1180
+ stack.splice(idx < 0 ? stack.length : idx, 0, middleware);
1181
+ return this;
1182
+ }
1183
+ _plugin(plugin) {
1184
+ if (plugin.handler) {
1185
+ const middleware = plugin.handler.bind(plugin);
1186
+ middleware.plugin = plugin;
1187
+ middleware.priority = plugin.priority;
1188
+ return this._middleware(middleware);
1189
+ }
1190
+ return this;
1191
+ }
1192
+ /** Add hook */
1193
+ hook(url, ...mid) {
1194
+ const m = url.match(/^([A-Z]+) (.*)/);
1195
+ let method = '*';
1196
+ if (m)
1197
+ [method, url] = [m[1], m[2]];
1198
+ return this._add(method, url, 'hook', mid);
1199
+ }
1200
+ /** Check if middleware allready added */
1201
+ has(mid) {
1202
+ return this._stack.includes(mid) || (mid.name && !!this._stack.find(f => f.name === mid.name)) || false;
1203
+ }
1204
+ }
1205
+ export class MicroServer extends EventEmitter {
1206
+ constructor(config) {
1207
+ super();
1208
+ this._ready = false;
1209
+ this.plugins = {};
1210
+ this._methods = {};
1211
+ let promise = Promise.resolve();
1212
+ this._init = (f, ...args) => {
1213
+ promise = promise.then(() => f.apply(this, args)).catch(e => this.emit('error', e));
1214
+ };
1215
+ this.config = {
1216
+ maxBodySize: defaultMaxBodySize,
1217
+ methods: defaultMethods,
1218
+ ...config,
1219
+ root: path.normalize(config.root || process.cwd())
1220
+ };
1221
+ (config.methods || defaultMethods).split(',').map(s => s.trim()).forEach(m => this._methods[m] = true);
1222
+ this.router = new Router(this);
1223
+ this.servers = new Set();
1224
+ this.sockets = new Set();
1225
+ if (config.routes)
1226
+ this.use(config.routes);
1227
+ for (const key in MicroServer.plugins) {
1228
+ if (config[key])
1229
+ this.router.add(MicroServer.plugins[key], config[key]);
1230
+ }
1231
+ if (config.listen)
1232
+ this._init(() => {
1233
+ this.listen({
1234
+ tls: config.tls,
1235
+ listen: config.listen || 8080
1236
+ });
1237
+ });
1238
+ }
1239
+ /** Add one time listener or call immediatelly for 'ready' */
1240
+ once(name, cb) {
1241
+ if (name === 'ready' && this._ready)
1242
+ cb();
1243
+ else
1244
+ super.once(name, cb);
1245
+ return this;
1246
+ }
1247
+ /** Add listener and call immediatelly for 'ready' */
1248
+ on(name, cb) {
1249
+ if (name === 'ready' && this._ready)
1250
+ cb();
1251
+ super.on(name, cb);
1252
+ return this;
1253
+ }
1254
+ /** Listen server, should be used only if config.listen is not set */
1255
+ listen(config) {
1256
+ const listen = (config?.listen || this.config.listen || 0) + '';
1257
+ const handler = config?.handler || this.handler.bind(this);
1258
+ const tlsConfig = config ? config.tls : this.config.tls;
1259
+ const readFile = (data) => data && (data.indexOf('\n') > 0 ? data : fs.readFileSync(data));
1260
+ function tlsOptions() {
1261
+ return {
1262
+ cert: readFile(tlsConfig?.cert),
1263
+ key: readFile(tlsConfig?.key),
1264
+ ca: readFile(tlsConfig?.ca)
1265
+ };
1266
+ }
1267
+ function tlsOptionsReload(srv) {
1268
+ if (tlsConfig?.cert && tlsConfig.cert.indexOf('\n') < 0) {
1269
+ let debounce;
1270
+ fs.watch(tlsConfig.cert, () => {
1271
+ clearTimeout(debounce);
1272
+ debounce = setTimeout(() => {
1273
+ debounce = undefined;
1274
+ srv.setSecureContext(tlsOptions());
1275
+ }, 2000);
1276
+ });
1277
+ }
1278
+ }
1279
+ return new Promise((resolve) => {
1280
+ let readyCount = 0;
1281
+ this._ready = false;
1282
+ const ready = (srv) => {
1283
+ if (srv)
1284
+ readyCount++;
1285
+ if (readyCount >= this.servers.size) {
1286
+ if (!this._ready) {
1287
+ this._ready = true;
1288
+ if (this.servers.size === 0)
1289
+ this.close();
1290
+ else
1291
+ this.emit('ready');
1292
+ resolve();
1293
+ }
1294
+ }
1295
+ };
1296
+ const reg = /^((?<proto>\w+):\/\/)?(?<host>(\[[^\]]+\]|[a-z][^:,]+|\d+\.\d+\.\d+\.\d+))?:?(?<port>\d+)?/;
1297
+ listen.split(',').forEach(listen => {
1298
+ let { proto, host, port } = reg.exec(listen)?.groups || {};
1299
+ let srv;
1300
+ switch (proto) {
1301
+ case 'tcp':
1302
+ if (!config?.handler)
1303
+ throw new Error('Handler is required for tcp');
1304
+ srv = net.createServer(handler);
1305
+ break;
1306
+ case 'tls':
1307
+ if (!config?.handler)
1308
+ throw new Error('Handler is required for tls');
1309
+ srv = tls.createServer(tlsOptions(), handler);
1310
+ tlsOptionsReload(srv);
1311
+ break;
1312
+ case 'https':
1313
+ port = port || '443';
1314
+ srv = https.createServer(tlsOptions(), handler);
1315
+ tlsOptionsReload(srv);
1316
+ break;
1317
+ default:
1318
+ port = port || '80';
1319
+ srv = http.createServer(handler);
1320
+ break;
1321
+ }
1322
+ this.servers.add(srv);
1323
+ if (port === '0') // skip listening
1324
+ ready(srv);
1325
+ else {
1326
+ srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
1327
+ const addr = srv.address();
1328
+ this.emit('listen', addr.port, addr.address, srv);
1329
+ ready(srv);
1330
+ });
1331
+ }
1332
+ srv.on('error', err => {
1333
+ this.servers.delete(srv);
1334
+ srv.close();
1335
+ ready();
1336
+ this.emit('error', err);
1337
+ });
1338
+ srv.on('connection', s => {
1339
+ this.sockets.add(s);
1340
+ s.once('close', () => this.sockets.delete(s));
1341
+ });
1342
+ srv.on('upgrade', this.handlerUpgrade.bind(this));
1343
+ ready();
1344
+ });
1345
+ });
1346
+ }
1347
+ /** bind middleware or create one from string like: 'redirect:302,https://redirect.to', 'error:422', 'param:name=value', 'acl:users/get', 'model:User', 'group:Users', 'user:admin' */
1348
+ bind(fn) {
1349
+ if (typeof fn === 'string') {
1350
+ let name = fn;
1351
+ let idx = name.indexOf(':');
1352
+ if (idx < 0 && name.includes('=')) {
1353
+ name = 'param:' + name;
1354
+ idx = 5;
1355
+ }
1356
+ if (idx >= 0) {
1357
+ const v = name.slice(idx + 1);
1358
+ const type = name.slice(0, idx);
1359
+ // predefined middlewares
1360
+ switch (type) {
1361
+ // redirect:302,https://redirect.to
1362
+ case 'redirect': {
1363
+ let redirect = v.split(','), code = parseInt(v[0]);
1364
+ if (!code || code < 301 || code > 399)
1365
+ code = 302;
1366
+ return (req, res) => res.redirect(code, redirect[1] || v);
1367
+ }
1368
+ // error:422
1369
+ case 'error':
1370
+ return (req, res) => res.error(parseInt(v) || 422);
1371
+ // param:name=value
1372
+ case 'param': {
1373
+ idx = v.indexOf('=');
1374
+ if (idx > 0) {
1375
+ const prm = v.slice(0, idx), val = v.slice(idx + 1);
1376
+ return (req, res, next) => { req.params[prm] = val; return next(); };
1377
+ }
1378
+ break;
1379
+ }
1380
+ case 'model': {
1381
+ const model = v;
1382
+ return (req, res) => {
1383
+ res.isJson = true;
1384
+ req.params.model = model;
1385
+ req.model = Model.models[model];
1386
+ if (!req.model) {
1387
+ console.error(`Data model ${model} not defined for request ${req.path}`);
1388
+ return res.error(422);
1389
+ }
1390
+ return req.model.handler(req, res);
1391
+ };
1392
+ }
1393
+ // user:userid
1394
+ // group:user_groupid
1395
+ // acl:validacl
1396
+ case 'user':
1397
+ case 'group':
1398
+ case 'acl':
1399
+ return (req, res, next) => {
1400
+ if (type === 'user' && v === req.user?.id)
1401
+ return next();
1402
+ if (type === 'acl') {
1403
+ req.params.acl = v;
1404
+ if (req.auth?.acl(v))
1405
+ return next();
1406
+ }
1407
+ if (type === 'group') {
1408
+ req.params.group = v;
1409
+ if (req.user?.group === v)
1410
+ return next();
1411
+ }
1412
+ const accept = req.headers.accept || '';
1413
+ if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
1414
+ if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
1415
+ return res.redirect(302, req.auth.options.redirect);
1416
+ else if (req.auth.options.mode !== 'cookie') {
1417
+ res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
1418
+ return res.error(401);
1419
+ }
1420
+ }
1421
+ return res.error('Permission denied');
1422
+ };
1423
+ }
1424
+ }
1425
+ throw new Error('Invalid option: ' + name);
1426
+ }
1427
+ if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
1428
+ return fn.handler.bind(fn);
1429
+ if (typeof fn !== 'function')
1430
+ throw new Error('Invalid middleware: ' + String.toString.call(fn));
1431
+ return fn.bind(this);
1432
+ }
1433
+ /** Add middleware, routes, etc.. see {Router.add} */
1434
+ use(...args) {
1435
+ this.router.add(...args);
1436
+ return this;
1437
+ }
1438
+ /** Default server handler */
1439
+ handler(req, res) {
1440
+ this.requestInit(req, res);
1441
+ // limit input data size
1442
+ if (parseInt(req.headers['content-length'] || '-1') > (this.config.maxBodySize || defaultMaxBodySize)) {
1443
+ req.pause();
1444
+ res.error(413);
1445
+ return;
1446
+ }
1447
+ this.handlerInit(req, res, () => this.handlerLast(req, res));
1448
+ }
1449
+ requestInit(req, res) {
1450
+ Object.setPrototypeOf(req, ServerRequest.prototype);
1451
+ req._init(this.router);
1452
+ if (res) {
1453
+ Object.setPrototypeOf(res, ServerResponse.prototype);
1454
+ res._init(this.router);
1455
+ }
1456
+ }
1457
+ /** Preprocess request, used by {MicroServer.handler} */
1458
+ handlerInit(req, res, next) {
1459
+ let cors = this.config.cors;
1460
+ if (cors && req.headers.origin) {
1461
+ if (cors === true)
1462
+ cors = '*';
1463
+ if (typeof cors === 'string')
1464
+ cors = { origin: cors, headers: 'Content-Type', credentials: true };
1465
+ if (cors.origin)
1466
+ res.setHeader('Access-Control-Allow-Origin', cors.origin);
1467
+ if (cors.headers)
1468
+ res.setHeader('Access-Control-Allow-Headers', cors.headers);
1469
+ if (cors.credentials)
1470
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
1471
+ if (cors.expose)
1472
+ res.setHeader('Access-Control-Expose-Headers', cors.expose);
1473
+ if (cors.maxAge)
1474
+ res.setHeader('Access-Control-Max-Age', cors.maxAge);
1475
+ }
1476
+ if (req.method === 'OPTIONS') {
1477
+ res.statusCode = 204;
1478
+ res.setHeader('Allow', this.config.methods || defaultMethods);
1479
+ res.end();
1480
+ return;
1481
+ }
1482
+ if (!req.method || !this._methods[req.method]) {
1483
+ res.statusCode = 405;
1484
+ res.setHeader('Allow', this.config.methods || defaultMethods);
1485
+ res.end();
1486
+ return;
1487
+ }
1488
+ if (req.method === 'HEAD') {
1489
+ req.method = 'GET';
1490
+ res.headersOnly = true;
1491
+ }
1492
+ return req.bodyDecode(res, this.config, () => {
1493
+ if ((req.rawBodySize && req.rawBody[0] && (req.rawBody[0][0] === 91 || req.rawBody[0][0] === 123))
1494
+ || req.headers.accept?.includes?.('json') || req.headers['content-type']?.includes?.('json'))
1495
+ res.isJson = true;
1496
+ return req.router.handler(req, res, next);
1497
+ });
1498
+ }
1499
+ /** Last request handler */
1500
+ handlerLast(req, res, next) {
1501
+ if (res.headersSent)
1502
+ return;
1503
+ if (!next)
1504
+ next = () => res.error(404);
1505
+ return next();
1506
+ }
1507
+ /** Default upgrade handler, used for WebSockets */
1508
+ handlerUpgrade(req, socket, head) {
1509
+ this.requestInit(req);
1510
+ //req.headers = head
1511
+ //(req as any)['head'] = head
1512
+ const host = req.headers.host || '';
1513
+ const router = this.vhosts?.[host] || this.router;
1514
+ const res = {
1515
+ get headersSent() {
1516
+ return socket.bytesWritten > 0;
1517
+ },
1518
+ statusCode: 200,
1519
+ socket,
1520
+ server,
1521
+ write(data) {
1522
+ if (res.headersSent)
1523
+ throw new Error('Headers already sent');
1524
+ let code = res.statusCode || 403;
1525
+ if (code < 400) {
1526
+ data = 'Invalid WebSocket response';
1527
+ console.error(data);
1528
+ code = 500;
1529
+ }
1530
+ if (!data)
1531
+ data = http.STATUS_CODES[code] || '';
1532
+ const headers = [
1533
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
1534
+ 'Connection: close',
1535
+ 'Content-Type: text/html',
1536
+ `Content-Length: ${Buffer.byteLength(data)}`,
1537
+ '',
1538
+ data
1539
+ ];
1540
+ socket.write(headers.join('\r\n'), () => { socket.destroy(); });
1541
+ },
1542
+ error(code) {
1543
+ res.statusCode = code || 403;
1544
+ res.write();
1545
+ },
1546
+ end(data) {
1547
+ res.write(data);
1548
+ },
1549
+ send(data) {
1550
+ res.write(data);
1551
+ },
1552
+ setHeader() { }
1553
+ };
1554
+ router.handler(req, res, () => res.error(404), 'WEBSOCKET');
1555
+ }
1556
+ /** Close server instance */
1557
+ async close() {
1558
+ return new Promise((resolve) => {
1559
+ let count = 0;
1560
+ function done() {
1561
+ count--;
1562
+ if (!count)
1563
+ setTimeout(() => resolve(), 2);
1564
+ }
1565
+ for (const s of this.servers) {
1566
+ count++;
1567
+ s.once('close', done);
1568
+ s.close();
1569
+ }
1570
+ this.servers.clear();
1571
+ for (const s of this.sockets) {
1572
+ count++;
1573
+ s.once('close', done);
1574
+ s.destroy();
1575
+ }
1576
+ this.sockets.clear();
1577
+ }).then(() => {
1578
+ this._ready = false;
1579
+ this.emit('close');
1580
+ });
1581
+ }
1582
+ /** Add route, alias to `server.router.add('GET ' + url, ...args)` */
1583
+ get(url, ...args) {
1584
+ this.router.add('GET ' + url, ...args);
1585
+ return this;
1586
+ }
1587
+ /** Add route, alias to `server.router.add('POST ' + url, ...args)` */
1588
+ post(url, ...args) {
1589
+ this.router.add('POST ' + url, ...args);
1590
+ return this;
1591
+ }
1592
+ /** Add route, alias to `server.router.add('PUT ' + url, ...args)` */
1593
+ put(url, ...args) {
1594
+ this.router.add('PUT ' + url, ...args);
1595
+ return this;
1596
+ }
1597
+ /** Add route, alias to `server.router.add('PATCH ' + url, ...args)` */
1598
+ patch(url, ...args) {
1599
+ this.router.add('PATCH ' + url, ...args);
1600
+ return this;
1601
+ }
1602
+ /** Add route, alias to `server.router.add('DELETE ' + url, ...args)` */
1603
+ delete(url, ...args) {
1604
+ this.router.add('DELETE ' + url, ...args);
1605
+ return this;
1606
+ }
1607
+ /** Add websocket handler, alias to `server.router.add('WEBSOCKET ' + url, ...args)` */
1608
+ websocket(url, ...args) {
1609
+ this.router.add('WEBSOCKET ' + url, ...args);
1610
+ return this;
1611
+ }
1612
+ /** Add router hook, alias to `server.router.hook(url, ...args)` */
1613
+ hook(url, ...args) {
1614
+ this.router.hook(url, args.filter((o) => o));
1615
+ return this;
1616
+ }
1617
+ }
1618
+ MicroServer.plugins = {};
1619
+ /** Trust proxy plugin, adds `req.ip` and `req.localip` */
1620
+ class TrustProxyPlugin extends Plugin {
1621
+ constructor(router, options) {
1622
+ super(router);
1623
+ this.priority = 110;
1624
+ this.trustProxy = [];
1625
+ this.trustProxy = options || [];
1626
+ }
1627
+ isLocal(ip) {
1628
+ return !!ip.match(/^(127\.|10\.|192\.168\.|172\.16\.|fe80|fc|fd|::)/);
1629
+ }
1630
+ handler(req, res, next) {
1631
+ req.ip = req.socket.remoteAddress || '::1';
1632
+ req.localip = this.isLocal(req.ip);
1633
+ const xip = req.headers['x-real-ip'] || req.headers['x-forwarded-for'];
1634
+ if (xip) {
1635
+ if (!this.trustProxy.includes(req.ip))
1636
+ return res.error(400);
1637
+ if (req.headers['x-forwarded-proto'] === 'https') {
1638
+ req.protocol = 'https';
1639
+ req.secure = true;
1640
+ }
1641
+ req.ip = Array.isArray(xip) ? xip[0] : xip;
1642
+ req.localip = this.isLocal(req.ip);
1643
+ }
1644
+ return next();
1645
+ }
1646
+ }
1647
+ MicroServer.plugins.trustProxy = TrustProxyPlugin;
1648
+ /** Virtual host plugin */
1649
+ class VHostPlugin extends Plugin {
1650
+ constructor(router, options) {
1651
+ super(router);
1652
+ this.priority = 100;
1653
+ const server = router.server;
1654
+ if (!server.vhosts)
1655
+ server.vhosts = {};
1656
+ else
1657
+ this.handler = undefined;
1658
+ for (const host in options) {
1659
+ if (!server.vhosts[host])
1660
+ server.vhosts[host] = new Router(server);
1661
+ server.vhosts[host].add(options[host]);
1662
+ }
1663
+ }
1664
+ handler(req, res, next) {
1665
+ const host = req.headers.host || '';
1666
+ const router = req.router.server.vhosts?.[host];
1667
+ if (router) {
1668
+ req.router = res.router = router;
1669
+ router.handler(req, res, () => req.router.server.handlerLast(req, res));
1670
+ }
1671
+ else
1672
+ next();
1673
+ }
1674
+ }
1675
+ MicroServer.plugins.vhost = VHostPlugin;
1676
+ const etagPrefix = crypto.randomBytes(4).toString('hex');
1677
+ /**
1678
+ * Static files middleware plugin
1679
+ * Usage: server.use('static', '/public')
1680
+ * Usage: server.use('static', { root: 'public', path: '/static' })
1681
+ */
1682
+ class StaticPlugin extends Plugin {
1683
+ constructor(router, options) {
1684
+ super(router);
1685
+ if (!options)
1686
+ options = {};
1687
+ if (typeof options === 'string')
1688
+ options = { path: options };
1689
+ this.mimeTypes = { ...StaticPlugin.mimeTypes, ...options.mimeTypes };
1690
+ this.root = path.resolve((options.root || options?.path || 'public').replace(/^\//, '')) + path.sep;
1691
+ this.ignore = (options.ignore || []).map((p) => path.normalize(path.join(this.root, p)) + path.sep);
1692
+ this.index = options.index || 'index.html';
1693
+ this.handlers = options.handlers;
1694
+ this.lastModified = options.lastModified !== false;
1695
+ this.etag = options.etag !== false;
1696
+ this.maxAge = options.maxAge;
1697
+ router.add('GET /' + (options.path?.replace(/^[.\/]*/, '') || '').replace(/\/$/, '') + '/:path*', this.staticHandler.bind(this));
1698
+ }
1699
+ /** Default static files handler */
1700
+ staticHandler(req, res, next) {
1701
+ if (req.method !== 'GET')
1702
+ return next();
1703
+ let filename = path.normalize(path.join(this.root, (req.params && req.params.path) || req.pathname));
1704
+ if (!filename.startsWith(this.root)) // check root access
1705
+ return next();
1706
+ const firstch = path.basename(filename)[0];
1707
+ if (firstch === '.' || firstch === '_') // hidden file
1708
+ return next();
1709
+ if (filename.endsWith(path.sep))
1710
+ filename += this.index;
1711
+ const ext = path.extname(filename);
1712
+ const mimeType = this.mimeTypes[ext];
1713
+ if (!mimeType)
1714
+ return next();
1715
+ // check ignore access
1716
+ for (let i = 0; i < this.ignore.length; i++) {
1717
+ if (filename.startsWith(this.ignore[i]))
1718
+ return next();
1719
+ }
1720
+ fs.stat(filename, (err, stats) => {
1721
+ if (err || stats.isDirectory())
1722
+ return next();
1723
+ const handler = this.handlers?.[ext];
1724
+ if (handler) {
1725
+ req.filename = filename;
1726
+ return handler.call(this, req, res, next);
1727
+ }
1728
+ const etagMatch = req.headers['if-none-match'];
1729
+ const etagTime = req.headers['if-modified-since'];
1730
+ const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1731
+ res.setHeader('Content-Type', mimeType);
1732
+ if (this.lastModified || req.params.lastModified)
1733
+ res.setHeader('Last-Modified', stats.mtime.toUTCString());
1734
+ if (this.etag || req.params.etag)
1735
+ res.setHeader('Etag', etag);
1736
+ if (this.maxAge || req.params.maxAge)
1737
+ res.setHeader('Cache-Control', 'max-age=' + (this.maxAge || req.params.maxAge));
1738
+ if (res.headersOnly) {
1739
+ res.setHeader('Content-Length', stats.size);
1740
+ return res.end();
1741
+ }
1742
+ if (etagMatch === etag || etagTime === stats.mtime.toUTCString()) {
1743
+ res.statusCode = 304;
1744
+ return res.end();
1745
+ }
1746
+ res.setHeader('Content-Length', stats.size);
1747
+ fs.createReadStream(filename).pipe(res);
1748
+ });
1749
+ }
1750
+ }
1751
+ /** Default mime types */
1752
+ StaticPlugin.mimeTypes = {
1753
+ '.ico': 'image/x-icon',
1754
+ '.htm': 'text/html',
1755
+ '.html': 'text/html',
1756
+ '.txt': 'text/plain',
1757
+ '.js': 'text/javascript',
1758
+ '.json': 'application/json',
1759
+ '.css': 'text/css',
1760
+ '.png': 'image/png',
1761
+ '.jpg': 'image/jpeg',
1762
+ '.mp3': 'audio/mpeg',
1763
+ '.svg': 'image/svg+xml',
1764
+ '.pdf': 'application/pdf',
1765
+ '.woff': 'application/x-font-woff',
1766
+ '.woff2': 'application/x-font-woff2',
1767
+ '.ttf': 'application/x-font-ttf'
1768
+ };
1769
+ MicroServer.plugins.static = StaticPlugin;
1770
+ export class ProxyPlugin extends Plugin {
1771
+ constructor(router, options) {
1772
+ super(router);
1773
+ if (typeof options !== 'object')
1774
+ options = { remote: options };
1775
+ if (!options.remote)
1776
+ throw new Error('Invalid param');
1777
+ this.remoteUrl = new URL(options.remote);
1778
+ this.regex = options.match ? new RegExp(options.match) : undefined;
1779
+ this.headers = options.headers;
1780
+ this.validHeaders = { ...ProxyPlugin.validHeaders, ...options?.validHeaders };
1781
+ if (options.path && options.path !== '/') {
1782
+ this.handler = undefined;
1783
+ router.add(options.path + '/:path*', this.proxyHandler.bind(this));
1784
+ }
1785
+ }
1786
+ /** Default proxy handler */
1787
+ proxyHandler(req, res, next) {
1788
+ const reqOptions = {
1789
+ method: req.method,
1790
+ headers: {},
1791
+ host: this.remoteUrl.hostname,
1792
+ port: parseInt(this.remoteUrl.port) || (this.remoteUrl.protocol === 'https:' ? 443 : 80),
1793
+ path: this.remoteUrl.pathname
1794
+ };
1795
+ const rawHeaders = req.rawHeaders;
1796
+ let path = req.params.path;
1797
+ if (path)
1798
+ path += (req.path.match(/\?.*/) || [''])[0];
1799
+ if (!path && this.regex) {
1800
+ const m = this.regex.exec(req.path);
1801
+ if (!m)
1802
+ return next();
1803
+ path = m.length > 1 ? m[1] : m[0];
1804
+ }
1805
+ if (!path)
1806
+ path = req.path || '';
1807
+ if (!path.startsWith('/'))
1808
+ path = '/' + path;
1809
+ reqOptions.path += path;
1810
+ if (!reqOptions.headers)
1811
+ reqOptions.headers = {};
1812
+ for (let i = 0; i < rawHeaders.length; i += 2) {
1813
+ const n = rawHeaders[i], nlow = n.toLowerCase();
1814
+ if (this.validHeaders[nlow] && nlow !== 'host')
1815
+ reqOptions.headers[n] = rawHeaders[i + 1];
1816
+ }
1817
+ if (this.headers)
1818
+ Object.assign(reqOptions.headers, this.headers);
1819
+ if (!reqOptions.headers.Host && !reqOptions.headers.host)
1820
+ reqOptions.headers.Host = reqOptions.host;
1821
+ const conn = this.remoteUrl.protocol === 'https:' ? https.request(reqOptions) : http.request(reqOptions);
1822
+ conn.on('response', (response) => {
1823
+ res.statusCode = response.statusCode || 502;
1824
+ for (let i = 0; i < response.rawHeaders.length; i += 2) {
1825
+ const n = response.rawHeaders[i], nlow = n.toLowerCase();
1826
+ if (nlow !== 'transfer-encoding' && nlow !== 'connection')
1827
+ res.setHeader(n, response.rawHeaders[i + 1]);
1828
+ }
1829
+ response.on('data', chunk => {
1830
+ res.write(chunk);
1831
+ });
1832
+ response.on('end', () => {
1833
+ res.end();
1834
+ });
1835
+ });
1836
+ conn.on('error', () => res.error(502));
1837
+ // Content-Length must be allready defined
1838
+ if (req.rawBody.length) {
1839
+ const postStream = new Readable();
1840
+ req.rawBody.forEach(chunk => {
1841
+ postStream.push(chunk);
1842
+ });
1843
+ postStream.push(null);
1844
+ postStream.pipe(res);
1845
+ }
1846
+ else
1847
+ conn.end();
1848
+ }
1849
+ /** Proxy plugin handler as middleware */
1850
+ handler(req, res, next) {
1851
+ return this.proxyHandler(req, res, next);
1852
+ }
1853
+ }
1854
+ /** Default valid headers */
1855
+ ProxyPlugin.validHeaders = {
1856
+ authorization: true,
1857
+ accept: true,
1858
+ 'accept-encoding': true,
1859
+ 'accept-language': true,
1860
+ 'cache-control': true,
1861
+ cookie: true,
1862
+ 'content-type': true,
1863
+ 'content-length': true,
1864
+ host: true,
1865
+ referer: true,
1866
+ 'if-match': true,
1867
+ 'if-none-match': true,
1868
+ 'if-modified-since': true,
1869
+ 'user-agent': true,
1870
+ date: true,
1871
+ range: true
1872
+ };
1873
+ MicroServer.plugins.proxy = ProxyPlugin;
1874
+ /** Authentication class */
1875
+ export class Auth {
1876
+ constructor(options) {
1877
+ let token = options?.token || defaultToken;
1878
+ if (!token || token.length !== 32)
1879
+ token = defaultToken;
1880
+ if (token.length !== 32)
1881
+ token = crypto.createHash('sha256').update(token).digest();
1882
+ if (!(token instanceof Buffer))
1883
+ token = Buffer.from(token);
1884
+ this.options = {
1885
+ token,
1886
+ users: options?.users,
1887
+ mode: options?.mode || 'cookie',
1888
+ defaultAcl: options?.defaultAcl || { '*': false },
1889
+ expire: options?.expire || defaultExpire,
1890
+ cache: options?.cache || {}
1891
+ };
1892
+ if (typeof options?.users === 'function')
1893
+ this.users = options.users;
1894
+ else
1895
+ this.users = async (usrid, psw) => {
1896
+ const users = this.options.users;
1897
+ const usr = users?.[usrid];
1898
+ if (usr && (psw === undefined || this.checkPassword(usrid, psw, usr.password || '')))
1899
+ return usr;
1900
+ };
1901
+ }
1902
+ /** Decode token */
1903
+ decode(data) {
1904
+ data = data.replace(/-/g, '+').replace(/\./g, '/');
1905
+ const iv = Buffer.from(data.slice(0, 22), 'base64');
1906
+ try {
1907
+ const decipher = crypto.createDecipheriv('aes-256-gcm', this.options.token, iv);
1908
+ decipher.setAuthTag(Buffer.from(data.slice(22, 44), 'base64'));
1909
+ const dec = decipher.update(data.slice(44), 'base64', 'utf8') + decipher.final('utf8');
1910
+ const match = dec.match(/^(.*);([0-9a-f]{8});$/);
1911
+ if (match) {
1912
+ const expire = parseInt(match[2], 16) + 946681200 - Math.floor(new Date().getTime() / 1000);
1913
+ if (expire > 0)
1914
+ return {
1915
+ data: match[1],
1916
+ expire
1917
+ };
1918
+ }
1919
+ }
1920
+ catch (e) {
1921
+ }
1922
+ return {
1923
+ data: '',
1924
+ expire: -1
1925
+ };
1926
+ }
1927
+ /** Encode token */
1928
+ encode(data, expire) {
1929
+ if (!expire)
1930
+ expire = this.options.expire || defaultExpire;
1931
+ data = data + ';' + ('0000000' + Math.floor(new Date().getTime() / 1000 - 946681200 + expire).toString(16)).slice(-8) + ';';
1932
+ const iv = crypto.randomBytes(16);
1933
+ const cipher = crypto.createCipheriv('aes-256-gcm', this.options.token, iv);
1934
+ let encrypted = cipher.update(data, 'utf8', 'base64') + cipher.final('base64');
1935
+ encrypted = iv.toString('base64').slice(0, 22) + cipher.getAuthTag().toString('base64').slice(0, 22) + encrypted;
1936
+ return encrypted.replace(/==?/, '').replace(/\//g, '.').replace(/\+/g, '-');
1937
+ }
1938
+ /**
1939
+ * Check acl over authenticated user with: `id`, `group/*`, `*`
1940
+ * @param {string} id - to authenticate: `id`, `group/id`, `model/action`, comma separated best: true => false => def
1941
+ * @param {boolean} [def=false] - default access
1942
+ */
1943
+ acl(id, def = false) {
1944
+ if (!this.req?.user)
1945
+ return false;
1946
+ const reqAcl = this.req.user.acl || this.options.defaultAcl;
1947
+ if (!reqAcl)
1948
+ return def;
1949
+ // this points to req
1950
+ let access;
1951
+ const list = (id || '').split(',');
1952
+ list.forEach(id => access ||= id === 'auth' ? true : reqAcl[id]);
1953
+ if (access !== undefined)
1954
+ return access;
1955
+ list.forEach(id => {
1956
+ const p = id.lastIndexOf('/');
1957
+ if (p > 0)
1958
+ access ||= reqAcl[id.slice(0, p + 1) + '*'];
1959
+ });
1960
+ if (access === undefined)
1961
+ access = reqAcl['*'];
1962
+ return access ?? def;
1963
+ }
1964
+ /**
1965
+ * Authenticate user and setup cookie
1966
+ * @param {string|UserInfo} usr - user id used with options.users to retrieve user object. User object must contain `id` and `acl` object (Ex. usr = {id:'usr', acl:{'users/*':true}})
1967
+ * @param {string} [psw] - user password (if used for user authentication with options.users)
1968
+ * @param {number} [expire] - expire time in seconds (default: options.expire)
1969
+ */
1970
+ async token(usr, psw, expire) {
1971
+ let data;
1972
+ if (typeof usr === 'object' && usr && (usr.id || usr._id))
1973
+ data = JSON.stringify(usr);
1974
+ else if (typeof usr === 'string') {
1975
+ if (psw !== undefined) {
1976
+ const userInfo = await this.users(usr, psw);
1977
+ data = userInfo?.id || userInfo?._id;
1978
+ }
1979
+ else
1980
+ data = usr;
1981
+ }
1982
+ if (data)
1983
+ return this.encode(data, expire);
1984
+ }
1985
+ /**
1986
+ * Authenticate user and setup cookie
1987
+ */
1988
+ async login(usr, psw, options) {
1989
+ let usrInfo;
1990
+ if (typeof usr === 'object')
1991
+ usrInfo = usr;
1992
+ if (typeof usr === 'string')
1993
+ usrInfo = await this.users(usr, psw, options?.salt);
1994
+ if (usrInfo?.id || usrInfo?._id) {
1995
+ const expire = Math.min(34560000, options?.expire || this.options.expire || defaultExpire);
1996
+ const expireTime = new Date().getTime() + expire * 1000;
1997
+ const token = await this.token((usrInfo?.id || usrInfo?._id), undefined, expire);
1998
+ if (token && this.res && this.req) {
1999
+ const oldToken = this.req.tokenId;
2000
+ if (oldToken)
2001
+ delete this.options.cache?.[oldToken];
2002
+ this.req.tokenId = token;
2003
+ if (this.options.mode === 'cookie')
2004
+ this.res.setHeader('set-cookie', `token=${token}; max-age=${expire}; path=${this.req.baseUrl || '/'}`);
2005
+ if (this.options.cache)
2006
+ this.options.cache[token] = { data: usrInfo, time: expireTime };
2007
+ }
2008
+ }
2009
+ return usrInfo;
2010
+ }
2011
+ /** Logout logged in user */
2012
+ logout() {
2013
+ if (this.req && this.res) {
2014
+ const oldToken = this.req.tokenId;
2015
+ if (oldToken)
2016
+ delete this.options.cache?.[oldToken];
2017
+ if (this.options.mode === 'cookie')
2018
+ this.res.setHeader('set-cookie', `token=; max-age=-1; path=${this.req.baseUrl || '/'}`);
2019
+ else {
2020
+ if (this.req.headers.authentication) {
2021
+ this.res.setHeader('set-cookie', 'token=');
2022
+ this.res.error(401);
2023
+ }
2024
+ }
2025
+ this.req.user = undefined;
2026
+ }
2027
+ }
2028
+ /** Get hashed string from user and password */
2029
+ password(usr, psw, salt) {
2030
+ return Auth.password(usr, psw, salt);
2031
+ }
2032
+ /** Get hashed string from user and password */
2033
+ static password(usr, psw, salt) {
2034
+ if (usr)
2035
+ psw = crypto.createHash('sha512').update(usr + '|' + psw).digest('hex');
2036
+ if (salt) {
2037
+ salt = salt === '*' ? crypto.randomBytes(32).toString('hex') : (salt.length > 128 ? salt.slice(0, salt.length - 128) : salt);
2038
+ psw = salt + crypto.createHash('sha512').update(psw + salt).digest('hex');
2039
+ }
2040
+ return psw;
2041
+ }
2042
+ /** Validate user password */
2043
+ checkPassword(usr, psw, storedPsw, salt) {
2044
+ return Auth.checkPassword(usr, psw, storedPsw, salt);
2045
+ }
2046
+ /** Validate user password */
2047
+ static checkPassword(usr, psw, storedPsw, salt) {
2048
+ let success = false;
2049
+ if (usr && storedPsw) {
2050
+ if (storedPsw.length > 128) { // salted hash
2051
+ if (psw.length < 128) // plain == salted-hash
2052
+ success = this.password(usr, psw, storedPsw) === storedPsw;
2053
+ else if (psw.length === 128) // hash == salted-hash
2054
+ success = this.password('', psw, storedPsw) === storedPsw;
2055
+ else // rnd-salted-hash === salted-hash
2056
+ success = psw === this.password('', storedPsw, psw);
2057
+ }
2058
+ else if (storedPsw.length === 128) { // hash
2059
+ if (psw.length < 128) // plain == hash
2060
+ success = this.password(usr, psw) === storedPsw;
2061
+ else if (psw.length === 128) // hash == hash
2062
+ success = psw === storedPsw;
2063
+ else if (salt) // rnd-salted-hash === hash
2064
+ success = psw === this.password('', this.password('', storedPsw, salt), psw);
2065
+ }
2066
+ else { // plain
2067
+ if (psw.length < 128) // plain == plain
2068
+ success = psw === storedPsw;
2069
+ else if (psw.length === 128) // hash == plain
2070
+ success = psw === this.password(usr, storedPsw);
2071
+ else if (salt) // rnd-salted-hash == plain
2072
+ success = psw === this.password('', this.password(usr, storedPsw, salt), psw);
2073
+ }
2074
+ }
2075
+ return success;
2076
+ }
2077
+ /** Clear user cache if users setting where changed */
2078
+ clearCache() {
2079
+ const cache = this.options.cache;
2080
+ if (cache)
2081
+ Object.keys(cache).forEach(k => delete cache[k]);
2082
+ }
2083
+ }
2084
+ /*
2085
+ // Client login implementation
2086
+ async function login (username, password) {
2087
+ function hex (b) { return Array.from(Uint8Array.from(b)).map(b => b.toString(16).padStart(2, "0")).join("") }
2088
+ async function hash (data) { return hex(await crypto.subtle.digest('sha-512', new TextEncoder().encode(data)))}
2089
+ const rnd = hex(crypto.getRandomValues(new Int8Array(32)))
2090
+ return rnd + await hash(await hash(username + '|' + password) + rnd)
2091
+ }
2092
+
2093
+ // Server login implementation
2094
+ // password should be stored with `req.auth.password(req, user, password)` but may be in plain form too
2095
+
2096
+ server.use('auth', {
2097
+ users: {
2098
+ testuser: {
2099
+ acl: {
2100
+ 'user/update': true,
2101
+ 'messages/*': true,
2102
+ },
2103
+ password: <hash-password>
2104
+ },
2105
+ admin: {
2106
+ acl: {
2107
+ 'user/*': true,
2108
+ 'messages/*': true,
2109
+ },
2110
+ password: <hash-password>
2111
+ }
2112
+ }
2113
+ })
2114
+ //or
2115
+ server.use('auth', {
2116
+ async users (usr, psw) {
2117
+ const obj = await db.getUser(usr)
2118
+ if (!obj.disabled && this.checkPassword(usr, psw, obj.password)) {
2119
+ const {password, ...res} = obj // remove password field
2120
+ return res
2121
+ }
2122
+ }
2123
+ })
2124
+
2125
+ async function loginMiddleware(req, res) {
2126
+ const user = await req.auth.login(req.body.username || '', req.body.password || '')
2127
+ if (user)
2128
+ res.jsonSuccess(user)
2129
+ else
2130
+ res.jsonError('Access denied')
2131
+ }
2132
+
2133
+ // More secure way is to store salted hashes on server `req.auth.password(user, password, '*')`
2134
+ // and corespondingly 1 extra step is needed in athentication to retrieve salt from passwod hash `password.slice(0, 64)`
2135
+ // client function will be:
2136
+ async function login (username, password, salt) {
2137
+ function hex (b) { return Array.from(Uint8Array.from(b)).map(b => b.toString(16).padStart(2, "0")).join("") }
2138
+ async function hash (data) { return hex(await crypto.subtle.digest('sha-512', new TextEncoder().encode(data)))}
2139
+ const rnd = hex(crypto.getRandomValues(new Int8Array(32)))
2140
+ return rnd + await hash(await hash(salt + await hash(await hash(username + '|' + password) + salt)) + rnd)
2141
+ }
2142
+ */
2143
+ /** Authentication plugin */
2144
+ class AuthPlugin extends Plugin {
2145
+ constructor(router, options) {
2146
+ super(router);
2147
+ if (router.auth)
2148
+ throw new Error('Auth plugin already initialized');
2149
+ this.options = {
2150
+ mode: 'cookie',
2151
+ token: defaultToken,
2152
+ expire: defaultExpire,
2153
+ defaultAcl: { '*': false },
2154
+ cache: {},
2155
+ ...options,
2156
+ cacheCleanup: new Date().getTime()
2157
+ };
2158
+ if (this.options.token === defaultToken)
2159
+ console.warn('Default token in auth plugin');
2160
+ router.auth = new Auth(this.options);
2161
+ }
2162
+ /** Authentication middleware */
2163
+ async handler(req, res, next) {
2164
+ const options = this.options, cache = options.cache;
2165
+ const auth = new Auth(options);
2166
+ req.auth = auth;
2167
+ auth.req = req;
2168
+ auth.res = res;
2169
+ const authorization = req.headers.authorization || '';
2170
+ if (authorization.startsWith('Basic ')) {
2171
+ let usr = cache?.[authorization];
2172
+ if (usr)
2173
+ req.user = usr.data;
2174
+ else {
2175
+ const usrpsw = Buffer.from(authorization.slice(6), 'base64').toString('utf-8'), pos = usrpsw.indexOf(':'), username = usrpsw.slice(0, pos), psw = usrpsw.slice(pos + 1);
2176
+ if (username && psw)
2177
+ req.user = await auth.users(username, psw);
2178
+ if (!req.user)
2179
+ return res.error(401);
2180
+ if (cache) // 1 hour to expire in cache
2181
+ cache[authorization] = { data: req.user, time: new Date().getTime() + Math.min(3600000, (options.expire || defaultExpire) * 1000) };
2182
+ }
2183
+ return next();
2184
+ }
2185
+ const cookie = req.headers.cookie, cookies = cookie ? cookie.split(/;\s+/g) : [];
2186
+ const sid = cookies.find(s => s.startsWith('token='));
2187
+ let token = '';
2188
+ if (authorization.startsWith('Bearer '))
2189
+ token = authorization.slice(7);
2190
+ if (sid)
2191
+ token = sid.slice(sid.indexOf('=') + 1);
2192
+ if (!token)
2193
+ token = req.get.token;
2194
+ if (token) {
2195
+ const now = new Date().getTime();
2196
+ let usr, expire;
2197
+ if (cache && (!options.cacheCleanup || options.cacheCleanup > now)) {
2198
+ options.cacheCleanup = now + 600000;
2199
+ process.nextTick(() => Object.entries(cache).forEach((entry) => {
2200
+ if (entry[1].time < now)
2201
+ delete cache[entry[0]];
2202
+ }));
2203
+ }
2204
+ // check in cache
2205
+ req.tokenId = token;
2206
+ let usrCache = cache?.[token];
2207
+ if (usrCache && usrCache.time > now)
2208
+ [req.user, expire] = [usrCache.data, Math.floor((usrCache.time - now) / 1000)];
2209
+ else {
2210
+ const usrData = auth.decode(token);
2211
+ if (!usrData.data) {
2212
+ req.auth.logout();
2213
+ return new AccessDenied();
2214
+ }
2215
+ if (usrData.data.startsWith('{')) {
2216
+ try {
2217
+ usr = JSON.parse(usrData.data);
2218
+ req.user = usr;
2219
+ }
2220
+ catch (e) { }
2221
+ }
2222
+ else {
2223
+ req.user = await auth.users(usrData.data);
2224
+ if (!req.user) {
2225
+ req.auth.logout();
2226
+ return new AccessDenied();
2227
+ }
2228
+ }
2229
+ expire = usrData.expire;
2230
+ if (req.user && cache)
2231
+ cache[token] = { data: req.user, time: expire };
2232
+ }
2233
+ // renew
2234
+ if (req.user && expire < (options.expire || defaultExpire) / 2)
2235
+ await req.auth.login(req.user);
2236
+ }
2237
+ if (!res.headersSent)
2238
+ return next();
2239
+ }
2240
+ }
2241
+ MicroServer.plugins.auth = AuthPlugin;
2242
+ /** Create microserver */
2243
+ export function create(config) { return new MicroServer(config); }
2244
+ /** JSON File store */
2245
+ export class FileStore {
2246
+ constructor(options) {
2247
+ this._queue = Promise.resolve();
2248
+ this._cache = {};
2249
+ this._dir = options?.dir || 'data';
2250
+ this._cacheTimeout = options?.cacheTimeout || 2000;
2251
+ this._cacheItems = options?.cacheItems || 10;
2252
+ this._debounceTimeout = options?.debounceTimeout || 1000;
2253
+ this._iter = 0;
2254
+ }
2255
+ /** cleanup cache */
2256
+ cleanup() {
2257
+ if (this._iter > this._cacheItems) {
2258
+ this._iter = 0;
2259
+ const now = new Date().getTime();
2260
+ const keys = Object.keys(this._cache);
2261
+ if (keys.length > this._cacheItems) {
2262
+ keys.forEach(n => {
2263
+ if (now - this._cache[n].atime > this._cacheTimeout)
2264
+ delete this._cache[n];
2265
+ });
2266
+ }
2267
+ }
2268
+ }
2269
+ async _sync(cb) {
2270
+ let r;
2271
+ let p = new Promise(resolve => r = resolve);
2272
+ this._queue = this._queue.then(async () => {
2273
+ try {
2274
+ r(await cb());
2275
+ }
2276
+ catch (e) {
2277
+ r(e);
2278
+ }
2279
+ });
2280
+ return p;
2281
+ }
2282
+ async close() {
2283
+ await this.sync();
2284
+ this._iter = 0;
2285
+ this._cache = {};
2286
+ }
2287
+ async sync() {
2288
+ for (const name in this._cache) {
2289
+ for (const key in this._cache)
2290
+ await this._cache[key].data.__sync__?.();
2291
+ }
2292
+ await this._queue;
2293
+ }
2294
+ /** load json file data */
2295
+ async load(name, autosave = false) {
2296
+ let item = this._cache[name];
2297
+ if (item && new Date().getTime() - item.atime < this._cacheTimeout)
2298
+ return item.data;
2299
+ return this._sync(async () => {
2300
+ item = this._cache[name];
2301
+ if (item && new Date().getTime() - item.atime < this._cacheTimeout)
2302
+ return item.data;
2303
+ try {
2304
+ const stat = await fs.promises.lstat(path.join(this._dir, name));
2305
+ if (item?.mtime !== stat.mtime.getTime()) {
2306
+ let data = JSON.parse(await fs.promises.readFile(path.join(this._dir, name), 'utf8') || '{}');
2307
+ this._iter++;
2308
+ this.cleanup();
2309
+ if (autosave)
2310
+ data = this.observe(data, () => this.save(name, data));
2311
+ this._cache[name] = {
2312
+ atime: new Date().getTime(),
2313
+ mtime: stat.mtime.getTime(),
2314
+ data: data
2315
+ };
2316
+ return data;
2317
+ }
2318
+ }
2319
+ catch {
2320
+ delete this._cache[name];
2321
+ }
2322
+ return item?.data;
2323
+ });
2324
+ }
2325
+ /** save data */
2326
+ async save(name, data) {
2327
+ this._iter++;
2328
+ const item = {
2329
+ atime: new Date().getTime(),
2330
+ mtime: new Date().getTime(),
2331
+ data: data
2332
+ };
2333
+ this._cache[name] = item;
2334
+ return this._sync(async () => {
2335
+ if (this._cache[name] === item) {
2336
+ this.cleanup();
2337
+ try {
2338
+ await fs.promises.writeFile(path.join(this._dir, name), JSON.stringify(this._cache[name].data), 'utf8');
2339
+ }
2340
+ catch {
2341
+ }
2342
+ }
2343
+ });
2344
+ }
2345
+ /** load all files in directory */
2346
+ async all(name, autosave = false) {
2347
+ return this._sync(async () => {
2348
+ const files = await fs.promises.readdir(name ? path.join(this._dir, name) : this._dir);
2349
+ const res = {};
2350
+ await Promise.all(files.map(file => (file.startsWith('.') && !file.startsWith('_') && !file.startsWith('$')) &&
2351
+ this.load(name ? name + '/' + file : file, autosave)
2352
+ .then(data => { res[file] = data; })));
2353
+ return res;
2354
+ });
2355
+ }
2356
+ /** delete data file */
2357
+ async delete(name) {
2358
+ delete this._cache[name];
2359
+ return this._sync(async () => {
2360
+ if (this._cache[name])
2361
+ return;
2362
+ try {
2363
+ await fs.promises.unlink(path.join(this._dir, name));
2364
+ }
2365
+ catch {
2366
+ }
2367
+ });
2368
+ }
2369
+ /** Observe data object */
2370
+ observe(data, cb) {
2371
+ function debounce(func, debounceTime) {
2372
+ const maxTotalDebounceTime = debounceTime * 2;
2373
+ let timeoutId;
2374
+ let lastCallTime = Date.now();
2375
+ let _args;
2376
+ const abort = () => {
2377
+ if (timeoutId)
2378
+ clearTimeout(timeoutId);
2379
+ _args = undefined;
2380
+ timeoutId = undefined;
2381
+ };
2382
+ const exec = () => {
2383
+ const args = _args;
2384
+ if (args) {
2385
+ abort();
2386
+ lastCallTime = Date.now();
2387
+ func(...args);
2388
+ }
2389
+ };
2390
+ const start = (...args) => {
2391
+ const currentTime = Date.now();
2392
+ const timeSinceLastCall = currentTime - lastCallTime;
2393
+ abort();
2394
+ if (timeSinceLastCall >= maxTotalDebounceTime) {
2395
+ func(...args);
2396
+ lastCallTime = currentTime;
2397
+ }
2398
+ else {
2399
+ _args = args;
2400
+ timeoutId = setTimeout(exec, Math.max(debounceTime - timeSinceLastCall, 0));
2401
+ }
2402
+ };
2403
+ start.abort = abort;
2404
+ start.immediate = exec;
2405
+ return start;
2406
+ }
2407
+ const changed = debounce((target, key) => cb.call(data, target, key, target[key]), this._debounceTimeout);
2408
+ const handler = {
2409
+ get(target, key) {
2410
+ if (key === '__sync__')
2411
+ return changed.immediate;
2412
+ if (typeof target[key] === 'object' && target[key] !== null)
2413
+ return new Proxy(target[key], handler);
2414
+ return target[key];
2415
+ },
2416
+ set(target, key, value) {
2417
+ if (target[key] === value)
2418
+ return true;
2419
+ if (value && typeof value === 'object')
2420
+ value = { ...value };
2421
+ target[key] = value;
2422
+ changed(target, key);
2423
+ return true;
2424
+ },
2425
+ deleteProperty(target, key) {
2426
+ delete target[key];
2427
+ changed(target, key);
2428
+ return true;
2429
+ }
2430
+ };
2431
+ return new Proxy(data, handler);
2432
+ }
2433
+ }
2434
+ let globalObjectId = crypto.randomBytes(8);
2435
+ function newObjectId() {
2436
+ for (let i = 7; i >= 0; i--)
2437
+ if (++globalObjectId[i] < 256)
2438
+ break;
2439
+ return (new Date().getTime() / 1000 | 0).toString(16) + globalObjectId.toString('hex');
2440
+ }
2441
+ export class Model {
2442
+ /** Define model */
2443
+ static define(name, fields, options) {
2444
+ options = options || {};
2445
+ if (!options.collection && this.collections)
2446
+ options.collection = this.collections.collection(name);
2447
+ const inst = options?.class
2448
+ ? new options.class(fields, { name, ...options })
2449
+ : new Model(fields, { name, ...options });
2450
+ Model.models[name] = inst;
2451
+ return inst;
2452
+ }
2453
+ /** Create model acording to description */
2454
+ constructor(fields, options) {
2455
+ const model = this.model = {};
2456
+ this.name = options?.name || this.__proto__.constructor.name;
2457
+ this.collection = options?.collection;
2458
+ this.handler = this.handler.bind(this);
2459
+ for (const n in fields) {
2460
+ const modelField = this.model[n] = { name: n };
2461
+ let field = fields[n];
2462
+ let fieldType, isArray = false;
2463
+ if (typeof field === 'object' && !Array.isArray(field) && !(field instanceof Model))
2464
+ fieldType = field.type;
2465
+ else {
2466
+ fieldType = field;
2467
+ field = {};
2468
+ }
2469
+ if (Array.isArray(fieldType)) {
2470
+ isArray = true;
2471
+ fieldType = fieldType[0];
2472
+ }
2473
+ if (typeof fieldType === 'function')
2474
+ fieldType = fieldType.name;
2475
+ let validateType;
2476
+ if (fieldType instanceof Model) {
2477
+ modelField.model = fieldType;
2478
+ fieldType = 'model';
2479
+ validateType = (value, options) => modelField.model?.validate(value, options);
2480
+ }
2481
+ else {
2482
+ fieldType = fieldType.toString().toLowerCase();
2483
+ switch (fieldType) {
2484
+ case "objectid":
2485
+ validateType = (value, options) => {
2486
+ if (typeof value === 'string')
2487
+ return value;
2488
+ if (typeof value === 'object' && value.constructor.name.toLowerCase() === 'objectid')
2489
+ return JSON.stringify(value);
2490
+ throw new InvalidData(options.name, 'field type');
2491
+ };
2492
+ break;
2493
+ case 'string':
2494
+ validateType = (value, options) => {
2495
+ if (typeof value === 'string')
2496
+ return value;
2497
+ if (typeof value === 'number')
2498
+ return value.toString();
2499
+ throw new InvalidData(options.name, 'field type');
2500
+ };
2501
+ break;
2502
+ case 'number':
2503
+ validateType = (value, options) => {
2504
+ if (typeof value === 'number')
2505
+ return value;
2506
+ throw new InvalidData(options.name, 'field type');
2507
+ };
2508
+ break;
2509
+ case 'int':
2510
+ validateType = (value, options) => {
2511
+ if (typeof value === 'number' && Number.isInteger(value))
2512
+ return value;
2513
+ throw new InvalidData(options.name, 'field type');
2514
+ };
2515
+ break;
2516
+ case "json":
2517
+ case "object":
2518
+ fieldType = 'object';
2519
+ validateType = (value, options) => {
2520
+ if (typeof value === 'object')
2521
+ return value;
2522
+ throw new InvalidData(options.name, 'field type');
2523
+ };
2524
+ break;
2525
+ case 'boolean':
2526
+ validateType = (value, options) => {
2527
+ if (typeof value === 'boolean')
2528
+ return value;
2529
+ throw new InvalidData(options.name, 'field type');
2530
+ };
2531
+ break;
2532
+ case 'array':
2533
+ isArray = true;
2534
+ fieldType = 'any';
2535
+ validateType = (value) => value;
2536
+ break;
2537
+ case 'date':
2538
+ validateType = (value, options) => {
2539
+ if (typeof value === 'string')
2540
+ value = new Date(value);
2541
+ if (value instanceof Date && !isNaN(value.getTime()))
2542
+ return value;
2543
+ throw new InvalidData(options.name, 'field type');
2544
+ };
2545
+ break;
2546
+ case '*':
2547
+ case 'any':
2548
+ fieldType = 'any';
2549
+ validateType = (value) => value;
2550
+ break;
2551
+ default:
2552
+ throw new InvalidData(n, 'field type ' + fieldType);
2553
+ }
2554
+ }
2555
+ modelField.type = fieldType + (isArray ? '[]' : '');
2556
+ const validators = [validateType];
2557
+ const validate = (value, options) => validators.reduce((v, f) => {
2558
+ v = f(v, options);
2559
+ if (v === Error)
2560
+ throw new InvalidData(options.name, 'field value');
2561
+ if (v instanceof Error)
2562
+ throw v;
2563
+ return v;
2564
+ }, value);
2565
+ if (isArray)
2566
+ modelField.validate = (value, options) => {
2567
+ if (Array.isArray(value))
2568
+ return value.map((v, i) => validate(v, { ...options, name: options.name + '[' + i + ']' }));
2569
+ throw new InvalidData(options.name, 'field type');
2570
+ };
2571
+ else
2572
+ modelField.validate = validate;
2573
+ modelField.required = field.required ? this._fieldFunction(field.required, false) : undefined;
2574
+ modelField.canWrite = this._fieldFunction(field.canWrite, typeof field.canWrite === 'string' && field.canWrite.startsWith('$') ? false : true);
2575
+ modelField.canRead = this._fieldFunction(field.canRead, typeof field.canRead === 'string' && field.canRead.startsWith('$') ? false : true);
2576
+ if (field.default !== undefined) {
2577
+ const def = field.default;
2578
+ if (typeof def === 'function' && def.name === 'ObjectId')
2579
+ modelField.default = () => newObjectId();
2580
+ else if (def === Date)
2581
+ modelField.default = () => new Date();
2582
+ else if (typeof def !== 'function')
2583
+ modelField.default = this._fieldFunction(def);
2584
+ else
2585
+ modelField.default = def;
2586
+ }
2587
+ if (field.minimum !== undefined) {
2588
+ const minimum = field.minimum;
2589
+ validators.push((value, options) => {
2590
+ try {
2591
+ if (value >= minimum)
2592
+ return value;
2593
+ }
2594
+ catch (e) { }
2595
+ return Error;
2596
+ });
2597
+ }
2598
+ if (field.maximum !== undefined) {
2599
+ const maximum = field.maximum;
2600
+ validators.push((value, options) => {
2601
+ try {
2602
+ if (value <= maximum)
2603
+ return value;
2604
+ }
2605
+ catch (e) { }
2606
+ return Error;
2607
+ });
2608
+ }
2609
+ if (field.enum) {
2610
+ const enumField = field.enum;
2611
+ validators.push((value, options) => enumField.includes(value) ? value : Error);
2612
+ }
2613
+ if (field.format && modelField.type === 'string') {
2614
+ let format = field.format;
2615
+ switch (format) {
2616
+ case 'date':
2617
+ format = '^\\d{4}-\\d{2}-\\d{2}$';
2618
+ break;
2619
+ case 'time':
2620
+ format = '^\\d{2}:\\d{2}(:\\d{2})?$';
2621
+ break;
2622
+ case 'date-time':
2623
+ format = '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}(:\\d{2})?$';
2624
+ break;
2625
+ case 'url':
2626
+ format = '^https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-A-Za-z0-9+&@#/%=~_|]';
2627
+ break;
2628
+ case 'email':
2629
+ format = '^[a-zA-Z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$';
2630
+ break;
2631
+ }
2632
+ const regex = new RegExp(format);
2633
+ validators.push((value, options) => regex.test(value) ? value : Error);
2634
+ }
2635
+ if (field.validate)
2636
+ validators.push(field.validate);
2637
+ }
2638
+ }
2639
+ /** Validate data over model */
2640
+ validate(data, options) {
2641
+ options = options || {};
2642
+ const prefix = options.name ? options.name + '.' : '';
2643
+ if (options.validate === false)
2644
+ return data;
2645
+ const res = {};
2646
+ for (const name in this.model) {
2647
+ const field = this.model[name];
2648
+ const paramOptions = { ...options, field, name: prefix + name, model: this };
2649
+ const canWrite = field.canWrite(paramOptions), canRead = field.canRead(paramOptions), required = field.required?.(paramOptions);
2650
+ if (options.readOnly) {
2651
+ if (canRead === false || !field.type)
2652
+ continue;
2653
+ if (data[name] !== undefined) {
2654
+ res[name] = data[name];
2655
+ continue;
2656
+ }
2657
+ else if (!required)
2658
+ continue;
2659
+ }
2660
+ let v = canWrite === false ? undefined : data[name];
2661
+ if (v === undefined) {
2662
+ if (options.default !== false && field.default)
2663
+ v = field.default.length ? field.default(paramOptions) : field.default();
2664
+ if (v !== undefined) {
2665
+ res[name] = v;
2666
+ continue;
2667
+ }
2668
+ if (required && canWrite !== false && (!options.insert || name !== '_id'))
2669
+ throw new InvalidData('missing ' + name, 'field');
2670
+ continue;
2671
+ }
2672
+ if (options.readOnly) {
2673
+ res[name] = v;
2674
+ continue;
2675
+ }
2676
+ if (!field.type)
2677
+ continue;
2678
+ res[name] = this._validateField(v, paramOptions);
2679
+ }
2680
+ return res;
2681
+ }
2682
+ _fieldFunction(value, def) {
2683
+ if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
2684
+ const names = value.slice(2, -1).split('.');
2685
+ if (names.length === 1) {
2686
+ const n = names[0];
2687
+ if (n === 'now' || n === 'Date')
2688
+ return () => new Date();
2689
+ if (n === 'ObjectId')
2690
+ return () => newObjectId();
2691
+ return (options) => options[n] ?? def;
2692
+ }
2693
+ return (options) => names.reduce((p, n) => p = typeof p === 'object' ? p[n] : undefined, options) ?? def;
2694
+ }
2695
+ if (value === undefined)
2696
+ return () => def;
2697
+ return () => value;
2698
+ }
2699
+ _validateField(value, options) {
2700
+ const field = options.field;
2701
+ if (value == null) {
2702
+ if (field.required?.(options) && (!options.insert || options.name !== '_id'))
2703
+ throw new InvalidData('missing ' + options.name, 'field');
2704
+ return null;
2705
+ }
2706
+ return field.validate(value, options);
2707
+ }
2708
+ /** Generate filter for data queries */
2709
+ getFilter(data, options) {
2710
+ const res = {};
2711
+ if (data._id)
2712
+ res._id = data._id;
2713
+ for (const name in this.model) {
2714
+ if (!(name in res)) {
2715
+ const field = this.model[name];
2716
+ const paramOptions = { ...options, field, name, model: this };
2717
+ if ((!options?.required && name in data) || (field.required && field.default)) {
2718
+ if (typeof field.required === 'function' && field.required(paramOptions) && field.default && (!(name in data) || field.canWrite(options) === false))
2719
+ res[name] = options?.default !== false ? field.default.length ? field.default(paramOptions) : field.default() : data[name];
2720
+ else if (name in data)
2721
+ res[name] = options?.validate !== false ? this._validateField(data[name], paramOptions) : data[name];
2722
+ }
2723
+ }
2724
+ }
2725
+ if (typeof options?.projection === 'object')
2726
+ for (const name in options.projection) {
2727
+ if (name !== '_id' && name in this.model && !res[name])
2728
+ res[name] = options.projection[name];
2729
+ }
2730
+ return res;
2731
+ }
2732
+ /** Find one document */
2733
+ async findOne(query, options) {
2734
+ if (this.collection instanceof Promise)
2735
+ this.collection = await this.collection;
2736
+ if (!this.collection)
2737
+ throw new AccessDenied('Database not configured');
2738
+ const doc = await this.collection.findOne(this.getFilter(query, { readOnly: true, ...options }));
2739
+ return doc ? this.validate(doc, { readOnly: true }) : undefined;
2740
+ }
2741
+ /** Find many documents */
2742
+ async findMany(query, options) {
2743
+ if (this.collection instanceof Promise)
2744
+ this.collection = await this.collection;
2745
+ if (!this.collection)
2746
+ throw new AccessDenied('Database not configured');
2747
+ const res = [];
2748
+ await this.collection.find(this.getFilter(query || {}, options)).forEach((doc) => res.push(this.validate(doc, { readOnly: true })));
2749
+ return res;
2750
+ }
2751
+ /** Insert a new document */
2752
+ async insert(data, options) {
2753
+ return this.update(data, { ...options, insert: true });
2754
+ }
2755
+ /** Update one matching document */
2756
+ async update(query, options) {
2757
+ if (this.collection instanceof Promise)
2758
+ this.collection = await this.collection;
2759
+ if (!this.collection)
2760
+ throw new AccessDenied('Database not configured');
2761
+ if (options?.validate !== false)
2762
+ query = this.validate(query, options);
2763
+ const unset = query.$unset || {};
2764
+ for (const n in query) {
2765
+ if (query[n] === undefined || query[n] === null) {
2766
+ query.$unset = unset;
2767
+ unset[n] = 1;
2768
+ delete query[n];
2769
+ }
2770
+ }
2771
+ const res = await this.collection.findAndModify({ query: this.getFilter(query, { required: true, validate: false, default: false }), update: query, upsert: options?.insert });
2772
+ }
2773
+ /** Delete one matching document */
2774
+ async delete(query, options) {
2775
+ if (this.collection instanceof Promise)
2776
+ this.collection = await this.collection;
2777
+ if (!this.collection)
2778
+ throw new AccessDenied('Database not configured');
2779
+ if (query._id)
2780
+ await this.collection.deleteOne(this.getFilter(query, options));
2781
+ }
2782
+ /** Microserver middleware */
2783
+ handler(req, res) {
2784
+ res.isJson = true;
2785
+ let filter, filterStr = req.get.filter;
2786
+ if (filterStr) {
2787
+ try {
2788
+ if (!filterStr.startsWith('{'))
2789
+ filterStr = Buffer.from(filterStr, 'base64').toString('utf-8');
2790
+ filter = JSON.parse(filterStr);
2791
+ }
2792
+ catch {
2793
+ }
2794
+ }
2795
+ switch (req.method) {
2796
+ case 'GET':
2797
+ if ('id' in req.params)
2798
+ return this.findOne({ _id: req.params.id }, { user: req.user, params: req.params, projection: filter }).then(res => ({ data: res }));
2799
+ return this.findMany({}, { user: req.user, params: req.params, projection: filter }).then(res => ({ data: res }));
2800
+ case 'POST':
2801
+ if (!req.body)
2802
+ return res.error(422);
2803
+ return this.update(req.body, { user: req.user, params: req.params, insert: true, projection: filter }).then(res => ({ data: res }));
2804
+ case 'PUT':
2805
+ if (!req.body)
2806
+ return res.error(422);
2807
+ req.body._id = req.params.id;
2808
+ return this.update(req.body, { user: req.user, params: req.params, insert: false, projection: filter }).then(res => ({ data: res }));
2809
+ case 'DELETE':
2810
+ return this.delete({ _id: req.params.id }, { user: req.user, params: req.params, projection: filter }).then(res => ({ data: res }));
2811
+ default:
2812
+ return res.error(422);
2813
+ }
2814
+ }
2815
+ }
2816
+ Model.models = {};
2817
+ /** Collection factory */
2818
+ class MicroCollections {
2819
+ constructor(options) {
2820
+ this.options = options;
2821
+ }
2822
+ /** Get collection */
2823
+ async collection(name) {
2824
+ return new MicroCollection({ ...this.options, name });
2825
+ }
2826
+ }
2827
+ /** minimalistic indexed mongo type collection with persistance for usage with Model */
2828
+ export class MicroCollection {
2829
+ /** Get collections factory */
2830
+ static collections(options) {
2831
+ return new MicroCollections(options);
2832
+ }
2833
+ constructor(options = {}) {
2834
+ this.name = options.name || this.constructor.name;
2835
+ const load = options.load ?? (options.store && ((col) => options.store?.load(col.name, true)));
2836
+ this.data = options.data || {};
2837
+ this._save = options.save;
2838
+ this._ready = load?.(this)?.catch(() => { }).then(data => {
2839
+ this.data = data || {};
2840
+ });
2841
+ }
2842
+ /** check for collection is ready */
2843
+ async checkReady() {
2844
+ if (this._ready) {
2845
+ await this._ready;
2846
+ this._ready = undefined;
2847
+ }
2848
+ }
2849
+ /** Query document with query filter */
2850
+ queryDocument(query, data) {
2851
+ if (query && data)
2852
+ for (const n in query) {
2853
+ if (query[n] === null)
2854
+ if (data[n] != null)
2855
+ return;
2856
+ else
2857
+ continue;
2858
+ if (n.startsWith('$') || typeof query[n] === 'object')
2859
+ console.warn(`Invalid query field: ${n}`);
2860
+ if (data[n] !== query[n])
2861
+ return;
2862
+ }
2863
+ return data;
2864
+ }
2865
+ /** Count all documents */
2866
+ async count() {
2867
+ await this.checkReady();
2868
+ return Object.keys(this.data).length;
2869
+ }
2870
+ /** Find one matching document */
2871
+ async findOne(query) {
2872
+ await this.checkReady();
2873
+ const id = query._id;
2874
+ if (id)
2875
+ return this.queryDocument(query, this.data[id]);
2876
+ let res;
2877
+ await this.find(query).forEach((doc) => (res = doc) && false);
2878
+ return res;
2879
+ }
2880
+ /** Find all matching documents */
2881
+ find(query) {
2882
+ return {
2883
+ forEach: async (cb, self) => {
2884
+ await this._ready;
2885
+ let count = 0;
2886
+ for (const id in this.data)
2887
+ if (this.queryDocument(query, this.data[id])) {
2888
+ count++;
2889
+ if (cb.call(self ?? this, this.data[id]) === false)
2890
+ break;
2891
+ if (query.limit && count >= query.limit)
2892
+ break;
2893
+ }
2894
+ return count;
2895
+ },
2896
+ all: async () => {
2897
+ await this._ready;
2898
+ return Object.values(this.data).filter(doc => this.queryDocument(query, doc));
2899
+ }
2900
+ };
2901
+ }
2902
+ /** Find and modify one matching document */
2903
+ async findAndModify(options) {
2904
+ if (!options.query)
2905
+ return 0;
2906
+ await this.checkReady();
2907
+ const id = ((options.upsert || options.new) && !options.query._id) ? newObjectId() : options.query._id;
2908
+ if (!id) {
2909
+ let count = 0;
2910
+ this.find(options.query).forEach((doc) => {
2911
+ if (this.queryDocument(options.query, doc)) {
2912
+ Object.assign(doc, options.update);
2913
+ if (this._save) {
2914
+ if (!this._ready)
2915
+ this._ready = Promise.resolve();
2916
+ this._ready = this._ready.then(async () => {
2917
+ this.data[doc._id] = await this._save?.(doc._id, doc, this) || this.data[doc._id];
2918
+ });
2919
+ }
2920
+ count++;
2921
+ if (options.limit && count >= options.limit)
2922
+ return false;
2923
+ }
2924
+ });
2925
+ return count;
2926
+ }
2927
+ let oldData = this.queryDocument(options.query, this.data[id]);
2928
+ if (!oldData) {
2929
+ if (!options.upsert && !options.new)
2930
+ throw new InvalidData(`Document not found`);
2931
+ oldData = { _id: id };
2932
+ }
2933
+ else {
2934
+ if (options.new)
2935
+ throw new InvalidData(`Document dupplicate`);
2936
+ }
2937
+ if (options.update) {
2938
+ for (const n in options.update) {
2939
+ if (!n.startsWith('$'))
2940
+ oldData[n] = options.update[n];
2941
+ }
2942
+ if (options.update.$unset) {
2943
+ for (const n in options.update.$unset)
2944
+ delete oldData[n];
2945
+ }
2946
+ }
2947
+ if (this._save)
2948
+ this.data[id] = await this._save(id, this.data[id], this) || this.data[id];
2949
+ return 1;
2950
+ }
2951
+ /** Insert one document */
2952
+ async insertOne(doc) {
2953
+ await this.checkReady();
2954
+ if (doc._id && this.data[doc._id])
2955
+ throw new InvalidData(`Document ${doc._id} dupplicate`);
2956
+ if (!doc._id)
2957
+ doc._id = { _id: newObjectId(), ...doc };
2958
+ else
2959
+ doc = { ...doc };
2960
+ this.data[doc._id] = doc;
2961
+ if (this._save)
2962
+ this.data[doc._id] = doc = await this._save(doc._id, doc, this) || doc;
2963
+ return doc;
2964
+ }
2965
+ /** Insert multiple documents */
2966
+ async insert(docs) {
2967
+ await this.checkReady();
2968
+ docs.forEach(doc => {
2969
+ if (doc._id && this.data[doc._id])
2970
+ throw new InvalidData(`Document ${doc._id} dupplicate`);
2971
+ });
2972
+ for (let i = 0; i < docs.length; i++)
2973
+ docs[i] = await this.insertOne(docs[i]);
2974
+ return docs;
2975
+ }
2976
+ /** Delete one matching document */
2977
+ async deleteOne(query) {
2978
+ const id = query._id;
2979
+ if (!id)
2980
+ return;
2981
+ await this.checkReady();
2982
+ delete this.data[id];
2983
+ }
2984
+ /** Delete all matching documents */
2985
+ async deleteMany(query) {
2986
+ let count = 0;
2987
+ await this.checkReady();
2988
+ this.find(query).forEach((doc) => {
2989
+ if (this.queryDocument(query, doc)) {
2990
+ count++;
2991
+ delete this.data[doc._id];
2992
+ if (this._save) {
2993
+ if (!this._ready)
2994
+ this._ready = Promise.resolve();
2995
+ this._ready = this._ready.then(async () => { this._save?.(doc._id, undefined, this); });
2996
+ }
2997
+ }
2998
+ });
2999
+ return count;
3000
+ }
3001
+ }