webhoster 0.1.0 → 0.3.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.
Files changed (86) hide show
  1. package/.eslintrc.json +74 -58
  2. package/.github/copilot-instructions.md +100 -0
  3. package/.github/workflows/test-matrix.yml +37 -0
  4. package/.test/benchmark.js +28 -0
  5. package/.test/constants.js +4 -0
  6. package/{test → .test}/http2server.js +1 -1
  7. package/{test → .test}/httpserver.js +1 -1
  8. package/{test → .test}/index.js +178 -192
  9. package/.test/multipromise.js +32 -0
  10. package/{test → .test}/tls.js +3 -3
  11. package/{test → .test}/urlencoded.js +3 -0
  12. package/.vscode/launch.json +24 -3
  13. package/README.md +116 -90
  14. package/data/CookieObject.js +14 -14
  15. package/errata/socketio.js +6 -11
  16. package/examples/starter.js +11 -0
  17. package/helpers/HeadersParser.js +7 -8
  18. package/helpers/HttpListener.js +387 -42
  19. package/helpers/RequestHeaders.js +43 -36
  20. package/helpers/RequestReader.js +27 -26
  21. package/helpers/ResponseHeaders.js +47 -36
  22. package/jsconfig.json +1 -1
  23. package/lib/HttpHandler.js +447 -277
  24. package/lib/HttpRequest.js +383 -39
  25. package/lib/HttpResponse.js +316 -52
  26. package/lib/HttpTransaction.js +146 -0
  27. package/middleware/AutoHeadersMiddleware.js +73 -0
  28. package/middleware/CORSMiddleware.js +45 -47
  29. package/middleware/CaseInsensitiveHeadersMiddleware.js +5 -11
  30. package/middleware/ContentDecoderMiddleware.js +81 -35
  31. package/middleware/ContentEncoderMiddleware.js +179 -132
  32. package/middleware/ContentLengthMiddleware.js +66 -43
  33. package/middleware/ContentWriterMiddleware.js +5 -11
  34. package/middleware/HashMiddleware.js +68 -40
  35. package/middleware/HeadMethodMiddleware.js +24 -22
  36. package/middleware/MethodMiddleware.js +29 -36
  37. package/middleware/PathMiddleware.js +49 -66
  38. package/middleware/ReadFormData.js +99 -0
  39. package/middleware/SendHeadersMiddleware.js +0 -2
  40. package/middleware/SendJsonMiddleware.js +131 -0
  41. package/middleware/SendStringMiddleware.js +87 -0
  42. package/package.json +38 -29
  43. package/polyfill/FormData.js +164 -0
  44. package/rollup.config.js +0 -1
  45. package/scripts/test-all-sync.sh +6 -0
  46. package/scripts/test-all.sh +7 -0
  47. package/templates/starter.js +53 -0
  48. package/test/fixtures/stream.js +68 -0
  49. package/test/helpers/HttpListener/construct.js +18 -0
  50. package/test/helpers/HttpListener/customOptions.js +22 -0
  51. package/test/helpers/HttpListener/doubleCreate.js +40 -0
  52. package/test/helpers/HttpListener/events.js +77 -0
  53. package/test/helpers/HttpListener/http.js +31 -0
  54. package/test/helpers/HttpListener/http2.js +41 -0
  55. package/test/helpers/HttpListener/https.js +38 -0
  56. package/test/helpers/HttpListener/startAll.js +31 -0
  57. package/test/helpers/HttpListener/stopNotStarted.js +23 -0
  58. package/test/lib/HttpHandler/class.js +8 -0
  59. package/test/lib/HttpHandler/handleRequest.js +11 -0
  60. package/test/lib/HttpHandler/middleware.js +941 -0
  61. package/test/lib/HttpHandler/parse.js +41 -0
  62. package/test/lib/HttpRequest/class.js +8 -0
  63. package/test/lib/HttpRequest/downstream.js +171 -0
  64. package/test/lib/HttpRequest/properties.js +101 -0
  65. package/test/lib/HttpRequest/read.js +518 -0
  66. package/test/lib/HttpResponse/class.js +8 -0
  67. package/test/lib/HttpResponse/properties.js +59 -0
  68. package/test/lib/HttpResponse/send.js +275 -0
  69. package/test/lib/HttpTransaction/class.js +8 -0
  70. package/test/lib/HttpTransaction/ping.js +50 -0
  71. package/test/lib/HttpTransaction/push.js +89 -0
  72. package/test/middleware/SendJsonMiddleware.js +222 -0
  73. package/test/sanity.js +10 -0
  74. package/test/templates/starter.js +93 -0
  75. package/tsconfig.json +12 -0
  76. package/types/index.js +61 -34
  77. package/types/typings.d.ts +8 -9
  78. package/utils/AsyncObject.js +6 -3
  79. package/utils/CaseInsensitiveObject.js +2 -3
  80. package/utils/function.js +1 -7
  81. package/utils/headers.js +42 -0
  82. package/utils/qualityValues.js +1 -1
  83. package/utils/stream.js +4 -20
  84. package/index.cjs +0 -3200
  85. package/test/constants.js +0 -4
  86. /package/{test → .test}/cookietester.js +0 -0
@@ -1,7 +1,5 @@
1
1
  /** @typedef {import('../types').IMiddleware} IMiddleware */
2
2
  /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
3
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
4
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
5
3
  /** @typedef {import('../types').RequestMethod} RequestMethod */
6
4
 
7
5
  /**
@@ -15,14 +13,13 @@
15
13
  * Indicates which methods are supported by the response’s URL for the purposes of the CORS protocol.
16
14
  * @prop {string[]} [allowHeaders]
17
15
  * Indicates which headers are supported by the response’s URL for the purposes of the CORS protocol.
18
- * @prop {number} [maxAge]
16
+ * @prop {number} [maxAge=5]
19
17
  * Indicates the number of seconds (5 by default) the information provided by the
20
18
  * `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers can be cached.
21
19
  * @prop {string[]} [exposeHeaders]
22
20
  * Indicates which headers can be exposed as part of the response by listing their names.
23
21
  */
24
22
 
25
- /** @implements {IMiddleware} */
26
23
  export default class CORSMiddleware {
27
24
  /** @param {CORSMiddlewareOptions} [options] */
28
25
  constructor(options = {}) {
@@ -34,66 +31,67 @@ export default class CORSMiddleware {
34
31
  this.exposeHeaders = options.exposeHeaders;
35
32
  }
36
33
 
37
- /**
38
- * @param {MiddlewareFunctionParams} params
39
- * @return {MiddlewareFunctionResult}
40
- */
41
- execute({ req, res }) {
42
- if (('origin' in req.headers) === false) {
43
- // not CORS
44
- return 'continue';
34
+ static OK_BUFFER = Buffer.from('OK', 'ascii');
35
+
36
+ static ACCESS_CONTROL_ALLOW_HEADERS_ALL = [
37
+ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'TRACE', 'PATCH',
38
+ ].join(',');
39
+
40
+ /** @type {MiddlewareFunction} */
41
+ execute({ request, response }) {
42
+ if (('origin' in request.headers) === false) {
43
+ // not CORS
44
+ return true; // CONTINUE
45
45
  }
46
+
47
+ // CORS Request
46
48
  if (!this.allowOrigin) {
47
- // Unspecified default of '*'
48
- res.headers['access-control-allow-origin'] = '*';
49
+ // Unspecified default of '*'
50
+ response.headers['access-control-allow-origin'] = '*';
49
51
  } else {
50
- this.allowOrigin.some((origin) => {
52
+ for (const origin of this.allowOrigin) {
51
53
  if (origin === '*') {
52
- res.headers['access-control-allow-origin'] = '*';
53
- return true;
54
+ response.headers['access-control-allow-origin'] = '*';
55
+ break;
54
56
  }
55
57
  if (typeof origin === 'string') {
56
- if (req.headers.origin?.toLowerCase() === origin.toLowerCase()) {
57
- res.headers['access-control-allow-origin'] = req.headers.origin;
58
- return true;
58
+ if (request.headers.origin?.toLowerCase() === origin.toLowerCase()) {
59
+ response.headers['access-control-allow-origin'] = request.headers.origin;
60
+ break;
59
61
  }
60
- return false;
62
+ } else if (origin.test(request.headers.origin)) {
63
+ response.headers['access-control-allow-origin'] = request.headers.origin;
64
+ break;
61
65
  }
62
- if (origin.test(req.headers.origin)) {
63
- res.headers['access-control-allow-origin'] = req.headers.origin;
64
- return true;
65
- }
66
- return false;
67
- });
66
+ }
68
67
  }
68
+
69
69
  if (this.allowCredentials) {
70
- res.headers['access-control-allow-credentials'] = 'true';
70
+ response.headers['access-control-allow-credentials'] = 'true';
71
71
  }
72
- if (req.method === 'OPTIONS') {
73
- if (this.allowMethods) {
74
- res.headers['access-control-allow-methods'] = this.allowMethods.join(',');
75
- } else {
76
- res.headers['access-control-allow-methods'] = [
77
- 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'TRACE', 'PATCH',
78
- ].join(',');
79
- }
80
- if (this.allowHeaders) {
81
- res.headers['access-control-allow-headers'] = this.allowHeaders.join(',');
82
- } else {
83
- res.headers['access-control-allow-headers'] = req.headers['access-control-request-headers'];
84
- }
72
+
73
+ if (request.method === 'OPTIONS') {
74
+ response.headers['access-control-allow-methods'] = this.allowMethods
75
+ ? this.allowMethods.join(',')
76
+ : CORSMiddleware.ACCESS_CONTROL_ALLOW_HEADERS_ALL;
77
+ response.headers['access-control-allow-headers'] = this.allowHeaders
78
+ ? this.allowHeaders.join(',')
79
+ : request.headers['access-control-request-headers'];
85
80
  if (this.maxAge != null) {
86
- res.headers['access-control-max-age'] = this.maxAge.toString(10);
81
+ response.headers['access-control-max-age'] = this.maxAge.toString(10);
87
82
  }
88
83
  // 200 instead of 204 for compatibility
89
- res.status = 200;
90
- res.stream.end('OK');
91
- return 'end';
84
+ // Manual handling for faster response
85
+ response.status = 200;
86
+ response.headers['content-length'] = '0';
87
+ response.sendHeaders(true, true);
88
+ return 0; // END
92
89
  }
93
90
 
91
+ // Non-CORS-preflight request
94
92
  if (this.exposeHeaders) {
95
- res.headers['access-control-expose-headers'] = this.exposeHeaders.join(',');
93
+ response.headers['access-control-expose-headers'] = this.exposeHeaders.join(',');
96
94
  }
97
- return 'continue';
95
+ return true; // CONTINUE
98
96
  }
99
97
  }
@@ -1,8 +1,7 @@
1
1
  import CaseInsensitiveObject from '../utils/CaseInsensitiveObject.js';
2
2
 
3
3
  /** @typedef {import('../types').IMiddleware} IMiddleware */
4
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
5
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
4
+ /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
6
5
 
7
6
  /**
8
7
  * @typedef {Object} CaseInsensitiveHeadersMiddlewareOptions
@@ -10,7 +9,6 @@ import CaseInsensitiveObject from '../utils/CaseInsensitiveObject.js';
10
9
  * @prop {boolean} [response=false] Mutate response headers to be case-insensistive
11
10
  */
12
11
 
13
- /** @implements {IMiddleware} */
14
12
  export default class CaseInsensitiveHeadersMiddleware {
15
13
  /** @param {CaseInsensitiveHeadersMiddlewareOptions} options */
16
14
  constructor(options) {
@@ -18,19 +16,15 @@ export default class CaseInsensitiveHeadersMiddleware {
18
16
  this.response = options.response === true;
19
17
  }
20
18
 
21
- /**
22
- * @param {!MiddlewareFunctionParams} params
23
- * @return {MiddlewareFunctionResult}
24
- */
25
- execute({ req, res }) {
19
+ /** @type {MiddlewareFunction} */
20
+ execute({ request, response }) {
26
21
  if (this.request) {
27
22
  // @ts-ignore Coerce
28
- req.headers = new CaseInsensitiveObject(req.headers || {});
23
+ request.headers = new CaseInsensitiveObject(request.headers || {});
29
24
  }
30
25
  if (this.response) {
31
26
  // @ts-ignore Coerce
32
- res.headers = new CaseInsensitiveObject(res.headers || {});
27
+ response.headers = new CaseInsensitiveObject(response.headers || {});
33
28
  }
34
- return 'continue';
35
29
  }
36
30
  }
@@ -1,10 +1,9 @@
1
- import { PassThrough, Transform } from 'stream';
2
- import { createBrotliDecompress, createGunzip, createInflate } from 'zlib';
1
+ import { Transform } from 'node:stream';
2
+ import {
3
+ BrotliDecompress, Gunzip, Inflate,
4
+ } from 'node:zlib';
3
5
 
4
- /** @typedef {import('../types').IMiddleware} IMiddleware */
5
6
  /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
6
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
7
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
8
7
 
9
8
  /**
10
9
  * @typedef ContentDecoderMiddlewareOptions
@@ -12,10 +11,11 @@ import { createBrotliDecompress, createGunzip, createInflate } from 'zlib';
12
11
  * @prop {boolean} [respondNotAcceptable=false]
13
12
  */
14
13
 
14
+ const CONTINUE = true;
15
+
15
16
  /**
16
17
  * Implements `Accept-Encoding`
17
18
  * https://tools.ietf.org/html/rfc7231#section-5.3.4
18
- * @implements {IMiddleware}
19
19
  */
20
20
  export default class ContentDecoderMiddleware {
21
21
  /** @param {ContentDecoderMiddlewareOptions} [options] */
@@ -24,68 +24,114 @@ export default class ContentDecoderMiddleware {
24
24
  this.respondNotAcceptable = options.respondNotAcceptable === true;
25
25
  }
26
26
 
27
- /**
28
- * @param {!MiddlewareFunctionParams} params
29
- * @return {MiddlewareFunctionResult}
30
- */
31
- execute({ req, res }) {
32
- switch (req.method) {
27
+ /** @type {MiddlewareFunction} */
28
+ execute({ request, response }) {
29
+ switch (request.method) {
33
30
  case 'HEAD':
34
31
  case 'GET':
35
- return 'continue';
32
+ return CONTINUE;
36
33
  default:
37
34
  }
38
35
 
39
- res.headers['accept-encoding'] = 'gzip, deflate, br';
40
- const contentEncoding = (req.headers['content-encoding'] ?? '').trim().toLowerCase();
36
+ // TODO: Use transforms
37
+
38
+ response.headers['accept-encoding'] = 'gzip, deflate, br';
39
+ const contentEncoding = request.headers['content-encoding'];
40
+ if (!contentEncoding) return CONTINUE;
41
41
 
42
- switch (contentEncoding) {
42
+ switch (contentEncoding.trim().toLowerCase()) {
43
43
  case '':
44
44
  case 'identity':
45
- return 'continue';
45
+ return CONTINUE;
46
46
  case 'gzip':
47
47
  case 'br':
48
48
  case 'deflate':
49
49
  break;
50
50
  default:
51
51
  if (this.respondNotAcceptable) {
52
- res.status = 406;
53
- return 'end';
52
+ return 406;
54
53
  }
55
- return 'continue';
54
+ return CONTINUE;
56
55
  }
57
56
 
58
- const source = req.stream;
57
+ /** @type {import('stream').Readable} */
58
+ let inputStream;
59
+
60
+ // Don't built gZipStream until a read request is made
61
+ // By default, newDownstream <= inputStream
62
+ // On first read, newDownstream <= gZipStream <= inputStream
63
+ // Read request is intercepted by newDownstream
64
+
65
+ /** @type {import("zlib").Gunzip} */
66
+ let gzipStream;
59
67
  let initialized = false;
60
- const { chunkSize } = this;
61
- const newReadable = new PassThrough({
62
- read(...args) {
68
+
69
+ const gzipOptions = { chunkSize: this.chunkSize };
70
+ const newDownstream = new Transform({
71
+
72
+ read: (...args) => {
63
73
  if (!initialized) {
64
- /** @type {import("zlib").Gzip} */
65
- let gzipStream;
74
+ /** @type {import("zlib").Gzip} */
66
75
  switch (contentEncoding) {
67
76
  case 'deflate':
68
- gzipStream = createInflate({ chunkSize });
77
+ gzipStream = new Inflate(gzipOptions);
69
78
  break;
70
79
  case 'gzip':
71
- gzipStream = createGunzip({ chunkSize });
80
+ gzipStream = new Gunzip(gzipOptions);
72
81
  break;
73
82
  case 'br':
74
- gzipStream = createBrotliDecompress({ chunkSize });
83
+ gzipStream = new BrotliDecompress(gzipOptions);
75
84
  break;
76
85
  default:
77
86
  throw new Error('UNKNOWN_ENCODING');
78
87
  }
79
- source.pipe(gzipStream).pipe(this);
88
+ // From newDownstream <= inputStream
89
+ // To newDownstream <= gzipStream < =inputStream
90
+
91
+ // Forward errors
92
+ gzipStream.on('error', (err) => inputStream.emit('error', err));
93
+ gzipStream.on('data', (chunk) => newDownstream.push(chunk));
94
+
95
+ inputStream.on('end', () => gzipStream.end());
96
+ gzipStream.on('end', () => {
97
+ newDownstream.push(null);
98
+ if (newDownstream.readable) {
99
+ newDownstream.end();
100
+ }
101
+ });
102
+
103
+ if (inputStream.pause()) inputStream.resume();
80
104
  initialized = true;
81
105
  }
82
- if (source.isPaused()) source.resume();
83
- // eslint-disable-next-line no-underscore-dangle
106
+
84
107
  Transform.prototype._read.call(this, ...args);
85
108
  },
109
+ transform: (chunk, chunkEncoding, callback) => {
110
+ gzipStream.write(chunk, (err) => {
111
+ if (err) console.error(err);
112
+ callback(err);
113
+ });
114
+ },
115
+ flush: (callback) => {
116
+ if (gzipStream) {
117
+ gzipStream.flush(() => {
118
+ callback();
119
+ });
120
+ }
121
+ },
122
+ final: (callback) => {
123
+ if (gzipStream) {
124
+ gzipStream.end();
125
+ gzipStream.flush(() => {
126
+ callback();
127
+ });
128
+ }
129
+ },
86
130
  });
87
- source.pause();
88
- req.replaceStream(newReadable);
89
- return 'continue';
131
+
132
+ newDownstream.tag = 'ContentDecoder';
133
+ inputStream = request.addDownstream(newDownstream, { autoPause: true });
134
+
135
+ return CONTINUE;
90
136
  }
91
137
  }