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.
- package/.eslintrc.json +74 -58
- package/.github/copilot-instructions.md +100 -0
- package/.github/workflows/test-matrix.yml +37 -0
- package/.test/benchmark.js +28 -0
- package/.test/constants.js +4 -0
- package/{test → .test}/http2server.js +1 -1
- package/{test → .test}/httpserver.js +1 -1
- package/{test → .test}/index.js +178 -192
- package/.test/multipromise.js +32 -0
- package/{test → .test}/tls.js +3 -3
- package/{test → .test}/urlencoded.js +3 -0
- package/.vscode/launch.json +24 -3
- package/README.md +116 -90
- package/data/CookieObject.js +14 -14
- package/errata/socketio.js +6 -11
- package/examples/starter.js +11 -0
- package/helpers/HeadersParser.js +7 -8
- package/helpers/HttpListener.js +387 -42
- package/helpers/RequestHeaders.js +43 -36
- package/helpers/RequestReader.js +27 -26
- package/helpers/ResponseHeaders.js +47 -36
- package/jsconfig.json +1 -1
- package/lib/HttpHandler.js +447 -277
- package/lib/HttpRequest.js +383 -39
- package/lib/HttpResponse.js +316 -52
- package/lib/HttpTransaction.js +146 -0
- package/middleware/AutoHeadersMiddleware.js +73 -0
- package/middleware/CORSMiddleware.js +45 -47
- package/middleware/CaseInsensitiveHeadersMiddleware.js +5 -11
- package/middleware/ContentDecoderMiddleware.js +81 -35
- package/middleware/ContentEncoderMiddleware.js +179 -132
- package/middleware/ContentLengthMiddleware.js +66 -41
- package/middleware/ContentWriterMiddleware.js +5 -5
- package/middleware/HashMiddleware.js +68 -40
- package/middleware/HeadMethodMiddleware.js +24 -21
- package/middleware/MethodMiddleware.js +29 -36
- package/middleware/PathMiddleware.js +49 -66
- package/middleware/ReadFormData.js +99 -0
- package/middleware/SendJsonMiddleware.js +131 -0
- package/middleware/SendStringMiddleware.js +87 -0
- package/package.json +38 -29
- package/polyfill/FormData.js +164 -0
- package/rollup.config.js +0 -1
- package/scripts/check-teapot.mjs +40 -0
- package/scripts/test-all-sync.sh +6 -0
- package/scripts/test-all.sh +7 -0
- package/templates/starter.js +55 -0
- package/test/fixtures/stream.js +68 -0
- package/test/helpers/HttpListener/construct.js +18 -0
- package/test/helpers/HttpListener/customOptions.js +22 -0
- package/test/helpers/HttpListener/doubleCreate.js +40 -0
- package/test/helpers/HttpListener/events.js +77 -0
- package/test/helpers/HttpListener/http.js +31 -0
- package/test/helpers/HttpListener/http2.js +41 -0
- package/test/helpers/HttpListener/https.js +38 -0
- package/test/helpers/HttpListener/startAll.js +31 -0
- package/test/helpers/HttpListener/stopNotStarted.js +23 -0
- package/test/lib/HttpHandler/class.js +8 -0
- package/test/lib/HttpHandler/handleRequest.js +11 -0
- package/test/lib/HttpHandler/middleware.js +941 -0
- package/test/lib/HttpHandler/parse.js +41 -0
- package/test/lib/HttpRequest/class.js +8 -0
- package/test/lib/HttpRequest/downstream.js +171 -0
- package/test/lib/HttpRequest/properties.js +101 -0
- package/test/lib/HttpRequest/read.js +518 -0
- package/test/lib/HttpResponse/class.js +8 -0
- package/test/lib/HttpResponse/properties.js +59 -0
- package/test/lib/HttpResponse/send.js +275 -0
- package/test/lib/HttpTransaction/class.js +8 -0
- package/test/lib/HttpTransaction/ping.js +50 -0
- package/test/lib/HttpTransaction/push.js +89 -0
- package/test/middleware/SendJsonMiddleware.js +222 -0
- package/test/sanity.js +10 -0
- package/test/templates/error-teapot.js +47 -0
- package/test/templates/starter.js +93 -0
- package/tsconfig.json +12 -0
- package/types/index.js +61 -34
- package/types/typings.d.ts +8 -9
- package/utils/AsyncObject.js +6 -3
- package/utils/CaseInsensitiveObject.js +2 -3
- package/utils/function.js +1 -7
- package/utils/headers.js +42 -0
- package/utils/qualityValues.js +1 -1
- package/utils/stream.js +4 -20
- package/index.cjs +0 -3190
- package/test/constants.js +0 -4
- /package/{test → .test}/cookietester.js +0 -0
|
@@ -1,81 +1,109 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Transform } from 'stream';
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { Transform } from 'node:stream';
|
|
3
3
|
|
|
4
|
-
/** @typedef {import('
|
|
5
|
-
/** @typedef {import('../
|
|
6
|
-
/** @typedef {import('../
|
|
7
|
-
/** @typedef {import('../types').
|
|
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 {
|
|
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 {
|
|
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 {
|
|
29
|
-
* @return {
|
|
29
|
+
* @param {HttpResponse} response
|
|
30
|
+
* @return {void}
|
|
30
31
|
*/
|
|
31
|
-
|
|
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 =
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
60
|
-
if (!abort && hasData &&
|
|
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 (
|
|
64
|
-
|
|
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
|
-
|
|
70
|
+
response.headers.digest = `${algorithm}=${hash}`;
|
|
68
71
|
if ((algorithm === 'md5')) {
|
|
69
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
/** @type {MiddlewareFunction} */
|
|
106
|
+
execute({ response }) {
|
|
107
|
+
response.finalizers.push(this.finalizeResponse);
|
|
80
108
|
}
|
|
81
109
|
}
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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.
|
|
9
|
+
this.execute = HeadMethodMiddleware.Execute.bind(this);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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('../
|
|
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<
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
46
|
+
return false;
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
|
|
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
|
-
|
|
55
|
+
/** @type {MiddlewareFunction} */
|
|
56
|
+
static HEADORGET({ request }) { return request.method === 'HEAD' || request.method === 'GET'; }
|
|
71
57
|
|
|
72
|
-
|
|
58
|
+
/** @type {MiddlewareFunction} */
|
|
59
|
+
static GET({ request }) { return request.method === 'GET'; }
|
|
73
60
|
|
|
74
|
-
|
|
61
|
+
/** @type {MiddlewareFunction} */
|
|
62
|
+
static OPTIONS({ request }) { return request.method === 'OPTIONS'; }
|
|
75
63
|
|
|
76
|
-
|
|
64
|
+
/** @type {MiddlewareFunction} */
|
|
65
|
+
static HEAD({ request }) { return request.method === 'HEAD'; }
|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
/** @type {MiddlewareFunction} */
|
|
68
|
+
static PATCH({ request }) { return request.method === 'PATCH'; }
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
/** @type {MiddlewareFunction} */
|
|
71
|
+
static POST({ request }) { return request.method === 'POST'; }
|
|
81
72
|
|
|
82
|
-
|
|
73
|
+
/** @type {MiddlewareFunction} */
|
|
74
|
+
static PUT({ request }) { return request.method === 'PUT'; }
|
|
83
75
|
|
|
84
|
-
|
|
76
|
+
/** @type {MiddlewareFunction} */
|
|
77
|
+
static TRACE({ request }) { return request.method === 'TRACE'; }
|
|
85
78
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
2
|
|
|
3
|
-
/** @typedef {import('../types').
|
|
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})
|
|
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 {
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 {
|
|
106
|
-
* @param {number[]} treeIndex this node's treeIndex
|
|
78
|
+
* @param {HttpTransaction} transaction
|
|
107
79
|
* @return {string} joined base path
|
|
108
80
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
88
|
+
/* eslint-disable no-labels */
|
|
118
89
|
historyLoop: {
|
|
119
|
-
for (
|
|
120
|
-
const item = state.history[i];
|
|
90
|
+
for (const item of path.history) {
|
|
121
91
|
if (item.treeIndex.length >= treeIndex.length) break;
|
|
122
|
-
|
|
123
|
-
|
|
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 (
|
|
130
|
-
|
|
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
|
|
106
|
+
return posix.join(...paths);
|
|
136
107
|
}
|
|
137
108
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
135
|
+
PathMiddleware.WritePathState(transaction, result, posix.join(currentPath, result));
|
|
152
136
|
}
|
|
153
|
-
return
|
|
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
|
+
}
|