@radatek/microserver 2.0.2 → 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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 2.0.2
3
+ * @version 2.2.0
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -12,7 +12,7 @@ import tls from 'tls';
12
12
  import querystring from 'querystring';
13
13
  import { Readable } from 'stream';
14
14
  import fs from 'fs';
15
- import path from 'path';
15
+ import path, { basename, extname } from 'path';
16
16
  import crypto from 'crypto';
17
17
  import zlib from 'zlib';
18
18
  import { EventEmitter } from 'events';
@@ -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
- get: {},
79
+ query: {},
76
80
  params: {},
77
81
  paramsList: [],
78
82
  path: '/',
@@ -92,8 +96,8 @@ export class ServerRequest extends http.IncomingMessage {
92
96
  this.pathname = pathname;
93
97
  this.path = pathname.slice(pathname.lastIndexOf('/'));
94
98
  this.baseUrl = pathname.slice(0, pathname.length - this.path.length);
95
- this.get = {};
96
- parsedUrl.searchParams.forEach((v, k) => this.get[k] = v);
99
+ this.query = {};
100
+ parsedUrl.searchParams.forEach((v, k) => this.query[k] = v);
97
101
  }
98
102
  /** Rewrite request url */
99
103
  rewrite(url) {
@@ -313,8 +317,9 @@ export class ServerResponse extends http.ServerResponse {
313
317
  this.statusCode = 200;
314
318
  }
315
319
  /** Send error reponse */
316
- error(error, text) {
320
+ error(error) {
317
321
  let code = 0;
322
+ let text;
318
323
  if (error instanceof Error) {
319
324
  if ('statusCode' in error)
320
325
  code = error.statusCode;
@@ -322,7 +327,7 @@ export class ServerResponse extends http.ServerResponse {
322
327
  }
323
328
  else if (typeof error === 'number') {
324
329
  code = error;
325
- text = text || commonCodes[code] || 'Error';
330
+ text = commonCodes[code] || 'Error';
326
331
  }
327
332
  else
328
333
  text = error.toString();
@@ -399,19 +404,17 @@ export class ServerResponse extends http.ServerResponse {
399
404
  this.send(data);
400
405
  }
401
406
  /** Send json response in form { success: false, error: err } */
402
- jsonError(error, code) {
407
+ jsonError(error) {
403
408
  this.isJson = true;
404
- this.statusCode = code || 200;
405
409
  if (typeof error === 'number')
406
- [code, error] = [error, http.STATUS_CODES[error] || 'Error'];
410
+ error = http.STATUS_CODES[error] || 'Error';
407
411
  if (error instanceof Error)
408
412
  return this.json(error);
409
413
  this.json(typeof error === 'string' ? { success: false, error } : { success: false, ...error });
410
414
  }
411
415
  /** Send json response in form { success: true, ... } */
412
- jsonSuccess(data, code) {
416
+ jsonSuccess(data) {
413
417
  this.isJson = true;
414
- this.statusCode = code || 200;
415
418
  if (data instanceof Error)
416
419
  return this.json(data);
417
420
  this.json(typeof data === 'string' ? { success: true, message: data } : { success: true, ...data });
@@ -427,6 +430,18 @@ export class ServerResponse extends http.ServerResponse {
427
430
  this.statusCode = code || 302;
428
431
  this.end();
429
432
  }
433
+ /** Set status code */
434
+ status(code) {
435
+ this.statusCode = code;
436
+ return this;
437
+ }
438
+ download(path, filename) {
439
+ StaticPlugin.serveFile(this.req, this, {
440
+ path: path,
441
+ filename: filename || basename(path),
442
+ mimeType: StaticPlugin.mimeTypes[extname(path)] || 'application/octet-stream'
443
+ });
444
+ }
430
445
  }
431
446
  const EMPTY_BUFFER = Buffer.alloc(0);
432
447
  const DEFLATE_TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
@@ -681,7 +696,7 @@ export class WebSocket extends EventEmitter {
681
696
  headers = [
682
697
  `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}`,
683
698
  'Connection: close',
684
- 'Content-Type: text/html',
699
+ 'Content-Type: ' + message.startsWith('<') ? 'text/html' : 'text/plain',
685
700
  `Content-Length: ${Buffer.byteLength(message)}`,
686
701
  '',
687
702
  message
@@ -892,6 +907,48 @@ export class Controller {
892
907
  return routes;
893
908
  }
894
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
+ }
895
952
  /** Router */
896
953
  export class Router extends EventEmitter {
897
954
  /** @param {MicroServer} server */
@@ -901,8 +958,95 @@ export class Router extends EventEmitter {
901
958
  this._stack = [];
902
959
  this._stackAfter = [];
903
960
  this._tree = {};
961
+ this._waiter = new Waiter();
904
962
  this.server = server;
905
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
+ }
906
1050
  /** Handler */
907
1051
  handler(req, res, next, method) {
908
1052
  const nextAfter = next;
@@ -1002,7 +1146,7 @@ export class Router extends EventEmitter {
1002
1146
  url,
1003
1147
  middlewares
1004
1148
  });
1005
- middlewares = middlewares.map(i => this.server.bind(i));
1149
+ middlewares = middlewares.map(m => this.bind(m));
1006
1150
  let item = this._tree[method];
1007
1151
  if (!item)
1008
1152
  item = this._tree[method] = { tree: {} };
@@ -1038,7 +1182,6 @@ export class Router extends EventEmitter {
1038
1182
  if (!item[key])
1039
1183
  item[key] = [];
1040
1184
  item[key].push(...middlewares);
1041
- return this;
1042
1185
  }
1043
1186
  /** Clear routes and middlewares */
1044
1187
  clear() {
@@ -1052,71 +1195,79 @@ export class Router extends EventEmitter {
1052
1195
  *
1053
1196
  * @signature add(plugin: Plugin)
1054
1197
  * @param {Plugin} plugin plugin module instance
1055
- * @return {Router} current router
1198
+ * @return {Promise<>}
1056
1199
  *
1057
1200
  * @signature add(pluginid: string, ...args: any)
1058
1201
  * @param {string} pluginid pluginid module
1059
1202
  * @param {...any} args arguments passed to constructor
1060
- * @return {Router} current router
1203
+ * @return {Promise<>}
1061
1204
  *
1062
1205
  * @signature add(pluginClass: typeof Plugin, ...args: any)
1063
1206
  * @param {typeof Plugin} pluginClass plugin class
1064
1207
  * @param {...any} args arguments passed to constructor
1065
- * @return {Router} current router
1208
+ * @return {Promise<>}
1066
1209
  *
1067
1210
  * @signature add(middleware: Middleware)
1068
1211
  * @param {Middleware} middleware
1069
- * @return {Router} current router
1212
+ * @return {Promise<>}
1070
1213
  *
1071
1214
  * @signature add(methodUrl: string, ...middlewares: any)
1072
1215
  * @param {string} methodUrl 'METHOD /url' or '/url'
1073
1216
  * @param {...any} middlewares
1074
- * @return {Router} current router
1217
+ * @return {Promise<>}
1075
1218
  *
1076
1219
  * @signature add(methodUrl: string, controllerClass: typeof Controller)
1077
1220
  * @param {string} methodUrl 'METHOD /url' or '/url'
1078
1221
  * @param {typeof Controller} controllerClass
1079
- * @return {Router} current router
1222
+ * @return {Promise<>}
1080
1223
  *
1081
1224
  * @signature add(methodUrl: string, routes: Array<Array<any>>)
1082
1225
  * @param {string} methodUrl 'METHOD /url' or '/url'
1083
1226
  * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1084
- * @return {Router} current router
1227
+ * @return {Promise<>}
1085
1228
  *
1086
1229
  * @signature add(methodUrl: string, routes: Array<Array<any>>)
1087
1230
  * @param {string} methodUrl 'METHOD /url' or '/url'
1088
1231
  * @param {Array<Array<any>>} routes list with subroutes: ['METHOD /suburl', ...middlewares]
1089
- * @return {Router} current router
1232
+ * @return {Promise<>}
1090
1233
  *
1091
1234
  * @signature add(routes: { [key: string]: Array<any> })
1092
1235
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1093
- * @return {Router} current router
1236
+ * @return {Promise<>}
1094
1237
  *
1095
1238
  * @signature add(methodUrl: string, routes: { [key: string]: Array<any> })
1096
1239
  * @param {string} methodUrl 'METHOD /url' or '/url'
1097
1240
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1098
- * @return {Router} current router
1241
+ * @return {Promise<>}
1099
1242
  */
1100
- use(...args) {
1243
+ async use(...args) {
1101
1244
  if (!args[0])
1102
- return this;
1245
+ return;
1246
+ this.server._waiter.startJob();
1247
+ for (let i = 0; i < args.length; i++)
1248
+ args[i] = await args[i];
1103
1249
  // use(plugin)
1104
- if (args[0] instanceof Plugin)
1105
- return this._plugin(args[0]);
1250
+ if (args[0] instanceof Plugin) {
1251
+ await this._plugin(args[0]);
1252
+ return this.server._waiter.endJob();
1253
+ }
1106
1254
  // use(pluginid, ...args)
1107
1255
  if (typeof args[0] === 'string' && MicroServer.plugins[args[0]]) {
1108
1256
  const constructor = MicroServer.plugins[args[0]];
1109
1257
  const plugin = new constructor(this, ...args.slice(1));
1110
- return this._plugin(plugin);
1258
+ await this._plugin(plugin);
1259
+ return this.server._waiter.endJob();
1111
1260
  }
1112
1261
  // use(PluginClass, ...args)
1113
- if (args[0].prototype instanceof Plugin) {
1262
+ if (typeof args[0] === 'function' && args[0].prototype instanceof Plugin) {
1114
1263
  const plugin = new args[0](this, ...args.slice(1));
1115
- return this._plugin(plugin);
1264
+ await this._plugin(plugin);
1265
+ return this.server._waiter.endJob();
1116
1266
  }
1117
1267
  // use(middleware)
1118
- if (typeof args[0] === 'function') {
1119
- return this._middleware(args[0]);
1268
+ if (isFunction(args[0])) {
1269
+ this._middleware(args[0]);
1270
+ return this.server._waiter.endJob();
1120
1271
  }
1121
1272
  let method = '*', url = '/';
1122
1273
  if (typeof args[0] === 'string') {
@@ -1131,7 +1282,7 @@ export class Router extends EventEmitter {
1131
1282
  }
1132
1283
  // use('/url', ControllerClass)
1133
1284
  if (typeof args[0] === 'function' && args[0].prototype instanceof Controller) {
1134
- const routes = args[0].routes();
1285
+ const routes = await args[0].routes();
1135
1286
  if (routes)
1136
1287
  args[0] = routes;
1137
1288
  }
@@ -1139,17 +1290,17 @@ export class Router extends EventEmitter {
1139
1290
  if (Array.isArray(args[0])) {
1140
1291
  if (method !== '*')
1141
1292
  throw new Error('Invalid router usage');
1142
- args[0].forEach(item => {
1293
+ for (const item of args[0]) {
1143
1294
  if (Array.isArray(item)) {
1144
1295
  // [methodUrl, ...middlewares]
1145
1296
  if (typeof item[0] !== 'string' || !item[0].match(/^(\w+ )?\//))
1146
1297
  throw new Error('Url expected');
1147
- return this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1298
+ await this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1148
1299
  }
1149
1300
  else
1150
1301
  throw new Error('Invalid param');
1151
- });
1152
- return this;
1302
+ }
1303
+ return this.server._waiter.endJob();
1153
1304
  }
1154
1305
  // use('/url', {'METHOD /url': [...middlewares], ... } ])
1155
1306
  if (typeof args[0] === 'object' && args[0].constructor === Object) {
@@ -1158,44 +1309,47 @@ export class Router extends EventEmitter {
1158
1309
  for (const [subUrl, subArgs] of Object.entries(args[0])) {
1159
1310
  if (!subUrl.match(/^(\w+ )?\//))
1160
1311
  throw new Error('Url expected');
1161
- this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1312
+ await this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1162
1313
  }
1163
- return this;
1314
+ return this.server._waiter.endJob();
1164
1315
  }
1165
1316
  // use('/url', ...middleware)
1166
- return this._add(method, url, 'next', args.filter((o) => o));
1317
+ this._add(method, url, 'next', args.filter((o) => o));
1318
+ return this.server._waiter.endJob();
1167
1319
  }
1168
1320
  _middleware(middleware) {
1169
1321
  if (!middleware)
1170
- return this;
1322
+ return;
1171
1323
  const priority = (middleware?.priority || 0) - 1;
1172
1324
  const stack = priority < -1 ? this._stackAfter : this._stack;
1173
1325
  const idx = stack.findIndex(f => 'priority' in f
1174
1326
  && priority >= (f.priority || 0));
1175
1327
  stack.splice(idx < 0 ? stack.length : idx, 0, middleware);
1176
- return this;
1177
1328
  }
1178
- _plugin(plugin) {
1329
+ async _plugin(plugin) {
1330
+ let added;
1179
1331
  if (plugin.name) {
1180
1332
  if (this.plugins[plugin.name])
1181
1333
  throw new Error(`Plugin ${plugin.name} already added`);
1182
1334
  this.plugins[plugin.name] = plugin;
1335
+ added = plugin.name;
1183
1336
  }
1337
+ await plugin.initialise?.();
1184
1338
  if (plugin.handler) {
1185
1339
  const middleware = plugin.handler.bind(plugin);
1186
1340
  middleware.plugin = plugin;
1187
1341
  middleware.priority = plugin.priority;
1188
- return this._middleware(middleware);
1189
- }
1190
- if (plugin.routes) {
1191
- if (typeof plugin.routes === 'function')
1192
- this.use(plugin.routes());
1193
- else
1194
- this.use(plugin.routes);
1342
+ this._middleware(middleware);
1195
1343
  }
1196
- if (plugin.controller)
1197
- this.use(plugin.controller);
1198
- return this;
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];
1199
1353
  }
1200
1354
  /** Add hook */
1201
1355
  hook(url, ...mid) {
@@ -1203,7 +1357,7 @@ export class Router extends EventEmitter {
1203
1357
  let method = '*';
1204
1358
  if (m)
1205
1359
  [method, url] = [m[1], m[2]];
1206
- return this._add(method, url, 'hook', mid);
1360
+ this._add(method, url, 'hook', mid);
1207
1361
  }
1208
1362
  /** Check if middleware allready added */
1209
1363
  has(mid) {
@@ -1211,10 +1365,9 @@ export class Router extends EventEmitter {
1211
1365
  }
1212
1366
  }
1213
1367
  export class MicroServer extends EventEmitter {
1214
- get plugins() { return this.router.plugins; }
1215
1368
  constructor(config) {
1216
1369
  super();
1217
- this._ready = false;
1370
+ this._waiter = new EmitterWaiter(this);
1218
1371
  this._methods = {};
1219
1372
  let promise = Promise.resolve();
1220
1373
  this._init = (f, ...args) => {
@@ -1246,7 +1399,7 @@ export class MicroServer extends EventEmitter {
1246
1399
  }
1247
1400
  /** Add one time listener or call immediatelly for 'ready' */
1248
1401
  once(name, cb) {
1249
- if (name === 'ready' && this._ready)
1402
+ if (name === 'ready' && this.isReady())
1250
1403
  cb();
1251
1404
  else
1252
1405
  super.once(name, cb);
@@ -1254,11 +1407,22 @@ export class MicroServer extends EventEmitter {
1254
1407
  }
1255
1408
  /** Add listener and call immediatelly for 'ready' */
1256
1409
  on(name, cb) {
1257
- if (name === 'ready' && this._ready)
1410
+ if (name === 'ready' && this.isReady())
1258
1411
  cb();
1259
1412
  super.on(name, cb);
1260
1413
  return this;
1261
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
+ }
1262
1426
  /** Listen server, should be used only if config.listen is not set */
1263
1427
  listen(config) {
1264
1428
  const listen = (config?.listen || this.config.listen || 0) + '';
@@ -1284,164 +1448,62 @@ export class MicroServer extends EventEmitter {
1284
1448
  });
1285
1449
  }
1286
1450
  }
1287
- return new Promise((resolve) => {
1288
- let readyCount = 0;
1289
- this._ready = false;
1290
- const ready = (srv) => {
1291
- if (srv)
1292
- readyCount++;
1293
- if (readyCount >= this.servers.size) {
1294
- if (!this._ready) {
1295
- this._ready = true;
1296
- if (this.servers.size === 0)
1297
- this.close();
1298
- else
1299
- this.emit('ready');
1300
- resolve();
1301
- }
1302
- }
1303
- };
1304
- const reg = /^((?<proto>\w+):\/\/)?(?<host>(\[[^\]]+\]|[a-z][^:,]+|\d+\.\d+\.\d+\.\d+))?:?(?<port>\d+)?/;
1305
- listen.split(',').forEach(listen => {
1306
- let { proto, host, port } = reg.exec(listen)?.groups || {};
1307
- let srv;
1308
- switch (proto) {
1309
- case 'tcp':
1310
- if (!config?.handler)
1311
- throw new Error('Handler is required for tcp');
1312
- srv = net.createServer(handler);
1313
- break;
1314
- case 'tls':
1315
- if (!config?.handler)
1316
- throw new Error('Handler is required for tls');
1317
- srv = tls.createServer(tlsOptions(), handler);
1318
- tlsOptionsReload(srv);
1319
- break;
1320
- case 'https':
1321
- port = port || '443';
1322
- srv = https.createServer(tlsOptions(), handler);
1323
- tlsOptionsReload(srv);
1324
- break;
1325
- default:
1326
- port = port || '80';
1327
- srv = http.createServer(handler);
1328
- break;
1329
- }
1330
- this.servers.add(srv);
1331
- if (port === '0') // skip listening
1332
- ready(srv);
1333
- else {
1334
- srv.listen(parseInt(port), host?.replace(/[\[\]]/g, '') || '0.0.0.0', () => {
1335
- const addr = srv.address();
1336
- this.emit('listen', addr.port, addr.address, srv);
1337
- ready(srv);
1338
- });
1339
- }
1340
- srv.on('error', err => {
1341
- this.servers.delete(srv);
1342
- srv.close();
1343
- ready();
1344
- this.emit('error', err);
1345
- });
1346
- srv.on('connection', s => {
1347
- this.sockets.add(s);
1348
- 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();
1349
1487
  });
1350
- srv.on('upgrade', this.handlerUpgrade.bind(this));
1351
- ready();
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));
1352
1499
  });
1500
+ srv.on('upgrade', this.handlerUpgrade.bind(this));
1353
1501
  });
1502
+ return this._waiter.wait('ready');
1354
1503
  }
1355
- /** 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' */
1356
- bind(fn) {
1357
- if (typeof fn === 'string') {
1358
- let name = fn;
1359
- let idx = name.indexOf(':');
1360
- if (idx < 0 && name.includes('=')) {
1361
- name = 'param:' + name;
1362
- idx = 5;
1363
- }
1364
- if (idx >= 0) {
1365
- const v = name.slice(idx + 1);
1366
- const type = name.slice(0, idx);
1367
- // predefined middlewares
1368
- switch (type) {
1369
- // redirect:302,https://redirect.to
1370
- case 'redirect': {
1371
- let redirect = v.split(','), code = parseInt(v[0]);
1372
- if (!code || code < 301 || code > 399)
1373
- code = 302;
1374
- return (req, res) => res.redirect(code, redirect[1] || v);
1375
- }
1376
- // error:422
1377
- case 'error':
1378
- return (req, res) => res.error(parseInt(v) || 422);
1379
- // param:name=value
1380
- case 'param': {
1381
- idx = v.indexOf('=');
1382
- if (idx > 0) {
1383
- const prm = v.slice(0, idx), val = v.slice(idx + 1);
1384
- return (req, res, next) => { req.params[prm] = val; return next(); };
1385
- }
1386
- break;
1387
- }
1388
- case 'model': {
1389
- const model = v;
1390
- return (req, res) => {
1391
- res.isJson = true;
1392
- req.params.model = model;
1393
- req.model = Model.models[model];
1394
- if (!req.model) {
1395
- console.error(`Data model ${model} not defined for request ${req.path}`);
1396
- return res.error(422);
1397
- }
1398
- return req.model.handler(req, res);
1399
- };
1400
- }
1401
- // user:userid
1402
- // group:user_groupid
1403
- // acl:validacl
1404
- case 'user':
1405
- case 'group':
1406
- case 'acl':
1407
- return (req, res, next) => {
1408
- if (type === 'user' && v === req.user?.id)
1409
- return next();
1410
- if (type === 'acl') {
1411
- req.params.acl = v;
1412
- if (req.auth?.acl(v))
1413
- return next();
1414
- }
1415
- if (type === 'group') {
1416
- req.params.group = v;
1417
- if (req.user?.group === v)
1418
- return next();
1419
- }
1420
- const accept = req.headers.accept || '';
1421
- if (!res.isJson && req.auth?.options.redirect && req.method === 'GET' && !accept.includes('json') && (accept.includes('html') || accept.includes('*/*'))) {
1422
- if (req.auth.options.redirect && req.url !== req.auth.options.redirect)
1423
- return res.redirect(302, req.auth.options.redirect);
1424
- else if (req.auth.options.mode !== 'cookie') {
1425
- res.setHeader('WWW-Authenticate', `Basic realm="${req.auth.options.realm}"`);
1426
- return res.error(401);
1427
- }
1428
- }
1429
- return res.error('Permission denied');
1430
- };
1431
- }
1432
- }
1433
- throw new Error('Invalid option: ' + name);
1434
- }
1435
- if (fn && typeof fn === 'object' && 'handler' in fn && typeof fn.handler === 'function')
1436
- return fn.handler.bind(fn);
1437
- if (typeof fn !== 'function')
1438
- throw new Error('Invalid middleware: ' + String.toString.call(fn));
1439
- return fn.bind(this);
1440
- }
1441
- /** Add middleware, routes, etc.. see {Router.add} */
1504
+ /** Add middleware, routes, etc.. see {router.use} */
1442
1505
  use(...args) {
1443
- this.router.use(...args);
1444
- return this;
1506
+ return this.router.use(...args);
1445
1507
  }
1446
1508
  /** Default server handler */
1447
1509
  handler(req, res) {
@@ -1583,36 +1645,41 @@ export class MicroServer extends EventEmitter {
1583
1645
  }
1584
1646
  this.sockets.clear();
1585
1647
  }).then(() => {
1586
- this._ready = false;
1587
1648
  this.emit('close');
1649
+ this._waiter.resolve("close");
1588
1650
  });
1589
1651
  }
1590
- /** Add route, alias to `server.router.add('GET ' + url, ...args)` */
1652
+ /** Add route, alias to `server.router.use(url, ...args)` */
1653
+ all(url, ...args) {
1654
+ this.router.use(url, ...args);
1655
+ return this;
1656
+ }
1657
+ /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
1591
1658
  get(url, ...args) {
1592
1659
  this.router.use('GET ' + url, ...args);
1593
1660
  return this;
1594
1661
  }
1595
- /** Add route, alias to `server.router.add('POST ' + url, ...args)` */
1662
+ /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
1596
1663
  post(url, ...args) {
1597
1664
  this.router.use('POST ' + url, ...args);
1598
1665
  return this;
1599
1666
  }
1600
- /** Add route, alias to `server.router.add('PUT ' + url, ...args)` */
1667
+ /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
1601
1668
  put(url, ...args) {
1602
1669
  this.router.use('PUT ' + url, ...args);
1603
1670
  return this;
1604
1671
  }
1605
- /** Add route, alias to `server.router.add('PATCH ' + url, ...args)` */
1672
+ /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
1606
1673
  patch(url, ...args) {
1607
1674
  this.router.use('PATCH ' + url, ...args);
1608
1675
  return this;
1609
1676
  }
1610
- /** Add route, alias to `server.router.add('DELETE ' + url, ...args)` */
1677
+ /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
1611
1678
  delete(url, ...args) {
1612
1679
  this.router.use('DELETE ' + url, ...args);
1613
1680
  return this;
1614
1681
  }
1615
- /** Add websocket handler, alias to `server.router.add('WEBSOCKET ' + url, ...args)` */
1682
+ /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
1616
1683
  websocket(url, ...args) {
1617
1684
  this.router.use('WEBSOCKET ' + url, ...args);
1618
1685
  return this;
@@ -1697,7 +1764,7 @@ class StaticPlugin extends Plugin {
1697
1764
  options = {};
1698
1765
  if (typeof options === 'string')
1699
1766
  options = { path: options };
1700
- this.mimeTypes = { ...StaticPlugin.mimeTypes, ...options.mimeTypes };
1767
+ this.mimeTypes = options.mimeTypes ? { ...StaticPlugin.mimeTypes, ...options.mimeTypes } : Object.freeze(StaticPlugin.mimeTypes);
1701
1768
  this.root = path.resolve((options.root || options?.path || 'public').replace(/^\//, '')) + path.sep;
1702
1769
  this.ignore = (options.ignore || []).map((p) => path.normalize(path.join(this.root, p)) + path.sep);
1703
1770
  this.index = options.index || 'index.html';
@@ -1714,7 +1781,7 @@ class StaticPlugin extends Plugin {
1714
1781
  let filename = path.normalize(path.join(this.root, (req.params && req.params.path) || req.pathname));
1715
1782
  if (!filename.startsWith(this.root)) // check root access
1716
1783
  return next();
1717
- const firstch = path.basename(filename)[0];
1784
+ const firstch = basename(filename)[0];
1718
1785
  if (firstch === '.' || firstch === '_') // hidden file
1719
1786
  return next();
1720
1787
  if (filename.endsWith(path.sep))
@@ -1736,27 +1803,61 @@ class StaticPlugin extends Plugin {
1736
1803
  req.filename = filename;
1737
1804
  return handler.call(this, req, res, next);
1738
1805
  }
1739
- const etagMatch = req.headers['if-none-match'];
1740
- const etagTime = req.headers['if-modified-since'];
1741
- const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1742
- res.setHeader('Content-Type', mimeType);
1743
- if (this.lastModified || req.params.lastModified)
1806
+ StaticPlugin.serveFile(req, res, {
1807
+ path: filename,
1808
+ mimeType,
1809
+ stats
1810
+ });
1811
+ });
1812
+ }
1813
+ static serveFile(req, res, options) {
1814
+ const filePath = options.root ? path.join(options.root, options.path) : options.path;
1815
+ const statRes = (err, stats) => {
1816
+ if (err)
1817
+ return res.error(err);
1818
+ if (!stats.isFile())
1819
+ return res.error(404);
1820
+ if (!res.getHeader('Content-Type')) {
1821
+ if (options.mimeType)
1822
+ res.setHeader('Content-Type', options.mimeType);
1823
+ else
1824
+ res.setHeader('Content-Type', this.mimeTypes[path.extname(options.path)] || 'application/octet-stream');
1825
+ }
1826
+ if (options.filename)
1827
+ res.setHeader('Content-Disposition', 'attachment; filename="' + options.filename + '"');
1828
+ if (options.lastModified !== false)
1744
1829
  res.setHeader('Last-Modified', stats.mtime.toUTCString());
1745
- if (this.etag || req.params.etag)
1746
- res.setHeader('Etag', etag);
1747
- if (this.maxAge || req.params.maxAge)
1748
- res.setHeader('Cache-Control', 'max-age=' + (this.maxAge || req.params.maxAge));
1830
+ res.setHeader('Content-Length', stats.size);
1831
+ if (options.etag !== false) {
1832
+ const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1833
+ if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === stats.mtime.toUTCString()) {
1834
+ res.statusCode = 304;
1835
+ res.headersOnly = true;
1836
+ }
1837
+ }
1838
+ if (options.maxAge)
1839
+ res.setHeader('Cache-Control', 'max-age=' + options.maxAge);
1749
1840
  if (res.headersOnly) {
1750
- res.setHeader('Content-Length', stats.size);
1751
- return res.end();
1841
+ res.end();
1842
+ return;
1752
1843
  }
1753
- if (etagMatch === etag || etagTime === stats.mtime.toUTCString()) {
1754
- res.statusCode = 304;
1755
- return res.end();
1844
+ const streamOptions = { start: 0, end: stats.size - 1 };
1845
+ if (options.range !== false) {
1846
+ const range = req.headers['range'];
1847
+ if (range && range.startsWith('bytes=')) {
1848
+ const parts = range.slice(6).split('-');
1849
+ streamOptions.start = parseInt(parts[0]) || 0;
1850
+ streamOptions.end = parts[1] ? parseInt(parts[1]) : stats.size - 1;
1851
+ res.setHeader('Content-Range', `bytes ${streamOptions.start}-${streamOptions.end}/${stats.size}`);
1852
+ res.setHeader('Content-Length', streamOptions.end - streamOptions.start + 1);
1853
+ }
1756
1854
  }
1757
- res.setHeader('Content-Length', stats.size);
1758
- fs.createReadStream(filename).pipe(res);
1759
- });
1855
+ fs.createReadStream(filePath, streamOptions).pipe(res);
1856
+ };
1857
+ if (!options.stats)
1858
+ fs.stat(filePath, statRes);
1859
+ else
1860
+ statRes(null, options.stats);
1760
1861
  }
1761
1862
  }
1762
1863
  /** Default mime types */
@@ -1770,12 +1871,17 @@ StaticPlugin.mimeTypes = {
1770
1871
  '.css': 'text/css',
1771
1872
  '.png': 'image/png',
1772
1873
  '.jpg': 'image/jpeg',
1773
- '.mp3': 'audio/mpeg',
1774
1874
  '.svg': 'image/svg+xml',
1875
+ '.mp3': 'audio/mpeg',
1876
+ '.ogg': 'audio/ogg',
1877
+ '.mp4': 'video/mp4',
1775
1878
  '.pdf': 'application/pdf',
1776
1879
  '.woff': 'application/x-font-woff',
1777
1880
  '.woff2': 'application/x-font-woff2',
1778
- '.ttf': 'application/x-font-ttf'
1881
+ '.ttf': 'application/x-font-ttf',
1882
+ '.gz': 'application/gzip',
1883
+ '.zip': 'application/zip',
1884
+ '.tgz': 'application/gzip',
1779
1885
  };
1780
1886
  MicroServer.plugins.static = StaticPlugin;
1781
1887
  export class ProxyPlugin extends Plugin {
@@ -2201,7 +2307,7 @@ class AuthPlugin extends Plugin {
2201
2307
  if (sid)
2202
2308
  token = sid.slice(sid.indexOf('=') + 1);
2203
2309
  if (!token)
2204
- token = req.get.token;
2310
+ token = req.query.token;
2205
2311
  if (token) {
2206
2312
  const now = new Date().getTime();
2207
2313
  let usr, expire;
@@ -2794,7 +2900,7 @@ export class Model {
2794
2900
  /** Microserver middleware */
2795
2901
  handler(req, res) {
2796
2902
  res.isJson = true;
2797
- let filter, filterStr = req.get.filter;
2903
+ let filter, filterStr = req.query.filter;
2798
2904
  if (filterStr) {
2799
2905
  try {
2800
2906
  if (!filterStr.startsWith('{'))