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,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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
+
// Unspecified default of '*'
|
|
50
|
+
response.headers['access-control-allow-origin'] = '*';
|
|
49
51
|
} else {
|
|
50
|
-
this.allowOrigin
|
|
52
|
+
for (const origin of this.allowOrigin) {
|
|
51
53
|
if (origin === '*') {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
response.headers['access-control-allow-origin'] = '*';
|
|
55
|
+
break;
|
|
54
56
|
}
|
|
55
57
|
if (typeof origin === 'string') {
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
if (request.headers.origin?.toLowerCase() === origin.toLowerCase()) {
|
|
59
|
+
response.headers['access-control-allow-origin'] = request.headers.origin;
|
|
60
|
+
break;
|
|
59
61
|
}
|
|
60
|
-
|
|
62
|
+
} else if (origin.test(request.headers.origin)) {
|
|
63
|
+
response.headers['access-control-allow-origin'] = request.headers.origin;
|
|
64
|
+
break;
|
|
61
65
|
}
|
|
62
|
-
|
|
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
|
-
|
|
70
|
+
response.headers['access-control-allow-credentials'] = 'true';
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
+
response.headers['access-control-max-age'] = this.maxAge.toString(10);
|
|
87
82
|
}
|
|
88
83
|
// 200 instead of 204 for compatibility
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
93
|
+
response.headers['access-control-expose-headers'] = this.exposeHeaders.join(',');
|
|
96
94
|
}
|
|
97
|
-
return
|
|
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').
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
request.headers = new CaseInsensitiveObject(request.headers || {});
|
|
29
24
|
}
|
|
30
25
|
if (this.response) {
|
|
31
26
|
// @ts-ignore Coerce
|
|
32
|
-
|
|
27
|
+
response.headers = new CaseInsensitiveObject(response.headers || {});
|
|
33
28
|
}
|
|
34
|
-
return 'continue';
|
|
35
29
|
}
|
|
36
30
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
32
|
+
return CONTINUE;
|
|
36
33
|
default:
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
return 'end';
|
|
52
|
+
return 406;
|
|
54
53
|
}
|
|
55
|
-
return
|
|
54
|
+
return CONTINUE;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
68
|
+
|
|
69
|
+
const gzipOptions = { chunkSize: this.chunkSize };
|
|
70
|
+
const newDownstream = new Transform({
|
|
71
|
+
|
|
72
|
+
read: (...args) => {
|
|
63
73
|
if (!initialized) {
|
|
64
|
-
|
|
65
|
-
let gzipStream;
|
|
74
|
+
/** @type {import("zlib").Gzip} */
|
|
66
75
|
switch (contentEncoding) {
|
|
67
76
|
case 'deflate':
|
|
68
|
-
gzipStream =
|
|
77
|
+
gzipStream = new Inflate(gzipOptions);
|
|
69
78
|
break;
|
|
70
79
|
case 'gzip':
|
|
71
|
-
gzipStream =
|
|
80
|
+
gzipStream = new Gunzip(gzipOptions);
|
|
72
81
|
break;
|
|
73
82
|
case 'br':
|
|
74
|
-
gzipStream =
|
|
83
|
+
gzipStream = new BrotliDecompress(gzipOptions);
|
|
75
84
|
break;
|
|
76
85
|
default:
|
|
77
86
|
throw new Error('UNKNOWN_ENCODING');
|
|
78
87
|
}
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
|
|
132
|
+
newDownstream.tag = 'ContentDecoder';
|
|
133
|
+
inputStream = request.addDownstream(newDownstream, { autoPause: true });
|
|
134
|
+
|
|
135
|
+
return CONTINUE;
|
|
90
136
|
}
|
|
91
137
|
}
|