webhoster 0.1.1 → 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 (85) 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/test-all-sync.sh +6 -0
  45. package/scripts/test-all.sh +7 -0
  46. package/templates/starter.js +53 -0
  47. package/test/fixtures/stream.js +68 -0
  48. package/test/helpers/HttpListener/construct.js +18 -0
  49. package/test/helpers/HttpListener/customOptions.js +22 -0
  50. package/test/helpers/HttpListener/doubleCreate.js +40 -0
  51. package/test/helpers/HttpListener/events.js +77 -0
  52. package/test/helpers/HttpListener/http.js +31 -0
  53. package/test/helpers/HttpListener/http2.js +41 -0
  54. package/test/helpers/HttpListener/https.js +38 -0
  55. package/test/helpers/HttpListener/startAll.js +31 -0
  56. package/test/helpers/HttpListener/stopNotStarted.js +23 -0
  57. package/test/lib/HttpHandler/class.js +8 -0
  58. package/test/lib/HttpHandler/handleRequest.js +11 -0
  59. package/test/lib/HttpHandler/middleware.js +941 -0
  60. package/test/lib/HttpHandler/parse.js +41 -0
  61. package/test/lib/HttpRequest/class.js +8 -0
  62. package/test/lib/HttpRequest/downstream.js +171 -0
  63. package/test/lib/HttpRequest/properties.js +101 -0
  64. package/test/lib/HttpRequest/read.js +518 -0
  65. package/test/lib/HttpResponse/class.js +8 -0
  66. package/test/lib/HttpResponse/properties.js +59 -0
  67. package/test/lib/HttpResponse/send.js +275 -0
  68. package/test/lib/HttpTransaction/class.js +8 -0
  69. package/test/lib/HttpTransaction/ping.js +50 -0
  70. package/test/lib/HttpTransaction/push.js +89 -0
  71. package/test/middleware/SendJsonMiddleware.js +222 -0
  72. package/test/sanity.js +10 -0
  73. package/test/templates/starter.js +93 -0
  74. package/tsconfig.json +12 -0
  75. package/types/index.js +61 -34
  76. package/types/typings.d.ts +8 -9
  77. package/utils/AsyncObject.js +6 -3
  78. package/utils/CaseInsensitiveObject.js +2 -3
  79. package/utils/function.js +1 -7
  80. package/utils/headers.js +42 -0
  81. package/utils/qualityValues.js +1 -1
  82. package/utils/stream.js +4 -20
  83. package/index.cjs +0 -3190
  84. package/test/constants.js +0 -4
  85. /package/{test → .test}/cookietester.js +0 -0
@@ -0,0 +1,93 @@
1
+ import test from 'ava';
2
+ import http from 'node:http';
3
+ import HttpListener from '../../helpers/HttpListener.js';
4
+ import HttpHandler from '../../lib/HttpHandler.js';
5
+ import * as starter from '../../templates/starter.js';
6
+
7
+ test.serial('examples/starter starts singleton listener and responds', async (t) => {
8
+ // Import the example. It applies middleware and starts the singleton listener.
9
+ await import('../../examples/starter.js');
10
+
11
+ const listener = HttpListener.defaultInstance;
12
+ const server = listener.httpServer;
13
+ t.truthy(server, 'singleton server started');
14
+
15
+ const addr = server.address();
16
+ t.truthy(addr && addr.port, 'server bound to a port');
17
+
18
+ const result = await new Promise((resolve, reject) => {
19
+ const request = http.get({ port: addr.port, path: '/' }, (res) => {
20
+ let data = '';
21
+ res.setEncoding('utf8');
22
+ res.on('data', (chunk) => { data += chunk; });
23
+ res.on('end', () => resolve({ status: res.statusCode, body: data }));
24
+ });
25
+ request.on('error', reject);
26
+ });
27
+
28
+ t.is(result.status, 200);
29
+ t.true(typeof result.body === 'string');
30
+ t.true(result.body.length > 0, 'response body present');
31
+
32
+ await listener.stopHttpServer();
33
+ });
34
+
35
+ test.serial('starter.start applies default error handler and middleware when none provided', async (t) => {
36
+ const handler = HttpHandler.defaultInstance;
37
+ const listener = HttpListener.defaultInstance;
38
+
39
+ const mwLength = handler.middleware.length;
40
+ const ehLength = handler.errorHandlers.length;
41
+
42
+ // Start with empty options to exercise the default-error-handler branch
43
+ // Use port 0 and loopback host to avoid port collisions in CI.
44
+ await starter.start({ host: '127.0.0.1', port: 0 });
45
+
46
+ t.truthy(listener.httpServer, 'httpServer started');
47
+
48
+ t.true(handler.middleware.length >= mwLength + 8, 'starter middleware applied');
49
+ t.true(handler.errorHandlers.length > ehLength, 'default error handler added');
50
+
51
+ const lastError = handler.errorHandlers.at(-1);
52
+ t.is(typeof lastError.onError, 'function');
53
+
54
+ // Invoke the default onError to exercise its code path (logging + 500).
55
+ const result = lastError.onError({ request: { url: '/x' }, error: new Error('boom') });
56
+ t.is(result, 500);
57
+
58
+ await listener.stopHttpServer();
59
+
60
+ // restore handler arrays to previous lengths
61
+ handler.middleware.splice(mwLength);
62
+ handler.errorHandlers.splice(ehLength);
63
+ });
64
+
65
+ test.serial('starter.start respects provided errorHandlers and pushes middleware by reference', async (t) => {
66
+ const handler = HttpHandler.defaultInstance;
67
+ const listener = HttpListener.defaultInstance;
68
+
69
+ const mwLength = handler.middleware.length;
70
+ const ehLength = handler.errorHandlers.length;
71
+
72
+ const customHandlers = [{ onError: () => 418 }];
73
+ const customMiddleware = [() => 'custom'];
74
+
75
+ await starter.start({
76
+ middleware: customMiddleware, errorHandlers: customHandlers, host: '127.0.0.1', port: 0,
77
+ });
78
+
79
+ t.truthy(listener.httpServer, 'httpServer started');
80
+
81
+ // The supplied middleware array should be pushed as a single entry (by reference)
82
+ const lastMw = handler.middleware.at(-1);
83
+ t.is(lastMw, customMiddleware);
84
+
85
+ // The implementation does not automatically append provided `errorHandlers`;
86
+ // it only avoids adding the default when `options.errorHandlers` is supplied.
87
+ t.is(handler.errorHandlers.length, ehLength);
88
+
89
+ await listener.stopHttpServer();
90
+
91
+ handler.middleware.splice(mwLength);
92
+ handler.errorHandlers.splice(ehLength);
93
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "checkJs": true,
4
+ "noImplicitAny": true,
5
+ "target": "es2022",
6
+ "module": "esnext",
7
+ "moduleResolution": "node"
8
+ },
9
+ "exclude": [
10
+ "**/node_modules/*"
11
+ ]
12
+ }
package/types/index.js CHANGED
@@ -1,55 +1,82 @@
1
- /** @typedef {import('../lib/HttpRequest').default} HttpRequest */
2
- /** @typedef {import('../lib/HttpResponse').default} HttpResponse */
1
+ /**
2
+ * @template {any} [T=any]
3
+ * @typedef {import('../lib/HttpTransaction.js').default<T>} HttpTransaction<T>
4
+ */
5
+ /** @typedef {import('../lib/HttpRequest.js').default} HttpRequest */
6
+ /** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
7
+
8
+ /**
9
+ * @typedef {Object} MediaType
10
+ * @prop {string} type
11
+ * @prop {string} tree
12
+ * @prop {string} subtype
13
+ * @prop {string} suffix
14
+ * @prop {Record<string, string>} parameters
15
+ */
16
+
17
+ /** @typedef {import('node:stream').Duplex} Duplex */
3
18
 
4
19
  /** @typedef {import('./typings').Middleware} Middleware */
5
20
 
21
+ /** @typedef {true|false|0} MiddlewareFlowInstruction */
22
+
6
23
  /** @typedef {'GET'|'HEAD'|'POST'|'PUT'|'DELETE'|'CONNECT'|'OPTIONS'|'TRACE'|'PATCH'} RequestMethod */
7
24
 
8
- /** @typedef {'end'|'break'|'continue'|boolean|null|undefined|void} MiddlewareFunctionResultType */
25
+ /** @typedef {0|true|false|null|undefined|void} MiddlewareFunctionResultType */
26
+
9
27
  /** @typedef {Promise<Middleware>|Middleware} MiddlewareFunctionResult */
10
- /** @typedef {true|false} MiddlewareContinueBoolean */
28
+
29
+ /** @typedef {Record<string,any>|number|import('stream').Readable|Buffer|string} MiddlewareContent */
11
30
 
12
31
  /**
13
- * @typedef {Object} IMiddleware
14
- * @prop {MiddlewareFunction} execute
15
- * @prop {MiddlewareErrorHandlerFunction} [onError]
32
+ * @template {any} [T=any]
33
+ * @callback MiddlewareFunction
34
+ * @param {!HttpTransaction<T>} transaction
35
+ * @return {MiddlewareFunctionResult}
16
36
  */
17
37
 
18
38
  /**
19
- * @typedef {Object} HandlerState
20
- * @prop {number[]} treeIndex Middleware level
39
+ * @template {any} [T=any]
40
+ * @callback MiddlewareResponseFunction
41
+ * @param {{ response: HttpTransaction<T>['response'] }} transaction
42
+ * @return {MiddlewareFunctionResult}
21
43
  */
22
44
 
23
45
  /**
24
- * @typedef {Object} MiddlewareFunctionParams
25
- * @prop {HttpRequest} req
26
- * @prop {HttpResponse} res
27
- * @prop {HandlerState} state
46
+ * @this {undefined}
47
+ * @callback StaticMiddlewareFunction
48
+ * @param {!HttpTransaction} transaction
49
+ * @return {MiddlewareFunctionResult}
28
50
  */
29
51
 
30
52
  /**
31
- * @typedef {Object} MiddlewareErrorHandlerParams
32
- * @prop {HttpRequest} req
33
- * @prop {HttpResponse} res
34
- * @prop {HandlerState} state
35
- * @prop {any} [err]
53
+ * @callback ResponseFinalizer
54
+ * @param {!HttpResponse} response
55
+ * @return {void|null|boolean|Promise<void|null|boolean>}
36
56
  */
37
57
 
38
58
  /**
39
- * @callback MiddlewareFunction
40
- * @param {!MiddlewareFunctionParams} params
41
- * @return {MiddlewareFunctionResult}
59
+ * @typedef MiddlewareExecutor
60
+ * @prop {MiddlewareFunction} execute
42
61
  */
43
62
 
44
63
  /**
45
- * @callback MiddlewareErrorHandlerFunction
46
- * @param {MiddlewareErrorHandlerParams} params
47
- * @return {MiddlewareFunctionResult}
64
+ * @typedef {Object} MiddlewareStaticExecutor
65
+ * @prop {StaticMiddlewareFunction} Execute
66
+ */
67
+
68
+ /**
69
+ * @typedef {Object} MiddlewareStaticErrorHandler
70
+ * @prop {StaticMiddlewareFunction} OnError
71
+ */
72
+
73
+ /**
74
+ * @typedef MiddlewareErrorHandler
75
+ * @prop {MiddlewareFunction} onError
48
76
  */
49
77
 
50
78
  /**
51
- * @typedef {Object} MiddlewareErrorHandler
52
- * @prop {MiddlewareErrorHandlerFunction} onError
79
+ * @typedef {MiddlewareExecutor|MiddlewareStaticExecutor|MiddlewareErrorHandler|MiddlewareStaticErrorHandler|MiddlewareStaticExecutor} IMiddleware
53
80
  */
54
81
 
55
82
  /**
@@ -57,8 +84,8 @@
57
84
  * @prop {string} [name='']
58
85
  * A cookie name can be any US-ASCII characters, except control characters, spaces, or tabs.
59
86
  * It also must not contain a separator character like the following:( ) < > &#64; , ; : \ " / [ ] ? = { }.
60
- * * __Secure- prefix: Cookies names starting with __Secure- (dash is part of the prefix) must be set with the secure flag from a secure page (HTTPS).
61
- * * __Host- prefix: Cookies with names starting with __Host- must be set with the secure flag, must be from a secure page (HTTPS), must not have a domain specified (and therefore aren't sent to subdomains) and the path must be /.
87
+ * - __Secure- prefix: Cookies names starting with __Secure- (dash is part of the prefix) must be set with the secure flag from a secure page (HTTPS).
88
+ * - __Host- prefix: Cookies with names starting with __Host- must be set with the secure flag, must be from a secure page (HTTPS), must not have a domain specified (and therefore aren't sent to subdomains) and the path must be /.
62
89
  * @prop {string} [value='']
63
90
  * A cookie value can optionally be wrapped in double quotes and include any US-ASCII characters excluding
64
91
  * control characters, Whitespace, double quotes, comma, semicolon, and backslash.
@@ -76,9 +103,9 @@
76
103
  * If both Expires and Max-Age are set, Max-Age has precedence.
77
104
  * @prop {string} [domain]
78
105
  * Host to which the cookie will be sent.
79
- * * If omitted, defaults to the host of the current document URL, not including subdomains.
80
- * * Contrary to earlier specifications, leading dots in domain names (.example.com) are ignored.
81
- * * Multiple host/domain values are not allowed, but if a domain is specified, then subdomains are always included.
106
+ * - If omitted, defaults to the host of the current document URL, not including subdomains.
107
+ * - Contrary to earlier specifications, leading dots in domain names (.example.com) are ignored.
108
+ * - Multiple host/domain values are not allowed, but if a domain is specified, then subdomains are always included.
82
109
  * @prop {string} [path]
83
110
  * A path that must exist in the requested URL, or the browser won't send the Cookie header.
84
111
  *
@@ -96,13 +123,13 @@
96
123
  * @prop {'Strict'|'Lax'|'None'} [sameSite]
97
124
  * Asserts that a cookie must not be sent with cross-origin requests,
98
125
  * providing some protection against cross-site request forgery attacks (CSRF).
99
- * * Strict: The browser sends the cookie only for same-site requests
126
+ * - Strict: The browser sends the cookie only for same-site requests
100
127
  * (that is, requests originating from the same site that set the cookie).
101
128
  * If the request originated from a different URL than the current one,
102
129
  * no cookies with the SameSite=Strict attribute are sent.
103
- * * Lax: The cookie is withheld on cross-site subrequests, such as calls to load images or frames,
130
+ * - Lax: The cookie is withheld on cross-site subrequests, such as calls to load images or frames,
104
131
  * but is sent when a user navigates to the URL from an external site, such as by following a link.
105
- * * None: The browser sends the cookie with both cross-site and same-site requests.
132
+ * - None: The browser sends the cookie with both cross-site and same-site requests.
106
133
  */
107
134
 
108
135
  export default {};
@@ -1,15 +1,14 @@
1
1
  import {
2
2
  IMiddleware,
3
+ MiddlewareContent,
3
4
  MiddlewareFunction,
4
- MiddlewareErrorHandler,
5
- MiddlewareContinueBoolean,
5
+ MiddlewareFunctionResultType,
6
6
  } from './index.js';
7
7
 
8
- export type Middleware = MiddlewareFunction
8
+ export type Middleware =
9
9
  | IMiddleware
10
- | MiddlewareErrorHandler
11
- | Iterable<Middleware>
12
- | Map<any, Middleware>
13
- | {[key:string]: Middleware, onError?: undefined}
14
- | MiddlewareContinueBoolean
15
- | 'end'|'break'|'continue'|null|undefined|void;
10
+ | MiddlewareFunction
11
+ | Middleware[]
12
+ | MiddlewareContent
13
+ | Set<Middleware>
14
+ | MiddlewareFunctionResultType
@@ -32,7 +32,7 @@ export default class AsyncObject {
32
32
 
33
33
  /**
34
34
  * @param {T} [value]
35
- * @return {Promise<T>}
35
+ * @return {T}
36
36
  */
37
37
  set(value) {
38
38
  this.value = value;
@@ -41,7 +41,7 @@ export default class AsyncObject {
41
41
  while (this.pendingPromises.length) {
42
42
  this.pendingPromises.shift().resolve(value);
43
43
  }
44
- return Promise.resolve(value);
44
+ return value;
45
45
  }
46
46
 
47
47
  /**
@@ -57,7 +57,10 @@ export default class AsyncObject {
57
57
  }
58
58
  }
59
59
 
60
- /** @return {void} */
60
+ /**
61
+ * Clear value and mark busy
62
+ * @return {void}
63
+ */
61
64
  prepare() {
62
65
  this.value = undefined;
63
66
  this.busy = true;
@@ -7,10 +7,10 @@ export default class CaseInsensitiveObject {
7
7
  // eslint-disable-next-line @typescript-eslint/no-this-alias
8
8
  const instance = this;
9
9
  const proxy = new Proxy(instance, CaseInsensitiveObject.defaultProxyHandler);
10
- Object.entries(object).forEach(([key, value]) => {
10
+ for (const [key, value] of Object.entries(object)) {
11
11
  // @ts-ignore Coerce
12
12
  this[key] = value;
13
- });
13
+ }
14
14
  return proxy;
15
15
  }
16
16
  }
@@ -30,4 +30,3 @@ CaseInsensitiveObject.defaultProxyHandler = {
30
30
  return Reflect.deleteProperty(target, typeof p === 'string' ? p.toLowerCase() : p);
31
31
  },
32
32
  };
33
-
package/utils/function.js CHANGED
@@ -1,7 +1 @@
1
- // eslint-disable-next-line jsdoc/require-returns-check
2
- /**
3
- * @param {any} [args]
4
- * @return {any}
5
- */
6
- // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
7
- export function noop(...args) {}
1
+ export const noop = () => {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ *
3
+ * @param {?string} contentType
4
+ * @return {import('../types/index.js').MediaType}
5
+ */
6
+ export function parseContentType(contentType) {
7
+ let type;
8
+ let tree;
9
+ let subtype;
10
+ let suffix;
11
+ /** @type {Record<string,string>} */
12
+ const parameters = {};
13
+ if (contentType) {
14
+ for (const directive of contentType.split(';')) {
15
+ let [key, value] = directive.split('=');
16
+ key = key.trim().toLowerCase();
17
+ if (value === undefined) {
18
+ let rest;
19
+ [type, rest] = key.split('/');
20
+ const treeEntries = rest.split('.');
21
+ const subtypeSuffix = treeEntries.pop();
22
+ tree = treeEntries.join('.');
23
+ [subtype, suffix] = subtypeSuffix.split('+');
24
+ continue;
25
+ }
26
+
27
+ value = value.trim();
28
+ const firstQuote = value.indexOf('"');
29
+ const lastQuote = value.lastIndexOf('"');
30
+ if (firstQuote !== -1 && lastQuote !== -1) {
31
+ if (firstQuote === lastQuote) {
32
+ throw new Error('ERR_CONTENT_TYPE');
33
+ }
34
+ value = value.slice(firstQuote + 1, lastQuote);
35
+ }
36
+ parameters[key] = value;
37
+ }
38
+ }
39
+ return {
40
+ type, subtype, suffix, tree, parameters,
41
+ };
42
+ }
@@ -20,7 +20,7 @@ export function parseQualityValues(input) {
20
20
  const trimmedSpec = specifier?.trim();
21
21
  const trimmedSValue = sValue?.trim();
22
22
  if (trimmedSpec === 'q') {
23
- const parsedQ = parseFloat(trimmedSValue);
23
+ const parsedQ = Number.parseFloat(trimmedSValue);
24
24
  return { q: Number.isNaN(parsedQ) ? 1 : parsedQ };
25
25
  }
26
26
  return { [trimmedSpec]: trimmedSValue };
package/utils/stream.js CHANGED
@@ -1,23 +1,7 @@
1
- /** @typedef {import('stream').Readable} Readable */
2
-
3
1
  /**
4
- * @param {Readable} readable
5
- * @return {Promise<any>}
2
+ * @param {import('node:stream').Writable|import('node:http').ServerResponse} writableLike
3
+ * @return {boolean}
6
4
  */
7
- export async function readStreamChunk(readable) {
8
- return new Promise((resolve, reject) => readable.once('data', resolve).once('error', reject));
9
- }
10
-
11
- /**
12
- * @param {Readable} readable
13
- * @return {Promise<any[]>}
14
- */
15
- export async function readAllChunks(readable) {
16
- return new Promise((resolve, reject) => {
17
- /** @type {any[]} */
18
- const chunks = [];
19
- readable.on('data', chunks.push);
20
- readable.on('end', () => resolve(chunks));
21
- readable.on('error', reject);
22
- });
5
+ export function isWritable(writableLike) {
6
+ return (!writableLike.destroyed && !writableLike.writableEnded);
23
7
  }