@radatek/microserver 2.0.0 → 2.1.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.0
3
+ * @version 2.1.0
4
4
  * @package @radatek/microserver
5
5
  * @copyright Darius Kisonas 2022
6
6
  * @license MIT
@@ -8,6 +8,7 @@
8
8
  import http from 'http';
9
9
  import net from 'net';
10
10
  import { Readable } from 'stream';
11
+ import fs from 'fs';
11
12
  import { EventEmitter } from 'events';
12
13
  export declare class Warning extends Error {
13
14
  constructor(text: string);
@@ -31,9 +32,16 @@ export declare class WebSocketError extends Error {
31
32
  statusCode: number;
32
33
  constructor(text?: string, code?: number);
33
34
  }
35
+ export type Routes = () => {
36
+ [key: string]: Array<any>;
37
+ } | {
38
+ [key: string]: Array<any>;
39
+ };
34
40
  export declare abstract class Plugin {
41
+ name?: string;
35
42
  priority?: number;
36
43
  handler?(req: ServerRequest, res: ServerResponse, next: Function): void;
44
+ routes?: () => Routes | Routes;
37
45
  constructor(router: Router, ...args: any);
38
46
  }
39
47
  interface PluginClass {
@@ -57,8 +65,8 @@ export declare class ServerRequest extends http.IncomingMessage {
57
65
  baseUrl: string;
58
66
  /** Original url */
59
67
  originalUrl?: string;
60
- /** GET parameters */
61
- get: {
68
+ /** Query parameters */
69
+ query: {
62
70
  [key: string]: string;
63
71
  };
64
72
  /** Router named parameters */
@@ -109,17 +117,20 @@ export declare class ServerResponse extends http.ServerResponse {
109
117
  headersOnly: boolean;
110
118
  private constructor();
111
119
  /** Send error reponse */
112
- error(error: string | number | Error, text?: string): void;
120
+ error(error: string | number | Error): void;
113
121
  /** Sets Content-Type acording to data and sends response */
114
122
  send(data?: string | Buffer | Error | Readable | object): void;
115
123
  /** Send json response */
116
124
  json(data: any): void;
117
125
  /** Send json response in form { success: false, error: err } */
118
- jsonError(error: string | number | object | Error, code?: number): void;
126
+ jsonError(error: string | number | object | Error): void;
119
127
  /** Send json response in form { success: true, ... } */
120
- jsonSuccess(data?: object | string, code?: number): void;
128
+ jsonSuccess(data?: object | string): void;
121
129
  /** Send redirect response to specified URL with optional status code (default: 302) */
122
130
  redirect(code: number | string, url?: string): void;
131
+ /** Set status code */
132
+ status(code: number): this;
133
+ download(path: string, filename?: string): void;
123
134
  }
124
135
  /** WebSocket options */
125
136
  export interface WebSocketOptions {
@@ -215,6 +226,9 @@ export interface Middleware {
215
226
  export declare class Router extends EventEmitter {
216
227
  server: MicroServer;
217
228
  auth?: Auth;
229
+ plugins: {
230
+ [key: string]: Plugin;
231
+ };
218
232
  /** @param {MicroServer} server */
219
233
  constructor(server: MicroServer);
220
234
  /** Handler */
@@ -272,7 +286,7 @@ export declare class Router extends EventEmitter {
272
286
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
273
287
  * @return {Router} current router
274
288
  */
275
- add(...args: any): Router;
289
+ use(...args: any): Router;
276
290
  /** Add hook */
277
291
  hook(url: string, ...mid: Middleware[]): Router;
278
292
  /** Check if middleware allready added */
@@ -353,7 +367,7 @@ export declare class MicroServer extends EventEmitter {
353
367
  static plugins: {
354
368
  [key: string]: PluginClass;
355
369
  };
356
- plugins: {
370
+ get plugins(): {
357
371
  [key: string]: Plugin;
358
372
  };
359
373
  constructor(config: MicroServerConfig);
@@ -365,7 +379,7 @@ export declare class MicroServer extends EventEmitter {
365
379
  listen(config?: ListenConfig): Promise<unknown>;
366
380
  /** 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' */
367
381
  bind(fn: string | Function | object): Function;
368
- /** Add middleware, routes, etc.. see {Router.add} */
382
+ /** Add middleware, routes, etc.. see {router.use} */
369
383
  use(...args: any): MicroServer;
370
384
  /** Default server handler */
371
385
  handler(req: ServerRequest, res: ServerResponse): void;
@@ -378,17 +392,19 @@ export declare class MicroServer extends EventEmitter {
378
392
  handlerUpgrade(req: ServerRequest, socket: net.Socket, head: any): void;
379
393
  /** Close server instance */
380
394
  close(): Promise<void>;
381
- /** Add route, alias to `server.router.add('GET ' + url, ...args)` */
395
+ /** Add route, alias to `server.router.use(url, ...args)` */
396
+ all(url: string, ...args: any): MicroServer;
397
+ /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
382
398
  get(url: string, ...args: any): MicroServer;
383
- /** Add route, alias to `server.router.add('POST ' + url, ...args)` */
399
+ /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
384
400
  post(url: string, ...args: any): MicroServer;
385
- /** Add route, alias to `server.router.add('PUT ' + url, ...args)` */
401
+ /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
386
402
  put(url: string, ...args: any): MicroServer;
387
- /** Add route, alias to `server.router.add('PATCH ' + url, ...args)` */
403
+ /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
388
404
  patch(url: string, ...args: any): MicroServer;
389
- /** Add route, alias to `server.router.add('DELETE ' + url, ...args)` */
405
+ /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
390
406
  delete(url: string, ...args: any): MicroServer;
391
- /** Add websocket handler, alias to `server.router.add('WEBSOCKET ' + url, ...args)` */
407
+ /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
392
408
  websocket(url: string, ...args: any): MicroServer;
393
409
  /** Add router hook, alias to `server.router.hook(url, ...args)` */
394
410
  hook(url: string, ...args: any): MicroServer;
@@ -418,6 +434,26 @@ export interface StaticOptions {
418
434
  /** Max file age in seconds */
419
435
  maxAge?: number;
420
436
  }
437
+ export interface ServeFileOptions {
438
+ /** path */
439
+ path: string;
440
+ /** root */
441
+ root?: string;
442
+ /** file name */
443
+ filename?: string;
444
+ /** file mime type */
445
+ mimeType?: string;
446
+ /** last modified date */
447
+ lastModified?: boolean;
448
+ /** etag */
449
+ etag?: boolean;
450
+ /** max age */
451
+ maxAge?: number;
452
+ /** range */
453
+ range?: boolean;
454
+ /** stat */
455
+ stats?: fs.Stats;
456
+ }
421
457
  /** Proxy plugin options */
422
458
  export interface ProxyPluginOptions {
423
459
  /** Base path */
@@ -576,7 +612,7 @@ export declare class FileStore {
576
612
  /** load json file data */
577
613
  load(name: string, autosave?: boolean): Promise<any>;
578
614
  /** save data */
579
- save(name: string, data: any): Promise<void>;
615
+ save(name: string, data: any): Promise<any>;
580
616
  /** load all files in directory */
581
617
  all(name: string, autosave?: boolean): Promise<{
582
618
  [key: string]: any;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * MicroServer
3
- * @version 2.0.0
3
+ * @version 2.1.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';
@@ -92,8 +92,8 @@ export class ServerRequest extends http.IncomingMessage {
92
92
  this.pathname = pathname;
93
93
  this.path = pathname.slice(pathname.lastIndexOf('/'));
94
94
  this.baseUrl = pathname.slice(0, pathname.length - this.path.length);
95
- this.get = {};
96
- parsedUrl.searchParams.forEach((v, k) => this.get[k] = v);
95
+ this.query = {};
96
+ parsedUrl.searchParams.forEach((v, k) => this.query[k] = v);
97
97
  }
98
98
  /** Rewrite request url */
99
99
  rewrite(url) {
@@ -313,8 +313,9 @@ export class ServerResponse extends http.ServerResponse {
313
313
  this.statusCode = 200;
314
314
  }
315
315
  /** Send error reponse */
316
- error(error, text) {
316
+ error(error) {
317
317
  let code = 0;
318
+ let text;
318
319
  if (error instanceof Error) {
319
320
  if ('statusCode' in error)
320
321
  code = error.statusCode;
@@ -322,7 +323,7 @@ export class ServerResponse extends http.ServerResponse {
322
323
  }
323
324
  else if (typeof error === 'number') {
324
325
  code = error;
325
- text = text || commonCodes[code] || 'Error';
326
+ text = commonCodes[code] || 'Error';
326
327
  }
327
328
  else
328
329
  text = error.toString();
@@ -399,19 +400,17 @@ export class ServerResponse extends http.ServerResponse {
399
400
  this.send(data);
400
401
  }
401
402
  /** Send json response in form { success: false, error: err } */
402
- jsonError(error, code) {
403
+ jsonError(error) {
403
404
  this.isJson = true;
404
- this.statusCode = code || 200;
405
405
  if (typeof error === 'number')
406
- [code, error] = [error, http.STATUS_CODES[error] || 'Error'];
406
+ error = http.STATUS_CODES[error] || 'Error';
407
407
  if (error instanceof Error)
408
408
  return this.json(error);
409
409
  this.json(typeof error === 'string' ? { success: false, error } : { success: false, ...error });
410
410
  }
411
411
  /** Send json response in form { success: true, ... } */
412
- jsonSuccess(data, code) {
412
+ jsonSuccess(data) {
413
413
  this.isJson = true;
414
- this.statusCode = code || 200;
415
414
  if (data instanceof Error)
416
415
  return this.json(data);
417
416
  this.json(typeof data === 'string' ? { success: true, message: data } : { success: true, ...data });
@@ -427,6 +426,18 @@ export class ServerResponse extends http.ServerResponse {
427
426
  this.statusCode = code || 302;
428
427
  this.end();
429
428
  }
429
+ /** Set status code */
430
+ status(code) {
431
+ this.statusCode = code;
432
+ return this;
433
+ }
434
+ download(path, filename) {
435
+ StaticPlugin.serveFile(this.req, this, {
436
+ path: path,
437
+ filename: filename || basename(path),
438
+ mimeType: StaticPlugin.mimeTypes[extname(path)] || 'application/octet-stream'
439
+ });
440
+ }
430
441
  }
431
442
  const EMPTY_BUFFER = Buffer.alloc(0);
432
443
  const DEFLATE_TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
@@ -897,6 +908,7 @@ export class Router extends EventEmitter {
897
908
  /** @param {MicroServer} server */
898
909
  constructor(server) {
899
910
  super();
911
+ this.plugins = {};
900
912
  this._stack = [];
901
913
  this._stackAfter = [];
902
914
  this._tree = {};
@@ -1096,32 +1108,26 @@ export class Router extends EventEmitter {
1096
1108
  * @param { {[key: string]: Array<any>} } routes list with subroutes: 'METHOD /suburl': [...middlewares]
1097
1109
  * @return {Router} current router
1098
1110
  */
1099
- add(...args) {
1111
+ use(...args) {
1100
1112
  if (!args[0])
1101
1113
  return this;
1102
- // add(plugin)
1114
+ // use(plugin)
1103
1115
  if (args[0] instanceof Plugin)
1104
1116
  return this._plugin(args[0]);
1105
- // add(pluginid, ...args)
1117
+ // use(pluginid, ...args)
1106
1118
  if (typeof args[0] === 'string' && MicroServer.plugins[args[0]]) {
1107
1119
  const constructor = MicroServer.plugins[args[0]];
1108
1120
  const plugin = new constructor(this, ...args.slice(1));
1109
1121
  return this._plugin(plugin);
1110
1122
  }
1123
+ // use(PluginClass, ...args)
1111
1124
  if (args[0].prototype instanceof Plugin) {
1112
1125
  const plugin = new args[0](this, ...args.slice(1));
1113
1126
  return this._plugin(plugin);
1114
1127
  }
1128
+ // use(middleware)
1115
1129
  if (typeof args[0] === 'function') {
1116
- class Middleware extends Plugin {
1117
- constructor() {
1118
- super(...arguments);
1119
- this.priority = args[0].priority;
1120
- }
1121
- }
1122
- const middleware = new Middleware(this);
1123
- middleware.handler = args[0];
1124
- return this._plugin(middleware);
1130
+ return this._middleware(args[0]);
1125
1131
  }
1126
1132
  let method = '*', url = '/';
1127
1133
  if (typeof args[0] === 'string') {
@@ -1134,13 +1140,13 @@ export class Router extends EventEmitter {
1134
1140
  throw new Error(`Invalid url ${url}`);
1135
1141
  args = args.slice(1);
1136
1142
  }
1137
- // add('/url', ControllerClass)
1143
+ // use('/url', ControllerClass)
1138
1144
  if (typeof args[0] === 'function' && args[0].prototype instanceof Controller) {
1139
1145
  const routes = args[0].routes();
1140
1146
  if (routes)
1141
1147
  args[0] = routes;
1142
1148
  }
1143
- // add('/url', [ ['METHOD /url', ...], {'METHOD } ])
1149
+ // use('/url', [ ['METHOD /url', ...], {'METHOD } ])
1144
1150
  if (Array.isArray(args[0])) {
1145
1151
  if (method !== '*')
1146
1152
  throw new Error('Invalid router usage');
@@ -1149,25 +1155,25 @@ export class Router extends EventEmitter {
1149
1155
  // [methodUrl, ...middlewares]
1150
1156
  if (typeof item[0] !== 'string' || !item[0].match(/^(\w+ )?\//))
1151
1157
  throw new Error('Url expected');
1152
- return this.add(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1158
+ return this.use(item[0].replace(/\//, (url === '/' ? '' : url) + '/'), ...item.slice(1));
1153
1159
  }
1154
1160
  else
1155
1161
  throw new Error('Invalid param');
1156
1162
  });
1157
1163
  return this;
1158
1164
  }
1159
- // add('/url', {'METHOD /url': [...middlewares], ... } ])
1165
+ // use('/url', {'METHOD /url': [...middlewares], ... } ])
1160
1166
  if (typeof args[0] === 'object' && args[0].constructor === Object) {
1161
1167
  if (method !== '*')
1162
1168
  throw new Error('Invalid router usage');
1163
1169
  for (const [subUrl, subArgs] of Object.entries(args[0])) {
1164
1170
  if (!subUrl.match(/^(\w+ )?\//))
1165
1171
  throw new Error('Url expected');
1166
- this.add(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1172
+ this.use(subUrl.replace(/\//, (url === '/' ? '' : url) + '/'), ...(Array.isArray(subArgs) ? subArgs : [subArgs]));
1167
1173
  }
1168
1174
  return this;
1169
1175
  }
1170
- // add('/url', ...middleware)
1176
+ // use('/url', ...middleware)
1171
1177
  return this._add(method, url, 'next', args.filter((o) => o));
1172
1178
  }
1173
1179
  _middleware(middleware) {
@@ -1176,17 +1182,28 @@ export class Router extends EventEmitter {
1176
1182
  const priority = (middleware?.priority || 0) - 1;
1177
1183
  const stack = priority < -1 ? this._stackAfter : this._stack;
1178
1184
  const idx = stack.findIndex(f => 'priority' in f
1179
- && priority > (f.priority || 0));
1185
+ && priority >= (f.priority || 0));
1180
1186
  stack.splice(idx < 0 ? stack.length : idx, 0, middleware);
1181
1187
  return this;
1182
1188
  }
1183
1189
  _plugin(plugin) {
1190
+ if (plugin.name) {
1191
+ if (this.plugins[plugin.name])
1192
+ throw new Error(`Plugin ${plugin.name} already added`);
1193
+ this.plugins[plugin.name] = plugin;
1194
+ }
1184
1195
  if (plugin.handler) {
1185
1196
  const middleware = plugin.handler.bind(plugin);
1186
1197
  middleware.plugin = plugin;
1187
1198
  middleware.priority = plugin.priority;
1188
1199
  return this._middleware(middleware);
1189
1200
  }
1201
+ if (plugin.routes) {
1202
+ if (typeof plugin.routes === 'function')
1203
+ this.use(plugin.routes());
1204
+ else
1205
+ this.use(plugin.routes);
1206
+ }
1190
1207
  return this;
1191
1208
  }
1192
1209
  /** Add hook */
@@ -1203,10 +1220,10 @@ export class Router extends EventEmitter {
1203
1220
  }
1204
1221
  }
1205
1222
  export class MicroServer extends EventEmitter {
1223
+ get plugins() { return this.router.plugins; }
1206
1224
  constructor(config) {
1207
1225
  super();
1208
1226
  this._ready = false;
1209
- this.plugins = {};
1210
1227
  this._methods = {};
1211
1228
  let promise = Promise.resolve();
1212
1229
  this._init = (f, ...args) => {
@@ -1226,7 +1243,7 @@ export class MicroServer extends EventEmitter {
1226
1243
  this.use(config.routes);
1227
1244
  for (const key in MicroServer.plugins) {
1228
1245
  if (config[key])
1229
- this.router.add(MicroServer.plugins[key], config[key]);
1246
+ this.router.use(MicroServer.plugins[key], config[key]);
1230
1247
  }
1231
1248
  if (config.listen)
1232
1249
  this._init(() => {
@@ -1430,9 +1447,9 @@ export class MicroServer extends EventEmitter {
1430
1447
  throw new Error('Invalid middleware: ' + String.toString.call(fn));
1431
1448
  return fn.bind(this);
1432
1449
  }
1433
- /** Add middleware, routes, etc.. see {Router.add} */
1450
+ /** Add middleware, routes, etc.. see {router.use} */
1434
1451
  use(...args) {
1435
- this.router.add(...args);
1452
+ this.router.use(...args);
1436
1453
  return this;
1437
1454
  }
1438
1455
  /** Default server handler */
@@ -1579,34 +1596,39 @@ export class MicroServer extends EventEmitter {
1579
1596
  this.emit('close');
1580
1597
  });
1581
1598
  }
1582
- /** Add route, alias to `server.router.add('GET ' + url, ...args)` */
1599
+ /** Add route, alias to `server.router.use(url, ...args)` */
1600
+ all(url, ...args) {
1601
+ this.router.use(url, ...args);
1602
+ return this;
1603
+ }
1604
+ /** Add route, alias to `server.router.use('GET ' + url, ...args)` */
1583
1605
  get(url, ...args) {
1584
- this.router.add('GET ' + url, ...args);
1606
+ this.router.use('GET ' + url, ...args);
1585
1607
  return this;
1586
1608
  }
1587
- /** Add route, alias to `server.router.add('POST ' + url, ...args)` */
1609
+ /** Add route, alias to `server.router.use('POST ' + url, ...args)` */
1588
1610
  post(url, ...args) {
1589
- this.router.add('POST ' + url, ...args);
1611
+ this.router.use('POST ' + url, ...args);
1590
1612
  return this;
1591
1613
  }
1592
- /** Add route, alias to `server.router.add('PUT ' + url, ...args)` */
1614
+ /** Add route, alias to `server.router.use('PUT ' + url, ...args)` */
1593
1615
  put(url, ...args) {
1594
- this.router.add('PUT ' + url, ...args);
1616
+ this.router.use('PUT ' + url, ...args);
1595
1617
  return this;
1596
1618
  }
1597
- /** Add route, alias to `server.router.add('PATCH ' + url, ...args)` */
1619
+ /** Add route, alias to `server.router.use('PATCH ' + url, ...args)` */
1598
1620
  patch(url, ...args) {
1599
- this.router.add('PATCH ' + url, ...args);
1621
+ this.router.use('PATCH ' + url, ...args);
1600
1622
  return this;
1601
1623
  }
1602
- /** Add route, alias to `server.router.add('DELETE ' + url, ...args)` */
1624
+ /** Add route, alias to `server.router.use('DELETE ' + url, ...args)` */
1603
1625
  delete(url, ...args) {
1604
- this.router.add('DELETE ' + url, ...args);
1626
+ this.router.use('DELETE ' + url, ...args);
1605
1627
  return this;
1606
1628
  }
1607
- /** Add websocket handler, alias to `server.router.add('WEBSOCKET ' + url, ...args)` */
1629
+ /** Add websocket handler, alias to `server.router.use('WEBSOCKET ' + url, ...args)` */
1608
1630
  websocket(url, ...args) {
1609
- this.router.add('WEBSOCKET ' + url, ...args);
1631
+ this.router.use('WEBSOCKET ' + url, ...args);
1610
1632
  return this;
1611
1633
  }
1612
1634
  /** Add router hook, alias to `server.router.hook(url, ...args)` */
@@ -1621,6 +1643,7 @@ class TrustProxyPlugin extends Plugin {
1621
1643
  constructor(router, options) {
1622
1644
  super(router);
1623
1645
  this.priority = 110;
1646
+ this.name = 'trustProxy';
1624
1647
  this.trustProxy = [];
1625
1648
  this.trustProxy = options || [];
1626
1649
  }
@@ -1651,14 +1674,16 @@ class VHostPlugin extends Plugin {
1651
1674
  super(router);
1652
1675
  this.priority = 100;
1653
1676
  const server = router.server;
1654
- if (!server.vhosts)
1677
+ if (!server.vhosts) {
1655
1678
  server.vhosts = {};
1679
+ this.name = 'vhost';
1680
+ }
1656
1681
  else
1657
1682
  this.handler = undefined;
1658
1683
  for (const host in options) {
1659
1684
  if (!server.vhosts[host])
1660
1685
  server.vhosts[host] = new Router(server);
1661
- server.vhosts[host].add(options[host]);
1686
+ server.vhosts[host].use(options[host]);
1662
1687
  }
1663
1688
  }
1664
1689
  handler(req, res, next) {
@@ -1686,7 +1711,7 @@ class StaticPlugin extends Plugin {
1686
1711
  options = {};
1687
1712
  if (typeof options === 'string')
1688
1713
  options = { path: options };
1689
- this.mimeTypes = { ...StaticPlugin.mimeTypes, ...options.mimeTypes };
1714
+ this.mimeTypes = options.mimeTypes ? { ...StaticPlugin.mimeTypes, ...options.mimeTypes } : Object.freeze(StaticPlugin.mimeTypes);
1690
1715
  this.root = path.resolve((options.root || options?.path || 'public').replace(/^\//, '')) + path.sep;
1691
1716
  this.ignore = (options.ignore || []).map((p) => path.normalize(path.join(this.root, p)) + path.sep);
1692
1717
  this.index = options.index || 'index.html';
@@ -1694,7 +1719,7 @@ class StaticPlugin extends Plugin {
1694
1719
  this.lastModified = options.lastModified !== false;
1695
1720
  this.etag = options.etag !== false;
1696
1721
  this.maxAge = options.maxAge;
1697
- router.add('GET /' + (options.path?.replace(/^[.\/]*/, '') || '').replace(/\/$/, '') + '/:path*', this.staticHandler.bind(this));
1722
+ router.use('GET /' + (options.path?.replace(/^[.\/]*/, '') || '').replace(/\/$/, '') + '/:path*', this.staticHandler.bind(this));
1698
1723
  }
1699
1724
  /** Default static files handler */
1700
1725
  staticHandler(req, res, next) {
@@ -1703,7 +1728,7 @@ class StaticPlugin extends Plugin {
1703
1728
  let filename = path.normalize(path.join(this.root, (req.params && req.params.path) || req.pathname));
1704
1729
  if (!filename.startsWith(this.root)) // check root access
1705
1730
  return next();
1706
- const firstch = path.basename(filename)[0];
1731
+ const firstch = basename(filename)[0];
1707
1732
  if (firstch === '.' || firstch === '_') // hidden file
1708
1733
  return next();
1709
1734
  if (filename.endsWith(path.sep))
@@ -1725,27 +1750,61 @@ class StaticPlugin extends Plugin {
1725
1750
  req.filename = filename;
1726
1751
  return handler.call(this, req, res, next);
1727
1752
  }
1728
- const etagMatch = req.headers['if-none-match'];
1729
- const etagTime = req.headers['if-modified-since'];
1730
- const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1731
- res.setHeader('Content-Type', mimeType);
1732
- if (this.lastModified || req.params.lastModified)
1753
+ StaticPlugin.serveFile(req, res, {
1754
+ path: filename,
1755
+ mimeType,
1756
+ stats
1757
+ });
1758
+ });
1759
+ }
1760
+ static serveFile(req, res, options) {
1761
+ const filePath = options.root ? path.join(options.root, options.path) : options.path;
1762
+ const statRes = (err, stats) => {
1763
+ if (err)
1764
+ return res.error(err);
1765
+ if (!stats.isFile())
1766
+ return res.error(404);
1767
+ if (!res.getHeader('Content-Type')) {
1768
+ if (options.mimeType)
1769
+ res.setHeader('Content-Type', options.mimeType);
1770
+ else
1771
+ res.setHeader('Content-Type', this.mimeTypes[path.extname(options.path)] || 'application/octet-stream');
1772
+ }
1773
+ if (options.filename)
1774
+ res.setHeader('Content-Disposition', 'attachment; filename="' + options.filename + '"');
1775
+ if (options.lastModified !== false)
1733
1776
  res.setHeader('Last-Modified', stats.mtime.toUTCString());
1734
- if (this.etag || req.params.etag)
1735
- res.setHeader('Etag', etag);
1736
- if (this.maxAge || req.params.maxAge)
1737
- res.setHeader('Cache-Control', 'max-age=' + (this.maxAge || req.params.maxAge));
1777
+ res.setHeader('Content-Length', stats.size);
1778
+ if (options.etag !== false) {
1779
+ const etag = '"' + etagPrefix + stats.mtime.getTime().toString(32) + '"';
1780
+ if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === stats.mtime.toUTCString()) {
1781
+ res.statusCode = 304;
1782
+ res.headersOnly = true;
1783
+ }
1784
+ }
1785
+ if (options.maxAge)
1786
+ res.setHeader('Cache-Control', 'max-age=' + options.maxAge);
1738
1787
  if (res.headersOnly) {
1739
- res.setHeader('Content-Length', stats.size);
1740
- return res.end();
1788
+ res.end();
1789
+ return;
1741
1790
  }
1742
- if (etagMatch === etag || etagTime === stats.mtime.toUTCString()) {
1743
- res.statusCode = 304;
1744
- return res.end();
1791
+ const streamOptions = { start: 0, end: stats.size - 1 };
1792
+ if (options.range !== false) {
1793
+ const range = req.headers['range'];
1794
+ if (range && range.startsWith('bytes=')) {
1795
+ const parts = range.slice(6).split('-');
1796
+ streamOptions.start = parseInt(parts[0]) || 0;
1797
+ streamOptions.end = parts[1] ? parseInt(parts[1]) : stats.size - 1;
1798
+ res.setHeader('Content-Range', `bytes ${streamOptions.start}-${streamOptions.end}/${stats.size}`);
1799
+ res.setHeader('Content-Length', streamOptions.end - streamOptions.start + 1);
1800
+ }
1745
1801
  }
1746
- res.setHeader('Content-Length', stats.size);
1747
- fs.createReadStream(filename).pipe(res);
1748
- });
1802
+ fs.createReadStream(filePath, streamOptions).pipe(res);
1803
+ };
1804
+ if (!options.stats)
1805
+ fs.stat(filePath, statRes);
1806
+ else
1807
+ statRes(null, options.stats);
1749
1808
  }
1750
1809
  }
1751
1810
  /** Default mime types */
@@ -1759,12 +1818,17 @@ StaticPlugin.mimeTypes = {
1759
1818
  '.css': 'text/css',
1760
1819
  '.png': 'image/png',
1761
1820
  '.jpg': 'image/jpeg',
1762
- '.mp3': 'audio/mpeg',
1763
1821
  '.svg': 'image/svg+xml',
1822
+ '.mp3': 'audio/mpeg',
1823
+ '.ogg': 'audio/ogg',
1824
+ '.mp4': 'video/mp4',
1764
1825
  '.pdf': 'application/pdf',
1765
1826
  '.woff': 'application/x-font-woff',
1766
1827
  '.woff2': 'application/x-font-woff2',
1767
- '.ttf': 'application/x-font-ttf'
1828
+ '.ttf': 'application/x-font-ttf',
1829
+ '.gz': 'application/gzip',
1830
+ '.zip': 'application/zip',
1831
+ '.tgz': 'application/gzip',
1768
1832
  };
1769
1833
  MicroServer.plugins.static = StaticPlugin;
1770
1834
  export class ProxyPlugin extends Plugin {
@@ -1780,7 +1844,7 @@ export class ProxyPlugin extends Plugin {
1780
1844
  this.validHeaders = { ...ProxyPlugin.validHeaders, ...options?.validHeaders };
1781
1845
  if (options.path && options.path !== '/') {
1782
1846
  this.handler = undefined;
1783
- router.add(options.path + '/:path*', this.proxyHandler.bind(this));
1847
+ router.use(options.path + '/:path*', this.proxyHandler.bind(this));
1784
1848
  }
1785
1849
  }
1786
1850
  /** Default proxy handler */
@@ -1816,8 +1880,7 @@ export class ProxyPlugin extends Plugin {
1816
1880
  }
1817
1881
  if (this.headers)
1818
1882
  Object.assign(reqOptions.headers, this.headers);
1819
- if (!reqOptions.headers.Host && !reqOptions.headers.host)
1820
- reqOptions.headers.Host = reqOptions.host;
1883
+ reqOptions.setHost = true;
1821
1884
  const conn = this.remoteUrl.protocol === 'https:' ? https.request(reqOptions) : http.request(reqOptions);
1822
1885
  conn.on('response', (response) => {
1823
1886
  res.statusCode = response.statusCode || 502;
@@ -2144,6 +2207,7 @@ async function login (username, password, salt) {
2144
2207
  class AuthPlugin extends Plugin {
2145
2208
  constructor(router, options) {
2146
2209
  super(router);
2210
+ this.name = 'auth';
2147
2211
  if (router.auth)
2148
2212
  throw new Error('Auth plugin already initialized');
2149
2213
  this.options = {
@@ -2190,7 +2254,7 @@ class AuthPlugin extends Plugin {
2190
2254
  if (sid)
2191
2255
  token = sid.slice(sid.indexOf('=') + 1);
2192
2256
  if (!token)
2193
- token = req.get.token;
2257
+ token = req.query.token;
2194
2258
  if (token) {
2195
2259
  const now = new Date().getTime();
2196
2260
  let usr, expire;
@@ -2331,7 +2395,7 @@ export class FileStore {
2331
2395
  data: data
2332
2396
  };
2333
2397
  this._cache[name] = item;
2334
- return this._sync(async () => {
2398
+ this._sync(async () => {
2335
2399
  if (this._cache[name] === item) {
2336
2400
  this.cleanup();
2337
2401
  try {
@@ -2341,6 +2405,7 @@ export class FileStore {
2341
2405
  }
2342
2406
  }
2343
2407
  });
2408
+ return data;
2344
2409
  }
2345
2410
  /** load all files in directory */
2346
2411
  async all(name, autosave = false) {
@@ -2782,7 +2847,7 @@ export class Model {
2782
2847
  /** Microserver middleware */
2783
2848
  handler(req, res) {
2784
2849
  res.isJson = true;
2785
- let filter, filterStr = req.get.filter;
2850
+ let filter, filterStr = req.query.filter;
2786
2851
  if (filterStr) {
2787
2852
  try {
2788
2853
  if (!filterStr.startsWith('{'))
@@ -2924,11 +2989,12 @@ export class MicroCollection {
2924
2989
  });
2925
2990
  return count;
2926
2991
  }
2927
- let oldData = this.queryDocument(options.query, this.data[id]);
2928
- if (!oldData) {
2992
+ let doc = this.queryDocument(options.query, this.data[id]);
2993
+ if (!doc) {
2929
2994
  if (!options.upsert && !options.new)
2930
2995
  throw new InvalidData(`Document not found`);
2931
- oldData = { _id: id };
2996
+ doc = { _id: id };
2997
+ this.data[id] = doc;
2932
2998
  }
2933
2999
  else {
2934
3000
  if (options.new)
@@ -2937,15 +3003,15 @@ export class MicroCollection {
2937
3003
  if (options.update) {
2938
3004
  for (const n in options.update) {
2939
3005
  if (!n.startsWith('$'))
2940
- oldData[n] = options.update[n];
3006
+ doc[n] = options.update[n];
2941
3007
  }
2942
3008
  if (options.update.$unset) {
2943
3009
  for (const n in options.update.$unset)
2944
- delete oldData[n];
3010
+ delete doc[n];
2945
3011
  }
2946
3012
  }
2947
3013
  if (this._save)
2948
- this.data[id] = await this._save(id, this.data[id], this) || this.data[id];
3014
+ this.data[id] = await this._save(id, doc, this) || doc;
2949
3015
  return 1;
2950
3016
  }
2951
3017
  /** Insert one document */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radatek/microserver",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "HTTP MicroServer",
5
5
  "author": "Darius Kisonas",
6
6
  "license": "MIT",
@@ -20,11 +20,5 @@
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/radateklt/microserver.git"
23
- },
24
- "devDependencies": {
25
- "@types/node": "^22.14.1"
26
- },
27
- "scripts": {
28
- "build": "pnpm --package typescript dlx tsc -d -m nodenext -t es2021 microserver.ts --outDir dist && sed -Ei 's/\\\"use strict\\\";\\n\\n//g' dist/microserver.js && sed -Ei ':a;N;$!ba;s/\\n +private _[^\\n]+//g;s/export \\{\\};//g' dist/microserver.d.ts"
29
23
  }
30
24
  }
package/readme.md CHANGED
@@ -1,35 +1,36 @@
1
1
  ## HTTP MicroServer
2
2
 
3
- Lightweight all-in-one http web server
3
+ Lightweight all-in-one http web server without dependencies
4
4
 
5
5
  Features:
6
6
  - fast REST API router
7
7
  - form/json body decoder
8
8
  - file upload
9
+ - websockets
9
10
  - authentication
10
11
  - plain/hashed passwords
11
12
  - virtual hosts
12
13
  - static files
14
+ - rewrite
15
+ - redirect
13
16
  - reverse proxy routes
14
17
  - trust ip for reverse proxy
15
18
  - json file storage with autosave
16
- - websockets (single file module from ws package)
17
19
  - tls with automatic certificate reload
18
20
  - data model with validation and mongodb interface
19
- - simple file/memory storage adapter for model
21
+ - simple file/memory storage
20
22
  - promises as middleware
21
23
  - controller class
22
24
  - access rights per route
23
25
  - access rights per model field
24
- - rewrite
25
- - redirect
26
- - express like interface
27
26
 
28
27
  ### Usage examples:
29
28
 
30
29
  Simple router:
31
30
 
32
31
  ```ts
32
+ import { MicroServer, AccessDenied } from '@radatek/microserver'
33
+
33
34
  const server = new MicroServer({
34
35
  listen: 8080,
35
36
  auth: {
@@ -47,7 +48,7 @@ server.use('POST /api/login',
47
48
  (req: ServerRequset, res: ServerResponse) =>
48
49
  {
49
50
  const user = await req.auth.login(req.body.user, req.body.password)
50
- return user ? {user} : 403
51
+ return user ? {user} : new AccessDenied()
51
52
  })
52
53
  server.use('GET /api/protected', 'acl:auth',
53
54
  (req: ServerRequset, res: ServerResponse) =>
@@ -55,49 +56,14 @@ server.use('GET /api/protected', 'acl:auth',
55
56
  server.use('static', {root:'public'})
56
57
  ```
57
58
 
58
- Using `Controller` class:
59
-
60
- ```ts
61
- class RestApi extends Controller {
62
- static acl = '' // default acl
63
-
64
- gethello(id) {
65
- return {message:'Hello ' + id + '!'}
66
- }
67
-
68
- async postlogin() {
69
- const user = await this.auth.login(this.body.user, this.body.password)
70
- return user ? {user} : 403
71
- }
72
-
73
- static 'acl:protected' = 'user'
74
- static 'url:protected' = 'GET /protected'
75
- protected() {
76
- return {message:'Protected'}
77
- }
78
- }
79
-
80
- const server = new MicroServer({
81
- listen: 8080,
82
- auth: {
83
- users: {
84
- usr: {
85
- password: 'secret',
86
- acl: {user: true}
87
- }
88
- }
89
- }
90
- })
91
- server.use('/api', RestApi)
92
- ```
93
-
94
- `Model.handler` automatically detects usage for standard functions: get,insert,update,delete
59
+ Using data schema:
95
60
 
96
61
  ```js
97
- import { MicroServer, Model, MicroCollection, FileStore } from '@dariuski/microserver'
62
+ import { MicroServer, Model, MicroCollection, FileStore } from '@radatek/microserver'
98
63
 
99
64
  const usersCollection = new MicroCollection({ store: new FileStore({ dir: 'data' }), name: 'users' })
100
- //const usersCollection = await db.collection('users')
65
+ // or using MicroDB collection
66
+ const usersCollection = await db.collection('users')
101
67
 
102
68
  const userProfile = new Model({
103
69
  _id: 'string',
@@ -115,7 +81,7 @@ const server = new MicroServer({
115
81
  }
116
82
  })
117
83
 
118
- userProfile.insert({name: 'admin', password: 'secret', role: 'admin', acl: {'user/*': true}})
84
+ await userProfile.insert({name: 'admin', password: 'secret', role: 'admin', acl: {'user/*': true}})
119
85
 
120
86
  server.use('POST /login', async (req) => {
121
87
  const user = await req.auth.login(req.body.user, req.body.password)
@@ -134,3 +100,40 @@ server.use('PUT /admin/user/:id', 'acl:user/update', userProfile)
134
100
  // delete user if has acl 'user/update'
135
101
  server.use('DELETE /admin/user/:id', 'acl:user/delete', userProfile)
136
102
  ```
103
+
104
+ Using controller:
105
+
106
+ ```ts
107
+ const server = new MicroServer({
108
+ listen: 8080,
109
+ auth: {
110
+ users: {
111
+ usr: {
112
+ password: 'secret',
113
+ acl: {user: true}
114
+ }
115
+ }
116
+ }
117
+ })
118
+
119
+ class RestApi extends Controller {
120
+ static acl = '' // default acl
121
+
122
+ gethello(id) {
123
+ return {message:'Hello ' + id + '!'}
124
+ }
125
+
126
+ async postlogin() {
127
+ const user = await this.auth.login(this.body.user, this.body.password)
128
+ return user ? {user} : 403
129
+ }
130
+
131
+ static 'acl:protected' = 'user'
132
+ static 'url:protected' = 'GET /protected'
133
+ protected() {
134
+ return {message:'Protected'}
135
+ }
136
+ }
137
+
138
+ server.use('/api', RestApi)
139
+ ```