webhoster 0.1.1 → 0.3.1

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 (87) 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 -41
  33. package/middleware/ContentWriterMiddleware.js +5 -5
  34. package/middleware/HashMiddleware.js +68 -40
  35. package/middleware/HeadMethodMiddleware.js +24 -21
  36. package/middleware/MethodMiddleware.js +29 -36
  37. package/middleware/PathMiddleware.js +49 -66
  38. package/middleware/ReadFormData.js +99 -0
  39. package/middleware/SendJsonMiddleware.js +131 -0
  40. package/middleware/SendStringMiddleware.js +87 -0
  41. package/package.json +38 -29
  42. package/polyfill/FormData.js +164 -0
  43. package/rollup.config.js +0 -1
  44. package/scripts/check-teapot.mjs +40 -0
  45. package/scripts/test-all-sync.sh +6 -0
  46. package/scripts/test-all.sh +7 -0
  47. package/templates/starter.js +55 -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/error-teapot.js +47 -0
  75. package/test/templates/starter.js +93 -0
  76. package/tsconfig.json +12 -0
  77. package/types/index.js +61 -34
  78. package/types/typings.d.ts +8 -9
  79. package/utils/AsyncObject.js +6 -3
  80. package/utils/CaseInsensitiveObject.js +2 -3
  81. package/utils/function.js +1 -7
  82. package/utils/headers.js +42 -0
  83. package/utils/qualityValues.js +1 -1
  84. package/utils/stream.js +4 -20
  85. package/index.cjs +0 -3190
  86. package/test/constants.js +0 -4
  87. /package/{test → .test}/cookietester.js +0 -0
@@ -1,81 +1,109 @@
1
- import crypto from 'crypto';
2
- import { Transform } from 'stream';
1
+ import { createHash } from 'node:crypto';
2
+ import { Transform } from 'node:stream';
3
3
 
4
- /** @typedef {import('../types').IMiddleware} IMiddleware */
5
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
6
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
7
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
4
+ /** @typedef {import('node:crypto').BinaryToTextEncoding} BinaryToTextEncoding */
5
+ /** @typedef {import('../lib/HttpRequest.js').default} HttpRequest */
6
+ /** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
7
+ /** @typedef {import('../types/index.js').MiddlewareFunction} MiddlewareFunction */
8
+ /** @typedef {import('../types/index.js').ResponseFinalizer} ResponseFinalizer */
8
9
 
9
10
  const DEFAULT_ALGORITHM = 'sha1';
10
- /** @type {crypto.HexBase64Latin1Encoding} */
11
+ /** @type {BinaryToTextEncoding} */
11
12
  const DEFAULT_DIGEST = 'base64';
12
13
 
13
14
  /**
14
15
  * @typedef {Object} HashMiddlewareOptions
15
16
  * @prop {'md5'|'sha1'|'sha256'|'sha512'} [algorithm=DEFAULT_ALGORITHM]
16
- * @prop {crypto.HexBase64Latin1Encoding} [digest=DEFAULT_DIGEST]
17
+ * @prop {BinaryToTextEncoding} [digest=DEFAULT_DIGEST]
17
18
  */
18
19
 
19
- /** @implements {IMiddleware} */
20
20
  export default class HashMiddleware {
21
21
  /** @param {HashMiddlewareOptions} options */
22
22
  constructor(options = {}) {
23
23
  this.algorithm = options.algorithm || DEFAULT_ALGORITHM;
24
24
  this.digest = options.digest || DEFAULT_DIGEST;
25
+ this.finalizeResponse = this.finalizeResponse.bind(this);
25
26
  }
26
27
 
27
28
  /**
28
- * @param {MiddlewareFunctionParams} params
29
- * @return {MiddlewareFunctionResult}
29
+ * @param {HttpResponse} response
30
+ * @return {void}
30
31
  */
31
- execute({ res }) {
32
+ addTransformStream(response) {
33
+ if (response.headers.etag != null || response.headers.digest != null) return;
34
+
32
35
  const { algorithm, digest } = this;
33
36
  let hasData = false;
34
37
  let length = 0;
35
38
  let abort = false;
36
- const hashStream = crypto.createHash(algorithm);
37
- const newWritable = new Transform({
39
+ const hashStream = createHash(algorithm);
40
+ response.pipes.push(new Transform({
41
+ objectMode: true,
38
42
  transform(chunk, encoding, callback) {
39
43
  length += chunk.length;
40
44
  hasData = true;
41
- if (!abort && res.headersSent) {
42
- abort = true;
43
- hashStream.destroy();
44
- }
45
- if (abort) {
46
- callback(null, chunk);
47
- return;
48
- }
49
- // Manually pipe
50
- const needsDrain = !hashStream.write(chunk);
51
- if (needsDrain) {
52
- hashStream.once('drain', () => {
53
- callback(null, chunk);
54
- });
55
- } else {
56
- callback(null, chunk);
45
+
46
+ if (!abort) {
47
+ if (response.headersSent) {
48
+ abort = true;
49
+ hashStream.destroy();
50
+ } else {
51
+ // Manually pipe
52
+ const isSync = hashStream.write(chunk, (error) => {
53
+ if (!isSync) {
54
+ callback(error, chunk);
55
+ }
56
+ });
57
+ if (!isSync) return;
58
+ }
57
59
  }
60
+ callback(null, chunk);
58
61
  },
59
- flush(callback) {
60
- if (!abort && hasData && res.status !== 206 && !res.headersSent) {
62
+ final(callback) {
63
+ if (!abort && hasData && response.status !== 206 && !response.headersSent) {
61
64
  const hash = hashStream.digest(digest);
62
65
  // https://tools.ietf.org/html/rfc7232#section-2.3
63
- if (res.headers.etag == null) {
64
- res.headers.etag = `${algorithm === 'md5' ? 'W/' : ''}"${length.toString(16)}-${hash}"`;
66
+ if (response.headers.etag == null) {
67
+ response.headers.etag = `${algorithm === 'md5' ? 'W/' : ''}"${length.toString(16)}-${hash}"`;
65
68
  }
66
69
  if (digest === 'base64') {
67
- res.headers.digest = `${algorithm}=${hash}`;
70
+ response.headers.digest = `${algorithm}=${hash}`;
68
71
  if ((algorithm === 'md5')) {
69
- res.headers['content-md5'] = hash;
72
+ response.headers['content-md5'] = hash;
70
73
  }
71
74
  }
72
75
  }
73
76
  callback();
74
77
  },
75
- });
78
+ }));
79
+ }
80
+
81
+ /** @type {ResponseFinalizer} */
82
+ finalizeResponse(response) {
83
+ if (response.status === 206 || response.body == null) return;
84
+ if (response.isStreaming) {
85
+ this.addTransformStream(response);
86
+ return;
87
+ }
88
+ if (!Buffer.isBuffer(response.body) || response.body.byteLength === 0) return;
89
+
90
+ const { algorithm, digest } = this;
91
+ const hash = createHash(algorithm).update(response.body).digest(digest);
92
+
93
+ // https://tools.ietf.org/html/rfc7232#section-2.3
94
+ if (response.headers.etag == null) {
95
+ response.headers.etag = `${algorithm === 'md5' ? 'W/' : ''}"${response.body.byteLength.toString(16)}-${hash}"`;
96
+ }
97
+ if (digest === 'base64') {
98
+ response.headers.digest = `${algorithm}=${hash}`;
99
+ if ((algorithm === 'md5')) {
100
+ response.headers['content-md5'] = hash;
101
+ }
102
+ }
103
+ }
76
104
 
77
- const destination = res.replaceStream(newWritable);
78
- newWritable.pipe(destination);
79
- return 'continue';
105
+ /** @type {MiddlewareFunction} */
106
+ execute({ response }) {
107
+ response.finalizers.push(this.finalizeResponse);
80
108
  }
81
109
  }
@@ -1,29 +1,32 @@
1
- import { PassThrough } from 'stream';
1
+ /** @typedef {import('../types/index.js').HttpTransaction} HttpTransaction */
2
+ /** @typedef {import('../types/index.js').MiddlewareFunction} MiddlewareFunction */
3
+ /** @typedef {import('../types/index.js').ResponseFinalizer} ResponseFinalizer */
2
4
 
3
- /** @typedef {import('../types').IMiddleware} IMiddleware */
4
- /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
5
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
6
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
5
+ import { Transform } from 'node:stream';
7
6
 
8
- /** @implements {IMiddleware} */
9
7
  export default class HeadMethodMiddleware {
10
8
  constructor() {
11
- this.execute = HeadMethodMiddleware.execute.bind(this);
9
+ this.execute = HeadMethodMiddleware.Execute.bind(this);
12
10
  }
13
11
 
14
- /**
15
- * @param {MiddlewareFunctionParams} params
16
- * @return {MiddlewareFunctionResult}
17
- */
18
- static execute({ req, res }) {
19
- if (req.method !== 'HEAD') {
20
- return 'continue';
12
+ /** @type {ConstructorParameters<typeof Transform>[0]} */
13
+ static DEFAULT_TRANSFORM_OPTIONS = {
14
+ objectMode: true,
15
+ transform(chunk, encoding, callback) { callback(); },
16
+ };
17
+
18
+ /** @type {ResponseFinalizer} */
19
+ static FinalizeResponse(response) {
20
+ if (response.isStreaming) {
21
+ response.pipes.push(new Transform(HeadMethodMiddleware.DEFAULT_TRANSFORM_OPTIONS));
22
+ return;
21
23
  }
22
- const newWritable = new PassThrough({});
23
- const destination = res.replaceStream(newWritable);
24
- newWritable.on('end', () => {
25
- destination.end();
26
- });
27
- return 'continue';
24
+ response.body = null;
25
+ }
26
+
27
+ /** @type {MiddlewareFunction} */
28
+ static Execute({ request, response }) {
29
+ if (request.method !== 'HEAD') return;
30
+ response.finalizers.push(HeadMethodMiddleware.FinalizeResponse);
28
31
  }
29
- }
32
+ }
@@ -1,8 +1,5 @@
1
- /** @typedef {import('../lib').HttpRequest} HttpRequest */
2
- /** @typedef {import('../types').IMiddleware} IMiddleware */
1
+ /** @typedef {import('../types').HttpTransaction} HttpTransaction */
3
2
  /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
4
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
5
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
6
3
  /** @typedef {import('../types').RequestMethod} RequestMethod */
7
4
 
8
5
  /** @typedef {RegExp|RequestMethod} MethodEntry */
@@ -12,7 +9,6 @@
12
9
  * @prop {MethodEntry|MethodEntry[]} method
13
10
  */
14
11
 
15
- /** @implements {IMiddleware} */
16
12
  export default class MethodMiddleware {
17
13
  /** @param {MethodMiddlewareOptions|MethodEntry|MethodEntry[]} options */
18
14
  constructor(options) {
@@ -25,21 +21,9 @@ export default class MethodMiddleware {
25
21
  }
26
22
  }
27
23
 
28
- /** @type {Map<RequestMethod, MethodMiddleware>} */
24
+ /** @type {Map<MethodEntry, MethodMiddleware>} */
29
25
  static cache = new Map();
30
26
 
31
- /**
32
- * @param {RequestMethod} name
33
- * @return {MethodMiddleware}
34
- */
35
- static byMethod(name) {
36
- let m = MethodMiddleware.cache.get(name);
37
- if (m) return m;
38
- m = new MethodMiddleware(name);
39
- MethodMiddleware.cache.set(name, m);
40
- return m;
41
- }
42
-
43
27
  /**
44
28
  * @param {RequestMethod} method
45
29
  * @param {RegExp | string} input
@@ -52,34 +36,43 @@ export default class MethodMiddleware {
52
36
  return input.test(method) === true;
53
37
  }
54
38
 
55
- /**
56
- * @param {MiddlewareFunctionParams} params
57
- * @return {MiddlewareFunctionResult}
58
- */
59
- execute({ req }) {
60
- for (let i = 0; i < this.method.length; i++) {
61
- if (MethodMiddleware.test(req.method, this.method[i])) {
62
- return 'continue';
39
+ /** @type {MiddlewareFunction} */
40
+ execute({ request }) {
41
+ for (const method of this.method) {
42
+ if (MethodMiddleware.test(request.method, method)) {
43
+ return true;
63
44
  }
64
45
  }
65
- return 'break';
46
+ return false;
66
47
  }
67
48
 
68
- static get CONNECT() { return MethodMiddleware.byMethod('CONNECT'); }
49
+ /** @type {MiddlewareFunction} */
50
+ static CONNECT({ request }) { return request.method === 'CONNECT'; }
51
+
52
+ /** @type {MiddlewareFunction} */
53
+ static DELETE({ request }) { return request.method === 'DELETE'; }
69
54
 
70
- static get DELETE() { return MethodMiddleware.byMethod('DELETE'); }
55
+ /** @type {MiddlewareFunction} */
56
+ static HEADORGET({ request }) { return request.method === 'HEAD' || request.method === 'GET'; }
71
57
 
72
- static get GET() { return MethodMiddleware.byMethod('GET'); }
58
+ /** @type {MiddlewareFunction} */
59
+ static GET({ request }) { return request.method === 'GET'; }
73
60
 
74
- static get OPTIONS() { return MethodMiddleware.byMethod('OPTIONS'); }
61
+ /** @type {MiddlewareFunction} */
62
+ static OPTIONS({ request }) { return request.method === 'OPTIONS'; }
75
63
 
76
- static get HEAD() { return MethodMiddleware.byMethod('HEAD'); }
64
+ /** @type {MiddlewareFunction} */
65
+ static HEAD({ request }) { return request.method === 'HEAD'; }
77
66
 
78
- static get PATCH() { return MethodMiddleware.byMethod('PATCH'); }
67
+ /** @type {MiddlewareFunction} */
68
+ static PATCH({ request }) { return request.method === 'PATCH'; }
79
69
 
80
- static get POST() { return MethodMiddleware.byMethod('POST'); }
70
+ /** @type {MiddlewareFunction} */
71
+ static POST({ request }) { return request.method === 'POST'; }
81
72
 
82
- static get PUT() { return MethodMiddleware.byMethod('PUT'); }
73
+ /** @type {MiddlewareFunction} */
74
+ static PUT({ request }) { return request.method === 'PUT'; }
83
75
 
84
- static get TRACE() { return MethodMiddleware.byMethod('TRACE'); }
76
+ /** @type {MiddlewareFunction} */
77
+ static TRACE({ request }) { return request.method === 'TRACE'; }
85
78
  }
@@ -1,10 +1,8 @@
1
- import { join as joinPath, relative as relativePath } from 'path';
1
+ import { posix } from 'node:path';
2
2
 
3
- /** @typedef {import('../types').HttpRequest} HttpRequest */
3
+ /** @typedef {import('../types').HttpTransaction} HttpTransaction */
4
4
  /** @typedef {import('../types').IMiddleware} IMiddleware */
5
5
  /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
6
- /** @typedef {import('../types').MiddlewareFunctionParams} MiddlewareFunctionParams */
7
- /** @typedef {import('../types').MiddlewareFunctionResult} MiddlewareFunctionResult */
8
6
  /** @typedef {import('../types').RequestMethod} RequestMethod */
9
7
 
10
8
  /** @typedef {RegExp|string} PathEntry */
@@ -24,42 +22,20 @@ import { join as joinPath, relative as relativePath } from 'path';
24
22
  /**
25
23
  * @typedef {Object} PathMiddlewareOptions
26
24
  * @prop {PathEntry|PathEntry[]} [path]
27
- * @prop {string} [key='path']
28
25
  * @prop {boolean} [absolute=false]
29
26
  * Path is not relative to previous PathMiddleware. Defaults to `false`.
30
27
  * @prop {boolean} [subPath=false]
31
28
  * Path values are subpaths. Default to `false`;
32
29
  */
33
30
 
34
- /** @implements {IMiddleware} */
35
31
  export default class PathMiddleware {
36
- /** @param {PathMiddlewareOptions|PathEntry|PathEntry[]} options */
37
- constructor(options) {
38
- if (Array.isArray(options)) {
39
- this.path = options;
40
- this.key = 'path';
41
- this.absolute = false;
42
- this.subPath = false;
43
- } else if (typeof options === 'string' || options instanceof RegExp) {
44
- this.path = [options];
45
- this.key = 'path';
46
- this.absolute = false;
47
- this.subPath = false;
48
- } else {
49
- this.path = Array.isArray(options.path) ? options.path : [options.path];
50
- this.key = options.key || 'path';
51
- this.absolute = options.absolute === true;
52
- this.subPath = options.subPath === true;
53
- }
54
- }
55
-
56
32
  /**
57
33
  * @param {PathEntry|PathEntry[]} entry
58
34
  */
59
35
  static SUBPATH(entry) {
60
36
  const path = Array.isArray(entry) ? entry : [entry];
61
37
  return new PathMiddleware({
62
- path: path.map((p) => (typeof p === 'string' ? RegExp(`^(${p})/*.*$`) : p)),
38
+ path: path.map((p) => (typeof p === 'string' ? new RegExp(`^(${p})($|(/.*$))`) : p)),
63
39
  subPath: true,
64
40
  });
65
41
  }
@@ -82,78 +58,85 @@ export default class PathMiddleware {
82
58
  }
83
59
 
84
60
  /**
85
- * @param {HttpRequest} req
61
+ * @param {HttpTransaction} transaction
86
62
  * @param {string} base new base subpath
87
- * @param {number[]} treeIndex this node's treeIndex
88
63
  * @param {string} currentPath
89
64
  * @return {void}
90
65
  */
91
- writePathState(req, base, treeIndex, currentPath) {
92
- /** @type {PathState} */
93
- let state = req.locals[this.key];
94
- if (!state) {
95
- state = { history: [], currentPath };
96
- req.locals[this.key] = state;
97
- } else if (!state.history) {
98
- state.history = [];
66
+ static WritePathState(transaction, base, currentPath) {
67
+ const { state } = transaction;
68
+ if (!state.path) {
69
+ state.path = { history: [], currentPath };
70
+ } else if (!state.path.history) {
71
+ state.path.history = [];
99
72
  }
100
- state.history.push({ base, treeIndex: [...treeIndex] });
101
- state.currentPath = currentPath;
73
+ state.path.history.push({ base, treeIndex: [...state.treeIndex] });
74
+ state.path.currentPath = currentPath;
102
75
  }
103
76
 
104
77
  /**
105
- * @param {HttpRequest} req
106
- * @param {number[]} treeIndex this node's treeIndex
78
+ * @param {HttpTransaction} transaction
107
79
  * @return {string} joined base path
108
80
  */
109
- readPathState(req, treeIndex) {
110
- /** @type {PathState} */
111
- const state = req.locals[this.key];
112
- if (!state || !state.history || !state.history.length) {
81
+ static ReadPathState(transaction) {
82
+ const { path, treeIndex } = transaction.state;
83
+ if (!path || !path.history || !path.history.length) {
113
84
  return '/';
114
85
  }
115
86
  const paths = [];
116
87
  let newLength = 0;
117
- /* eslint-disable no-labels, no-restricted-syntax */
88
+ /* eslint-disable no-labels */
118
89
  historyLoop: {
119
- for (let i = 0; i < state.history.length; i++) {
120
- const item = state.history[i];
90
+ for (const item of path.history) {
121
91
  if (item.treeIndex.length >= treeIndex.length) break;
122
- for (let j = 0; j < item.treeIndex.length - 1; j++) {
123
- if (item.treeIndex[j] !== treeIndex[j]) break historyLoop;
92
+ // TODO: Confirm length-1
93
+ for (let index = 0; index < item.treeIndex.length - 1; index++) {
94
+ if (item.treeIndex[index] !== treeIndex[index]) break historyLoop;
124
95
  }
125
96
  paths.push(item.base);
126
97
  newLength++;
127
98
  }
128
99
  }
129
- if (state.history.length !== newLength) {
130
- state.history.length = newLength;
100
+ if (path.history.length !== newLength) {
101
+ path.history.length = newLength;
131
102
  }
132
103
  if (!paths.length) {
133
104
  return '/';
134
105
  }
135
- return joinPath(...paths);
106
+ return posix.join(...paths);
136
107
  }
137
108
 
138
- /**
139
- * @param {MiddlewareFunctionParams} params
140
- * @return {MiddlewareFunctionResult}
141
- */
142
- execute({ req, state }) {
143
- const currentPath = this.absolute ? '' : this.readPathState(req, state.treeIndex);
144
- const comparison = this.absolute ? req.url.pathname : `/${relativePath(currentPath, req.url.pathname)}`;
109
+ /** @param {PathMiddlewareOptions|PathEntry|PathEntry[]} options */
110
+ constructor(options) {
111
+ if (Array.isArray(options)) {
112
+ this.path = options;
113
+ this.absolute = false;
114
+ this.subPath = false;
115
+ } else if (typeof options === 'string' || options instanceof RegExp) {
116
+ this.path = [options];
117
+ this.absolute = false;
118
+ this.subPath = false;
119
+ } else {
120
+ this.path = Array.isArray(options.path) ? options.path : [options.path];
121
+ this.absolute = options.absolute === true;
122
+ this.subPath = options.subPath === true;
123
+ }
124
+ }
145
125
 
146
- for (let i = 0; i < this.path.length; i++) {
147
- const path = this.path[i];
126
+ /** @type {MiddlewareFunction} */
127
+ execute(transaction) {
128
+ const currentPath = this.absolute ? '' : PathMiddleware.ReadPathState(transaction);
129
+ const comparison = this.absolute ? transaction.request.pathname : `/${posix.relative(currentPath, transaction.request.pathname)}`;
130
+
131
+ for (const path of this.path) {
148
132
  const result = PathMiddleware.test(comparison, path);
149
133
  if (result) {
150
134
  if (this.subPath) {
151
- this.writePathState(req, result, state.treeIndex, joinPath(currentPath, result));
135
+ PathMiddleware.WritePathState(transaction, result, posix.join(currentPath, result));
152
136
  }
153
- return 'continue';
137
+ return true;
154
138
  }
155
139
  }
156
-
157
- return 'break';
140
+ return false;
158
141
  }
159
142
  }
@@ -0,0 +1,99 @@
1
+ /** @typedef {import('../lib/HttpRequest.js').default} HttpRequest */
2
+ /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
3
+
4
+ /**
5
+ * The application/x-www-form-urlencoded format is in many ways an aberrant monstrosity,
6
+ * the result of many years of implementation accidents and compromises leading to a set of
7
+ * requirements necessary for interoperability, but in no way representing good design practices.
8
+ * In particular, readers are cautioned to pay close attention to the twisted details
9
+ * involving repeated (and in some cases nested) conversions between character encodings and byte sequences.
10
+ * Unfortunately the format is in widespread use due to the prevalence of HTML forms. [HTML]
11
+ * @see https://url.spec.whatwg.org/#urlencoded-parsing
12
+ * @param {HttpRequest} request
13
+ * @return {Promise<FormData>}
14
+ */
15
+ async function parseFormUrlEncoded(request) {
16
+ const output = new FormData();
17
+ // https://url.spec.whatwg.org/#urlencoded-parsing
18
+
19
+ const buffer = await request.buffer();
20
+ const { bufferEncoding } = request;
21
+
22
+ const sequences = [];
23
+ let startIndex = 0;
24
+ for (let index = 0; index < buffer.length; index += 1) {
25
+ if (buffer[index] === 0x26) {
26
+ sequences.push(buffer.subarray(startIndex, index));
27
+ startIndex = index + 1;
28
+ }
29
+ if (index === buffer.length - 1) {
30
+ sequences.push(buffer.subarray(startIndex, index + 1));
31
+ break;
32
+ }
33
+ }
34
+
35
+ for (const bytes of sequences) {
36
+ if (!bytes.length) continue;
37
+
38
+ // Find 0x3D and replace 0x2B in one loop for better performance
39
+ let indexOf0x3D = -1;
40
+ for (let index = 0; index < bytes.length; index += 1) {
41
+ switch (bytes[index]) {
42
+ case 0x3D:
43
+ if (indexOf0x3D === -1) {
44
+ indexOf0x3D = index;
45
+ }
46
+ break;
47
+ case 0x2B:
48
+ // Replace bytes on original stream for memory conservation
49
+ bytes[index] = 0x20;
50
+ break;
51
+ default:
52
+ }
53
+ }
54
+ let name;
55
+ let value;
56
+ if (indexOf0x3D === -1) {
57
+ name = bytes;
58
+ value = bytes.subarray(bytes.length, 0);
59
+ } else {
60
+ name = bytes.subarray(0, indexOf0x3D);
61
+ value = bytes.subarray(indexOf0x3D + 1);
62
+ }
63
+ const nameString = decodeURIComponent(name.toString(bufferEncoding));
64
+ const valueString = decodeURIComponent(value.toString(bufferEncoding));
65
+ output.append(nameString, valueString);
66
+ }
67
+ return output;
68
+ }
69
+
70
+ const CONTENT_READER = {
71
+ type: 'application',
72
+ subtype: 'x-www-form-urlencoded',
73
+ parse: async (/** @type {HttpRequest} */ request) => Object.fromEntries(
74
+ await parseFormUrlEncoded(request),
75
+ ),
76
+ };
77
+
78
+ /**
79
+ * @this {HttpRequest}
80
+ * @return {Promise<FormData>}
81
+ */
82
+ async function formData() {
83
+ if (this.mediaType.type === 'application' && this.mediaType.subtype === 'x-www-form-urlencoded') {
84
+ return await parseFormUrlEncoded(this);
85
+ }
86
+ throw new Error('UNSUPPORTED');
87
+ }
88
+
89
+ export default class ReadFormData {
90
+ constructor() {
91
+ this.execute = ReadFormData.Execute.bind(this);
92
+ }
93
+
94
+ /** @type {MiddlewareFunction} */
95
+ static Execute({ request }) {
96
+ request.formData = formData.bind(request);
97
+ request.contentReaders.push(CONTENT_READER);
98
+ }
99
+ }