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