@radatek/microserver 2.3.11 → 3.0.1

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.
Files changed (3) hide show
  1. package/microserver.d.ts +332 -261
  2. package/microserver.js +1673 -1494
  3. package/package.json +2 -2
package/microserver.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 2.3.11
3
+ * @version 3.0.0
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -16,10 +16,12 @@ import path, { basename, extname } from 'path';
16
16
  import crypto from 'crypto';
17
17
  import zlib from 'zlib';
18
18
  import { EventEmitter } from 'events';
19
+ import { nextTick } from 'process';
19
20
  const defaultToken = 'wx)>:ZUqVc+E,u0EmkPz%ZW@TFDY^3vm';
20
21
  const defaultExpire = 24 * 60 * 60;
21
22
  const defaultMaxBodySize = 5 * 1024 * 1024;
22
23
  const defaultMethods = 'HEAD,GET,POST,PUT,PATCH,DELETE';
24
+ const defaultMaxFileSize = 20 * 1024 * 1024;
23
25
  function NOOP(...args) { }
24
26
  function isFunction(fn) {
25
27
  if (typeof fn !== 'function')
@@ -32,11 +34,12 @@ export class Warning extends Error {
32
34
  super(text);
33
35
  }
34
36
  }
35
- const commonCodes = { 404: 'Not found', 403: 'Access denied', 422: 'Invalid data' };
36
- const commonTexts = { 'Not found': 404, 'Access denied': 403, 'Permission denied': 422, 'Invalid data': 422, InvalidData: 422, AccessDenied: 403, NotFound: 404, Failed: 422, OK: 200 };
37
+ const commonCodes = { 404: 'Not found', 403: 'Access denied', 405: 'Not allowed', 422: 'Invalid data' };
38
+ const commonTexts = { 'Not found': 404, 'Access denied': 403, 'Not allowed': 405, 'Permission denied': 422, 'Invalid data': 422, InvalidData: 422, AccessDenied: 403, NotFound: 404, NotAllowed: 405, Failed: 422, OK: 200 };
37
39
  export class ResponseError extends Error {
38
40
  static getStatusCode(text) { return typeof text === 'number' ? text : text && commonTexts[text] || 500; }
39
41
  static getStatusText(text) { return typeof text === 'number' ? commonCodes[text] : text?.toString() || 'Error'; }
42
+ statusCode;
40
43
  constructor(text, statusCode) {
41
44
  super(ResponseError.getStatusText(text || statusCode || 500));
42
45
  this.statusCode = ResponseError.getStatusCode(statusCode || text) || 500;
@@ -54,31 +57,84 @@ export class NotFound extends ResponseError {
54
57
  constructor(text) { super(text, 404); }
55
58
  }
56
59
  export class WebSocketError extends Error {
60
+ statusCode;
57
61
  constructor(text, code) {
58
62
  super(text);
59
63
  this.statusCode = code || 1002;
60
64
  }
61
65
  }
66
+ function deferPromise(cb) {
67
+ let _resolve;
68
+ const p = new Promise((resolve, reject) => {
69
+ _resolve = (res) => {
70
+ cb?.(res);
71
+ if (res instanceof Error)
72
+ reject(res);
73
+ else
74
+ resolve(res);
75
+ };
76
+ });
77
+ p.resolve = _resolve;
78
+ return p;
79
+ }
62
80
  export class Plugin {
63
- constructor(router, ...args) { }
81
+ name;
82
+ priority;
83
+ constructor() { }
64
84
  }
85
+ // #region ServerRequest/ServerResponse
65
86
  /** Extended http.IncomingMessage */
66
87
  export class ServerRequest extends http.IncomingMessage {
67
- constructor(router) {
88
+ /** Request client IP */
89
+ ip;
90
+ /** Request from local network */
91
+ localip;
92
+ /** Request is secure (https) */
93
+ secure;
94
+ /** Request whole path */
95
+ path = '/';
96
+ /** Request pathname */
97
+ pathname = '/';
98
+ /** Base url */
99
+ baseUrl = '/';
100
+ /** Original url */
101
+ originalUrl;
102
+ /** Query parameters */
103
+ query;
104
+ /** Router named parameters */
105
+ params;
106
+ /** Router named parameters list */
107
+ paramsList;
108
+ /** Router */
109
+ server;
110
+ /** Authentication object */
111
+ auth;
112
+ /** Authenticated user info */
113
+ user;
114
+ /** Model used for request */
115
+ model;
116
+ /** Authentication token id */
117
+ tokenId;
118
+ /** Request raw body */
119
+ rawBody;
120
+ /** Request raw body size */
121
+ rawBodySize;
122
+ _body;
123
+ _isReady;
124
+ constructor(server) {
68
125
  super(new net.Socket());
69
- /** Request whole path */
70
- this.path = '/';
71
- /** Request pathname */
72
- this.pathname = '/';
73
- /** Base url */
74
- this.baseUrl = '/';
75
- this._init(router);
76
- }
77
- _init(router) {
78
- Object.assign(this, {
79
- router,
80
- auth: router.auth,
81
- protocol: 'encrypted' in this.socket && this.socket.encrypted ? 'https' : 'http',
126
+ ServerRequest.extend(this, server);
127
+ }
128
+ static extend(req, server) {
129
+ const reqNew = Object.setPrototypeOf(req, ServerRequest.prototype);
130
+ let ip = req.socket.remoteAddress || '::1';
131
+ if (ip.startsWith('::ffff:'))
132
+ ip = ip.slice(7);
133
+ Object.assign(reqNew, {
134
+ server,
135
+ ip,
136
+ auth: server.auth,
137
+ protocol: 'encrypted' in req.socket && req.socket.encrypted ? 'https' : 'http',
82
138
  query: {},
83
139
  params: {},
84
140
  paramsList: [],
@@ -88,7 +144,16 @@ export class ServerRequest extends http.IncomingMessage {
88
144
  rawBody: [],
89
145
  rawBodySize: 0
90
146
  });
91
- this.updateUrl(this.url || '/');
147
+ reqNew.updateUrl(req.url || '/');
148
+ return reqNew;
149
+ }
150
+ get isReady() {
151
+ return this._isReady === undefined;
152
+ }
153
+ async waitReady() {
154
+ if (this._isReady === undefined)
155
+ return;
156
+ await this._isReady;
92
157
  }
93
158
  /** Update request url */
94
159
  updateUrl(url) {
@@ -101,6 +166,7 @@ export class ServerRequest extends http.IncomingMessage {
101
166
  this.baseUrl = pathname.slice(0, pathname.length - this.path.length);
102
167
  this.query = {};
103
168
  parsedUrl.searchParams.forEach((v, k) => this.query[k] = v);
169
+ return this;
104
170
  }
105
171
  /** Rewrite request url */
106
172
  rewrite(url) {
@@ -108,26 +174,6 @@ export class ServerRequest extends http.IncomingMessage {
108
174
  }
109
175
  /** Request body: JSON or POST parameters */
110
176
  get body() {
111
- if (!this._body) {
112
- if (this.method === 'GET')
113
- this._body = {};
114
- else {
115
- const contentType = this.headers['content-type'] || '', charset = contentType.match(/charset=(\S+)/);
116
- let bodyString = Buffer.concat(this.rawBody).toString((charset ? charset[1] : 'utf8'));
117
- this._body = {};
118
- if (bodyString.startsWith('{') || bodyString.startsWith('[')) {
119
- try {
120
- this._body = JSON.parse(bodyString);
121
- }
122
- catch {
123
- throw new Error('Invalid request format');
124
- }
125
- }
126
- else if (contentType.startsWith('application/x-www-form-urlencoded')) {
127
- this._body = querystring.parse(bodyString);
128
- }
129
- }
130
- }
131
177
  return this._body;
132
178
  }
133
179
  /** Alias to body */
@@ -136,188 +182,28 @@ export class ServerRequest extends http.IncomingMessage {
136
182
  }
137
183
  /** Get websocket */
138
184
  get websocket() {
139
- if (!this._websocket) {
140
- if (!this.headers.upgrade)
141
- throw new Error('Invalid WebSocket request');
142
- this._websocket = new WebSocket(this, {
143
- permessageDeflate: this.router.server.config.websocketCompress,
144
- maxPayload: this.router.server.config.websocketMaxPayload || 1024 * 1024,
145
- maxWindowBits: this.router.server.config.websocketMaxWindowBits || 10
146
- });
147
- }
148
- if (!this._websocket.ready)
149
- throw new Error('Invalid WebSocket request');
150
- return this._websocket;
185
+ throw new Error('WebSocket not initialized');
151
186
  }
152
187
  /** get files list in request */
153
188
  async files() {
154
- this.resume();
155
- delete this.headers.connection;
156
- const files = this._files;
157
- if (files) {
158
- if (files.resolve !== NOOP)
159
- throw new Error('Invalid request files usage');
160
- return new Promise((resolve, reject) => {
161
- files.resolve = err => {
162
- files.done = true;
163
- files.resolve = NOOP;
164
- if (err)
165
- reject(err);
166
- else
167
- resolve(files.list);
168
- };
169
- if (files.done)
170
- files.resolve();
171
- });
172
- }
173
- }
174
- /** Decode request body */
175
- bodyDecode(res, options, next) {
176
- const contentType = (this.headers['content-type'] || '').split(';');
177
- const maxSize = options.maxBodySize || defaultMaxBodySize;
178
- if (contentType.includes('multipart/form-data')) {
179
- const chunkParse = (chunk) => {
180
- const files = this._files;
181
- if (!files || files.done)
182
- return;
183
- chunk = files.chunk = files.chunk ? Buffer.concat([files.chunk, chunk]) : chunk;
184
- const p = chunk.indexOf(files.boundary) || -1;
185
- if (p >= 0 && chunk.length - p >= 2) {
186
- if (files.last) {
187
- if (p > 0)
188
- files.last.write(chunk.subarray(0, p));
189
- files.last.srtream.close();
190
- delete files.last.srtream;
191
- files.last = undefined;
192
- }
193
- let pe = p + files.boundary.length;
194
- if (chunk[pe] === 13 && chunk[pe + 1] === 10) {
195
- chunk = files.chunk = chunk.subarray(p);
196
- // next header
197
- pe = chunk.indexOf('\r\n\r\n');
198
- if (pe > 0) { // whole header
199
- const header = chunk.toString('utf8', files.boundary.length + 2, pe);
200
- chunk = chunk.subarray(pe + 4);
201
- const fileInfo = header.match(/content-disposition: ([^\r\n]+)/i);
202
- const contentType = header.match(/content-type: ([^\r\n;]+)/i);
203
- let fieldName = '', fileName = '';
204
- if (fileInfo)
205
- fileInfo[1].replace(/(\w+)="?([^";]+)"?/, (_, n, v) => {
206
- if (n === 'name')
207
- fieldName = v;
208
- if (n === 'filename')
209
- fileName = v;
210
- return _;
211
- });
212
- if (fileName) {
213
- let file;
214
- do {
215
- file = path.resolve(path.join(files.uploadDir, crypto.randomBytes(16).toString('hex') + '.tmp'));
216
- } while (fs.existsSync(file));
217
- files.last = {
218
- name: fieldName,
219
- fileName: fileName,
220
- contentType: contentType && contentType[1],
221
- file: file,
222
- stream: fs.createWriteStream(file)
223
- };
224
- files.list.push(files.last);
225
- }
226
- else if (fieldName) {
227
- files.last = {
228
- name: fieldName,
229
- stream: {
230
- write: (chunk) => {
231
- if (!this._body)
232
- this._body = {};
233
- this._body[fieldName] = (this._body[fieldName] || '') + chunk.toString();
234
- },
235
- close() { }
236
- }
237
- };
238
- }
239
- }
240
- }
241
- else {
242
- files.chunk = undefined;
243
- files.done = true;
244
- }
245
- }
246
- else {
247
- if (chunk.length > 8096) {
248
- if (files.last)
249
- files.last.stream.write(chunk.subarray(0, files.boundary.length - 1));
250
- chunk = files.chunk = chunk.subarray(files.boundary.length - 1);
251
- }
252
- }
253
- };
254
- this.pause();
255
- //res.setHeader('Connection', 'close') // TODO: check if this is needed
256
- this._body = {};
257
- const files = this._files = {
258
- list: [],
259
- uploadDir: path.resolve(options.uploadDir || 'upload'),
260
- resolve: NOOP,
261
- boundary: ''
262
- };
263
- if (!contentType.find(l => {
264
- const p = l.indexOf('boundary=');
265
- if (p >= 0) {
266
- files.boundary = '\r\n--' + l.slice(p + 9).trim();
267
- return true;
268
- }
269
- }))
270
- return res.error(400);
271
- next();
272
- this.once('error', () => files.resolve(new ResponseError('Request error')))
273
- .on('data', chunk => chunkParse(chunk))
274
- .once('end', () => files.resolve(new Error('Request error')));
275
- res.on('finish', () => this._removeTempFiles());
276
- res.on('error', () => this._removeTempFiles());
277
- res.on('close', () => this._removeTempFiles());
278
- }
279
- else {
280
- this.once('error', err => console.error(err))
281
- .on('data', chunk => {
282
- this.rawBodySize += chunk.length;
283
- if (this.rawBodySize >= maxSize) {
284
- this.pause();
285
- res.setHeader('Connection', 'close');
286
- res.error(413);
287
- }
288
- else
289
- this.rawBody.push(chunk);
290
- })
291
- .once('end', next);
292
- }
293
- }
294
- _removeTempFiles() {
295
- if (this._files) {
296
- if (!this._files.done) {
297
- this.pause();
298
- this._files.resolve(new Error('Invalid request files usage'));
299
- }
300
- this._files.list.forEach(f => {
301
- if (f.stream)
302
- f.stream.close();
303
- if (f.file)
304
- fs.unlink(f.file, NOOP);
305
- });
306
- this._files = undefined;
307
- }
189
+ throw new Error('Upload not initialized');
308
190
  }
309
191
  }
310
192
  /** Extends http.ServerResponse */
311
193
  export class ServerResponse extends http.ServerResponse {
312
- constructor(router) {
313
- super(new http.IncomingMessage(new net.Socket()));
314
- this._init(router);
194
+ isJson;
195
+ headersOnly;
196
+ constructor(server) {
197
+ super(ServerRequest.extend(new http.IncomingMessage(new net.Socket()), server));
198
+ ServerResponse.extend(this);
315
199
  }
316
- _init(router) {
317
- this.router = router;
318
- this.isJson = false;
319
- this.headersOnly = false;
320
- this.statusCode = 200;
200
+ static extend(res) {
201
+ Object.setPrototypeOf(res, ServerResponse.prototype);
202
+ Object.assign(res, {
203
+ statusCode: 200,
204
+ isJson: false,
205
+ headersOnly: false
206
+ });
321
207
  }
322
208
  /** Send error reponse */
323
209
  error(error) {
@@ -366,13 +252,18 @@ export class ServerResponse extends http.ServerResponse {
366
252
  this.send(text != null ? text : (this.statusCode + ' ' + (commonCodes[this.statusCode] || http.STATUS_CODES[this.statusCode])));
367
253
  }
368
254
  catch (e) {
369
- this.statusCode = 500;
370
- this.send('Internal error');
371
- console.error(e);
255
+ this.status(500).send('Internal error');
256
+ this.req.server.emit('error', e);
372
257
  }
373
258
  }
374
259
  /** Sets Content-Type acording to data and sends response */
375
260
  send(data = '') {
261
+ if (this.headersSent)
262
+ return;
263
+ if (!this.req.complete) {
264
+ this.req.pause();
265
+ this.setHeader('Connection', 'close');
266
+ }
376
267
  if (data instanceof Readable)
377
268
  return (data.pipe(this, { end: true }), void 0);
378
269
  if (!this.getHeader('Content-Type') && !(data instanceof Buffer)) {
@@ -412,14 +303,14 @@ export class ServerResponse extends http.ServerResponse {
412
303
  if (typeof error === 'number')
413
304
  error = http.STATUS_CODES[error] || 'Error';
414
305
  if (error instanceof Error)
415
- return this.json(error);
306
+ return this.error(error);
416
307
  this.json(typeof error === 'string' ? { success: false, error } : { success: false, ...error });
417
308
  }
418
309
  /** Send json response in form { success: true, ... } */
419
310
  jsonSuccess(data) {
420
311
  this.isJson = true;
421
312
  if (data instanceof Error)
422
- return this.json(data);
313
+ return this.error(data);
423
314
  this.json(typeof data === 'string' ? { success: true, message: data } : { success: true, ...data });
424
315
  }
425
316
  /** Send redirect response to specified URL with optional status code (default: 302) */
@@ -433,1175 +324,1196 @@ export class ServerResponse extends http.ServerResponse {
433
324
  this.statusCode = code || 302;
434
325
  this.end();
435
326
  }
327
+ /** Rewrite URL */
328
+ rewrite(url) {
329
+ return this.req.rewrite(url);
330
+ }
436
331
  /** Set status code */
437
332
  status(code) {
438
333
  this.statusCode = code;
439
334
  return this;
440
335
  }
441
- file(path, filename) {
442
- StaticPlugin.serveFile(this.req, this, {
443
- path: path,
444
- filename: filename || basename(path),
445
- mimeType: StaticPlugin.mimeTypes[extname(path)] || 'application/octet-stream'
446
- });
336
+ /** Send file */
337
+ file(path) {
338
+ throw new Error('Not implemented');
447
339
  }
448
340
  }
449
- const EMPTY_BUFFER = Buffer.alloc(0);
450
- const DEFLATE_TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
451
- /** WebSocket class */
452
- export class WebSocket extends EventEmitter {
453
- constructor(req, options) {
341
+ /** Lighweight HTTP server */
342
+ export class MicroServer extends EventEmitter {
343
+ config;
344
+ auth;
345
+ _stack = [];
346
+ _router = new RouterPlugin();
347
+ _worker = new Worker();
348
+ /** all sockets */
349
+ sockets;
350
+ /** server instances */
351
+ servers;
352
+ /** @param {MicroServerConfig} [config] MicroServer configuration */
353
+ constructor(config) {
454
354
  super();
455
- this._buffers = [EMPTY_BUFFER];
456
- this._buffersLength = 0;
457
- this.ready = false;
458
- this._socket = req.socket;
459
- this._options = {
460
- maxPayload: 1024 * 1024,
461
- permessageDeflate: false,
462
- maxWindowBits: 15,
463
- timeout: 120000,
464
- ...options
465
- };
466
- this._socket.setTimeout(this._options.timeout || 120000);
467
- const key = req.headers['sec-websocket-key'];
468
- const upgrade = req.headers.upgrade;
469
- const version = +(req.headers['sec-websocket-version'] || 0);
470
- const extensions = req.headers['sec-websocket-extensions'];
471
- const headers = [];
472
- if (!key || !upgrade || upgrade.toLocaleLowerCase() !== 'websocket' || version !== 13 || req.method !== 'GET') {
473
- this._abort('Invalid WebSocket request', 400);
474
- return;
475
- }
476
- if (this._options.permessageDeflate && extensions?.includes('permessage-deflate')) {
477
- let header = 'Sec-WebSocket-Extensions: permessage-deflate';
478
- if ((this._options.maxWindowBits || 15) < 15 && extensions.includes('client_max_window_bits'))
479
- header += `; client_max_window_bits=${this._options.maxWindowBits}`;
480
- headers.push(header);
481
- this._options.deflate = true;
355
+ this.config = config || {};
356
+ this.use(this._router);
357
+ if (config) {
358
+ if (this.config.routes)
359
+ this.use(this.config.routes);
360
+ if (this.config.listen) {
361
+ this._worker.startJob();
362
+ nextTick(() => this.listen({ listen: this.config.listen }).finally(() => this._worker.endJob()));
363
+ }
482
364
  }
483
- this.ready = true;
484
- this._upgrade(key, headers);
485
365
  }
486
- _upgrade(key, headers = []) {
487
- const digest = crypto.createHash('sha1')
488
- .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
489
- .digest('base64');
490
- headers = [
491
- 'HTTP/1.1 101 Switching Protocols',
492
- 'Upgrade: websocket',
493
- 'Connection: Upgrade',
494
- `Sec-WebSocket-Accept: ${digest}`,
495
- ...headers,
496
- '',
497
- ''
498
- ];
499
- this._socket.write(headers.join('\r\n'));
500
- this._socket.on('error', this._errorHandler.bind(this));
501
- this._socket.on('data', this._dataHandler.bind(this));
502
- this._socket.on('close', () => this.emit('close'));
503
- this._socket.on('end', () => this.emit('end'));
366
+ on(event, listener) {
367
+ if (event === 'ready' && this.isReady)
368
+ listener();
369
+ super.on(event, listener);
370
+ return this;
504
371
  }
505
- /** Close connection */
506
- close(reason, data) {
507
- if (reason !== undefined) {
508
- const buffer = Buffer.alloc(2 + (data ? data.length : 0));
509
- buffer.writeUInt16BE(reason, 0);
510
- if (data)
511
- data.copy(buffer, 2);
512
- data = buffer;
513
- }
514
- return this._sendFrame(0x88, data || EMPTY_BUFFER, () => this._socket.destroy());
372
+ addListener(event, listener) { return super.addListener(event, listener); }
373
+ once(event, listener) {
374
+ if (event === 'ready' && this.isReady)
375
+ listener();
376
+ else
377
+ super.once(event, listener);
378
+ return this;
515
379
  }
516
- /** Generate WebSocket frame from data */
517
- static getFrame(data, options) {
518
- let msgType = 8;
519
- let dataLength = 0;
520
- if (typeof data === 'string') {
521
- msgType = 1;
522
- dataLength = Buffer.byteLength(data, 'utf8');
523
- }
524
- else if (data instanceof Buffer) {
525
- msgType = 2;
526
- dataLength = data.length;
527
- }
528
- else if (typeof data === 'number') {
529
- msgType = data;
380
+ off(event, listener) { return super.off(event, listener); }
381
+ removeListener(event, listener) { return super.removeListener(event, listener); }
382
+ get isReady() {
383
+ return !this._worker.isBusy();
384
+ }
385
+ async waitReady() {
386
+ if (this.isReady)
387
+ return;
388
+ return this._worker.wait();
389
+ }
390
+ /** Listen server, should be used only if config.listen is not set */
391
+ async listen(config) {
392
+ if (!config && this.config.listen) {
393
+ console.debug('listen automatically started from constructor');
394
+ return;
530
395
  }
531
- const headerSize = 2 + (dataLength < 126 ? 0 : dataLength < 65536 ? 2 : 8) + (dataLength && options?.mask ? 4 : 0);
532
- const frame = Buffer.allocUnsafe(headerSize + dataLength);
533
- frame[0] = 0x80 | msgType;
534
- frame[1] = dataLength > 65535 ? 127 : dataLength > 125 ? 126 : dataLength;
535
- if (dataLength > 65535)
536
- frame.writeBigUInt64BE(dataLength, 2);
537
- else if (dataLength > 125)
538
- frame.writeUInt16BE(dataLength, 2);
539
- if (dataLength && frame.length > dataLength) {
540
- if (typeof data === 'string')
541
- frame.write(data, headerSize, 'utf8');
542
- else
543
- data.copy(frame, headerSize);
396
+ if (!config)
397
+ throw new Error('listen config is required');
398
+ const listen = (config?.listen || this.config.listen || 0) + '';
399
+ const handler = config?.handler || this.handler.bind(this);
400
+ const tlsConfig = config ? config.tls : this.config.tls;
401
+ const readFile = (data) => data && (data.indexOf('\n') > 0 ? data : fs.readFileSync(data));
402
+ function tlsOptions() {
403
+ return {
404
+ cert: readFile(tlsConfig?.cert),
405
+ key: readFile(tlsConfig?.key),
406
+ ca: readFile(tlsConfig?.ca)
407
+ };
544
408
  }
545
- if (dataLength && options?.mask) {
546
- let i = headerSize, h = headerSize - 4;
547
- for (let i = 0; i < 4; i++)
548
- frame[h + i] = Math.floor(Math.random() * 256);
549
- for (let j = 0; j < dataLength; j++, i++) {
550
- frame[i] ^= frame[h + (j & 3)];
409
+ function tlsOptionsReload(srv) {
410
+ if (tlsConfig?.cert && tlsConfig.cert.indexOf('\n') < 0) {
411
+ let debounce;
412
+ fs.watch(tlsConfig.cert, () => {
413
+ clearTimeout(debounce);
414
+ debounce = setTimeout(() => {
415
+ debounce = undefined;
416
+ srv.setSecureContext(tlsOptions());
417
+ }, 2000);
418
+ });
551
419
  }
552
420
  }
553
- return frame;
554
- }
555
- /** Send data */
556
- send(data) {
557
- let msgType = typeof data === 'string' ? 1 : 2;
558
- if (typeof data === 'string')
559
- data = Buffer.from(data, 'utf8');
560
- if (this._options.deflate && data.length > 256) {
561
- const output = [];
562
- const deflate = zlib.createDeflateRaw({
563
- windowBits: this._options.maxWindowBits
421
+ const reg = /^((?<proto>\w+):\/\/)?(?<host>(\[[^\]]+\]|[a-z][^:,]+|\d+\.\d+\.\d+\.\d+))?:?(?<port>\d+)?/;
422
+ listen.split(',').forEach(listen => {
423
+ this._worker.startJob('listen');
424
+ let { proto, host, port } = reg.exec(listen)?.groups || {};
425
+ let srv;
426
+ switch (proto) {
427
+ case 'tcp':
428
+ if (!config?.handler)
429
+ throw new Error('Handler is required for tcp');
430
+ srv = net.createServer(config.handler);
431
+ break;
432
+ case 'tls':
433
+ if (!config?.handler)
434
+ throw new Error('Handler is required for tls');
435
+ srv = tls.createServer(tlsOptions(), config.handler);
436
+ tlsOptionsReload(srv);
437
+ break;
438
+ case 'https':
439
+ port = port || '443';
440
+ srv = https.createServer(tlsOptions(), handler);
441
+ tlsOptionsReload(srv);
442
+ break;
443
+ default:
444
+ port = port || '80';
445
+ srv = http.createServer(handler);
446
+ break;
447
+ }
448
+ if (!this.servers)
449
+ this.servers = new Set();
450
+ if (!this.sockets)
451
+ this.sockets = new Set();
452
+ this.servers.add(srv);
453
+ if (port === '0') // skip listening
454
+ this._worker.endJob('listen');
455
+ else {
456
+ srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
457
+ const addr = srv.address();
458
+ this.emit('listen', addr.port, addr.address, srv);
459
+ srv._ready = true;
460
+ this._worker.endJob('listen');
461
+ });
462
+ }
463
+ srv.on('error', err => {
464
+ srv.close();
465
+ this.servers.delete(srv);
466
+ if (!srv._ready)
467
+ this._worker.endJob('listen');
468
+ this.emit('error', err);
564
469
  });
565
- deflate.write(data);
566
- deflate.on('data', (chunk) => output.push(chunk));
567
- deflate.flush(() => {
568
- if (output.length > 0 && output[output.length - 1].length > 4)
569
- output[output.length - 1] = output[output.length - 1].subarray(0, output[output.length - 1].length - 4);
570
- this._sendFrame(0xC0 | msgType, Buffer.concat(output));
470
+ srv.on('connection', s => {
471
+ this.sockets.add(s);
472
+ s.once('close', () => this.sockets.delete(s));
571
473
  });
572
- }
573
- else
574
- return this._sendFrame(0x80 | msgType, data);
575
- }
576
- _errorHandler(error) {
577
- this.emit('error', error);
578
- if (this.ready)
579
- this.close(error instanceof WebSocketError && error.statusCode || 1002);
580
- else
581
- this._socket.destroy();
582
- this.ready = false;
474
+ });
475
+ return this._worker.wait('listen');
583
476
  }
584
- _headerLength(buffer) {
585
- if (this._frame)
586
- return 0;
587
- if (!buffer || buffer.length < 2)
588
- return 2;
589
- let hederInfo = buffer[1];
590
- return 2 + (hederInfo & 0x80 ? 4 : 0) + ((hederInfo & 0x7F) === 126 ? 2 : 0) + ((hederInfo & 0x7F) === 127 ? 8 : 0);
591
- }
592
- _dataHandler(data) {
593
- while (data.length) {
594
- let frame = this._frame;
595
- if (!frame) {
596
- let lastBuffer = this._buffers[this._buffers.length - 1];
597
- this._buffers[this._buffers.length - 1] = lastBuffer = Buffer.concat([lastBuffer, data]);
598
- let headerLength = this._headerLength(lastBuffer);
599
- if (lastBuffer.length < headerLength)
600
- return;
601
- const headerBits = lastBuffer[0];
602
- const lengthBits = lastBuffer[1] & 0x7F;
603
- this._buffers.pop();
604
- data = lastBuffer.subarray(headerLength);
605
- // parse header
606
- frame = this._frame = {
607
- fin: (headerBits & 0x80) !== 0,
608
- rsv1: (headerBits & 0x40) !== 0,
609
- opcode: headerBits & 0x0F,
610
- mask: (lastBuffer[1] & 0x80) ? lastBuffer.subarray(headerLength - 4, headerLength) : EMPTY_BUFFER,
611
- length: lengthBits === 126 ? lastBuffer.readUInt16BE(2) : lengthBits === 127 ? lastBuffer.readBigUInt64BE(2) : lengthBits,
612
- lengthReceived: 0,
613
- index: this._buffers.length
614
- };
615
- }
616
- let toRead = frame.length - frame.lengthReceived;
617
- if (toRead > data.length)
618
- toRead = data.length;
619
- if (this._options.maxPayload && this._options.maxPayload < this._buffersLength + frame.length) {
620
- this._errorHandler(new WebSocketError('Payload too big', 1009));
621
- return;
622
- }
623
- // unmask
624
- for (let i = 0, j = frame.lengthReceived; i < toRead; i++, j++)
625
- data[i] ^= frame.mask[j & 3];
626
- frame.lengthReceived += toRead;
627
- if (frame.lengthReceived < frame.length) {
628
- this._buffers.push(data);
629
- return;
477
+ /** 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' */
478
+ _bind(fn) {
479
+ if (typeof fn === 'string') {
480
+ let name = fn;
481
+ let idx = name.indexOf(':');
482
+ if (idx < 0 && name.includes('=')) {
483
+ name = 'param:' + name;
484
+ idx = 5;
630
485
  }
631
- this._buffers.push(data.subarray(0, toRead));
632
- this._buffersLength += toRead;
633
- data = data.subarray(toRead);
634
- if (frame.opcode >= 8) {
635
- const message = Buffer.concat(this._buffers.splice(frame.index));
636
- switch (frame.opcode) {
637
- case 8:
638
- if (!frame.length)
639
- this.emit('close');
640
- else {
641
- const code = message.readInt16BE(0);
642
- if (frame.length === 2)
643
- this.emit('close', code);
644
- else
645
- this.emit('close', code, message.subarray(2));
646
- }
647
- this._socket.destroy();
648
- return;
649
- case 9:
650
- if (message.length)
651
- this.emit('ping', message);
652
- else
653
- this.emit('ping');
654
- if (this._options.autoPong)
655
- this.pong(message);
656
- break;
657
- case 10:
658
- if (message.length)
659
- this.emit('pong', message);
660
- else
661
- this.emit('pong');
486
+ if (idx >= 0) {
487
+ const v = name.slice(idx + 1);
488
+ const type = name.slice(0, idx);
489
+ // predefined middlewares
490
+ switch (type) {
491
+ case 'response': {
492
+ if (v === 'json')
493
+ return (req, res, next) => { res.isJson = true; return next(); };
494
+ if (v === 'html')
495
+ return (req, res, next) => { res.setHeader('Content-Type', 'text/html'); return next(); };
496
+ if (v === 'end')
497
+ return (req, res, next) => { res.end(); };
662
498
  break;
663
- default:
664
- return this._errorHandler(new WebSocketError('Invalid WebSocket frame'));
665
- }
666
- }
667
- else if (frame.fin) {
668
- if (!frame.opcode)
669
- return this._errorHandler(new WebSocketError('Invalid WebSocket frame'));
670
- if (this._options.deflate && frame.rsv1) {
671
- const output = [];
672
- const inflate = zlib.createInflateRaw({
673
- windowBits: this._options.maxWindowBits
674
- });
675
- inflate.on('data', (chunk) => output.push(chunk));
676
- inflate.on('error', (err) => this._errorHandler(err));
677
- for (const buffer of this._buffers)
678
- inflate.write(buffer);
679
- inflate.write(DEFLATE_TRAILER);
680
- inflate.flush(() => {
681
- if (this.ready) {
682
- const message = Buffer.concat(output);
683
- this.emit('message', frame.opcode === 1 ? message.toString('utf8') : message);
499
+ }
500
+ // redirect:302,https://redirect.to
501
+ case 'redirect': {
502
+ let redirect = v.split(','), code = parseInt(v[0]);
503
+ if (!code || code < 301 || code > 399)
504
+ code = 302;
505
+ return (req, res) => res.redirect(code, redirect[1] || v);
506
+ }
507
+ // error:422
508
+ case 'error':
509
+ return (req, res) => res.error(parseInt(v) || 422);
510
+ // param:name=value
511
+ case 'param': {
512
+ idx = v.indexOf('=');
513
+ if (idx > 0) {
514
+ const prm = v.slice(0, idx), val = v.slice(idx + 1);
515
+ return (req, res, next) => { req.params[prm] = val; return next(); };
684
516
  }
685
- });
686
- }
687
- else {
688
- const message = Buffer.concat(this._buffers);
689
- this.emit('message', frame.opcode === 1 ? message.toString('utf8') : message);
517
+ break;
518
+ }
519
+ case 'model': {
520
+ const model = v;
521
+ return (req, res) => {
522
+ res.isJson = true;
523
+ req.params.model = model;
524
+ req.model = Model.models[model];
525
+ if (!req.model) {
526
+ console.error(`Data model ${model} not defined for request ${req.path}`);
527
+ return res.error(422);
528
+ }
529
+ return req.model.handler(req, res);
530
+ };
531
+ }
532
+ // user:userid
533
+ // group:user_groupid
534
+ // acl:validacl
535
+ case 'user':
536
+ case 'group':
537
+ case 'acl':
538
+ return (req, res, next) => {
539
+ if (type === 'user' && v === req.user?.id)
540
+ return next();
541
+ if (type === 'acl') {
542
+ req.params.acl = v;
543
+ if (req.auth?.acl(v))
544
+ return next();
545
+ }
546
+ if (type === 'group') {
547
+ req.params.group = v;
548
+ if (req.user?.group === v)
549
+ return next();
550
+ }
551
+ const accept = req.headers.accept || '';
552
+ if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
553
+ if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
554
+ return res.redirect(302, req.auth.options.redirect);
555
+ else if (req.auth.options.mode !== 'cookie') {
556
+ res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
557
+ return res.error(401);
558
+ }
559
+ }
560
+ return res.error('Permission denied');
561
+ };
690
562
  }
691
- this._buffers = [];
692
- this._buffersLength = 0;
693
563
  }
694
- this._frame = undefined;
695
- this._buffers.push(EMPTY_BUFFER);
564
+ throw new Error('Invalid option: ' + name);
696
565
  }
566
+ if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
567
+ return fn.handler.bind(fn);
568
+ if (typeof fn !== 'function')
569
+ throw new Error('Invalid middleware: ' + String.toString.call(fn));
570
+ return fn.bind(this);
697
571
  }
698
- _abort(message, code, headers) {
699
- code = code || 400;
700
- message = message || http.STATUS_CODES[code] || 'Closed';
701
- headers = [
702
- `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
703
- 'Connection: close',
704
- 'Content-Type: ' + message.startsWith('<') ? 'text/html' : 'text/plain',
705
- `Content-Length: ${Buffer.byteLength(message)}`,
706
- '',
707
- message
708
- ];
709
- this._socket.once('finish', () => {
710
- this._socket.destroy();
711
- this.emit('close');
712
- });
713
- this._socket.end(headers.join('\r\n'));
714
- this.emit('error', new Error(message));
572
+ /** Default server handler */
573
+ handler(req, res) {
574
+ ServerRequest.extend(req, this);
575
+ ServerResponse.extend(res);
576
+ if (req.readable) {
577
+ req._isReady = deferPromise((err) => {
578
+ req._isReady = undefined;
579
+ if (err) {
580
+ if (!res.headersSent)
581
+ res.error('statusCode' in err ? err.statusCode : 400);
582
+ }
583
+ });
584
+ }
585
+ this._router.walk(this._stack, req, res, () => res.error(404));
586
+ //this.handlerRouter(req, res, () => this.handlerLast(req, res))
715
587
  }
716
- /** Send ping frame */
717
- ping(buffer) {
718
- this._sendFrame(0x89, buffer || EMPTY_BUFFER);
588
+ /** Last request handler */
589
+ handlerLast(req, res, next) {
590
+ if (res.headersSent || res.closed)
591
+ return;
592
+ if (!next)
593
+ next = () => res.error(404);
594
+ return next();
719
595
  }
720
- /** Send pong frame */
721
- pong(buffer) {
722
- this._sendFrame(0x8A, buffer || EMPTY_BUFFER);
596
+ /** Clear routes and middlewares */
597
+ clear() {
598
+ this._stack = [];
599
+ this._router.clear();
600
+ this._plugin(this._router);
601
+ return this;
723
602
  }
724
- _sendFrame(opcode, data, cb) {
725
- if (!this.ready)
603
+ /**
604
+ * Add middleware route.
605
+ * Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
606
+ * RouteURL: 'METHOD /suburl', 'METHOD', '* /suburl'
607
+ */
608
+ async use(...args) {
609
+ if (!args[0])
726
610
  return;
727
- const dataLength = data.length;
728
- const headerSize = 2 + (dataLength < 126 ? 0 : dataLength < 65536 ? 2 : 8);
729
- const frame = Buffer.allocUnsafe(headerSize + (dataLength < 4096 ? dataLength : 0));
730
- frame[0] = opcode;
731
- frame[1] = dataLength > 65535 ? 127 : dataLength > 125 ? 126 : dataLength;
732
- if (dataLength > 65535)
733
- frame.writeBigUInt64BE(dataLength, 2);
734
- else if (dataLength > 125)
735
- frame.writeUInt16BE(dataLength, 2);
736
- if (dataLength && frame.length > dataLength) {
737
- data.copy(frame, headerSize);
738
- this._socket.write(frame, cb);
611
+ this._worker.startJob();
612
+ for (let i = 0; i < args.length; i++)
613
+ if (args[i] instanceof Promise)
614
+ args[i] = await args[i];
615
+ // use(plugin)
616
+ if (args[0] instanceof Plugin) {
617
+ await this._plugin(args[0]);
618
+ return this._worker.endJob();
739
619
  }
740
- else
741
- this._socket.write(frame, () => this._socket.write(data, cb));
620
+ // use(PluginClass, options?: any)
621
+ if (typeof args[0] === 'function' && args[0].prototype instanceof Plugin) {
622
+ const pluginid = args[0].name.toLowerCase().replace(/plugin$/, '');
623
+ const plugin = new args[0](args[1] || this.config[pluginid], this);
624
+ await this._plugin(plugin);
625
+ return this._worker.endJob();
626
+ }
627
+ // use(middleware)
628
+ if (isFunction(args[0])) {
629
+ this.addStack(args[0]);
630
+ return this._worker.endJob();
631
+ }
632
+ let method = '*', url = '/';
633
+ if (typeof args[0] === 'string') {
634
+ const m = args[0].match(/^([A-Z]+) (.*)/);
635
+ if (m)
636
+ [method, url] = [m[1], m[2]];
637
+ else
638
+ url = args[0];
639
+ if (!url.startsWith('/'))
640
+ throw new Error(`Invalid url ${url}`);
641
+ args = args.slice(1);
642
+ }
643
+ let routes = args[0];
644
+ // use('/url', ControllerClass)
645
+ if (typeof args[0] === 'function' && args[0].prototype instanceof Controller) {
646
+ routes = args[0].routes();
647
+ }
648
+ // use('/url', [ ['METHOD /url', ...], {'METHOD } ])
649
+ if (Array.isArray(routes)) {
650
+ if (method !== '*')
651
+ throw new Error('Invalid router usage');
652
+ for (const item of routes) {
653
+ if (Array.isArray(item)) {
654
+ // [methodUrl, ...middlewares]
655
+ if (typeof item[0] !== 'string' || !item[0].match(/^(\w+ )?\//))
656
+ throw new Error('Url expected');
657
+ await this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
658
+ }
659
+ else
660
+ throw new Error('Invalid param');
661
+ }
662
+ return this._worker.endJob();
663
+ }
664
+ // use('/url', {'METHOD /url': [...middlewares], ... } ])
665
+ if (typeof routes === 'object' && routes.constructor === Object) {
666
+ if (method !== '*')
667
+ throw new Error('Invalid router usage');
668
+ for (const [subUrl, subArgs] of Object.entries(args[0])) {
669
+ if (!subUrl.match(/^(\w+ )?\//))
670
+ throw new Error('Url expected');
671
+ await this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
672
+ }
673
+ return this._worker.endJob();
674
+ }
675
+ // use('/url', ...middleware)
676
+ this._router.add(method, url, args.filter(o => o).map((o) => this._bind(o)), true);
677
+ return this._worker.endJob();
742
678
  }
743
- }
744
- const server = {};
745
- /**
746
- * Controller for dynamic routes
747
- *
748
- * @example
749
- * ```js
750
- * class MyController extends Controller {
751
- * static model = MyModel;
752
- * static acl = 'auth';
753
- *
754
- * static 'acl:index' = '';
755
- * static 'url:index' = 'GET /index';
756
- * async index (req, res) {
757
- * res.send('Hello World')
758
- * }
759
- *
760
- * //function name prefixes translated to HTTP methods:
761
- * // all => GET, get => GET, insert => POST, post => POST,
762
- * // update => PUT, put => PUT, delete => DELETE,
763
- * // modify => PATCH, patch => PATCH,
764
- * // websocket => internal WebSocket
765
- * // automatic acl will be: class_name + '/' + function_name_prefix
766
- * // automatic url will be: method + ' /' + class_name + '/' + function_name_without_prefix
767
- *
768
- * //static 'acl:allUsers' = 'MyController/all';
769
- * //static 'url:allUsers' = 'GET /MyController/Users';
770
- * async allUsers () {
771
- * return ['usr1', 'usr2', 'usr3']
772
- * }
773
- *
774
- * //static 'acl:getOrder' = 'MyController/get';
775
- * //static 'url:getOrder' = 'GET /Users/:id/:id1';
776
- * static 'group:getOrder' = 'orders';
777
- * static 'model:getOrder' = OrderModel;
778
- * async getOrder (id: string, id1: string) {
779
- * return {id, extras: id1, type: 'order'}
780
- * }
781
- *
782
- * //static 'acl:insertOrder' = 'MyController/insert';
783
- * //static 'url:insertOrder' = 'POST /Users/:id';
784
- * static 'model:insertOrder' = OrderModel;
785
- * async insertOrder (id: string, id1: string) {
786
- * return {id, extras: id1, type: 'order'}
787
- * }
788
- *
789
- * static 'acl:POST /login' = '';
790
- * async 'POST /login' () {
791
- * return {id, extras: id1, type: 'order'}
792
- * }
793
- * }
794
- * ```
795
- */
796
- export class Controller {
797
- get model() {
798
- return this.req.model;
679
+ async _plugin(plugin) {
680
+ if (plugin.handler) {
681
+ const middleware = plugin.handler.bind(plugin);
682
+ middleware.plugin = plugin;
683
+ middleware.priority = plugin.priority;
684
+ this.addStack(middleware);
685
+ }
686
+ if (plugin.routes) {
687
+ const routes = isFunction(plugin.routes) ? await plugin.routes() : plugin.routes;
688
+ if (routes)
689
+ await this.use(routes);
690
+ }
691
+ if (plugin.handler && plugin.name) {
692
+ this.emit('plugin', plugin.name);
693
+ this.emit('plugin:' + plugin.name);
694
+ this._worker.endJob('plugin:' + plugin.name);
695
+ }
799
696
  }
800
- constructor(req, res) {
801
- this.req = req;
802
- this.res = res;
803
- res.isJson = true;
697
+ addStack(middleware) {
698
+ if (middleware.plugin?.name && this.getPlugin(middleware.plugin.name))
699
+ throw new Error(`Plugin ${middleware.plugin.name} already added`);
700
+ const priority = middleware.priority || 0;
701
+ const idx = this._stack.findLastIndex(f => (f.priority || 0) <= priority);
702
+ this._stack.splice(idx + 1, 0, middleware);
804
703
  }
805
- /** Generate routes for this controller */
806
- static routes() {
807
- const routes = [];
808
- const prefix = Object.getOwnPropertyDescriptor(this, 'name')?.enumerable ? this.name + '/' : '';
809
- // iterate throught decorators
810
- Object.getOwnPropertyNames(this.prototype).forEach(key => {
811
- if (key === 'constructor' || key.startsWith('_'))
812
- return;
813
- const func = this.prototype[key];
814
- if (typeof func !== 'function')
815
- return;
816
- const thisStatic = this;
817
- let url = thisStatic['url:' + key];
818
- let acl = thisStatic['acl:' + key] ?? thisStatic['acl'];
819
- const user = thisStatic['user:' + key] ?? thisStatic['user'];
820
- const group = thisStatic['group:' + key] ?? thisStatic['group'];
821
- const modelName = thisStatic['model:' + key] ?? thisStatic['model'];
822
- let method = '';
823
- if (!url)
824
- key = key.replaceAll('$', '/');
825
- if (!url && key.startsWith('/')) {
826
- method = '*';
827
- url = key;
828
- }
829
- let keyMatch = !url && key.match(/^(all|get|put|post|patch|insert|update|modify|delete|websocket)[/_]?([\w_/-]*)$/i);
830
- if (keyMatch) {
831
- method = keyMatch[1];
832
- url = '/' + prefix + keyMatch[2];
833
- }
834
- keyMatch = !url && key.match(/^([*\w]+) (.+)$/);
835
- if (keyMatch) {
836
- method = keyMatch[1];
837
- url = keyMatch[2].startsWith('/') ? keyMatch[2] : ('/' + prefix + keyMatch[1]);
838
- }
839
- keyMatch = !method && url?.match(/^([*\w]+) (.+)$/);
840
- if (keyMatch) {
841
- method = keyMatch[1];
842
- url = keyMatch[2].startsWith('/') ? keyMatch[2] : ('/' + prefix + keyMatch[2]);
843
- }
844
- if (!method)
845
- return;
846
- let autoAcl = method.toLowerCase();
847
- switch (autoAcl) {
848
- case '*':
849
- autoAcl = '';
850
- break;
851
- case 'post':
852
- autoAcl = 'insert';
853
- break;
854
- case 'put':
855
- autoAcl = 'update';
856
- break;
857
- case 'patch':
858
- autoAcl = 'modify';
859
- break;
860
- }
861
- method = method.toUpperCase();
862
- switch (method) {
863
- case '*':
864
- break;
865
- case 'GET':
866
- case 'POST':
867
- case 'PUT':
868
- case 'PATCH':
869
- case 'DELETE':
870
- case 'WEBSOCKET':
871
- break;
872
- case 'ALL':
873
- method = 'GET';
874
- break;
875
- case 'INSERT':
876
- method = 'POST';
877
- break;
878
- case 'UPDATE':
879
- method = 'PUT';
880
- break;
881
- case 'MODIFY':
882
- method = 'PATCH';
883
- break;
884
- default:
885
- throw new Error('Invalid url method for: ' + key);
886
- }
887
- if (user === undefined && group === undefined && acl === undefined)
888
- acl = prefix + autoAcl;
889
- // add params if not available in url
890
- if (func.length && !url.includes(':')) {
891
- let args = ['/:id'];
892
- for (let i = 1; i < func.length; i++)
893
- args.push('/:id' + i);
894
- url += args.join('');
895
- }
896
- const list = [method + ' ' + url.replace(/\/\//g, '/')];
897
- if (acl)
898
- list.push('acl:' + acl);
899
- if (user)
900
- list.push('user:' + user);
901
- if (group)
902
- list.push('group:' + group);
903
- list.push((req, res) => {
904
- res.isJson = true;
905
- const obj = new this(req, res);
906
- if (modelName) {
907
- req.model = modelName instanceof Model ? modelName : Model.models[modelName];
908
- if (!obj.model)
909
- throw new InvalidData(modelName, 'model');
910
- req.model = Model.dynamic(req.model, { controller: obj });
911
- }
912
- return func.apply(obj, req.paramsList);
913
- });
914
- routes.push(list);
915
- });
916
- return routes;
704
+ getPlugin(id) {
705
+ let p = this._stack.find(m => m.plugin?.name === id);
706
+ return p?.plugin;
917
707
  }
918
- }
919
- class WaiterJob {
920
- constructor() {
921
- this._waiters = [];
922
- this._busy = 0;
708
+ async waitPlugin(id) {
709
+ const p = this.getPlugin(id);
710
+ if (p)
711
+ return p;
712
+ this._worker.startJob('plugin:' + id);
713
+ await this._worker.wait('plugin:' + id);
714
+ return this.getPlugin(id);
923
715
  }
924
- start() {
925
- this._busy++;
716
+ /** Add route, alias to `server.router.use(url, ...args)` */
717
+ all(url, ...args) {
718
+ this.use('* ' + url, ...args);
719
+ return this;
926
720
  }
927
- end() {
928
- this._busy--;
929
- for (const resolve of this._waiters.splice(0))
930
- resolve();
721
+ /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
722
+ get(url, ...args) {
723
+ this.use('GET ' + url, ...args);
724
+ return this;
931
725
  }
932
- async wait() {
933
- if (!this._busy)
934
- return;
935
- return new Promise(resolve => this._waiters.push(resolve));
726
+ /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
727
+ post(url, ...args) {
728
+ this.use('POST ' + url, ...args);
729
+ return this;
936
730
  }
937
- }
938
- class Waiter {
939
- constructor() {
940
- this._id = 0;
941
- this._waiters = {};
731
+ /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
732
+ put(url, ...args) {
733
+ this.use('PUT ' + url, ...args);
734
+ return this;
942
735
  }
943
- isBusy(id) {
944
- const waiter = this._waiters[id || 'ready'];
945
- return !!waiter?._busy;
736
+ /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
737
+ patch(url, ...args) {
738
+ this.use('PATCH ' + url, ...args);
739
+ return this;
946
740
  }
947
- startJob(id) {
948
- let waiter = this._waiters[id || 'ready'];
949
- if (!waiter)
950
- waiter = this._waiters[id || 'ready'] = new WaiterJob();
951
- waiter._busy++;
741
+ /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
742
+ delete(url, ...args) {
743
+ this.use('DELETE ' + url, ...args);
744
+ return this;
952
745
  }
953
- endJob(id) {
954
- const waiter = this._waiters[id || 'ready'];
955
- if (waiter)
956
- waiter.end();
746
+ /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
747
+ websocket(url, ...args) {
748
+ this.use('WEBSOCKET ' + url, ...args);
749
+ return this;
957
750
  }
958
- get nextId() {
959
- return (++this._id).toString();
751
+ /** Add router hook, alias to `server.router.hook(url, ...args)` */
752
+ hook(url, ...args) {
753
+ const m = url.match(/^([A-Z]+) (.*)/) || ['', 'hook', url];
754
+ this._router.add(m[1], m[2], args.filter(m => m).map(m => this._bind(m)), false);
755
+ return this;
960
756
  }
961
- async wait(id) {
962
- const waiter = this._waiters[id || 'ready'];
963
- if (!waiter)
964
- return;
965
- return waiter.wait();
757
+ /** Check if middleware allready added */
758
+ has(mid) {
759
+ const check = (stack) => stack.includes(mid) || (mid.name && !!stack.find(f => f.name === mid.name)) || false;
760
+ return check(this._stack);
761
+ }
762
+ async close() {
763
+ this._worker.startJob('close');
764
+ this.servers?.forEach(srv => {
765
+ this._worker.startJob('close');
766
+ srv.close(() => this._worker.endJob('close'));
767
+ });
768
+ this.servers?.clear();
769
+ this.sockets?.forEach(s => {
770
+ if (s.readyState !== 'closed') {
771
+ this._worker.startJob('close');
772
+ s.once('close', () => this._worker.endJob('close'));
773
+ s.destroy();
774
+ }
775
+ });
776
+ this.sockets?.clear();
777
+ this._worker.endJob('close');
778
+ await this._worker.wait('close');
779
+ this.emit('close');
966
780
  }
967
781
  }
968
- /** Router */
969
- export class Router extends EventEmitter {
970
- /** @param {MicroServer} server */
971
- constructor(server) {
782
+ // #region RouterPlugin
783
+ class RouterItem {
784
+ _stack; // add if middlewares added if not last
785
+ _next; // next middlewares
786
+ _paramName; // param name if param is used
787
+ _paramWild;
788
+ _withParam; // next middlewares if param is used
789
+ _last;
790
+ }
791
+ class RouterPlugin extends Plugin {
792
+ priority = 100;
793
+ name = 'router';
794
+ //stack: Middleware[] = []
795
+ _tree = {};
796
+ constructor() {
972
797
  super();
973
- this.plugins = {};
974
- this._stack = [];
975
- this._stackAfter = [];
976
- this._tree = {};
977
- this._waiter = new Waiter();
978
- this.server = server;
979
798
  }
980
- /** 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' */
981
- bind(fn) {
982
- if (typeof fn === 'string') {
983
- let name = fn;
984
- let idx = name.indexOf(':');
985
- if (idx < 0 && name.includes('=')) {
986
- name = 'param:' + name;
987
- idx = 5;
988
- }
989
- if (name === 'json')
990
- return (req, res, next) => { res.isJson = true; return next(); };
991
- if (idx >= 0) {
992
- const v = name.slice(idx + 1);
993
- const type = name.slice(0, idx);
994
- // predefined middlewares
995
- switch (type) {
996
- // redirect:302,https://redirect.to
997
- case 'redirect': {
998
- let redirect = v.split(','), code = parseInt(v[0]);
999
- if (!code || code < 301 || code > 399)
1000
- code = 302;
1001
- return (req, res) => res.redirect(code, redirect[1] || v);
1002
- }
1003
- // error:422
1004
- case 'error':
1005
- return (req, res) => res.error(parseInt(v) || 422);
1006
- // param:name=value
1007
- case 'param': {
1008
- idx = v.indexOf('=');
1009
- if (idx > 0) {
1010
- const prm = v.slice(0, idx), val = v.slice(idx + 1);
1011
- return (req, res, next) => { req.params[prm] = val; return next(); };
1012
- }
1013
- break;
799
+ add(treeItem, path, middlewares, last) {
800
+ middlewares = middlewares.filter(m => m);
801
+ if (!middlewares.length)
802
+ return;
803
+ let node = this._tree[treeItem];
804
+ if (!node)
805
+ this._tree[treeItem] = node = {};
806
+ if (path && path !== '/') {
807
+ const segments = path.split('/').filter(s => s);
808
+ for (let i = 0; i < segments.length; i++) {
809
+ const seg = segments[i];
810
+ if (seg[0] === ':') {
811
+ const isWild = seg.endsWith('*');
812
+ const paramName = isWild ? seg.slice(1, -1) : seg.slice(1);
813
+ if (!node._withParam) {
814
+ node._withParam = { _paramName: paramName, _paramWild: isWild };
1014
815
  }
1015
- case 'model': {
1016
- const model = v;
1017
- return (req, res) => {
1018
- res.isJson = true;
1019
- req.params.model = model;
1020
- req.model = Model.models[model];
1021
- if (!req.model) {
1022
- console.error(`Data model ${model} not defined for request ${req.path}`);
1023
- return res.error(422);
1024
- }
1025
- return req.model.handler(req, res);
1026
- };
816
+ else {
817
+ if (node._withParam._paramName !== paramName)
818
+ throw new Error(`Router param already used: ${node._withParam._paramName} != ${paramName}`);
819
+ if (isWild)
820
+ node._withParam._paramWild = true;
1027
821
  }
1028
- // user:userid
1029
- // group:user_groupid
1030
- // acl:validacl
1031
- case 'user':
1032
- case 'group':
1033
- case 'acl':
1034
- return (req, res, next) => {
1035
- if (type === 'user' && v === req.user?.id)
1036
- return next();
1037
- if (type === 'acl') {
1038
- req.params.acl = v;
1039
- if (req.auth?.acl(v))
1040
- return next();
1041
- }
1042
- if (type === 'group') {
1043
- req.params.group = v;
1044
- if (req.user?.group === v)
1045
- return next();
1046
- }
1047
- const accept = req.headers.accept || '';
1048
- if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
1049
- if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
1050
- return res.redirect(302, req.auth.options.redirect);
1051
- else if (req.auth.options.mode !== 'cookie') {
1052
- res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
1053
- return res.error(401);
1054
- }
1055
- }
1056
- return res.error('Permission denied');
1057
- };
822
+ node = node._withParam;
823
+ if (isWild)
824
+ break;
825
+ }
826
+ else {
827
+ if (!node._next)
828
+ node._next = {};
829
+ if (!node._next[seg])
830
+ node._next[seg] = { _next: {} };
831
+ node = node._next[seg];
1058
832
  }
1059
833
  }
1060
- throw new Error('Invalid option: ' + name);
1061
834
  }
1062
- if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
1063
- return fn.handler.bind(fn);
1064
- if (typeof fn !== 'function')
1065
- throw new Error('Invalid middleware: ' + String.toString.call(fn));
1066
- return fn.bind(this);
835
+ if (last) {
836
+ node._last ||= [];
837
+ node._last.push(...middlewares);
838
+ }
839
+ else {
840
+ node._stack ||= [];
841
+ node._stack.push(...middlewares);
842
+ }
1067
843
  }
1068
- /** Handler */
1069
- handler(req, res, next, method) {
1070
- const nextAfter = next;
1071
- next = () => this._walkStack(this._stackAfter, req, res, nextAfter);
1072
- const walkTree = (method) => this._walkTree(this._tree[method], req, res, next);
1073
- const walk = method ?
1074
- () => { !walkTree(method) && next(); } :
1075
- () => { !walkTree(req.method || 'GET') && !walkTree('*') && next(); };
1076
- req.rewrite = (url) => {
1077
- if (req.originalUrl)
1078
- res.error(508);
1079
- req.updateUrl(url);
1080
- walk();
1081
- };
1082
- this._walkStack(this._stack, req, res, walk);
844
+ _getStack(path, treeItems) {
845
+ const out = [];
846
+ const segments = path.split('/').filter(s => s);
847
+ for (const key of treeItems) {
848
+ let node = this._tree[key];
849
+ if (!node)
850
+ continue;
851
+ for (let i = 0; i < segments.length && node; i++) {
852
+ const seg = segments[i];
853
+ if (node._stack)
854
+ out.push(...node._stack);
855
+ // static match
856
+ if (node._next?.[seg]) {
857
+ node = node._next[seg];
858
+ continue;
859
+ }
860
+ // param match
861
+ if (node._withParam) {
862
+ const paramNode = node._withParam;
863
+ const value = decodeURIComponent(paramNode._paramWild ? segments.slice(i).join('/') : seg), name = paramNode._paramName;
864
+ out.push((req, res, next) => { req.params[name] = value; req.paramsList.push(value); next(); });
865
+ node = paramNode;
866
+ if (paramNode._paramWild)
867
+ break;
868
+ else
869
+ continue;
870
+ }
871
+ node = undefined;
872
+ }
873
+ if (node?._stack)
874
+ out.push(...node._stack);
875
+ if (node?._last) {
876
+ out.push(...node._last);
877
+ return out;
878
+ }
879
+ }
880
+ return out;
1083
881
  }
1084
- _walkStack(rstack, req, res, next) {
1085
- let rnexti = 0;
882
+ walk(stack, req, res, next) {
1086
883
  const sendData = (data) => {
1087
- if (!res.headersSent && data !== undefined) {
884
+ if (!res.headersSent && !res.closed && data !== undefined) {
1088
885
  if ((data === null || typeof data === 'string') && !res.isJson)
1089
886
  return res.send(data);
1090
887
  if (typeof data === 'object' &&
1091
- (data instanceof Buffer || data instanceof Readable || (data instanceof Error && !res.isJson)))
888
+ (data instanceof Buffer || data instanceof Readable || data instanceof Error))
1092
889
  return res.send(data);
1093
890
  return res.jsonSuccess(data);
1094
891
  }
1095
892
  };
1096
- const rnext = () => {
1097
- const cb = rstack[rnexti++];
1098
- if (cb) {
1099
- try {
1100
- req.router = this;
1101
- const p = cb(req, res, rnext);
1102
- if (p instanceof Promise)
1103
- p.catch(e => e).then(sendData);
1104
- else
1105
- sendData(p);
1106
- }
1107
- catch (e) {
1108
- sendData(e);
1109
- }
1110
- }
1111
- else
893
+ let idx = 0;
894
+ const callNext = () => {
895
+ if (res.headersSent || res.closed)
896
+ return;
897
+ const fn = stack[idx++];
898
+ if (!fn)
1112
899
  return next();
900
+ try {
901
+ const r = fn(req, res, callNext);
902
+ if (r instanceof Promise)
903
+ r.catch(e => e).then(sendData);
904
+ else
905
+ sendData(r);
906
+ }
907
+ catch (e) {
908
+ sendData(e);
909
+ }
1113
910
  };
1114
- return rnext();
1115
- }
1116
- _walkTree(item, req, res, next) {
1117
- // TODO: walk recursively and add to stack all possibilities: /api/user/:id, /api/:last*. set params and paramsList pro stack record
1118
- req.params = {};
1119
- req.paramsList = [];
1120
- const rstack = [];
1121
- const reg = /\/([^/]*)/g;
1122
- let m;
1123
- let lastItem, done;
1124
- while (m = reg.exec(req.pathname)) {
1125
- const name = m[1];
1126
- if (!item || done) {
1127
- item = undefined;
1128
- break;
1129
- }
1130
- if (lastItem !== item) {
1131
- lastItem = item;
1132
- item.hook?.forEach((hook) => rstack.push(hook.bind(item)));
1133
- }
1134
- if (!item.tree) { // last
1135
- if (item.name) {
1136
- req.params[item.name] += '/' + name;
1137
- req.paramsList[req.paramsList.length - 1] = req.params[item.name];
911
+ return callNext();
912
+ }
913
+ clear() {
914
+ this._tree = {};
915
+ }
916
+ handler(req, res, next) {
917
+ this.walk(this._getStack(req.pathname, ['hook', req.method, '*']), req, res, next);
918
+ }
919
+ }
920
+ export class CorsPlugin extends Plugin {
921
+ priority = -100;
922
+ name = 'cors';
923
+ options;
924
+ constructor(options) {
925
+ super();
926
+ if (!options) {
927
+ this.handler = undefined;
928
+ return;
929
+ }
930
+ if (options === true)
931
+ options = '*';
932
+ this.options = typeof options === 'string' ? { origin: options, headers: 'Content-Type', credentials: true } : options;
933
+ }
934
+ handler(req, res, next) {
935
+ if (this.options && req.headers.origin) {
936
+ const cors = this.options;
937
+ if (cors.origin)
938
+ res.setHeader('Access-Control-Allow-Origin', cors.origin);
939
+ if (cors.headers)
940
+ res.setHeader('Access-Control-Allow-Headers', cors.headers);
941
+ if (cors.credentials)
942
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
943
+ if (cors.expose)
944
+ res.setHeader('Access-Control-Expose-Headers', cors.expose);
945
+ if (cors.maxAge)
946
+ res.setHeader('Access-Control-Max-Age', cors.maxAge);
947
+ }
948
+ return next();
949
+ }
950
+ }
951
+ // #enregion CorsPlugin
952
+ // #region MethodsPlugin
953
+ export class MethodsPlugin extends Plugin {
954
+ priority = -90;
955
+ name = 'methods';
956
+ _methods;
957
+ _methodsIdx;
958
+ constructor(methods) {
959
+ super();
960
+ this._methods = methods || defaultMethods;
961
+ this._methodsIdx = this._methods.split(',').reduce((acc, m) => (acc[m] = true, acc), {});
962
+ }
963
+ handler(req, res, next) {
964
+ if (req.method === 'GET' || req.headers.upgrade)
965
+ return next();
966
+ if (req.method === 'OPTIONS') {
967
+ res.setHeader('Allow', this._methods);
968
+ return res.status(204).end();
969
+ }
970
+ if (!req.method || !this._methodsIdx?.[req.method]) {
971
+ res.setHeader('Allow', this._methods);
972
+ return res.status(405).end();
973
+ }
974
+ if (req.method === 'HEAD') {
975
+ req.method = 'GET';
976
+ res.headersOnly = true;
977
+ }
978
+ return next();
979
+ }
980
+ }
981
+ export class BodyPlugin extends Plugin {
982
+ priority = -80;
983
+ name = 'body';
984
+ _maxBodySize;
985
+ constructor(options) {
986
+ super();
987
+ this._maxBodySize = options?.maxBodySize || defaultMaxBodySize;
988
+ }
989
+ handler(req, res, next) {
990
+ if (req.complete || req.method === 'GET') {
991
+ if (!req.body)
992
+ req._body = {};
993
+ return next();
994
+ }
995
+ req._isReady = deferPromise((err) => {
996
+ req._isReady = undefined;
997
+ if (err) {
998
+ if (!req.complete)
999
+ req.pause();
1000
+ if (!res.headersSent)
1001
+ res.error('statusCode' in err ? err.statusCode : 400);
1002
+ }
1003
+ else if (req.complete)
1004
+ res.removeHeader('Connection');
1005
+ });
1006
+ const contentType = req.headers['content-type'] || '';
1007
+ if (contentType.startsWith('multipart/form-data')) {
1008
+ req.pause();
1009
+ res.setHeader('Connection', 'close');
1010
+ return next();
1011
+ }
1012
+ if (parseInt(req.headers['content-length'] || '-1') > this._maxBodySize) {
1013
+ return req._isReady?.resolve(new ResponseError("too big", 413));
1014
+ }
1015
+ req.once('error', () => { })
1016
+ .on('data', chunk => {
1017
+ req.rawBodySize += chunk.length;
1018
+ if (req.rawBodySize >= this._maxBodySize)
1019
+ req._isReady?.resolve(new ResponseError("too big", 413));
1020
+ else
1021
+ req.rawBody.push(chunk);
1022
+ })
1023
+ .once('end', () => {
1024
+ req._isReady?.resolve();
1025
+ Object.defineProperty(req, 'body', {
1026
+ get: () => {
1027
+ if (!req._body) {
1028
+ let charset = contentType.match(/charset=(\S+)/)?.[1];
1029
+ if (charset !== 'utf8' && charset !== 'latin1' && charset !== 'ascii')
1030
+ charset = 'utf8';
1031
+ const bodyString = Buffer.concat(req.rawBody).toString(charset);
1032
+ if (contentType.startsWith('application/x-www-form-urlencoded')) {
1033
+ req._body = querystring.parse(bodyString);
1034
+ }
1035
+ else if (bodyString.startsWith('{') || bodyString.startsWith('[')) {
1036
+ try {
1037
+ req._body = JSON.parse(bodyString);
1038
+ }
1039
+ catch {
1040
+ return res.jsonError(405);
1041
+ }
1042
+ }
1043
+ else
1044
+ req._body = {};
1045
+ }
1046
+ return req._body;
1047
+ },
1048
+ configurable: true,
1049
+ enumerable: true
1050
+ });
1051
+ return next();
1052
+ });
1053
+ }
1054
+ }
1055
+ export class UploadPlugin extends Plugin {
1056
+ priority = -70;
1057
+ name = 'upload';
1058
+ _maxFileSize;
1059
+ _uploadDir;
1060
+ constructor(options) {
1061
+ super();
1062
+ this._maxFileSize = options?.maxFileSize || defaultMaxFileSize;
1063
+ this._uploadDir = options?.uploadDir;
1064
+ }
1065
+ handler(req, res, next) {
1066
+ if (!req.readable || req.method === 'GET')
1067
+ return next();
1068
+ const contentType = req.headers['content-type'] || '';
1069
+ if (!contentType.startsWith('multipart/form-data'))
1070
+ return next();
1071
+ if (!req._isReady) {
1072
+ req._isReady = deferPromise((err) => {
1073
+ req._isReady = undefined;
1074
+ if (err) {
1075
+ req.pause();
1076
+ if (!res.headersSent)
1077
+ res.setHeader('Connection', 'close');
1078
+ res.error('statusCode' in err ? err.statusCode : 400);
1138
1079
  }
1139
1080
  else
1140
- done = true;
1141
- }
1142
- else {
1143
- item = item.tree[name] || item.param || item.last;
1144
- if (item && item.name) {
1145
- req.params[item.name] = name;
1146
- req.paramsList.push(name);
1081
+ res.removeHeader('Connection');
1082
+ });
1083
+ }
1084
+ req.pause();
1085
+ res.setHeader('Connection', 'close');
1086
+ if (!this._uploadDir)
1087
+ return res.error(405);
1088
+ const uploadDir = path.resolve(this._uploadDir);
1089
+ const files = [];
1090
+ req.files = async () => {
1091
+ if (req.isReady)
1092
+ return files;
1093
+ req.resume();
1094
+ await req.waitReady();
1095
+ return files;
1096
+ };
1097
+ const boundaryIdx = contentType.indexOf('boundary=');
1098
+ if (boundaryIdx < 0)
1099
+ return res.error(405);
1100
+ const boundary = Buffer.from('--' + contentType.slice(boundaryIdx + 9).trim());
1101
+ const lookahead = boundary.length + 6;
1102
+ let lastFile;
1103
+ let fileStream;
1104
+ let buffer = Buffer.alloc(0);
1105
+ const chunkParse = (chunk) => {
1106
+ buffer = Buffer.concat([buffer, chunk]);
1107
+ while (buffer.length > 0) {
1108
+ if (!fileStream) {
1109
+ const boundaryIndex = buffer.indexOf(boundary);
1110
+ if (boundaryIndex < 0)
1111
+ break;
1112
+ const headerEndIndex = buffer.indexOf('\r\n\r\n', boundaryIndex);
1113
+ if (headerEndIndex < 0)
1114
+ break;
1115
+ const header = buffer.subarray(boundaryIndex, headerEndIndex).toString();
1116
+ const contentType = header.match(/content-type: ([^\r\n;]+)/i);
1117
+ const filenameMatch = header.match(/filename="(.+?)"/);
1118
+ if (filenameMatch) {
1119
+ let filePath;
1120
+ do {
1121
+ filePath = path.resolve(path.join(uploadDir, crypto.randomBytes(16).toString('hex') + '.tmp'));
1122
+ } while (fs.existsSync(filePath));
1123
+ buffer = buffer.slice(headerEndIndex + 4);
1124
+ lastFile = {
1125
+ name: filenameMatch[1],
1126
+ fileName: filenameMatch[1],
1127
+ contentType: contentType && contentType[1] || undefined,
1128
+ filePath,
1129
+ size: 0
1130
+ };
1131
+ fileStream = fs.createWriteStream(filePath);
1132
+ files.push(lastFile);
1133
+ }
1134
+ else {
1135
+ const nextBoundary = buffer.indexOf(boundary, boundaryIndex + boundary.length);
1136
+ if (nextBoundary === -1)
1137
+ break;
1138
+ buffer = buffer.subarray(nextBoundary);
1139
+ }
1140
+ }
1141
+ else {
1142
+ const nextBoundaryIndex = buffer.indexOf(boundary);
1143
+ const nextBoundaryIndexEnd = nextBoundaryIndex + boundary.length;
1144
+ if (nextBoundaryIndex > 1 && buffer[nextBoundaryIndex - 2] === 13 && buffer[nextBoundaryIndex - 1] === 10
1145
+ && ((buffer[nextBoundaryIndexEnd] === 13 && buffer[nextBoundaryIndexEnd + 1] === 10)
1146
+ || (buffer[nextBoundaryIndexEnd] === 45 && buffer[nextBoundaryIndexEnd + 1] === 45))) {
1147
+ fileStream.write(buffer.subarray(0, nextBoundaryIndex - 2));
1148
+ fileStream.end();
1149
+ lastFile.size += nextBoundaryIndex - 2;
1150
+ fileStream = undefined;
1151
+ if (buffer[nextBoundaryIndexEnd] === 45)
1152
+ req._isReady?.resolve();
1153
+ buffer = buffer.subarray(nextBoundaryIndex);
1154
+ }
1155
+ else {
1156
+ const safeWriteLength = buffer.length - lookahead;
1157
+ if (safeWriteLength > 0) {
1158
+ lastFile.size += safeWriteLength;
1159
+ fileStream.write(buffer.subarray(0, safeWriteLength));
1160
+ buffer = buffer.subarray(safeWriteLength);
1161
+ }
1162
+ break;
1163
+ }
1147
1164
  }
1148
1165
  }
1149
- }
1150
- if (lastItem !== item)
1151
- item?.hook?.forEach((hook) => rstack.push(hook.bind(item)));
1152
- item?.next?.forEach((cb) => rstack.push(cb));
1153
- if (!rstack.length)
1166
+ };
1167
+ const _removeTempFiles = () => {
1168
+ if (!req.isReady)
1169
+ req._isReady?.resolve(new Error('Upload error'));
1170
+ if (fileStream) {
1171
+ fileStream.close();
1172
+ fileStream = undefined;
1173
+ }
1174
+ files.forEach(f => {
1175
+ if (f.filePath)
1176
+ fs.unlink(f.filePath, NOOP);
1177
+ delete f.filePath;
1178
+ });
1179
+ files.splice(0);
1180
+ req._isReady?.resolve();
1181
+ };
1182
+ next();
1183
+ req.once('error', () => req._isReady?.resolve(new Error('Upload error')))
1184
+ .on('data', chunk => chunkParse(chunk))
1185
+ .once('end', () => req._isReady?.resolve(new Error('Upload error')));
1186
+ res.once('finish', () => _removeTempFiles());
1187
+ res.once('error', () => _removeTempFiles());
1188
+ res.once('close', () => _removeTempFiles());
1189
+ }
1190
+ }
1191
+ const EMPTY_BUFFER = Buffer.alloc(0);
1192
+ const DEFLATE_TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
1193
+ /** WebSocket class */
1194
+ export class WebSocket extends EventEmitter {
1195
+ _socket;
1196
+ _frame;
1197
+ _buffers = [EMPTY_BUFFER];
1198
+ _buffersLength = 0;
1199
+ _options;
1200
+ ready = false;
1201
+ constructor(req, options) {
1202
+ super();
1203
+ this._socket = req.socket;
1204
+ this._options = {
1205
+ maxPayload: 1024 * 1024,
1206
+ permessageDeflate: false,
1207
+ maxWindowBits: 15,
1208
+ timeout: 120000,
1209
+ ...options
1210
+ };
1211
+ this._socket.setTimeout(this._options.timeout || 120000);
1212
+ const key = req.headers['sec-websocket-key'];
1213
+ const upgrade = req.headers.upgrade;
1214
+ const version = +(req.headers['sec-websocket-version'] || 0);
1215
+ const extensions = req.headers['sec-websocket-extensions'];
1216
+ const headers = [];
1217
+ if (!key || !upgrade || upgrade.toLocaleLowerCase() !== 'websocket' || version !== 13 || req.method !== 'GET') {
1218
+ this._abort('Invalid WebSocket request', 400);
1154
1219
  return;
1155
- this._walkStack(rstack, req, res, next);
1156
- return true;
1157
- }
1158
- _add(method, url, key, middlewares) {
1159
- if (key === 'next')
1160
- this.server.emit('route', {
1161
- method,
1162
- url,
1163
- middlewares
1164
- });
1165
- middlewares = middlewares.map(m => this.bind(m));
1166
- let item = this._tree[method];
1167
- if (!item)
1168
- item = this._tree[method] = { tree: {} };
1169
- if (!url.startsWith('/')) {
1170
- if (method === '*' && url === '') {
1171
- this._stack.push(...middlewares);
1172
- return this;
1173
- }
1174
- url = '/' + url;
1175
- }
1176
- const reg = /\/(:?)([^/*]+)(\*?)/g;
1177
- let m;
1178
- while (m = reg.exec(url)) {
1179
- const param = m[1], name = m[2], last = m[3];
1180
- if (last) {
1181
- item.last = { name: name };
1182
- item = item.last;
1183
- }
1184
- else {
1185
- if (!item.tree)
1186
- throw new Error('Invalid route path');
1187
- if (param) {
1188
- item = item.param = item.param || { tree: {}, name: name };
1189
- }
1190
- else {
1191
- let subitem = item.tree[name];
1192
- if (!subitem)
1193
- subitem = item.tree[name] = { tree: {} };
1194
- item = subitem;
1195
- }
1196
- }
1197
1220
  }
1198
- if (!item[key])
1199
- item[key] = [];
1200
- item[key].push(...middlewares);
1221
+ if (this._options.permessageDeflate && extensions?.includes('permessage-deflate')) {
1222
+ let header = 'Sec-WebSocket-Extensions: permessage-deflate';
1223
+ if ((this._options.maxWindowBits || 15) < 15 && extensions.includes('client_max_window_bits'))
1224
+ header += `; client_max_window_bits=${this._options.maxWindowBits}`;
1225
+ headers.push(header);
1226
+ this._options.deflate = true;
1227
+ }
1228
+ this.ready = true;
1229
+ this._upgrade(key, headers);
1201
1230
  }
1202
- /** Clear routes and middlewares */
1203
- clear() {
1204
- this._tree = {};
1205
- this._stack = [];
1206
- return this;
1231
+ _upgrade(key, headers = []) {
1232
+ const digest = crypto.createHash('sha1')
1233
+ .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
1234
+ .digest('base64');
1235
+ headers = [
1236
+ 'HTTP/1.1 101 Switching Protocols',
1237
+ 'Upgrade: websocket',
1238
+ 'Connection: Upgrade',
1239
+ `Sec-WebSocket-Accept: ${digest}`,
1240
+ ...headers,
1241
+ '',
1242
+ ''
1243
+ ];
1244
+ this._socket.write(headers.join('\r\n'));
1245
+ this._socket.on('error', this._errorHandler.bind(this));
1246
+ this._socket.on('data', this._dataHandler.bind(this));
1247
+ this._socket.on('close', () => this.emit('close'));
1248
+ this._socket.on('end', () => this.emit('end'));
1207
1249
  }
1208
- /**
1209
- * Add middleware route.
1210
- * Middlewares may return promises for res.jsonSuccess(...), throw errors for res.error(...), return string or {} for res.send(...)
1211
- *
1212
- * @signature add(plugin: Plugin)
1213
- * @param {Plugin} plugin plugin module instance
1214
- * @return {Promise<>}
1215
- *
1216
- * @signature add(pluginid: string, ...args: any)
1217
- * @param {string} pluginid pluginid module
1218
- * @param {...any} args arguments passed to constructor
1219
- * @return {Promise<>}
1220
- *
1221
- * @signature add(pluginClass: typeof Plugin, ...args: any)
1222
- * @param {typeof Plugin} pluginClass plugin class
1223
- * @param {...any} args arguments passed to constructor
1224
- * @return {Promise<>}
1225
- *
1226
- * @signature add(middleware: Middleware)
1227
- * @param {Middleware} middleware
1228
- * @return {Promise<>}
1229
- *
1230
- * @signature add(methodUrl: string, ...middlewares: any)
1231
- * @param {string} methodUrl 'METHOD /url' or '/url'
1232
- * @param {...any} middlewares
1233
- * @return {Promise<>}
1234
- *
1235
- * @signature add(methodUrl: string, controllerClass: typeof Controller)
1236
- * @param {string} methodUrl 'METHOD /url' or '/url'
1237
- * @param {typeof Controller} controllerClass
1238
- * @return {Promise<>}
1239
- *
1240
- * @signature add(methodUrl: string, routes: Array<Array<any>>)
1241
- * @param {string} methodUrl 'METHOD /url' or '/url'
1242
- * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1243
- * @return {Promise<>}
1244
- *
1245
- * @signature add(methodUrl: string, routes: Array<Array<any>>)
1246
- * @param {string} methodUrl 'METHOD /url' or '/url'
1247
- * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1248
- * @return {Promise<>}
1249
- *
1250
- * @signature add(routes: { [key: string]: Array<any> })
1251
- * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1252
- * @return {Promise<>}
1253
- *
1254
- * @signature add(methodUrl: string, routes: { [key: string]: Array<any> })
1255
- * @param {string} methodUrl 'METHOD /url' or '/url'
1256
- * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1257
- * @return {Promise<>}
1258
- */
1259
- async use(...args) {
1260
- if (!args[0])
1261
- return;
1262
- this.server._waiter.startJob();
1263
- for (let i = 0; i < args.length; i++)
1264
- args[i] = await args[i];
1265
- // use(plugin)
1266
- if (args[0] instanceof Plugin) {
1267
- await this._plugin(args[0]);
1268
- return this.server._waiter.endJob();
1250
+ /** Close connection */
1251
+ close(reason, data) {
1252
+ if (reason !== undefined) {
1253
+ const buffer = Buffer.alloc(2 + (data ? data.length : 0));
1254
+ buffer.writeUInt16BE(reason, 0);
1255
+ if (data)
1256
+ data.copy(buffer, 2);
1257
+ data = buffer;
1269
1258
  }
1270
- // use(pluginid, ...args)
1271
- if (typeof args[0] === 'string' && MicroServer.plugins[args[0]]) {
1272
- const constructor = MicroServer.plugins[args[0]];
1273
- const plugin = new constructor(this, ...args.slice(1));
1274
- await this._plugin(plugin);
1275
- return this.server._waiter.endJob();
1259
+ return this._sendFrame(0x88, data || EMPTY_BUFFER, () => this._socket.destroy());
1260
+ }
1261
+ /** Generate WebSocket frame from data */
1262
+ static getFrame(data, options) {
1263
+ let msgType = 8;
1264
+ let dataLength = 0;
1265
+ if (typeof data === 'string') {
1266
+ msgType = 1;
1267
+ dataLength = Buffer.byteLength(data, 'utf8');
1276
1268
  }
1277
- // use(PluginClass, ...args)
1278
- if (typeof args[0] === 'function' && args[0].prototype instanceof Plugin) {
1279
- const plugin = new args[0](this, ...args.slice(1));
1280
- await this._plugin(plugin);
1281
- return this.server._waiter.endJob();
1269
+ else if (data instanceof Buffer) {
1270
+ msgType = 2;
1271
+ dataLength = data.length;
1282
1272
  }
1283
- // use(middleware)
1284
- if (isFunction(args[0])) {
1285
- this._middleware(args[0]);
1286
- return this.server._waiter.endJob();
1273
+ else if (typeof data === 'number') {
1274
+ msgType = data;
1287
1275
  }
1288
- let method = '*', url = '/';
1289
- if (typeof args[0] === 'string') {
1290
- const m = args[0].match(/^([A-Z]+) (.*)/);
1291
- if (m)
1292
- [method, url] = [m[1], m[2]];
1276
+ const headerSize = 2 + (dataLength < 126 ? 0 : dataLength < 65536 ? 2 : 8) + (dataLength && options?.mask ? 4 : 0);
1277
+ const frame = Buffer.allocUnsafe(headerSize + dataLength);
1278
+ frame[0] = 0x80 | msgType;
1279
+ frame[1] = dataLength > 65535 ? 127 : dataLength > 125 ? 126 : dataLength;
1280
+ if (dataLength > 65535)
1281
+ frame.writeBigUInt64BE(dataLength, 2);
1282
+ else if (dataLength > 125)
1283
+ frame.writeUInt16BE(dataLength, 2);
1284
+ if (dataLength && frame.length > dataLength) {
1285
+ if (typeof data === 'string')
1286
+ frame.write(data, headerSize, 'utf8');
1293
1287
  else
1294
- url = args[0];
1295
- if (!url.startsWith('/'))
1296
- throw new Error(`Invalid url ${url}`);
1297
- args = args.slice(1);
1298
- }
1299
- // use('/url', ControllerClass)
1300
- if (typeof args[0] === 'function' && args[0].prototype instanceof Controller) {
1301
- const routes = await args[0].routes();
1302
- if (routes)
1303
- args[0] = routes;
1304
- }
1305
- // use('/url', [ ['METHOD /url', ...], {'METHOD } ])
1306
- if (Array.isArray(args[0])) {
1307
- if (method !== '*')
1308
- throw new Error('Invalid router usage');
1309
- for (const item of args[0]) {
1310
- if (Array.isArray(item)) {
1311
- // [methodUrl, ...middlewares]
1312
- if (typeof item[0] !== 'string' || !item[0].match(/^(\w+ )?\//))
1313
- throw new Error('Url expected');
1314
- await this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1315
- }
1316
- else
1317
- throw new Error('Invalid param');
1318
- }
1319
- return this.server._waiter.endJob();
1288
+ data.copy(frame, headerSize);
1320
1289
  }
1321
- // use('/url', {'METHOD /url': [...middlewares], ... } ])
1322
- if (typeof args[0] === 'object' && args[0].constructor === Object) {
1323
- if (method !== '*')
1324
- throw new Error('Invalid router usage');
1325
- for (const [subUrl, subArgs] of Object.entries(args[0])) {
1326
- if (!subUrl.match(/^(\w+ )?\//))
1327
- throw new Error('Url expected');
1328
- await this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1290
+ if (dataLength && options?.mask) {
1291
+ let i = headerSize, h = headerSize - 4;
1292
+ for (let i = 0; i < 4; i++)
1293
+ frame[h + i] = Math.floor(Math.random() * 256);
1294
+ for (let j = 0; j < dataLength; j++, i++) {
1295
+ frame[i] ^= frame[h + (j & 3)];
1329
1296
  }
1330
- return this.server._waiter.endJob();
1331
- }
1332
- // use('/url', ...middleware)
1333
- this._add(method, url, 'next', args.filter((o) => o));
1334
- return this.server._waiter.endJob();
1335
- }
1336
- _middleware(middleware) {
1337
- if (!middleware)
1338
- return;
1339
- const priority = (middleware?.priority || 0) - 1;
1340
- const stack = priority < -1 ? this._stackAfter : this._stack;
1341
- const idx = stack.findIndex(f => 'priority' in f
1342
- && priority >= (f.priority || 0));
1343
- stack.splice(idx < 0 ? stack.length : idx, 0, middleware);
1344
- }
1345
- async _plugin(plugin) {
1346
- let added;
1347
- if (plugin.name) {
1348
- if (this.plugins[plugin.name])
1349
- throw new Error(`Plugin ${plugin.name} already added`);
1350
- this.plugins[plugin.name] = plugin;
1351
- added = plugin.name;
1352
- }
1353
- await plugin.initialise?.();
1354
- if (plugin.handler) {
1355
- const middleware = plugin.handler.bind(plugin);
1356
- middleware.plugin = plugin;
1357
- middleware.priority = plugin.priority;
1358
- this._middleware(middleware);
1359
- }
1360
- if (plugin.routes)
1361
- await this.use(isFunction(plugin.routes) ? await plugin.routes() : plugin.routes);
1362
- if (added)
1363
- this.emit(added);
1364
- }
1365
- async waitPlugin(id) {
1366
- if (!this.plugins[id])
1367
- await this._waiter.wait(id);
1368
- return this.plugins[id];
1369
- }
1370
- /** Add hook */
1371
- hook(url, ...mid) {
1372
- const m = url.match(/^([A-Z]+) (.*)/);
1373
- if (m) {
1374
- const [method, url] = [m[1], m[2]];
1375
- this._add(method, url, 'hook', mid);
1376
- }
1377
- else {
1378
- for (const method of ['*', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
1379
- this._add(method, url, 'hook', mid);
1380
1297
  }
1298
+ return frame;
1381
1299
  }
1382
- /** Check if middleware allready added */
1383
- has(mid) {
1384
- return this._stack.includes(mid) || (mid.name && !!this._stack.find(f => f.name === mid.name)) || false;
1385
- }
1386
- }
1387
- export class MicroServer extends EventEmitter {
1388
- constructor(config) {
1389
- super();
1390
- this._waiter = new Waiter();
1391
- this._methods = {};
1392
- let promise = Promise.resolve();
1393
- this.config = {
1394
- maxBodySize: defaultMaxBodySize,
1395
- methods: defaultMethods,
1396
- ...config,
1397
- root: path.normalize(config.root || process.cwd())
1398
- };
1399
- (config.methods || defaultMethods).split(',').map(s => s.trim()).forEach(m => this._methods[m] = true);
1400
- this.router = new Router(this);
1401
- this.servers = new Set();
1402
- this.sockets = new Set();
1403
- if (config.routes)
1404
- this.use(config.routes);
1405
- for (const key in MicroServer.plugins) {
1406
- if (config[key])
1407
- this.router.use(MicroServer.plugins[key], config[key]);
1408
- }
1409
- if (config.listen) {
1410
- this._waiter.startJob();
1411
- this.listen({
1412
- tls: config.tls,
1413
- listen: config.listen || 8080
1414
- }).then(() => {
1415
- this._waiter.endJob();
1300
+ /** Send data */
1301
+ send(data) {
1302
+ let msgType = typeof data === 'string' ? 1 : 2;
1303
+ if (typeof data === 'string')
1304
+ data = Buffer.from(data, 'utf8');
1305
+ if (this._options.deflate && data.length > 256) {
1306
+ const output = [];
1307
+ const deflate = zlib.createDeflateRaw({
1308
+ windowBits: this._options.maxWindowBits
1309
+ });
1310
+ deflate.write(data);
1311
+ deflate.on('data', (chunk) => output.push(chunk));
1312
+ deflate.flush(() => {
1313
+ if (output.length > 0 && output[output.length - 1].length > 4)
1314
+ output[output.length - 1] = output[output.length - 1].subarray(0, output[output.length - 1].length - 4);
1315
+ this._sendFrame(0xC0 | msgType, Buffer.concat(output));
1416
1316
  });
1417
1317
  }
1418
- this._waiter.wait().then(() => this.emit('ready'));
1419
- }
1420
- /** Add one time listener or call immediatelly for 'ready' */
1421
- once(name, cb) {
1422
- if (name === 'ready' && this.isReady)
1423
- cb();
1424
1318
  else
1425
- super.once(name, cb);
1426
- return this;
1427
- }
1428
- /** Add listener and call immediatelly for 'ready' */
1429
- on(name, cb) {
1430
- if (name === 'ready' && this.isReady)
1431
- cb();
1432
- super.on(name, cb);
1433
- return this;
1434
- }
1435
- get isReady() {
1436
- return !this._waiter.isBusy();
1319
+ return this._sendFrame(0x80 | msgType, data);
1437
1320
  }
1438
- async waitReady() {
1439
- if (this.isReady)
1440
- return;
1441
- return this._waiter.wait();
1321
+ _errorHandler(error) {
1322
+ this.emit('error', error);
1323
+ if (this.ready)
1324
+ this.close(error instanceof WebSocketError && error.statusCode || 1002);
1325
+ else
1326
+ this._socket.destroy();
1327
+ this.ready = false;
1442
1328
  }
1443
- async waitPlugin(id) {
1444
- await this.router.waitPlugin(id);
1329
+ _headerLength(buffer) {
1330
+ if (this._frame)
1331
+ return 0;
1332
+ if (!buffer || buffer.length < 2)
1333
+ return 2;
1334
+ let hederInfo = buffer[1];
1335
+ return 2 + (hederInfo & 0x80 ? 4 : 0) + ((hederInfo & 0x7F) === 126 ? 2 : 0) + ((hederInfo & 0x7F) === 127 ? 8 : 0);
1445
1336
  }
1446
- /** Listen server, should be used only if config.listen is not set */
1447
- listen(config) {
1448
- const listen = (config?.listen || this.config.listen || 0) + '';
1449
- const handler = config?.handler || this.handler.bind(this);
1450
- const tlsConfig = config ? config.tls : this.config.tls;
1451
- const readFile = (data) => data && (data.indexOf('\n') > 0 ? data : fs.readFileSync(data));
1452
- function tlsOptions() {
1453
- return {
1454
- cert: readFile(tlsConfig?.cert),
1455
- key: readFile(tlsConfig?.key),
1456
- ca: readFile(tlsConfig?.ca)
1457
- };
1458
- }
1459
- function tlsOptionsReload(srv) {
1460
- if (tlsConfig?.cert && tlsConfig.cert.indexOf('\n') < 0) {
1461
- let debounce;
1462
- fs.watch(tlsConfig.cert, () => {
1463
- clearTimeout(debounce);
1464
- debounce = setTimeout(() => {
1465
- debounce = undefined;
1466
- srv.setSecureContext(tlsOptions());
1467
- }, 2000);
1468
- });
1337
+ _dataHandler(data) {
1338
+ while (data.length) {
1339
+ let frame = this._frame;
1340
+ if (!frame) {
1341
+ let lastBuffer = this._buffers[this._buffers.length - 1];
1342
+ this._buffers[this._buffers.length - 1] = lastBuffer = Buffer.concat([lastBuffer, data]);
1343
+ let headerLength = this._headerLength(lastBuffer);
1344
+ if (lastBuffer.length < headerLength)
1345
+ return;
1346
+ const headerBits = lastBuffer[0];
1347
+ const lengthBits = lastBuffer[1] & 0x7F;
1348
+ this._buffers.pop();
1349
+ data = lastBuffer.subarray(headerLength);
1350
+ // parse header
1351
+ frame = this._frame = {
1352
+ fin: (headerBits & 0x80) !== 0,
1353
+ rsv1: (headerBits & 0x40) !== 0,
1354
+ opcode: headerBits & 0x0F,
1355
+ mask: (lastBuffer[1] & 0x80) ? lastBuffer.subarray(headerLength - 4, headerLength) : EMPTY_BUFFER,
1356
+ length: lengthBits === 126 ? lastBuffer.readUInt16BE(2) : lengthBits === 127 ? lastBuffer.readBigUInt64BE(2) : lengthBits,
1357
+ lengthReceived: 0,
1358
+ index: this._buffers.length
1359
+ };
1360
+ }
1361
+ let toRead = frame.length - frame.lengthReceived;
1362
+ if (toRead > data.length)
1363
+ toRead = data.length;
1364
+ if (this._options.maxPayload && this._options.maxPayload < this._buffersLength + frame.length) {
1365
+ this._errorHandler(new WebSocketError('Payload too big', 1009));
1366
+ return;
1469
1367
  }
1470
- }
1471
- const reg = /^((?<proto>\w+):\/\/)?(?<host>(\[[^\]]+\]|[a-z][^:,]+|\d+\.\d+\.\d+\.\d+))?:?(?<port>\d+)?/;
1472
- listen.split(',').forEach(listen => {
1473
- this._waiter.startJob('listen');
1474
- let { proto, host, port } = reg.exec(listen)?.groups || {};
1475
- let srv;
1476
- switch (proto) {
1477
- case 'tcp':
1478
- if (!config?.handler)
1479
- throw new Error('Handler is required for tcp');
1480
- srv = net.createServer(handler);
1481
- break;
1482
- case 'tls':
1483
- if (!config?.handler)
1484
- throw new Error('Handler is required for tls');
1485
- srv = tls.createServer(tlsOptions(), handler);
1486
- tlsOptionsReload(srv);
1487
- break;
1488
- case 'https':
1489
- port = port || '443';
1490
- srv = https.createServer(tlsOptions(), handler);
1491
- tlsOptionsReload(srv);
1492
- break;
1493
- default:
1494
- port = port || '80';
1495
- srv = http.createServer(handler);
1496
- break;
1368
+ // unmask
1369
+ for (let i = 0, j = frame.lengthReceived; i < toRead; i++, j++)
1370
+ data[i] ^= frame.mask[j & 3];
1371
+ frame.lengthReceived += toRead;
1372
+ if (frame.lengthReceived < frame.length) {
1373
+ this._buffers.push(data);
1374
+ return;
1497
1375
  }
1498
- this.servers.add(srv);
1499
- if (port === '0') // skip listening
1500
- this._waiter.endJob('listen');
1501
- else {
1502
- srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
1503
- const addr = srv.address();
1504
- this.emit('listen', addr.port, addr.address, srv);
1505
- srv._ready = true;
1506
- this._waiter.endJob('listen');
1507
- });
1376
+ this._buffers.push(data.subarray(0, toRead));
1377
+ this._buffersLength += toRead;
1378
+ data = data.subarray(toRead);
1379
+ if (frame.opcode >= 8) {
1380
+ const message = Buffer.concat(this._buffers.splice(frame.index));
1381
+ switch (frame.opcode) {
1382
+ case 8:
1383
+ if (!frame.length)
1384
+ this.emit('close');
1385
+ else {
1386
+ const code = message.readInt16BE(0);
1387
+ if (frame.length === 2)
1388
+ this.emit('close', code);
1389
+ else
1390
+ this.emit('close', code, message.subarray(2));
1391
+ }
1392
+ this._socket.destroy();
1393
+ return;
1394
+ case 9:
1395
+ if (message.length)
1396
+ this.emit('ping', message);
1397
+ else
1398
+ this.emit('ping');
1399
+ if (this._options.autoPong)
1400
+ this.pong(message);
1401
+ break;
1402
+ case 10:
1403
+ if (message.length)
1404
+ this.emit('pong', message);
1405
+ else
1406
+ this.emit('pong');
1407
+ break;
1408
+ default:
1409
+ return this._errorHandler(new WebSocketError('Invalid WebSocket frame'));
1410
+ }
1508
1411
  }
1509
- srv.on('error', err => {
1510
- srv.close();
1511
- this.servers.delete(srv);
1512
- if (!srv._ready)
1513
- this._waiter.endJob('listen');
1514
- this.emit('error', err);
1515
- });
1516
- srv.on('connection', s => {
1517
- this.sockets.add(s);
1518
- s.once('close', () => this.sockets.delete(s));
1519
- });
1520
- srv.on('upgrade', this.handlerUpgrade.bind(this));
1521
- });
1522
- return this._waiter.wait('listen');
1412
+ else if (frame.fin) {
1413
+ if (!frame.opcode)
1414
+ return this._errorHandler(new WebSocketError('Invalid WebSocket frame'));
1415
+ if (this._options.deflate && frame.rsv1) {
1416
+ const output = [];
1417
+ const inflate = zlib.createInflateRaw({
1418
+ windowBits: this._options.maxWindowBits
1419
+ });
1420
+ inflate.on('data', (chunk) => output.push(chunk));
1421
+ inflate.on('error', (err) => this._errorHandler(err));
1422
+ for (const buffer of this._buffers)
1423
+ inflate.write(buffer);
1424
+ inflate.write(DEFLATE_TRAILER);
1425
+ inflate.flush(() => {
1426
+ if (this.ready) {
1427
+ const message = Buffer.concat(output);
1428
+ this.emit('message', frame.opcode === 1 ? message.toString('utf8') : message);
1429
+ }
1430
+ });
1431
+ }
1432
+ else {
1433
+ const message = Buffer.concat(this._buffers);
1434
+ this.emit('message', frame.opcode === 1 ? message.toString('utf8') : message);
1435
+ }
1436
+ this._buffers = [];
1437
+ this._buffersLength = 0;
1438
+ }
1439
+ this._frame = undefined;
1440
+ this._buffers.push(EMPTY_BUFFER);
1441
+ }
1523
1442
  }
1524
- /** Add middleware, routes, etc.. see {router.use} */
1525
- use(...args) {
1526
- return this.router.use(...args);
1443
+ _abort(message, code, headers) {
1444
+ code = code || 400;
1445
+ message = message || http.STATUS_CODES[code] || 'Closed';
1446
+ headers = [
1447
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
1448
+ 'Connection: close',
1449
+ 'Content-Type: ' + message.startsWith('<') ? 'text/html' : 'text/plain',
1450
+ `Content-Length: ${Buffer.byteLength(message)}`,
1451
+ '',
1452
+ message
1453
+ ];
1454
+ this._socket.once('finish', () => {
1455
+ this._socket.destroy();
1456
+ this.emit('close');
1457
+ });
1458
+ this._socket.end(headers.join('\r\n'));
1459
+ this.emit('error', new Error(message));
1527
1460
  }
1528
- /** Default server handler */
1529
- handler(req, res) {
1530
- this.requestInit(req, res);
1531
- // limit input data size
1532
- if (parseInt(req.headers['content-length'] || '-1') > (this.config.maxBodySize || defaultMaxBodySize)) {
1533
- req.pause();
1534
- res.error(413);
1535
- return;
1536
- }
1537
- this.handlerInit(req, res, () => this.handlerLast(req, res));
1461
+ /** Send ping frame */
1462
+ ping(buffer) {
1463
+ this._sendFrame(0x89, buffer || EMPTY_BUFFER);
1538
1464
  }
1539
- requestInit(req, res) {
1540
- Object.setPrototypeOf(req, ServerRequest.prototype);
1541
- req._init(this.router);
1542
- if (res) {
1543
- Object.setPrototypeOf(res, ServerResponse.prototype);
1544
- res._init(this.router);
1545
- }
1465
+ /** Send pong frame */
1466
+ pong(buffer) {
1467
+ this._sendFrame(0x8A, buffer || EMPTY_BUFFER);
1546
1468
  }
1547
- /** Preprocess request, used by {MicroServer.handler} */
1548
- handlerInit(req, res, next) {
1549
- let cors = this.config.cors;
1550
- if (cors && req.headers.origin) {
1551
- if (cors === true)
1552
- cors = '*';
1553
- if (typeof cors === 'string')
1554
- cors = { origin: cors, headers: 'Content-Type', credentials: true };
1555
- if (cors.origin)
1556
- res.setHeader('Access-Control-Allow-Origin', cors.origin);
1557
- if (cors.headers)
1558
- res.setHeader('Access-Control-Allow-Headers', cors.headers);
1559
- if (cors.credentials)
1560
- res.setHeader('Access-Control-Allow-Credentials', 'true');
1561
- if (cors.expose)
1562
- res.setHeader('Access-Control-Expose-Headers', cors.expose);
1563
- if (cors.maxAge)
1564
- res.setHeader('Access-Control-Max-Age', cors.maxAge);
1565
- }
1566
- if (req.method === 'OPTIONS') {
1567
- res.statusCode = 204;
1568
- res.setHeader('Allow', this.config.methods || defaultMethods);
1569
- res.end();
1570
- return;
1571
- }
1572
- if (!req.method || !this._methods[req.method]) {
1573
- res.statusCode = 405;
1574
- res.setHeader('Allow', this.config.methods || defaultMethods);
1575
- res.end();
1469
+ _sendFrame(opcode, data, cb) {
1470
+ if (!this.ready)
1576
1471
  return;
1472
+ const dataLength = data.length;
1473
+ const headerSize = 2 + (dataLength < 126 ? 0 : dataLength < 65536 ? 2 : 8);
1474
+ const frame = Buffer.allocUnsafe(headerSize + (dataLength < 4096 ? dataLength : 0));
1475
+ frame[0] = opcode;
1476
+ frame[1] = dataLength > 65535 ? 127 : dataLength > 125 ? 126 : dataLength;
1477
+ if (dataLength > 65535)
1478
+ frame.writeBigUInt64BE(dataLength, 2);
1479
+ else if (dataLength > 125)
1480
+ frame.writeUInt16BE(dataLength, 2);
1481
+ if (dataLength && frame.length > dataLength) {
1482
+ data.copy(frame, headerSize);
1483
+ this._socket.write(frame, cb);
1577
1484
  }
1578
- if (req.method === 'HEAD') {
1579
- req.method = 'GET';
1580
- res.headersOnly = true;
1581
- }
1582
- return req.bodyDecode(res, this.config, () => {
1583
- if ((req.rawBodySize && req.rawBody[0] && (req.rawBody[0][0] === 91 || req.rawBody[0][0] === 123))
1584
- || req.headers.accept?.includes?.('json') || req.headers['content-type']?.includes?.('json'))
1585
- res.isJson = true;
1586
- return req.router.handler(req, res, next);
1587
- });
1588
- }
1589
- /** Last request handler */
1590
- handlerLast(req, res, next) {
1591
- if (res.headersSent)
1592
- return;
1593
- if (!next)
1594
- next = () => res.error(404);
1595
- return next();
1485
+ else
1486
+ this._socket.write(frame, () => this._socket.write(data, cb));
1596
1487
  }
1597
- /** Default upgrade handler, used for WebSockets */
1598
- handlerUpgrade(req, socket, head) {
1599
- this.requestInit(req);
1600
- //req.headers = head
1601
- //(req as any)['head'] = head
1488
+ on(event, listener) { return super.on(event, listener); }
1489
+ addListener(event, listener) { return super.addListener(event, listener); }
1490
+ once(event, listener) { return super.once(event, listener); }
1491
+ off(event, listener) { return super.off(event, listener); }
1492
+ removeListener(event, listener) { return super.removeListener(event, listener); }
1493
+ }
1494
+ export class WebSocketPlugin extends Plugin {
1495
+ name = 'websocket';
1496
+ _handler;
1497
+ constructor(options, server) {
1498
+ super();
1499
+ if (!server)
1500
+ throw new Error('Server instance is required');
1501
+ this._handler = this.upgradeHandler.bind(this, server);
1502
+ server.servers?.forEach(srv => this.addUpgradeHandler(srv));
1503
+ server.on('listen', (port, address, srv) => this.addUpgradeHandler(srv));
1504
+ }
1505
+ addUpgradeHandler(srv) {
1506
+ if (!srv.listeners('upgrade').includes(this._handler))
1507
+ srv.on('upgrade', this._handler);
1508
+ }
1509
+ upgradeHandler(server, req, socket, head) {
1510
+ ServerRequest.extend(req, server);
1602
1511
  const host = req.headers.host || '';
1603
- const router = this.vhosts?.[host] || this.router;
1512
+ const vhostPlugin = server.getPlugin('vhost');
1513
+ const vserver = vhostPlugin?.vhosts?.[host] || server;
1514
+ req.method = 'WEBSOCKET';
1604
1515
  const res = {
1516
+ req,
1605
1517
  get headersSent() {
1606
1518
  return socket.bytesWritten > 0;
1607
1519
  },
@@ -1609,181 +1521,180 @@ export class MicroServer extends EventEmitter {
1609
1521
  socket,
1610
1522
  server,
1611
1523
  write(data) {
1612
- if (res.headersSent)
1613
- throw new Error('Headers already sent');
1614
- let code = res.statusCode || 403;
1615
- if (code < 400) {
1616
- data = 'Invalid WebSocket response';
1617
- console.error(data);
1618
- code = 500;
1619
- }
1620
- if (!data)
1621
- data = http.STATUS_CODES[code] || '';
1622
- const headers = [
1623
- `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
1624
- 'Connection: close',
1625
- 'Content-Type: text/html',
1626
- `Content-Length: ${Buffer.byteLength(data)}`,
1627
- '',
1628
- data
1629
- ];
1630
- socket.write(headers.join('\r\n'), () => { socket.destroy(); });
1631
- },
1632
- error(code) {
1633
- res.statusCode = code || 403;
1634
- res.write();
1635
- },
1636
- end(data) {
1637
- res.write(data);
1638
- },
1639
- send(data) {
1640
- res.write(data);
1641
- },
1642
- setHeader() { }
1643
- };
1644
- router.handler(req, res, () => res.error(404), 'WEBSOCKET');
1645
- }
1646
- /** Close server instance */
1647
- async close() {
1648
- return new Promise((resolve) => {
1649
- let count = 0;
1650
- function done() {
1651
- count--;
1652
- if (!count)
1653
- setTimeout(() => resolve(), 2);
1654
- }
1655
- for (const s of this.servers) {
1656
- count++;
1657
- s.once('close', done);
1658
- s.close();
1659
- }
1660
- this.servers.clear();
1661
- for (const s of this.sockets) {
1662
- count++;
1663
- s.once('close', done);
1664
- s.destroy();
1665
- }
1666
- this.sockets.clear();
1667
- }).then(() => {
1668
- this.emit('close');
1669
- });
1670
- }
1671
- /** Add route, alias to `server.router.use(url, ...args)` */
1672
- all(url, ...args) {
1673
- this.router.use(url, ...args);
1674
- return this;
1675
- }
1676
- /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
1677
- get(url, ...args) {
1678
- this.router.use('GET ' + url, ...args);
1679
- return this;
1680
- }
1681
- /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
1682
- post(url, ...args) {
1683
- this.router.use('POST ' + url, ...args);
1684
- return this;
1685
- }
1686
- /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
1687
- put(url, ...args) {
1688
- this.router.use('PUT ' + url, ...args);
1689
- return this;
1690
- }
1691
- /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
1692
- patch(url, ...args) {
1693
- this.router.use('PATCH ' + url, ...args);
1694
- return this;
1695
- }
1696
- /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
1697
- delete(url, ...args) {
1698
- this.router.use('DELETE ' + url, ...args);
1699
- return this;
1700
- }
1701
- /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
1702
- websocket(url, ...args) {
1703
- this.router.use('WEBSOCKET ' + url, ...args);
1704
- return this;
1524
+ if (res.headersSent)
1525
+ throw new Error('Headers already sent');
1526
+ let code = res.statusCode || 403;
1527
+ if (code < 400) {
1528
+ data = 'Invalid WebSocket response';
1529
+ server.emit('error', new Error(data));
1530
+ code = 500;
1531
+ }
1532
+ if (!data)
1533
+ data = http.STATUS_CODES[code] || '';
1534
+ const headers = [
1535
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
1536
+ 'Connection: close',
1537
+ 'Content-Type: text/html',
1538
+ `Content-Length: ${Buffer.byteLength(data)}`,
1539
+ '',
1540
+ data
1541
+ ];
1542
+ socket.write(headers.join('\r\n'), () => { socket.destroy(); });
1543
+ },
1544
+ error(code) {
1545
+ res.statusCode = code || 403;
1546
+ res.write();
1547
+ },
1548
+ end(data) {
1549
+ res.write(data);
1550
+ },
1551
+ send(data) {
1552
+ res.write(data);
1553
+ },
1554
+ getHeader() { },
1555
+ setHeader() { }
1556
+ };
1557
+ vserver.handler(req, res);
1558
+ //vserver.handlerRouter(req, res as unknown as ServerResponse, () => res.error(404))
1705
1559
  }
1706
- /** Add router hook, alias to `server.router.hook(url, ...args)` */
1707
- hook(url, ...args) {
1708
- this.router.hook(url, ...args.filter((o) => o));
1709
- return this;
1560
+ static create(req, options) {
1561
+ return new WebSocket(req, options);
1710
1562
  }
1711
1563
  }
1712
- MicroServer.plugins = {};
1564
+ // #endregion WebSocket
1565
+ // #region TrustProxyPlugin
1713
1566
  /** Trust proxy plugin, adds `req.ip` and `req.localip` */
1714
- class TrustProxyPlugin extends Plugin {
1715
- constructor(router, options) {
1716
- super(router);
1717
- this.priority = 110;
1718
- this.name = 'trustProxy';
1719
- this.trustProxy = [];
1720
- this.trustProxy = options || [];
1567
+ export class TrustProxyPlugin extends Plugin {
1568
+ priority = -60;
1569
+ name = 'trustproxy';
1570
+ _trustProxy = [];
1571
+ constructor(options) {
1572
+ super();
1573
+ this._trustProxy = options || [];
1721
1574
  }
1722
1575
  isLocal(ip) {
1723
1576
  return !!ip.match(/^(127\.|10\.|192\.168\.|172\.16\.|fe80|fc|fd|::)/);
1724
1577
  }
1725
1578
  handler(req, res, next) {
1726
- req.ip = req.socket.remoteAddress || '::1';
1727
1579
  req.localip = this.isLocal(req.ip);
1728
1580
  const xip = req.headers['x-real-ip'] || req.headers['x-forwarded-for'];
1729
1581
  if (xip) {
1730
- if (!this.trustProxy.includes(req.ip))
1582
+ if (!this._trustProxy.includes(req.ip))
1731
1583
  return res.error(400);
1732
- if (req.headers['x-forwarded-proto'] === 'https') {
1733
- req.protocol = 'https';
1584
+ if (req.headers['x-forwarded-proto'] === 'https')
1734
1585
  req.secure = true;
1735
- }
1736
1586
  req.ip = Array.isArray(xip) ? xip[0] : xip;
1737
1587
  req.localip = this.isLocal(req.ip);
1738
1588
  }
1739
1589
  return next();
1740
1590
  }
1741
1591
  }
1742
- MicroServer.plugins.trustProxy = TrustProxyPlugin;
1592
+ // #endregion TrustProxyPlugin
1593
+ // #region VHostPlugin
1743
1594
  /** Virtual host plugin */
1744
- class VHostPlugin extends Plugin {
1745
- constructor(router, options) {
1746
- super(router);
1747
- this.priority = 100;
1748
- const server = router.server;
1749
- if (!server.vhosts) {
1750
- server.vhosts = {};
1595
+ export class VHostPlugin extends Plugin {
1596
+ priority = -10;
1597
+ vhosts;
1598
+ constructor(options, server) {
1599
+ super();
1600
+ if (!server)
1601
+ throw new Error('Server instance is required');
1602
+ const vhostPlugin = server.getPlugin('vhost');
1603
+ let vhosts = vhostPlugin?.vhosts;
1604
+ if (!vhosts) {
1605
+ vhosts = this.vhosts = {};
1751
1606
  this.name = 'vhost';
1752
1607
  }
1753
- else
1608
+ else {
1754
1609
  this.handler = undefined;
1610
+ }
1755
1611
  for (const host in options) {
1756
- if (!server.vhosts[host])
1757
- server.vhosts[host] = new Router(server);
1758
- server.vhosts[host].use(options[host]);
1612
+ const v = options[host];
1613
+ if (v instanceof MicroServer) {
1614
+ vhosts[host] = v;
1615
+ continue;
1616
+ }
1617
+ if (!vhosts[host])
1618
+ vhosts[host] = new MicroServer({});
1619
+ vhosts[host].use(options[host]);
1759
1620
  }
1621
+ if (this.vhosts)
1622
+ server.on('close', () => {
1623
+ for (const host in this.vhosts)
1624
+ this.vhosts[host].emit('close');
1625
+ });
1760
1626
  }
1761
1627
  handler(req, res, next) {
1762
1628
  const host = req.headers.host || '';
1763
- const router = req.router.server.vhosts?.[host];
1764
- if (router) {
1765
- req.router = res.router = router;
1766
- router.handler(req, res, () => req.router.server.handlerLast(req, res));
1629
+ const server = this.vhosts?.[host];
1630
+ if (server) {
1631
+ req.server = server;
1632
+ server.handler(req, res);
1767
1633
  }
1768
1634
  else
1769
1635
  next();
1770
1636
  }
1771
1637
  }
1772
- MicroServer.plugins.vhost = VHostPlugin;
1773
1638
  const etagPrefix = crypto.randomBytes(4).toString('hex');
1639
+ //TODO: add precompressed .gz support
1640
+ //TODO: add unknown file extension handler
1774
1641
  /**
1775
1642
  * Static files middleware plugin
1776
1643
  * Usage: server.use('static', '/public')
1777
1644
  * Usage: server.use('static', { root: 'public', path: '/static' })
1778
1645
  */
1779
- class StaticPlugin extends Plugin {
1780
- constructor(router, options) {
1781
- super(router);
1646
+ export class StaticFilesPlugin extends Plugin {
1647
+ priority = 110;
1648
+ /** Default mime types */
1649
+ static mimeTypes = {
1650
+ '.ico': 'image/x-icon',
1651
+ '.htm': 'text/html',
1652
+ '.html': 'text/html',
1653
+ '.txt': 'text/plain',
1654
+ '.js': 'text/javascript',
1655
+ '.json': 'application/json',
1656
+ '.css': 'text/css',
1657
+ '.png': 'image/png',
1658
+ '.jpg': 'image/jpeg',
1659
+ '.svg': 'image/svg+xml',
1660
+ '.mp3': 'audio/mpeg',
1661
+ '.ogg': 'audio/ogg',
1662
+ '.mp4': 'video/mp4',
1663
+ '.pdf': 'application/pdf',
1664
+ '.woff': 'application/x-font-woff',
1665
+ '.woff2': 'application/x-font-woff2',
1666
+ '.ttf': 'application/x-font-ttf',
1667
+ '.gz': 'application/gzip',
1668
+ '.zip': 'application/zip',
1669
+ '.tgz': 'application/gzip',
1670
+ };
1671
+ /** Custom mime types */
1672
+ mimeTypes;
1673
+ /** File extension handlers */
1674
+ handlers;
1675
+ /** Files root directory */
1676
+ root;
1677
+ /** Ignore prefixes */
1678
+ ignore;
1679
+ /** Index file. default: 'index.html' */
1680
+ index;
1681
+ /** Update Last-Modified header. default: true */
1682
+ lastModified;
1683
+ /** Update ETag header. default: true */
1684
+ etag;
1685
+ /** Max file age in seconds (default: 31536000) */
1686
+ maxAge;
1687
+ prefix;
1688
+ constructor(options, server) {
1689
+ super();
1782
1690
  if (!options)
1783
1691
  options = {};
1784
1692
  if (typeof options === 'string')
1785
- options = { path: options };
1786
- this.mimeTypes = options.mimeTypes ? { ...StaticPlugin.mimeTypes, ...options.mimeTypes } : Object.freeze(StaticPlugin.mimeTypes);
1693
+ options = { root: options };
1694
+ // allow multiple instances
1695
+ if (server && !server.getPlugin('static'))
1696
+ this.name = 'static';
1697
+ this.mimeTypes = options.mimeTypes ? { ...StaticFilesPlugin.mimeTypes, ...options.mimeTypes } : Object.freeze(StaticFilesPlugin.mimeTypes);
1787
1698
  this.root = path.resolve((options.root || options?.path || 'public').replace(/^\//, '')) + path.sep;
1788
1699
  this.ignore = (options.ignore || []).map((p) => path.normalize(path.join(this.root, p)) + path.sep);
1789
1700
  this.index = options.index || 'index.html';
@@ -1791,13 +1702,26 @@ class StaticPlugin extends Plugin {
1791
1702
  this.lastModified = options.lastModified !== false;
1792
1703
  this.etag = options.etag !== false;
1793
1704
  this.maxAge = options.maxAge;
1794
- router.use('GET /' + (options.path?.replace(/^[.\/]*/, '') || '').replace(/\/$/, '') + '/:path*', this.staticHandler.bind(this));
1705
+ this.prefix = ('/' + (options.path?.replace(/^[.\/]*/, '') || '').replace(/\/$/, '')).replace(/\/$/, '');
1795
1706
  }
1796
1707
  /** Default static files handler */
1797
- staticHandler(req, res, next) {
1708
+ handler(req, res, next) {
1709
+ res.file = (path) => {
1710
+ this.serveFile(req, res, typeof path === 'object' ? path : {
1711
+ path,
1712
+ mimeType: StaticFilesPlugin.mimeTypes[extname(path)] || 'application/octet-stream'
1713
+ });
1714
+ };
1798
1715
  if (req.method !== 'GET')
1799
1716
  return next();
1800
- let filename = path.normalize(path.join(this.root, (req.params && req.params.path) || req.pathname));
1717
+ if (!('path' in req.params)) { // global handler
1718
+ if (req.path.startsWith(this.prefix) && (req.path === this.prefix || req.path[this.prefix.length] === '/')) {
1719
+ req.params.path = req.path.slice(this.prefix.length + 1);
1720
+ }
1721
+ else
1722
+ return next();
1723
+ }
1724
+ let filename = path.normalize(path.join(this.root, req.params.path));
1801
1725
  if (!filename.startsWith(this.root)) // check root access
1802
1726
  return next();
1803
1727
  const firstch = basename(filename)[0];
@@ -1822,15 +1746,15 @@ class StaticPlugin extends Plugin {
1822
1746
  req.filename = filename;
1823
1747
  return handler.call(this, req, res, next);
1824
1748
  }
1825
- StaticPlugin.serveFile(req, res, {
1749
+ this.serveFile(req, res, {
1826
1750
  path: filename,
1827
1751
  mimeType,
1828
1752
  stats
1829
1753
  });
1830
1754
  });
1831
1755
  }
1832
- static serveFile(req, res, options) {
1833
- const filePath = options.root ? path.join(options.root, options.path) : options.path;
1756
+ serveFile(req, res, options) {
1757
+ const filePath = path.isAbsolute(options.path) ? options.path : path.join(options.root || this.root, options.path);
1834
1758
  const statRes = (err, stats) => {
1835
1759
  if (err)
1836
1760
  return res.error(err);
@@ -1843,7 +1767,7 @@ class StaticPlugin extends Plugin {
1843
1767
  res.setHeader('Content-Type', this.mimeTypes[path.extname(options.path)] || 'application/octet-stream');
1844
1768
  }
1845
1769
  if (options.filename)
1846
- res.setHeader('Content-Disposition', 'attachment; filename="' + options.filename + '"');
1770
+ res.setHeader('Content-Disposition', 'attachment; filename="' + (options.filename === true ? path.basename(options.path) : options.filename) + '"');
1847
1771
  if (options.lastModified !== false)
1848
1772
  res.setHeader('Last-Modified', stats.mtime.toUTCString());
1849
1773
  res.setHeader('Content-Length', stats.size);
@@ -1879,33 +1803,38 @@ class StaticPlugin extends Plugin {
1879
1803
  statRes(null, options.stats);
1880
1804
  }
1881
1805
  }
1882
- /** Default mime types */
1883
- StaticPlugin.mimeTypes = {
1884
- '.ico': 'image/x-icon',
1885
- '.htm': 'text/html',
1886
- '.html': 'text/html',
1887
- '.txt': 'text/plain',
1888
- '.js': 'text/javascript',
1889
- '.json': 'application/json',
1890
- '.css': 'text/css',
1891
- '.png': 'image/png',
1892
- '.jpg': 'image/jpeg',
1893
- '.svg': 'image/svg+xml',
1894
- '.mp3': 'audio/mpeg',
1895
- '.ogg': 'audio/ogg',
1896
- '.mp4': 'video/mp4',
1897
- '.pdf': 'application/pdf',
1898
- '.woff': 'application/x-font-woff',
1899
- '.woff2': 'application/x-font-woff2',
1900
- '.ttf': 'application/x-font-ttf',
1901
- '.gz': 'application/gzip',
1902
- '.zip': 'application/zip',
1903
- '.tgz': 'application/gzip',
1904
- };
1905
- MicroServer.plugins.static = StaticPlugin;
1906
1806
  export class ProxyPlugin extends Plugin {
1907
- constructor(router, options) {
1908
- super(router);
1807
+ priority = 120;
1808
+ name = 'proxy';
1809
+ /** Default valid headers */
1810
+ static validHeaders = {
1811
+ authorization: true,
1812
+ accept: true,
1813
+ 'accept-encoding': true,
1814
+ 'accept-language': true,
1815
+ 'cache-control': true,
1816
+ cookie: true,
1817
+ 'content-type': true,
1818
+ 'content-length': true,
1819
+ host: true,
1820
+ referer: true,
1821
+ 'if-match': true,
1822
+ 'if-none-match': true,
1823
+ 'if-modified-since': true,
1824
+ 'user-agent': true,
1825
+ date: true,
1826
+ range: true
1827
+ };
1828
+ /** Current valid headers */
1829
+ validHeaders;
1830
+ /** Override headers to forward to remote */
1831
+ headers;
1832
+ /** Remote url */
1833
+ remoteUrl;
1834
+ /** Match regex filter */
1835
+ regex;
1836
+ constructor(options, server) {
1837
+ super();
1909
1838
  if (typeof options !== 'object')
1910
1839
  options = { remote: options };
1911
1840
  if (!options.remote)
@@ -1914,9 +1843,9 @@ export class ProxyPlugin extends Plugin {
1914
1843
  this.regex = options.match ? new RegExp(options.match) : undefined;
1915
1844
  this.headers = options.headers;
1916
1845
  this.validHeaders = { ...ProxyPlugin.validHeaders, ...options?.validHeaders };
1917
- if (options.path && options.path !== '/') {
1846
+ if (options.path && options.path !== '/' && server) {
1918
1847
  this.handler = undefined;
1919
- router.use(options.path + '/:path*', this.proxyHandler.bind(this));
1848
+ server.use(options.path + '/:path*', this.proxyHandler.bind(this));
1920
1849
  }
1921
1850
  }
1922
1851
  /** Default proxy handler */
@@ -1986,28 +1915,14 @@ export class ProxyPlugin extends Plugin {
1986
1915
  return this.proxyHandler(req, res, next);
1987
1916
  }
1988
1917
  }
1989
- /** Default valid headers */
1990
- ProxyPlugin.validHeaders = {
1991
- authorization: true,
1992
- accept: true,
1993
- 'accept-encoding': true,
1994
- 'accept-language': true,
1995
- 'cache-control': true,
1996
- cookie: true,
1997
- 'content-type': true,
1998
- 'content-length': true,
1999
- host: true,
2000
- referer: true,
2001
- 'if-match': true,
2002
- 'if-none-match': true,
2003
- 'if-modified-since': true,
2004
- 'user-agent': true,
2005
- date: true,
2006
- range: true
2007
- };
2008
- MicroServer.plugins.proxy = ProxyPlugin;
2009
1918
  /** Authentication class */
2010
1919
  export class Auth {
1920
+ /** Server request */
1921
+ req;
1922
+ /** Server response */
1923
+ res;
1924
+ /** Authentication options */
1925
+ options;
2011
1926
  constructor(options, req, res) {
2012
1927
  this.options = options;
2013
1928
  this.req = req;
@@ -2147,6 +2062,8 @@ export class Auth {
2147
2062
  }
2148
2063
  /** Get hashed string from user and password */
2149
2064
  static password(usr, psw, salt) {
2065
+ if (psw.length < 128)
2066
+ psw = crypto.createHash('sha512').update(psw).digest('hex');
2150
2067
  if (usr)
2151
2068
  psw = crypto.createHash('sha512').update(usr + '|' + psw).digest('hex');
2152
2069
  if (salt) {
@@ -2257,12 +2174,14 @@ async function login (username, password, salt) {
2257
2174
  }
2258
2175
  */
2259
2176
  /** Authentication plugin */
2260
- class AuthPlugin extends Plugin {
2261
- constructor(router, options) {
2262
- super(router);
2263
- this.name = 'auth';
2264
- if (router.auth)
2265
- throw new Error('Auth plugin already initialized');
2177
+ export class AuthPlugin extends Plugin {
2178
+ priority = 50;
2179
+ name = 'auth';
2180
+ options;
2181
+ constructor(options, server) {
2182
+ super();
2183
+ if (!server)
2184
+ throw new Error('Server instance is required');
2266
2185
  this.options = {
2267
2186
  mode: 'cookie',
2268
2187
  token: defaultToken,
@@ -2288,11 +2207,11 @@ class AuthPlugin extends Plugin {
2288
2207
  this.options.users = async (usrid, psw) => {
2289
2208
  const users = this.options.users;
2290
2209
  const usr = users?.[usrid];
2291
- if (usr && (psw === undefined || router.auth?.checkPassword(usrid, psw, usr.password || '')))
2210
+ if (usr && (psw === undefined || server.auth?.checkPassword(usrid, psw, usr.password || '')))
2292
2211
  return usr;
2293
2212
  };
2294
2213
  }
2295
- router.auth = new Auth(this.options);
2214
+ server.auth = new Auth(this.options);
2296
2215
  }
2297
2216
  /** Authentication middleware */
2298
2217
  async handler(req, res, next) {
@@ -2370,13 +2289,259 @@ class AuthPlugin extends Plugin {
2370
2289
  return next();
2371
2290
  }
2372
2291
  }
2373
- MicroServer.plugins.auth = AuthPlugin;
2374
- /** Create microserver */
2375
- export function create(config) { return new MicroServer(config); }
2292
+ // #endregion AuthPlugin
2293
+ export class StandardPlugins extends Plugin {
2294
+ constructor(options, server) {
2295
+ super();
2296
+ if (!server)
2297
+ throw new Error('Server instance is required');
2298
+ const config = server.config || {};
2299
+ const use = (plugin, id) => {
2300
+ if (!id)
2301
+ server.use(plugin);
2302
+ else if (id in config)
2303
+ server.use(plugin, config[id]);
2304
+ };
2305
+ use(MethodsPlugin);
2306
+ use(CorsPlugin, 'cors');
2307
+ use(TrustProxyPlugin, 'trustproxy');
2308
+ use(BodyPlugin);
2309
+ use(StaticFilesPlugin, 'static');
2310
+ use(AuthPlugin, 'auth');
2311
+ }
2312
+ }
2313
+ /**
2314
+ * Controller for dynamic routes
2315
+ *
2316
+ * @example
2317
+ * ```js
2318
+ * class MyController extends Controller {
2319
+ * static model = MyModel;
2320
+ * static acl = 'auth';
2321
+ *
2322
+ * static 'acl:index' = '';
2323
+ * static 'url:index' = 'GET /index';
2324
+ * async index (req, res) {
2325
+ * res.send('Hello World')
2326
+ * }
2327
+ *
2328
+ * //function name prefixes translated to HTTP methods:
2329
+ * // all => GET, get => GET, insert => POST, post => POST,
2330
+ * // update => PUT, put => PUT, delete => DELETE,
2331
+ * // modify => PATCH, patch => PATCH,
2332
+ * // websocket => internal WebSocket
2333
+ * // automatic acl will be: class_name + '/' + function_name_prefix
2334
+ * // automatic url will be: method + ' /' + class_name + '/' + function_name_without_prefix
2335
+ *
2336
+ * //static 'acl:allUsers' = 'MyController/all';
2337
+ * //static 'url:allUsers' = 'GET /MyController/Users';
2338
+ * async allUsers () {
2339
+ * return ['usr1', 'usr2', 'usr3']
2340
+ * }
2341
+ *
2342
+ * //static 'acl:getOrder' = 'MyController/get';
2343
+ * //static 'url:getOrder' = 'GET /Users/:id/:id1';
2344
+ * static 'group:getOrder' = 'orders';
2345
+ * static 'model:getOrder' = OrderModel;
2346
+ * async getOrder (id: string, id1: string) {
2347
+ * return {id, extras: id1, type: 'order'}
2348
+ * }
2349
+ *
2350
+ * //static 'acl:insertOrder' = 'MyController/insert';
2351
+ * //static 'url:insertOrder' = 'POST /Users/:id';
2352
+ * static 'model:insertOrder' = OrderModel;
2353
+ * async insertOrder (id: string, id1: string) {
2354
+ * return {id, extras: id1, type: 'order'}
2355
+ * }
2356
+ *
2357
+ * static 'acl:POST /login' = '';
2358
+ * async 'POST /login' () {
2359
+ * return {id, extras: id1, type: 'order'}
2360
+ * }
2361
+ * }
2362
+ * ```
2363
+ */
2364
+ export class Controller {
2365
+ req;
2366
+ res;
2367
+ get model() {
2368
+ return this.req.model;
2369
+ }
2370
+ constructor(req, res) {
2371
+ this.req = req;
2372
+ this.res = res;
2373
+ }
2374
+ /** Generate routes for this controller */
2375
+ static routes() {
2376
+ const routes = [];
2377
+ const prefix = Object.getOwnPropertyDescriptor(this, 'name')?.enumerable ? this.name + '/' : '';
2378
+ // iterate throught decorators
2379
+ Object.getOwnPropertyNames(this.prototype).forEach(key => {
2380
+ if (key === 'constructor' || key.startsWith('_'))
2381
+ return;
2382
+ const func = this.prototype[key];
2383
+ if (typeof func !== 'function')
2384
+ return;
2385
+ const thisStatic = this;
2386
+ let url = thisStatic['url:' + key];
2387
+ let acl = thisStatic['acl:' + key] ?? thisStatic['acl'];
2388
+ const user = thisStatic['user:' + key] ?? thisStatic['user'];
2389
+ const group = thisStatic['group:' + key] ?? thisStatic['group'];
2390
+ const modelName = thisStatic['model:' + key] ?? thisStatic['model'];
2391
+ let method = '';
2392
+ if (!url)
2393
+ key = key.replaceAll('$', '/');
2394
+ if (!url && key.startsWith('/')) {
2395
+ method = '*';
2396
+ url = key;
2397
+ }
2398
+ let keyMatch = !url && key.match(/^(all|get|put|post|patch|insert|update|modify|delete|websocket)[/_]?([\w_/-]*)$/i);
2399
+ if (keyMatch) {
2400
+ method = keyMatch[1];
2401
+ url = '/' + prefix + keyMatch[2];
2402
+ }
2403
+ keyMatch = !url && key.match(/^([*\w]+) (.+)$/);
2404
+ if (keyMatch) {
2405
+ method = keyMatch[1];
2406
+ url = keyMatch[2].startsWith('/') ? keyMatch[2] : ('/' + prefix + keyMatch[1]);
2407
+ }
2408
+ keyMatch = !method && url?.match(/^([*\w]+) (.+)$/);
2409
+ if (keyMatch) {
2410
+ method = keyMatch[1];
2411
+ url = keyMatch[2].startsWith('/') ? keyMatch[2] : ('/' + prefix + keyMatch[2]);
2412
+ }
2413
+ if (!method)
2414
+ return;
2415
+ let autoAcl = method.toLowerCase();
2416
+ switch (autoAcl) {
2417
+ case '*':
2418
+ autoAcl = '';
2419
+ break;
2420
+ case 'post':
2421
+ autoAcl = 'insert';
2422
+ break;
2423
+ case 'put':
2424
+ autoAcl = 'update';
2425
+ break;
2426
+ case 'patch':
2427
+ autoAcl = 'modify';
2428
+ break;
2429
+ }
2430
+ method = method.toUpperCase();
2431
+ switch (method) {
2432
+ case '*':
2433
+ break;
2434
+ case 'GET':
2435
+ case 'POST':
2436
+ case 'PUT':
2437
+ case 'PATCH':
2438
+ case 'DELETE':
2439
+ case 'WEBSOCKET':
2440
+ break;
2441
+ case 'ALL':
2442
+ method = 'GET';
2443
+ break;
2444
+ case 'INSERT':
2445
+ method = 'POST';
2446
+ break;
2447
+ case 'UPDATE':
2448
+ method = 'PUT';
2449
+ break;
2450
+ case 'MODIFY':
2451
+ method = 'PATCH';
2452
+ break;
2453
+ default:
2454
+ throw new Error('Invalid url method for: ' + key);
2455
+ }
2456
+ if (user === undefined && group === undefined && acl === undefined)
2457
+ acl = prefix + autoAcl;
2458
+ // add params if not available in url
2459
+ if (func.length && !url.includes(':')) {
2460
+ let args = ['/:id'];
2461
+ for (let i = 1; i < func.length; i++)
2462
+ args.push('/:id' + i);
2463
+ url += args.join('');
2464
+ }
2465
+ const list = [method + ' ' + url.replace(/\/\//g, '/'), 'response:json'];
2466
+ if (acl)
2467
+ list.push('acl:' + acl);
2468
+ if (user)
2469
+ list.push('user:' + user);
2470
+ if (group)
2471
+ list.push('group:' + group);
2472
+ list.push((req, res) => {
2473
+ res.isJson = true;
2474
+ const obj = new this(req, res);
2475
+ if (modelName) {
2476
+ req.model = modelName instanceof Model ? modelName : Model.models[modelName];
2477
+ if (!obj.model)
2478
+ throw new InvalidData(modelName, 'model');
2479
+ req.model = Model.dynamic(req.model, { controller: obj });
2480
+ }
2481
+ return func.apply(obj, req.paramsList);
2482
+ });
2483
+ routes.push(list);
2484
+ });
2485
+ return routes;
2486
+ }
2487
+ }
2488
+ // #endregion Controller
2489
+ // #region Worker
2490
+ class WorkerJob {
2491
+ _promises = [];
2492
+ _busy = 0;
2493
+ start() {
2494
+ this._busy++;
2495
+ }
2496
+ end() {
2497
+ this._busy--;
2498
+ if (this._busy === 0)
2499
+ for (const resolve of this._promises.splice(0))
2500
+ resolve();
2501
+ }
2502
+ async wait() {
2503
+ if (!this._busy)
2504
+ return;
2505
+ return new Promise(resolve => this._promises.push(resolve));
2506
+ }
2507
+ }
2508
+ class Worker {
2509
+ _id = 0;
2510
+ _jobs = {};
2511
+ isBusy(id) {
2512
+ const job = this._jobs[id || 'ready'];
2513
+ return !!job?._busy;
2514
+ }
2515
+ startJob(id) {
2516
+ let job = this._jobs[id || 'ready'];
2517
+ if (!job)
2518
+ job = this._jobs[id || 'ready'] = new WorkerJob();
2519
+ job.start();
2520
+ }
2521
+ endJob(id) {
2522
+ const job = this._jobs[id || 'ready'];
2523
+ if (job)
2524
+ job.end();
2525
+ }
2526
+ get nextId() {
2527
+ return (++this._id).toString();
2528
+ }
2529
+ async wait(id) {
2530
+ const job = this._jobs[id || 'ready'];
2531
+ if (!job)
2532
+ return;
2533
+ return job.wait();
2534
+ }
2535
+ }
2376
2536
  /** JSON File store */
2377
2537
  export class FileStore {
2538
+ _cache;
2539
+ _dir;
2540
+ _cacheTimeout;
2541
+ _cacheItems;
2542
+ _debounceTimeout;
2543
+ _iter;
2378
2544
  constructor(options) {
2379
- this._queue = Promise.resolve();
2380
2545
  this._cache = {};
2381
2546
  this._dir = options?.dir || 'data';
2382
2547
  this._cacheTimeout = options?.cacheTimeout || 2000;
@@ -2398,6 +2563,7 @@ export class FileStore {
2398
2563
  }
2399
2564
  }
2400
2565
  }
2566
+ _queue = Promise.resolve();
2401
2567
  async _sync(cb) {
2402
2568
  let r;
2403
2569
  let p = new Promise(resolve => r = resolve);
@@ -2564,6 +2730,8 @@ export class FileStore {
2564
2730
  return new Proxy(data, handler);
2565
2731
  }
2566
2732
  }
2733
+ // #endregion FileStore
2734
+ // #region Model
2567
2735
  let globalObjectId = crypto.randomBytes(8);
2568
2736
  function newObjectId() {
2569
2737
  for (let i = 7; i >= 0; i--)
@@ -2572,9 +2740,9 @@ function newObjectId() {
2572
2740
  return (new Date().getTime() / 1000 | 0).toString(16) + globalObjectId.toString('hex');
2573
2741
  }
2574
2742
  class ModelCollectionsInternal {
2575
- constructor() {
2576
- this._wait = new Promise(resolve => this._ready = resolve);
2577
- }
2743
+ _ready;
2744
+ _wait = new Promise(resolve => this._ready = resolve);
2745
+ _db;
2578
2746
  set db(db) {
2579
2747
  Promise.resolve(db).then(db => this._ready(this._db = db));
2580
2748
  }
@@ -2589,6 +2757,8 @@ class ModelCollectionsInternal {
2589
2757
  export class Models {
2590
2758
  }
2591
2759
  export class Model {
2760
+ static collections = new ModelCollectionsInternal();
2761
+ static models = {};
2592
2762
  static set db(db) {
2593
2763
  this.collections.db = db;
2594
2764
  }
@@ -2631,10 +2801,14 @@ export class Model {
2631
2801
  this.register(name, inst);
2632
2802
  return inst;
2633
2803
  }
2804
+ /** Model fields description */
2805
+ model;
2806
+ /** Model collection for persistance */
2807
+ collection;
2808
+ /** Custom options */
2809
+ options = {};
2634
2810
  /** Create model acording to description */
2635
2811
  constructor(schema, options) {
2636
- /** Custom options */
2637
- this.options = {};
2638
2812
  const model = this.model = {};
2639
2813
  this.options.name = options?.name || this.__proto__.constructor.name;
2640
2814
  this.collection = options?.collection;
@@ -3012,12 +3186,11 @@ export class Model {
3012
3186
  }
3013
3187
  }
3014
3188
  }
3015
- Model.collections = new ModelCollectionsInternal();
3016
- Model.models = {};
3017
3189
  /** Collection factory */
3018
3190
  export class MicroCollectionStore {
3191
+ _collections = new Map();
3192
+ _store;
3019
3193
  constructor(dataPath, storeTimeDelay) {
3020
- this._collections = new Map();
3021
3194
  if (dataPath)
3022
3195
  this._store = new FileStore({ dir: dataPath.replace(/^\w+:\/\//, ''), debounceTimeout: storeTimeDelay ?? 1000 });
3023
3196
  if (!Model.db)
@@ -3034,6 +3207,11 @@ export class MicroCollectionStore {
3034
3207
  }
3035
3208
  /** minimalistic indexed mongo type collection with persistance for usage with Model */
3036
3209
  export class MicroCollection {
3210
+ /** Collection name */
3211
+ name;
3212
+ /** Collection data */
3213
+ data;
3214
+ _save;
3037
3215
  constructor(options = {}) {
3038
3216
  this.name = options.name || this.constructor.name;
3039
3217
  this.data = options.data || {};
@@ -3198,3 +3376,4 @@ export class MicroCollection {
3198
3376
  return res;
3199
3377
  }
3200
3378
  }
3379
+ // #endregion MicroCollection