@radatek/microserver 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/microserver.d.ts +34 -22
- package/dist/microserver.js +254 -201
- package/microserver.ts +3803 -0
- package/package.json +9 -4
package/dist/microserver.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MicroServer
|
|
3
|
-
* @version 2.
|
|
3
|
+
* @version 2.2.0
|
|
4
4
|
* @package @radatek/microserver
|
|
5
5
|
* @copyright Darius Kisonas 2022
|
|
6
6
|
* @license MIT
|
|
@@ -21,6 +21,9 @@ const defaultExpire = 24 * 60 * 60;
|
|
|
21
21
|
const defaultMaxBodySize = 5 * 1024 * 1024;
|
|
22
22
|
const defaultMethods = 'HEAD,GET,POST,PUT,PATCH,DELETE';
|
|
23
23
|
function NOOP(...args) { }
|
|
24
|
+
function isFunction(fn) {
|
|
25
|
+
return typeof fn === 'function' && !fn.prototype?.constructor;
|
|
26
|
+
}
|
|
24
27
|
export class Warning extends Error {
|
|
25
28
|
constructor(text) {
|
|
26
29
|
super(text);
|
|
@@ -71,8 +74,9 @@ export class ServerRequest extends http.IncomingMessage {
|
|
|
71
74
|
_init(router) {
|
|
72
75
|
Object.assign(this, {
|
|
73
76
|
router,
|
|
77
|
+
auth: router.auth,
|
|
74
78
|
protocol: 'encrypted' in this.socket && this.socket.encrypted ? 'https' : 'http',
|
|
75
|
-
|
|
79
|
+
query: {},
|
|
76
80
|
params: {},
|
|
77
81
|
paramsList: [],
|
|
78
82
|
path: '/',
|
|
@@ -692,7 +696,7 @@ export class WebSocket extends EventEmitter {
|
|
|
692
696
|
headers = [
|
|
693
697
|
`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
|
|
694
698
|
'Connection: close',
|
|
695
|
-
'Content-Type: text/html',
|
|
699
|
+
'Content-Type: ' + message.startsWith('<') ? 'text/html' : 'text/plain',
|
|
696
700
|
`Content-Length: ${Buffer.byteLength(message)}`,
|
|
697
701
|
'',
|
|
698
702
|
message
|
|
@@ -903,6 +907,48 @@ export class Controller {
|
|
|
903
907
|
return routes;
|
|
904
908
|
}
|
|
905
909
|
}
|
|
910
|
+
class Waiter {
|
|
911
|
+
constructor() {
|
|
912
|
+
this._waiters = {};
|
|
913
|
+
this._id = 0;
|
|
914
|
+
this._busy = 0;
|
|
915
|
+
}
|
|
916
|
+
get busy() {
|
|
917
|
+
return this._busy > 0;
|
|
918
|
+
}
|
|
919
|
+
startJob() {
|
|
920
|
+
this._busy++;
|
|
921
|
+
}
|
|
922
|
+
endJob(id) {
|
|
923
|
+
this._busy--;
|
|
924
|
+
if (!this._busy)
|
|
925
|
+
this.resolve(id || 'ready');
|
|
926
|
+
}
|
|
927
|
+
get nextId() {
|
|
928
|
+
return (++this._id).toString();
|
|
929
|
+
}
|
|
930
|
+
async wait(id) {
|
|
931
|
+
return new Promise(resolve => (this._waiters[id] = this._waiters[id] || []).push(resolve));
|
|
932
|
+
}
|
|
933
|
+
resolve(id) {
|
|
934
|
+
const resolvers = this._waiters[id];
|
|
935
|
+
if (resolvers)
|
|
936
|
+
for (const resolve of resolvers)
|
|
937
|
+
resolve(undefined);
|
|
938
|
+
delete this._waiters[id];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
class EmitterWaiter extends Waiter {
|
|
942
|
+
constructor(emitter) {
|
|
943
|
+
super();
|
|
944
|
+
this._emitter = emitter;
|
|
945
|
+
}
|
|
946
|
+
resolve(id) {
|
|
947
|
+
if (id === 'ready')
|
|
948
|
+
this._emitter.emit(id);
|
|
949
|
+
super.resolve(id);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
906
952
|
/** Router */
|
|
907
953
|
export class Router extends EventEmitter {
|
|
908
954
|
/** @param {MicroServer} server */
|
|
@@ -912,8 +958,95 @@ export class Router extends EventEmitter {
|
|
|
912
958
|
this._stack = [];
|
|
913
959
|
this._stackAfter = [];
|
|
914
960
|
this._tree = {};
|
|
961
|
+
this._waiter = new Waiter();
|
|
915
962
|
this.server = server;
|
|
916
963
|
}
|
|
964
|
+
/** 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' */
|
|
965
|
+
bind(fn) {
|
|
966
|
+
if (typeof fn === 'string') {
|
|
967
|
+
let name = fn;
|
|
968
|
+
let idx = name.indexOf(':');
|
|
969
|
+
if (idx < 0 && name.includes('=')) {
|
|
970
|
+
name = 'param:' + name;
|
|
971
|
+
idx = 5;
|
|
972
|
+
}
|
|
973
|
+
if (idx >= 0) {
|
|
974
|
+
const v = name.slice(idx + 1);
|
|
975
|
+
const type = name.slice(0, idx);
|
|
976
|
+
// predefined middlewares
|
|
977
|
+
switch (type) {
|
|
978
|
+
// redirect:302,https://redirect.to
|
|
979
|
+
case 'redirect': {
|
|
980
|
+
let redirect = v.split(','), code = parseInt(v[0]);
|
|
981
|
+
if (!code || code < 301 || code > 399)
|
|
982
|
+
code = 302;
|
|
983
|
+
return (req, res) => res.redirect(code, redirect[1] || v);
|
|
984
|
+
}
|
|
985
|
+
// error:422
|
|
986
|
+
case 'error':
|
|
987
|
+
return (req, res) => res.error(parseInt(v) || 422);
|
|
988
|
+
// param:name=value
|
|
989
|
+
case 'param': {
|
|
990
|
+
idx = v.indexOf('=');
|
|
991
|
+
if (idx > 0) {
|
|
992
|
+
const prm = v.slice(0, idx), val = v.slice(idx + 1);
|
|
993
|
+
return (req, res, next) => { req.params[prm] = val; return next(); };
|
|
994
|
+
}
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
case 'model': {
|
|
998
|
+
const model = v;
|
|
999
|
+
return (req, res) => {
|
|
1000
|
+
res.isJson = true;
|
|
1001
|
+
req.params.model = model;
|
|
1002
|
+
req.model = Model.models[model];
|
|
1003
|
+
if (!req.model) {
|
|
1004
|
+
console.error(`Data model ${model} not defined for request ${req.path}`);
|
|
1005
|
+
return res.error(422);
|
|
1006
|
+
}
|
|
1007
|
+
return req.model.handler(req, res);
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
// user:userid
|
|
1011
|
+
// group:user_groupid
|
|
1012
|
+
// acl:validacl
|
|
1013
|
+
case 'user':
|
|
1014
|
+
case 'group':
|
|
1015
|
+
case 'acl':
|
|
1016
|
+
return (req, res, next) => {
|
|
1017
|
+
if (type === 'user' && v === req.user?.id)
|
|
1018
|
+
return next();
|
|
1019
|
+
if (type === 'acl') {
|
|
1020
|
+
req.params.acl = v;
|
|
1021
|
+
if (req.auth?.acl(v))
|
|
1022
|
+
return next();
|
|
1023
|
+
}
|
|
1024
|
+
if (type === 'group') {
|
|
1025
|
+
req.params.group = v;
|
|
1026
|
+
if (req.user?.group === v)
|
|
1027
|
+
return next();
|
|
1028
|
+
}
|
|
1029
|
+
const accept = req.headers.accept || '';
|
|
1030
|
+
if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
|
|
1031
|
+
if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
|
|
1032
|
+
return res.redirect(302, req.auth.options.redirect);
|
|
1033
|
+
else if (req.auth.options.mode !== 'cookie') {
|
|
1034
|
+
res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
|
|
1035
|
+
return res.error(401);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return res.error('Permission denied');
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
throw new Error('Invalid option: ' + name);
|
|
1043
|
+
}
|
|
1044
|
+
if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
|
|
1045
|
+
return fn.handler.bind(fn);
|
|
1046
|
+
if (typeof fn !== 'function')
|
|
1047
|
+
throw new Error('Invalid middleware: ' + String.toString.call(fn));
|
|
1048
|
+
return fn.bind(this);
|
|
1049
|
+
}
|
|
917
1050
|
/** Handler */
|
|
918
1051
|
handler(req, res, next, method) {
|
|
919
1052
|
const nextAfter = next;
|
|
@@ -1013,7 +1146,7 @@ export class Router extends EventEmitter {
|
|
|
1013
1146
|
url,
|
|
1014
1147
|
middlewares
|
|
1015
1148
|
});
|
|
1016
|
-
middlewares = middlewares.map(
|
|
1149
|
+
middlewares = middlewares.map(m => this.bind(m));
|
|
1017
1150
|
let item = this._tree[method];
|
|
1018
1151
|
if (!item)
|
|
1019
1152
|
item = this._tree[method] = { tree: {} };
|
|
@@ -1049,7 +1182,6 @@ export class Router extends EventEmitter {
|
|
|
1049
1182
|
if (!item[key])
|
|
1050
1183
|
item[key] = [];
|
|
1051
1184
|
item[key].push(...middlewares);
|
|
1052
|
-
return this;
|
|
1053
1185
|
}
|
|
1054
1186
|
/** Clear routes and middlewares */
|
|
1055
1187
|
clear() {
|
|
@@ -1063,71 +1195,79 @@ export class Router extends EventEmitter {
|
|
|
1063
1195
|
*
|
|
1064
1196
|
* @signature add(plugin: Plugin)
|
|
1065
1197
|
* @param {Plugin} plugin plugin module instance
|
|
1066
|
-
* @return {
|
|
1198
|
+
* @return {Promise<>}
|
|
1067
1199
|
*
|
|
1068
1200
|
* @signature add(pluginid: string, ...args: any)
|
|
1069
1201
|
* @param {string} pluginid pluginid module
|
|
1070
1202
|
* @param {...any} args arguments passed to constructor
|
|
1071
|
-
* @return {
|
|
1203
|
+
* @return {Promise<>}
|
|
1072
1204
|
*
|
|
1073
1205
|
* @signature add(pluginClass: typeof Plugin, ...args: any)
|
|
1074
1206
|
* @param {typeof Plugin} pluginClass plugin class
|
|
1075
1207
|
* @param {...any} args arguments passed to constructor
|
|
1076
|
-
* @return {
|
|
1208
|
+
* @return {Promise<>}
|
|
1077
1209
|
*
|
|
1078
1210
|
* @signature add(middleware: Middleware)
|
|
1079
1211
|
* @param {Middleware} middleware
|
|
1080
|
-
* @return {
|
|
1212
|
+
* @return {Promise<>}
|
|
1081
1213
|
*
|
|
1082
1214
|
* @signature add(methodUrl: string, ...middlewares: any)
|
|
1083
1215
|
* @param {string} methodUrl 'METHOD /url' or '/url'
|
|
1084
1216
|
* @param {...any} middlewares
|
|
1085
|
-
* @return {
|
|
1217
|
+
* @return {Promise<>}
|
|
1086
1218
|
*
|
|
1087
1219
|
* @signature add(methodUrl: string, controllerClass: typeof Controller)
|
|
1088
1220
|
* @param {string} methodUrl 'METHOD /url' or '/url'
|
|
1089
1221
|
* @param {typeof Controller} controllerClass
|
|
1090
|
-
* @return {
|
|
1222
|
+
* @return {Promise<>}
|
|
1091
1223
|
*
|
|
1092
1224
|
* @signature add(methodUrl: string, routes: Array<Array<any>>)
|
|
1093
1225
|
* @param {string} methodUrl 'METHOD /url' or '/url'
|
|
1094
1226
|
* @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
|
|
1095
|
-
* @return {
|
|
1227
|
+
* @return {Promise<>}
|
|
1096
1228
|
*
|
|
1097
1229
|
* @signature add(methodUrl: string, routes: Array<Array<any>>)
|
|
1098
1230
|
* @param {string} methodUrl 'METHOD /url' or '/url'
|
|
1099
1231
|
* @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
|
|
1100
|
-
* @return {
|
|
1232
|
+
* @return {Promise<>}
|
|
1101
1233
|
*
|
|
1102
1234
|
* @signature add(routes: { [key: string]: Array<any> })
|
|
1103
1235
|
* @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
|
|
1104
|
-
* @return {
|
|
1236
|
+
* @return {Promise<>}
|
|
1105
1237
|
*
|
|
1106
1238
|
* @signature add(methodUrl: string, routes: { [key: string]: Array<any> })
|
|
1107
1239
|
* @param {string} methodUrl 'METHOD /url' or '/url'
|
|
1108
1240
|
* @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
|
|
1109
|
-
* @return {
|
|
1241
|
+
* @return {Promise<>}
|
|
1110
1242
|
*/
|
|
1111
|
-
use(...args) {
|
|
1243
|
+
async use(...args) {
|
|
1112
1244
|
if (!args[0])
|
|
1113
|
-
return
|
|
1245
|
+
return;
|
|
1246
|
+
this.server._waiter.startJob();
|
|
1247
|
+
for (let i = 0; i < args.length; i++)
|
|
1248
|
+
args[i] = await args[i];
|
|
1114
1249
|
// use(plugin)
|
|
1115
|
-
if (args[0] instanceof Plugin)
|
|
1116
|
-
|
|
1250
|
+
if (args[0] instanceof Plugin) {
|
|
1251
|
+
await this._plugin(args[0]);
|
|
1252
|
+
return this.server._waiter.endJob();
|
|
1253
|
+
}
|
|
1117
1254
|
// use(pluginid, ...args)
|
|
1118
1255
|
if (typeof args[0] === 'string' && MicroServer.plugins[args[0]]) {
|
|
1119
1256
|
const constructor = MicroServer.plugins[args[0]];
|
|
1120
1257
|
const plugin = new constructor(this, ...args.slice(1));
|
|
1121
|
-
|
|
1258
|
+
await this._plugin(plugin);
|
|
1259
|
+
return this.server._waiter.endJob();
|
|
1122
1260
|
}
|
|
1123
1261
|
// use(PluginClass, ...args)
|
|
1124
|
-
if (args[0].prototype instanceof Plugin) {
|
|
1262
|
+
if (typeof args[0] === 'function' && args[0].prototype instanceof Plugin) {
|
|
1125
1263
|
const plugin = new args[0](this, ...args.slice(1));
|
|
1126
|
-
|
|
1264
|
+
await this._plugin(plugin);
|
|
1265
|
+
return this.server._waiter.endJob();
|
|
1127
1266
|
}
|
|
1128
1267
|
// use(middleware)
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1268
|
+
if (isFunction(args[0])) {
|
|
1269
|
+
this._middleware(args[0]);
|
|
1270
|
+
return this.server._waiter.endJob();
|
|
1131
1271
|
}
|
|
1132
1272
|
let method = '*', url = '/';
|
|
1133
1273
|
if (typeof args[0] === 'string') {
|
|
@@ -1142,7 +1282,7 @@ export class Router extends EventEmitter {
|
|
|
1142
1282
|
}
|
|
1143
1283
|
// use('/url', ControllerClass)
|
|
1144
1284
|
if (typeof args[0] === 'function' && args[0].prototype instanceof Controller) {
|
|
1145
|
-
const routes = args[0].routes();
|
|
1285
|
+
const routes = await args[0].routes();
|
|
1146
1286
|
if (routes)
|
|
1147
1287
|
args[0] = routes;
|
|
1148
1288
|
}
|
|
@@ -1150,17 +1290,17 @@ export class Router extends EventEmitter {
|
|
|
1150
1290
|
if (Array.isArray(args[0])) {
|
|
1151
1291
|
if (method !== '*')
|
|
1152
1292
|
throw new Error('Invalid router usage');
|
|
1153
|
-
args[0]
|
|
1293
|
+
for (const item of args[0]) {
|
|
1154
1294
|
if (Array.isArray(item)) {
|
|
1155
1295
|
// [methodUrl, ...middlewares]
|
|
1156
1296
|
if (typeof item[0] !== 'string' || !item[0].match(/^(\w+ )?\//))
|
|
1157
1297
|
throw new Error('Url expected');
|
|
1158
|
-
|
|
1298
|
+
await this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
|
|
1159
1299
|
}
|
|
1160
1300
|
else
|
|
1161
1301
|
throw new Error('Invalid param');
|
|
1162
|
-
}
|
|
1163
|
-
return this;
|
|
1302
|
+
}
|
|
1303
|
+
return this.server._waiter.endJob();
|
|
1164
1304
|
}
|
|
1165
1305
|
// use('/url', {'METHOD /url': [...middlewares], ... } ])
|
|
1166
1306
|
if (typeof args[0] === 'object' && args[0].constructor === Object) {
|
|
@@ -1169,42 +1309,47 @@ export class Router extends EventEmitter {
|
|
|
1169
1309
|
for (const [subUrl, subArgs] of Object.entries(args[0])) {
|
|
1170
1310
|
if (!subUrl.match(/^(\w+ )?\//))
|
|
1171
1311
|
throw new Error('Url expected');
|
|
1172
|
-
this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
|
|
1312
|
+
await this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
|
|
1173
1313
|
}
|
|
1174
|
-
return this;
|
|
1314
|
+
return this.server._waiter.endJob();
|
|
1175
1315
|
}
|
|
1176
1316
|
// use('/url', ...middleware)
|
|
1177
|
-
|
|
1317
|
+
this._add(method, url, 'next', args.filter((o) => o));
|
|
1318
|
+
return this.server._waiter.endJob();
|
|
1178
1319
|
}
|
|
1179
1320
|
_middleware(middleware) {
|
|
1180
1321
|
if (!middleware)
|
|
1181
|
-
return
|
|
1322
|
+
return;
|
|
1182
1323
|
const priority = (middleware?.priority || 0) - 1;
|
|
1183
1324
|
const stack = priority < -1 ? this._stackAfter : this._stack;
|
|
1184
1325
|
const idx = stack.findIndex(f => 'priority' in f
|
|
1185
1326
|
&& priority >= (f.priority || 0));
|
|
1186
1327
|
stack.splice(idx < 0 ? stack.length : idx, 0, middleware);
|
|
1187
|
-
return this;
|
|
1188
1328
|
}
|
|
1189
|
-
_plugin(plugin) {
|
|
1329
|
+
async _plugin(plugin) {
|
|
1330
|
+
let added;
|
|
1190
1331
|
if (plugin.name) {
|
|
1191
1332
|
if (this.plugins[plugin.name])
|
|
1192
1333
|
throw new Error(`Plugin ${plugin.name} already added`);
|
|
1193
1334
|
this.plugins[plugin.name] = plugin;
|
|
1335
|
+
added = plugin.name;
|
|
1194
1336
|
}
|
|
1337
|
+
await plugin.initialise?.();
|
|
1195
1338
|
if (plugin.handler) {
|
|
1196
1339
|
const middleware = plugin.handler.bind(plugin);
|
|
1197
1340
|
middleware.plugin = plugin;
|
|
1198
1341
|
middleware.priority = plugin.priority;
|
|
1199
|
-
|
|
1200
|
-
}
|
|
1201
|
-
if (plugin.routes) {
|
|
1202
|
-
if (typeof plugin.routes === 'function')
|
|
1203
|
-
this.use(plugin.routes());
|
|
1204
|
-
else
|
|
1205
|
-
this.use(plugin.routes);
|
|
1342
|
+
this._middleware(middleware);
|
|
1206
1343
|
}
|
|
1207
|
-
|
|
1344
|
+
if (plugin.routes)
|
|
1345
|
+
await this.use(isFunction(plugin.routes) ? await plugin.routes() : plugin.routes);
|
|
1346
|
+
if (added)
|
|
1347
|
+
this._waiter.resolve(added);
|
|
1348
|
+
}
|
|
1349
|
+
async waitPlugin(id) {
|
|
1350
|
+
if (!this.plugins[id])
|
|
1351
|
+
await this._waiter.wait(id);
|
|
1352
|
+
return this.plugins[id];
|
|
1208
1353
|
}
|
|
1209
1354
|
/** Add hook */
|
|
1210
1355
|
hook(url, ...mid) {
|
|
@@ -1212,7 +1357,7 @@ export class Router extends EventEmitter {
|
|
|
1212
1357
|
let method = '*';
|
|
1213
1358
|
if (m)
|
|
1214
1359
|
[method, url] = [m[1], m[2]];
|
|
1215
|
-
|
|
1360
|
+
this._add(method, url, 'hook', mid);
|
|
1216
1361
|
}
|
|
1217
1362
|
/** Check if middleware allready added */
|
|
1218
1363
|
has(mid) {
|
|
@@ -1220,10 +1365,9 @@ export class Router extends EventEmitter {
|
|
|
1220
1365
|
}
|
|
1221
1366
|
}
|
|
1222
1367
|
export class MicroServer extends EventEmitter {
|
|
1223
|
-
get plugins() { return this.router.plugins; }
|
|
1224
1368
|
constructor(config) {
|
|
1225
1369
|
super();
|
|
1226
|
-
this.
|
|
1370
|
+
this._waiter = new EmitterWaiter(this);
|
|
1227
1371
|
this._methods = {};
|
|
1228
1372
|
let promise = Promise.resolve();
|
|
1229
1373
|
this._init = (f, ...args) => {
|
|
@@ -1255,7 +1399,7 @@ export class MicroServer extends EventEmitter {
|
|
|
1255
1399
|
}
|
|
1256
1400
|
/** Add one time listener or call immediatelly for 'ready' */
|
|
1257
1401
|
once(name, cb) {
|
|
1258
|
-
if (name === 'ready' && this.
|
|
1402
|
+
if (name === 'ready' && this.isReady())
|
|
1259
1403
|
cb();
|
|
1260
1404
|
else
|
|
1261
1405
|
super.once(name, cb);
|
|
@@ -1263,11 +1407,22 @@ export class MicroServer extends EventEmitter {
|
|
|
1263
1407
|
}
|
|
1264
1408
|
/** Add listener and call immediatelly for 'ready' */
|
|
1265
1409
|
on(name, cb) {
|
|
1266
|
-
if (name === 'ready' && this.
|
|
1410
|
+
if (name === 'ready' && this.isReady())
|
|
1267
1411
|
cb();
|
|
1268
1412
|
super.on(name, cb);
|
|
1269
1413
|
return this;
|
|
1270
1414
|
}
|
|
1415
|
+
isReady() {
|
|
1416
|
+
return !this._waiter.busy;
|
|
1417
|
+
}
|
|
1418
|
+
async waitReady() {
|
|
1419
|
+
if (this.isReady())
|
|
1420
|
+
return;
|
|
1421
|
+
return this._waiter.wait("ready");
|
|
1422
|
+
}
|
|
1423
|
+
async waitPlugin(id) {
|
|
1424
|
+
await this.router.waitPlugin(id);
|
|
1425
|
+
}
|
|
1271
1426
|
/** Listen server, should be used only if config.listen is not set */
|
|
1272
1427
|
listen(config) {
|
|
1273
1428
|
const listen = (config?.listen || this.config.listen || 0) + '';
|
|
@@ -1293,164 +1448,62 @@ export class MicroServer extends EventEmitter {
|
|
|
1293
1448
|
});
|
|
1294
1449
|
}
|
|
1295
1450
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
this.
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
if (!
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
tlsOptionsReload(srv);
|
|
1333
|
-
break;
|
|
1334
|
-
default:
|
|
1335
|
-
port = port || '80';
|
|
1336
|
-
srv = http.createServer(handler);
|
|
1337
|
-
break;
|
|
1338
|
-
}
|
|
1339
|
-
this.servers.add(srv);
|
|
1340
|
-
if (port === '0') // skip listening
|
|
1341
|
-
ready(srv);
|
|
1342
|
-
else {
|
|
1343
|
-
srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
|
|
1344
|
-
const addr = srv.address();
|
|
1345
|
-
this.emit('listen', addr.port, addr.address, srv);
|
|
1346
|
-
ready(srv);
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
srv.on('error', err => {
|
|
1350
|
-
this.servers.delete(srv);
|
|
1351
|
-
srv.close();
|
|
1352
|
-
ready();
|
|
1353
|
-
this.emit('error', err);
|
|
1354
|
-
});
|
|
1355
|
-
srv.on('connection', s => {
|
|
1356
|
-
this.sockets.add(s);
|
|
1357
|
-
s.once('close', () => this.sockets.delete(s));
|
|
1451
|
+
const reg = /^((?<proto>\w+):\/\/)?(?<host>(\[[^\]]+\]|[a-z][^:,]+|\d+\.\d+\.\d+\.\d+))?:?(?<port>\d+)?/;
|
|
1452
|
+
listen.split(',').forEach(listen => {
|
|
1453
|
+
this._waiter.startJob();
|
|
1454
|
+
let { proto, host, port } = reg.exec(listen)?.groups || {};
|
|
1455
|
+
let srv;
|
|
1456
|
+
switch (proto) {
|
|
1457
|
+
case 'tcp':
|
|
1458
|
+
if (!config?.handler)
|
|
1459
|
+
throw new Error('Handler is required for tcp');
|
|
1460
|
+
srv = net.createServer(handler);
|
|
1461
|
+
break;
|
|
1462
|
+
case 'tls':
|
|
1463
|
+
if (!config?.handler)
|
|
1464
|
+
throw new Error('Handler is required for tls');
|
|
1465
|
+
srv = tls.createServer(tlsOptions(), handler);
|
|
1466
|
+
tlsOptionsReload(srv);
|
|
1467
|
+
break;
|
|
1468
|
+
case 'https':
|
|
1469
|
+
port = port || '443';
|
|
1470
|
+
srv = https.createServer(tlsOptions(), handler);
|
|
1471
|
+
tlsOptionsReload(srv);
|
|
1472
|
+
break;
|
|
1473
|
+
default:
|
|
1474
|
+
port = port || '80';
|
|
1475
|
+
srv = http.createServer(handler);
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
this.servers.add(srv);
|
|
1479
|
+
if (port === '0') // skip listening
|
|
1480
|
+
this._waiter.endJob();
|
|
1481
|
+
else {
|
|
1482
|
+
srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
|
|
1483
|
+
const addr = srv.address();
|
|
1484
|
+
this.emit('listen', addr.port, addr.address, srv);
|
|
1485
|
+
srv._ready = true;
|
|
1486
|
+
this._waiter.endJob();
|
|
1358
1487
|
});
|
|
1359
|
-
|
|
1360
|
-
|
|
1488
|
+
}
|
|
1489
|
+
srv.on('error', err => {
|
|
1490
|
+
srv.close();
|
|
1491
|
+
this.servers.delete(srv);
|
|
1492
|
+
if (!srv._ready)
|
|
1493
|
+
this._waiter.endJob();
|
|
1494
|
+
this.emit('error', err);
|
|
1495
|
+
});
|
|
1496
|
+
srv.on('connection', s => {
|
|
1497
|
+
this.sockets.add(s);
|
|
1498
|
+
s.once('close', () => this.sockets.delete(s));
|
|
1361
1499
|
});
|
|
1500
|
+
srv.on('upgrade', this.handlerUpgrade.bind(this));
|
|
1362
1501
|
});
|
|
1363
|
-
|
|
1364
|
-
/** 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' */
|
|
1365
|
-
bind(fn) {
|
|
1366
|
-
if (typeof fn === 'string') {
|
|
1367
|
-
let name = fn;
|
|
1368
|
-
let idx = name.indexOf(':');
|
|
1369
|
-
if (idx < 0 && name.includes('=')) {
|
|
1370
|
-
name = 'param:' + name;
|
|
1371
|
-
idx = 5;
|
|
1372
|
-
}
|
|
1373
|
-
if (idx >= 0) {
|
|
1374
|
-
const v = name.slice(idx + 1);
|
|
1375
|
-
const type = name.slice(0, idx);
|
|
1376
|
-
// predefined middlewares
|
|
1377
|
-
switch (type) {
|
|
1378
|
-
// redirect:302,https://redirect.to
|
|
1379
|
-
case 'redirect': {
|
|
1380
|
-
let redirect = v.split(','), code = parseInt(v[0]);
|
|
1381
|
-
if (!code || code < 301 || code > 399)
|
|
1382
|
-
code = 302;
|
|
1383
|
-
return (req, res) => res.redirect(code, redirect[1] || v);
|
|
1384
|
-
}
|
|
1385
|
-
// error:422
|
|
1386
|
-
case 'error':
|
|
1387
|
-
return (req, res) => res.error(parseInt(v) || 422);
|
|
1388
|
-
// param:name=value
|
|
1389
|
-
case 'param': {
|
|
1390
|
-
idx = v.indexOf('=');
|
|
1391
|
-
if (idx > 0) {
|
|
1392
|
-
const prm = v.slice(0, idx), val = v.slice(idx + 1);
|
|
1393
|
-
return (req, res, next) => { req.params[prm] = val; return next(); };
|
|
1394
|
-
}
|
|
1395
|
-
break;
|
|
1396
|
-
}
|
|
1397
|
-
case 'model': {
|
|
1398
|
-
const model = v;
|
|
1399
|
-
return (req, res) => {
|
|
1400
|
-
res.isJson = true;
|
|
1401
|
-
req.params.model = model;
|
|
1402
|
-
req.model = Model.models[model];
|
|
1403
|
-
if (!req.model) {
|
|
1404
|
-
console.error(`Data model ${model} not defined for request ${req.path}`);
|
|
1405
|
-
return res.error(422);
|
|
1406
|
-
}
|
|
1407
|
-
return req.model.handler(req, res);
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
// user:userid
|
|
1411
|
-
// group:user_groupid
|
|
1412
|
-
// acl:validacl
|
|
1413
|
-
case 'user':
|
|
1414
|
-
case 'group':
|
|
1415
|
-
case 'acl':
|
|
1416
|
-
return (req, res, next) => {
|
|
1417
|
-
if (type === 'user' && v === req.user?.id)
|
|
1418
|
-
return next();
|
|
1419
|
-
if (type === 'acl') {
|
|
1420
|
-
req.params.acl = v;
|
|
1421
|
-
if (req.auth?.acl(v))
|
|
1422
|
-
return next();
|
|
1423
|
-
}
|
|
1424
|
-
if (type === 'group') {
|
|
1425
|
-
req.params.group = v;
|
|
1426
|
-
if (req.user?.group === v)
|
|
1427
|
-
return next();
|
|
1428
|
-
}
|
|
1429
|
-
const accept = req.headers.accept || '';
|
|
1430
|
-
if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
|
|
1431
|
-
if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
|
|
1432
|
-
return res.redirect(302, req.auth.options.redirect);
|
|
1433
|
-
else if (req.auth.options.mode !== 'cookie') {
|
|
1434
|
-
res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
|
|
1435
|
-
return res.error(401);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
return res.error('Permission denied');
|
|
1439
|
-
};
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
throw new Error('Invalid option: ' + name);
|
|
1443
|
-
}
|
|
1444
|
-
if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
|
|
1445
|
-
return fn.handler.bind(fn);
|
|
1446
|
-
if (typeof fn !== 'function')
|
|
1447
|
-
throw new Error('Invalid middleware: ' + String.toString.call(fn));
|
|
1448
|
-
return fn.bind(this);
|
|
1502
|
+
return this._waiter.wait('ready');
|
|
1449
1503
|
}
|
|
1450
1504
|
/** Add middleware, routes, etc.. see {router.use} */
|
|
1451
1505
|
use(...args) {
|
|
1452
|
-
this.router.use(...args);
|
|
1453
|
-
return this;
|
|
1506
|
+
return this.router.use(...args);
|
|
1454
1507
|
}
|
|
1455
1508
|
/** Default server handler */
|
|
1456
1509
|
handler(req, res) {
|
|
@@ -1592,8 +1645,8 @@ export class MicroServer extends EventEmitter {
|
|
|
1592
1645
|
}
|
|
1593
1646
|
this.sockets.clear();
|
|
1594
1647
|
}).then(() => {
|
|
1595
|
-
this._ready = false;
|
|
1596
1648
|
this.emit('close');
|
|
1649
|
+
this._waiter.resolve("close");
|
|
1597
1650
|
});
|
|
1598
1651
|
}
|
|
1599
1652
|
/** Add route, alias to `server.router.use(url, ...args)` */
|