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