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