@entity-access/server-pages 1.0.28 → 1.0.30

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.
Files changed (72) hide show
  1. package/.vscode/settings.json +1 -0
  2. package/dist/Content.d.ts +6 -6
  3. package/dist/Content.d.ts.map +1 -1
  4. package/dist/Content.js +21 -37
  5. package/dist/Content.js.map +1 -1
  6. package/dist/Page.d.ts +11 -40
  7. package/dist/Page.d.ts.map +1 -1
  8. package/dist/Page.js +5 -3
  9. package/dist/Page.js.map +1 -1
  10. package/dist/ServerPages.d.ts +11 -5
  11. package/dist/ServerPages.d.ts.map +1 -1
  12. package/dist/ServerPages.js +102 -81
  13. package/dist/ServerPages.js.map +1 -1
  14. package/dist/core/LocalFile.d.ts +2 -1
  15. package/dist/core/LocalFile.d.ts.map +1 -1
  16. package/dist/core/LocalFile.js +8 -0
  17. package/dist/core/LocalFile.js.map +1 -1
  18. package/dist/core/RouteTree.js +1 -1
  19. package/dist/core/RouteTree.js.map +1 -1
  20. package/dist/core/SessionUser.d.ts +2 -2
  21. package/dist/core/SessionUser.d.ts.map +1 -1
  22. package/dist/core/SessionUser.js.map +1 -1
  23. package/dist/core/Wrapped.d.ts +70 -0
  24. package/dist/core/Wrapped.d.ts.map +1 -0
  25. package/dist/core/Wrapped.js +253 -0
  26. package/dist/core/Wrapped.js.map +1 -0
  27. package/dist/core/cached.d.ts +2 -0
  28. package/dist/core/cached.d.ts.map +1 -0
  29. package/dist/core/cached.js +3 -0
  30. package/dist/core/cached.js.map +1 -0
  31. package/dist/decorators/Authorize.d.ts +2 -0
  32. package/dist/decorators/Authorize.d.ts.map +1 -0
  33. package/dist/decorators/Authorize.js +3 -0
  34. package/dist/decorators/Authorize.js.map +1 -0
  35. package/dist/parsers/json/jsonParser.d.ts +2 -0
  36. package/dist/parsers/json/jsonParser.d.ts.map +1 -0
  37. package/dist/parsers/json/jsonParser.js +3 -0
  38. package/dist/parsers/json/jsonParser.js.map +1 -0
  39. package/dist/services/CookieService.d.ts +3 -3
  40. package/dist/services/CookieService.d.ts.map +1 -1
  41. package/dist/services/CookieService.js +5 -7
  42. package/dist/services/CookieService.js.map +1 -1
  43. package/dist/services/TokenService.d.ts +1 -0
  44. package/dist/services/TokenService.d.ts.map +1 -1
  45. package/dist/services/TokenService.js.map +1 -1
  46. package/dist/services/UserSessionProvider.d.ts +2 -1
  47. package/dist/services/UserSessionProvider.d.ts.map +1 -1
  48. package/dist/services/UserSessionProvider.js +3 -3
  49. package/dist/services/UserSessionProvider.js.map +1 -1
  50. package/dist/ssl/SelfSigned.d.ts +7 -0
  51. package/dist/ssl/SelfSigned.d.ts.map +1 -0
  52. package/dist/ssl/SelfSigned.js +62 -0
  53. package/dist/ssl/SelfSigned.js.map +1 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/package.json +3 -6
  56. package/self-signed/cert.crt +22 -0
  57. package/self-signed/key.pem +27 -0
  58. package/src/Content.tsx +28 -43
  59. package/src/Page.tsx +18 -62
  60. package/src/ServerPages.ts +114 -76
  61. package/src/core/LocalFile.ts +11 -2
  62. package/src/core/RouteTree.ts +1 -1
  63. package/src/core/SessionUser.ts +2 -2
  64. package/src/core/Wrapped.ts +359 -0
  65. package/src/core/cached.ts +3 -0
  66. package/src/decorators/Authorize.ts +3 -0
  67. package/src/parsers/json/jsonParser.ts +3 -0
  68. package/src/services/CookieService.ts +7 -9
  69. package/src/services/TokenService.ts +1 -0
  70. package/src/services/UserSessionProvider.ts +4 -2
  71. package/src/ssl/SelfSigned.ts +78 -0
  72. package/test.js +1 -3
package/src/Content.tsx CHANGED
@@ -1,13 +1,13 @@
1
1
  /* eslint-disable no-console */
2
2
  import { File } from "buffer";
3
- import { Response } from "express";
4
3
  import XNode from "./html/XNode.js";
5
4
  import { parse } from "path";
6
5
  import { LocalFile } from "./core/LocalFile.js";
7
6
  import SessionUser from "./core/SessionUser.js";
7
+ import { WrappedResponse } from "./core/Wrapped.js";
8
8
 
9
9
  export interface IPageResult {
10
- send(res: Response): Promise<any>;
10
+ send(res: WrappedResponse): Promise<any>;
11
11
  }
12
12
 
13
13
  export class TempFileResult implements IPageResult {
@@ -34,26 +34,18 @@ export class TempFileResult implements IPageResult {
34
34
  this.immutable = immutable;
35
35
  }
36
36
 
37
- send(res: Response<any, Record<string, any>>) {
38
- return new Promise<void>((resolve, reject) => {
39
- res = res.header("content-disposition", `${this.contentDisposition};filename=${encodeURIComponent(this.file.fileName)}`);
40
- res.sendFile(this.file.path,{
41
- headers: {
42
- "content-type": this.file.contentType
43
- },
44
- acceptRanges: true,
45
- cacheControl: this.cacheControl,
46
- maxAge: this.maxAge,
47
- etag: this.etag,
48
- immutable: this.immutable,
49
- lastModified: false
50
- }, (error) => {
51
- if(error) {
52
- reject(error);
53
- return;
54
- }
55
- resolve();
56
- });
37
+ send(res: WrappedResponse) {
38
+ res.setHeader("content-disposition", `${this.contentDisposition};filename=${encodeURIComponent(this.file.fileName)}`);
39
+ return res.sendFile(this.file.path,{
40
+ headers: {
41
+ "content-type": this.file.contentType
42
+ },
43
+ acceptRanges: true,
44
+ cacheControl: this.cacheControl,
45
+ maxAge: this.maxAge,
46
+ etag: this.etag,
47
+ immutable: this.immutable,
48
+ lastModified: false
57
49
  });
58
50
  }
59
51
 
@@ -86,22 +78,15 @@ export class FileResult implements IPageResult {
86
78
  this.fileName = parsed.base;
87
79
  }
88
80
 
89
- send(res: Response<any, Record<string, any>>) {
90
- return new Promise<void>((resolve, reject) => {
91
- res = res.header("content-disposition", `${this.contentDisposition};filename=${encodeURIComponent(this.fileName)}`);
92
- res.sendFile(this.filePath,{
93
- acceptRanges: true,
94
- cacheControl: this.cacheControl,
95
- maxAge: this.maxAge,
96
- etag: this.etag,
97
- immutable: this.immutable
98
- }, (error) => {
99
- if(error) {
100
- reject(error);
101
- return;
102
- }
103
- resolve();
104
- });
81
+ send(res: WrappedResponse) {
82
+
83
+ res.setHeader("content-disposition", `${this.contentDisposition};filename=${encodeURIComponent(this.fileName)}`);
84
+ return res.sendFile(this.filePath,{
85
+ acceptRanges: true,
86
+ cacheControl: this.cacheControl,
87
+ maxAge: this.maxAge,
88
+ etag: this.etag,
89
+ immutable: this.immutable
105
90
  });
106
91
  }
107
92
 
@@ -113,8 +98,8 @@ export class Redirect implements IPageResult {
113
98
 
114
99
  }
115
100
 
116
- async send(res: Response) {
117
- res.redirect(this.status, this.location);
101
+ async send(res: WrappedResponse) {
102
+ return res.sendRedirect(this.location);
118
103
  }
119
104
 
120
105
  }
@@ -157,10 +142,10 @@ export default class Content implements IPageResult {
157
142
  return p as Content;
158
143
  }
159
144
 
160
- public async send(res: Response, user?: SessionUser) {
145
+ public async send(res: WrappedResponse, user?: SessionUser) {
161
146
  const { status, body, contentType } = this;
162
- const r= res.setHeader("content-type", contentType)
163
- .status(status);
147
+ res.setHeader("content-type", contentType);
148
+ res.statusCode = status;
164
149
  if (typeof body === "string") {
165
150
  if (status >= 300) {
166
151
  const u = user ? `User: ${user.userID},${user.userName}` : "User: Anonymous";
package/src/Page.tsx CHANGED
@@ -2,73 +2,21 @@ import busboy from "busboy";
2
2
  import HtmlDocument from "./html/HtmlDocument.js";
3
3
  import XNode from "./html/XNode.js";
4
4
  import Content, { IPageResult, Redirect } from "./Content.js";
5
- import { ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
6
- import { Request } from "express";
7
5
  import { LocalFile } from "./core/LocalFile.js";
8
6
  import TempFolder from "./core/TempFolder.js";
9
7
  import SessionUser from "./core/SessionUser.js";
8
+ import { WrappedRequest, WrappedResponse } from "./core/Wrapped.js";
9
+ import { ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
10
10
 
11
11
  export const isPage = Symbol("isPage");
12
12
 
13
13
 
14
14
  export interface IRouteCheck {
15
+ scope: ServiceProvider;
15
16
  method: string;
16
17
  current: string;
17
18
  path: string[];
18
- sessionUser: SessionUser;
19
- params: any;
20
- }
21
-
22
- export interface IPageContext {
23
- /**
24
- * path till the current folder where this page is located, including the name of current folder itself.
25
- */
26
- currentPath: string[];
27
- /**
28
- * Path to the next children to be precessed.
29
- */
30
- childPath: string[];
31
-
32
- // /**
33
- // * List of all paths that were tried before executing this page.
34
- // */
35
- // notFoundPath: string[];
36
-
37
- /**
38
- * Query string if associated, empty object is always present.
39
- */
40
- query: any;
41
-
42
- body: any;
43
-
44
- url: string;
45
-
46
- signal:AbortSignal;
47
- /**
48
- * Request
49
- */
50
- // request: Request;
51
- /**
52
- * Response
53
- */
54
- // response: Response;
55
-
56
- /**
57
- * Request method
58
- */
59
- method: string;
60
-
61
- /**
62
- * Currently logged in user
63
- */
64
- sessionUser: SessionUser;
65
-
66
- /**
67
- * Actual file path of the page
68
- */
69
- filePath: string;
70
-
71
- disposables: Disposable[];
19
+ request: WrappedRequest;
72
20
  }
73
21
 
74
22
  export interface IFormData {
@@ -79,7 +27,7 @@ export interface IFormData {
79
27
  /**
80
28
  * Page should not contain any reference to underlying request/response objects.
81
29
  */
82
- export default class Page implements IPageContext {
30
+ export default class Page {
83
31
 
84
32
  static [isPage] = true;
85
33
 
@@ -89,10 +37,18 @@ export default class Page implements IPageContext {
89
37
  * @param pageContext page related items
90
38
  * @returns true if it can handle the path, default is true
91
39
  */
92
- static canHandle(pageContext: IRouteCheck) {
40
+ static canHandle(pageContext: IRouteCheck) : boolean | Promise<boolean> {
93
41
  return true;
94
42
  }
95
43
 
44
+ request: WrappedRequest;
45
+
46
+ response: WrappedResponse;
47
+
48
+ get params() {
49
+ return this.request?.query;
50
+ }
51
+
96
52
  signal: AbortSignal;
97
53
 
98
54
  currentPath: string[];
@@ -124,7 +80,7 @@ export default class Page implements IPageContext {
124
80
 
125
81
  disposables: Disposable[] = [];
126
82
 
127
- private formDataPromise;
83
+ private formDataPromise: Promise<IFormData>;
128
84
 
129
85
  constructor() {
130
86
  this.cacheControl = "no-cache, no-store, max-age=0";
@@ -135,13 +91,14 @@ export default class Page implements IPageContext {
135
91
  }
136
92
 
137
93
  readFormData(): Promise<IFormData> {
138
- this.formDataPromise ??= (async () => {
94
+
95
+ return this.formDataPromise ??= (async () => {
139
96
  let tempFolder: TempFolder;
140
97
  const result: IFormData = {
141
98
  fields: {},
142
99
  files: []
143
100
  };
144
- const req = (this as any).req as Request;
101
+ const req = this.request;
145
102
  const bb = busboy({ headers: req.headers , defParamCharset: "utf8" });
146
103
  const tasks = [];
147
104
  await new Promise((resolve, reject) => {
@@ -167,7 +124,6 @@ export default class Page implements IPageContext {
167
124
  await Promise.all(tasks);
168
125
  return result;
169
126
  })();
170
- return this.formDataPromise;
171
127
  }
172
128
 
173
129
 
@@ -1,18 +1,17 @@
1
1
  /* eslint-disable no-console */
2
2
  import { RegisterSingleton, ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
3
- import express, { Request, Response } from "express";
4
3
  import Page from "./Page.js";
5
4
  import Content from "./Content.js";
6
- import SessionUser from "./core/SessionUser.js";
7
5
  import RouteTree from "./core/RouteTree.js";
8
- import CookieService from "./services/CookieService.js";
9
- import cookieParser from "cookie-parser";
10
- import bodyParser from "body-parser";
11
6
  import { fileURLToPath } from "node:url";
12
7
  import { dirname, join } from "node:path";
13
- import { Server, Socket } from "socket.io";
8
+ import { Server } from "socket.io";
14
9
  import * as http from "http";
10
+ import * as http2 from "http2";
15
11
  import SocketService from "./socket/SocketService.js";
12
+ import { Wrapped, WrappedRequest, WrappedResponse } from "./core/Wrapped.js";
13
+ import { SecureContext, createSecureContext } from "node:tls";
14
+ import { SelfSigned } from "./ssl/SelfSigned.js";
16
15
 
17
16
  RegisterSingleton
18
17
  export default class ServerPages {
@@ -46,13 +45,22 @@ export default class ServerPages {
46
45
  * All services should be registered before calling build
47
46
  * @param app Express App
48
47
  */
49
- public async build(app = express(), { createSocketService = true, port = 80 } = {}) {
48
+ public async build({
49
+ createSocketService = true,
50
+ port = 8080,
51
+ protocol = "http",
52
+ disableHttp2Warning = false,
53
+ SNICallback
54
+ }:{
55
+ createSocketService?: boolean,
56
+ port: number,
57
+ disableHttp2Warning?: boolean,
58
+ protocol: "http" | "http2" | "https2",
59
+ SNICallback: (servername: string, cb: (err: Error | null, ctx?: SecureContext) => void) => void
60
+ }) {
50
61
  try {
51
- // etag must be set by individual request processors if needed.
52
- app.set("etag", false);
53
62
 
54
- app.use(cookieParser());
55
- app.use(bodyParser.json());
63
+ let httpServer = null as http.Server | http2.Http2Server | http2.Http2SecureServer;
56
64
 
57
65
  let socketServer = null as Server;
58
66
  if (createSocketService) {
@@ -61,95 +69,125 @@ export default class ServerPages {
61
69
  (ss as any).attach(socketServer);
62
70
  await (ss as any).init();
63
71
  }
64
- app.all(/./, (req, res, next) => this.process(req, res).then(next, next));
65
- return new Promise<http.Server>((resolve, reject) => {
66
- const server = app.listen(port, () => {
67
- resolve(server);
72
+
73
+ switch(protocol) {
74
+ case "http":
75
+ httpServer = http.createServer((req, res) => this.process(req, res))
76
+ break;
77
+ case "https2":
78
+ let sc = null;
79
+ SNICallback ??= (name, cb) => {
80
+ if (sc) {
81
+ return cb(null, sc);
82
+ }
83
+ const { key, cert } = SelfSigned.setupSelfSigned();
84
+ sc = createSecureContext({ key, cert });
85
+ cb(null, sc);
86
+ };
87
+ httpServer = http2.createSecureServer({
88
+ SNICallback
89
+ }, (req, res) => this.process(req, res))
90
+ break;
91
+ case "http2":
92
+ httpServer = http2.createSecureServer({
93
+ },(req, res) => this.process(req, res))
94
+ if (!disableHttp2Warning) {
95
+ console.warn("Http2 without SSL should not be used in production");
96
+ }
97
+ break;
98
+ }
99
+
100
+
101
+ await new Promise<void>((resolve, reject) => {
102
+ const server = httpServer.listen(port, () => {
103
+ resolve();
68
104
  });
69
105
  socketServer?.attach(server);
70
106
  });
107
+ return httpServer;
71
108
  } catch (error) {
72
109
  console.error(error);
73
110
  }
74
111
  return null;
75
112
  }
76
113
 
77
- protected async process(req: Request, resp: Response) {
114
+ protected async process(req: any, resp: any) {
115
+
116
+ req = Wrapped.request(req);
117
+ resp = Wrapped.response(req, resp);
118
+
119
+ req.response = resp;
78
120
 
79
121
  if((req as any).processed) {
80
122
  return;
81
123
  }
82
124
  (req as any).processed = true;
83
125
 
84
- // defaulting to no cache
85
- // static content delivery should override this
86
- resp.setHeader("cache-control", "no-cache");
87
-
88
- using scope = ServiceProvider.createScope(this);
89
- let sent = false;
90
- const acceptJson = req.accepts().some((s) => /\/json$/i.test(s));
91
126
  try {
92
127
 
93
- const cookieService = scope.resolve(CookieService);
128
+ // defaulting to no cache
129
+ // static content delivery should override this
130
+ resp.setHeader("cache-control", "no-cache");
131
+
132
+ using scope = ServiceProvider.createScope(this);
133
+ let sent = false;
134
+ const acceptJson = req.accepts().some((s) => /\/json$/i.test(s));
135
+
94
136
 
95
137
  try {
96
- await cookieService.createSessionUser(req, resp);
138
+ const path = req.path.split("/").filter((x) => x);
139
+ const method = req.method;
140
+ const { pageClass, childPath } = (await this.root.getRoute({
141
+ scope,
142
+ method,
143
+ current: "",
144
+ path,
145
+ request: req
146
+ })) ?? {
147
+ pageClass: Page,
148
+ childPath: path
149
+ };
150
+ const page = scope.create(pageClass);
151
+ page.method = method;
152
+ page.childPath = childPath;
153
+ page.request = req;
154
+ page.response = resp;
155
+ const content = await page.all(page.params);
156
+ resp.setHeader("cache-control", page.cacheControl);
157
+ resp.removeHeader("etag");
158
+ sent = true;
159
+ await content.send(resp);
97
160
  } catch (error) {
161
+ if (!sent) {
162
+ try {
163
+
164
+ if (acceptJson || error.errorModel) {
165
+ await Content.json(
166
+ {
167
+ ... error.errorModel ?? {},
168
+ message: error.message ?? error,
169
+ detail: error.stack ?? error,
170
+ }
171
+ , 500).send(resp);
172
+ return;
173
+ }
174
+
175
+ const content = Content.html(`<!DOCTYPE html>\n<html><body><pre>Server Error for ${req.url}\r\n${error?.stack ?? error}</pre></body></html>`, 500);
176
+ await content.send(resp);
177
+ } catch (e1) {
178
+ resp.send(e1.stack ?? e1, 500);
179
+ console.error(e1);
180
+ }
181
+ return;
182
+ }
98
183
  console.error(error);
99
184
  }
100
-
101
- const sessionUser = (req as any).user;
102
- scope.add(SessionUser, sessionUser);
103
- const path = req.path.split("/").filter((x) => x);
104
- const method = req.method;
105
- const params = { ... req.params, ... req.query, ... req.body ?? {} };
106
- const { pageClass, childPath } = (await this.root.getRoute({
107
- method,
108
- current: "",
109
- path,
110
- params,
111
- sessionUser
112
- })) ?? {
113
- pageClass: Page,
114
- childPath: path
115
- };
116
- const page = scope.create(pageClass);
117
- page.method = method;
118
- page.childPath = childPath;
119
- page.body = req.body;
120
- page.query = req.query;
121
- page.sessionUser = sessionUser;
122
- (page as any).req = req;
123
- (page as any).res = resp;
124
- const content = await page.all(params);
125
- resp.setHeader("cache-control", page.cacheControl);
126
- resp.removeHeader("etag");
127
- sent = true;
128
- await content.send(resp);
129
- } catch (error) {
130
- if (!sent) {
131
- try {
132
-
133
- if (acceptJson || error.errorModel) {
134
- await Content.json(
135
- {
136
- ... error.errorModel ?? {},
137
- message: error.message ?? error,
138
- detail: error.stack ?? error,
139
- }
140
- , 500).send(resp);
141
- return;
142
- }
143
-
144
- const content = Content.html(`<!DOCTYPE html>\n<html><body><pre>Server Error for ${req.url}\r\n${error?.stack ?? error}</pre></body></html>`, 500);
145
- await content.send(resp);
146
- } catch (e1) {
147
- resp.send(e1.stack ?? e1);
148
- console.error(e1);
185
+ } finally {
186
+ if(Array.isArray(req.disposables)) {
187
+ for (const iterator of req.disposables) {
188
+ iterator[Symbol.dispose]?.();
149
189
  }
150
- return;
151
190
  }
152
- console.error(error);
153
191
  }
154
192
  }
155
193
 
@@ -1,7 +1,7 @@
1
- import { createReadStream, createWriteStream, existsSync, statSync } from "fs";
1
+ import { createReadStream, createWriteStream, existsSync, read, statSync } from "fs";
2
2
  import { basename } from "path";
3
3
  import mime from "mime-types";
4
- import internal, { Stream } from "stream";
4
+ import internal, { Stream, Writable } from "stream";
5
5
  import { appendFile, open, readFile, writeFile } from "fs/promises";
6
6
 
7
7
 
@@ -54,6 +54,15 @@ export class LocalFile {
54
54
  return await readFile(this.path, { flag: "r" });
55
55
  }
56
56
 
57
+ public async writeTo(writable: Writable, start?: number, end?: number) {
58
+ const readable = createReadStream(this.path, { start, end });
59
+ return new Promise((resolve, reject) => {
60
+ readable.pipe(writable, { end: true })
61
+ .on("end", resolve)
62
+ .on("error", reject);
63
+ });
64
+ }
65
+
57
66
  public async delete() {
58
67
  return this.onDispose?.();
59
68
  }
@@ -60,7 +60,7 @@ export default class RouteTree {
60
60
  const pageClassPromise = (this.handler[method] ??= this.handler[method.toLowerCase()]) ?? this.handler["index"];
61
61
  if (pageClassPromise) {
62
62
  const pageClass = await pageClassPromise;
63
- if(pageClass.canHandle(rc)) {
63
+ if(await pageClass.canHandle(rc)) {
64
64
  return { pageClass, childPath: rc.path };
65
65
  }
66
66
  }
@@ -1,8 +1,8 @@
1
- import { Request, Response } from "express";
2
1
  import EntityAccessError from "@entity-access/entity-access/dist/common/EntityAccessError.js";
3
2
  import { RegisterScoped } from "@entity-access/entity-access/dist/di/di.js";
4
3
  import DateTime from "@entity-access/entity-access/dist/types/DateTime.js";
5
4
  import TokenService, { IAuthCookie } from "../services/TokenService.js";
5
+ import { WrappedResponse } from "./Wrapped.js";
6
6
 
7
7
  const secure = (process.env["SOCIAL_MAIL_AUTH_COOKIE_SECURE"] ?? "true") === "true";
8
8
 
@@ -48,7 +48,7 @@ export default class SessionUser {
48
48
  }
49
49
 
50
50
  constructor(
51
- private resp: Response,
51
+ private resp: WrappedResponse,
52
52
  private cookieName: string,
53
53
  private tokenService: TokenService
54
54
  ) {}