@nodefony/http 10.0.0-alpha.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 (155) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +77 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/index.js +108 -0
  7. package/dist/nodefony/command/assetsPublishCommand.js +102 -0
  8. package/dist/nodefony/command/certificatesCommand.js +47 -0
  9. package/dist/nodefony/command/networkCommand.js +27 -0
  10. package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
  11. package/dist/nodefony/config/config.js +335 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +93 -0
  13. package/dist/nodefony/interfaces/IContext.js +1 -0
  14. package/dist/nodefony/interfaces/ICookie.js +1 -0
  15. package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
  16. package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
  17. package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
  18. package/dist/nodefony/interfaces/IRequest.js +1 -0
  19. package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
  20. package/dist/nodefony/interfaces/IResponse.js +1 -0
  21. package/dist/nodefony/interfaces/ISession.js +1 -0
  22. package/dist/nodefony/interfaces/IUpload.js +1 -0
  23. package/dist/nodefony/interfaces/index.js +1 -0
  24. package/dist/nodefony/service/HttpAdminApi.js +376 -0
  25. package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
  26. package/dist/nodefony/service/audit-logger.js +159 -0
  27. package/dist/nodefony/service/certificates.js +545 -0
  28. package/dist/nodefony/service/error-renderer.js +320 -0
  29. package/dist/nodefony/service/http-kernel.js +948 -0
  30. package/dist/nodefony/service/pretty-request-logger.js +72 -0
  31. package/dist/nodefony/service/request-logger.js +54 -0
  32. package/dist/nodefony/service/servers/clientError.js +20 -0
  33. package/dist/nodefony/service/servers/server-http.js +135 -0
  34. package/dist/nodefony/service/servers/server-https.js +204 -0
  35. package/dist/nodefony/service/servers/server-static.js +192 -0
  36. package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
  37. package/dist/nodefony/service/servers/server-websocket.js +104 -0
  38. package/dist/nodefony/service/servers/serverShutdown.js +31 -0
  39. package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
  40. package/dist/nodefony/service/sessions/sessions-service.js +580 -0
  41. package/dist/nodefony/service/trace.js +72 -0
  42. package/dist/nodefony/service/upload/upload-service.js +171 -0
  43. package/dist/nodefony/src/assets/collectAssets.js +34 -0
  44. package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
  45. package/dist/nodefony/src/context/Context.js +415 -0
  46. package/dist/nodefony/src/context/domainMatcher.js +88 -0
  47. package/dist/nodefony/src/context/forwarded.js +185 -0
  48. package/dist/nodefony/src/context/http/HttpContext.js +309 -0
  49. package/dist/nodefony/src/context/http/Request.js +543 -0
  50. package/dist/nodefony/src/context/http/Response.js +368 -0
  51. package/dist/nodefony/src/context/http/parser.js +188 -0
  52. package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
  53. package/dist/nodefony/src/context/http2/Request.js +29 -0
  54. package/dist/nodefony/src/context/http2/Response.js +97 -0
  55. package/dist/nodefony/src/context/metaData.js +47 -0
  56. package/dist/nodefony/src/context/requestId.js +41 -0
  57. package/dist/nodefony/src/context/trustProxy.js +167 -0
  58. package/dist/nodefony/src/context/websocket/Response.js +181 -0
  59. package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
  60. package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
  61. package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
  62. package/dist/nodefony/src/cookies/cookie.js +258 -0
  63. package/dist/nodefony/src/errors/httpError.js +69 -0
  64. package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
  65. package/dist/nodefony/src/profiler/Profiler.js +139 -0
  66. package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
  67. package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
  68. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
  69. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
  70. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
  71. package/dist/nodefony/src/servers/portBinder.js +114 -0
  72. package/dist/nodefony/src/session/session.js +390 -0
  73. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
  74. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
  75. package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
  76. package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
  77. package/dist/types/index.d.ts +83 -0
  78. package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
  79. package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
  80. package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
  81. package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
  82. package/dist/types/nodefony/config/config.d.ts +197 -0
  83. package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
  84. package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
  85. package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
  86. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
  87. package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
  88. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
  89. package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
  90. package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
  91. package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
  92. package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
  93. package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
  94. package/dist/types/nodefony/interfaces/index.d.ts +7 -0
  95. package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
  96. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
  97. package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
  98. package/dist/types/nodefony/service/certificates.d.ts +246 -0
  99. package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
  100. package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
  101. package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
  102. package/dist/types/nodefony/service/request-logger.d.ts +18 -0
  103. package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
  104. package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
  105. package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
  106. package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
  107. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
  108. package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
  109. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
  110. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
  111. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
  112. package/dist/types/nodefony/service/trace.d.ts +39 -0
  113. package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
  114. package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
  115. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
  116. package/dist/types/nodefony/src/context/Context.d.ts +195 -0
  117. package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
  118. package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
  119. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
  120. package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
  121. package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
  122. package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
  123. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
  124. package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
  125. package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
  126. package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
  127. package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
  128. package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
  129. package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
  130. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
  131. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
  132. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
  133. package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
  134. package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
  135. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
  136. package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
  137. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
  138. package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
  139. package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
  140. package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
  141. package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
  142. package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
  143. package/dist/types/nodefony/src/session/session.d.ts +171 -0
  144. package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
  145. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
  146. package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
  147. package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
  148. package/docs/cookies.md +365 -0
  149. package/docs/index.md +163 -0
  150. package/docs/observabilite.md +460 -0
  151. package/docs/rate-limit.md +372 -0
  152. package/docs/servers.md +935 -0
  153. package/docs/session.md +768 -0
  154. package/docs/upload.md +460 -0
  155. package/package.json +101 -0
@@ -0,0 +1,97 @@
1
+ import HttpResponse from "../http/Response.js";
2
+ import { extend } from "nodefony";
3
+ import http from "node:http";
4
+ import http2 from "node:http2";
5
+ //#region nodefony/src/context/http2/Response.ts
6
+ const HTTP2_HEADER_STATUS = http2.constants.HTTP2_HEADER_STATUS;
7
+ var Http2Response = class extends HttpResponse {
8
+ statusCode = 200;
9
+ stream = null;
10
+ streamId;
11
+ constructor(response, context) {
12
+ super(response, context);
13
+ if (response) {
14
+ this.stream = response.stream;
15
+ this.streamId = this.stream.id;
16
+ }
17
+ if (this.stream && this.stream.pushAllowed) this.context.pushAllowed = true;
18
+ }
19
+ isHeaderSent() {
20
+ if (this.stream) return this.stream.headersSent;
21
+ return super.isHeaderSent();
22
+ }
23
+ writeHead(statusCode, headers) {
24
+ if (this.stream) {
25
+ if (statusCode) this.setStatusCode(statusCode);
26
+ if (this.stream.destroyed || this.stream.closed) {
27
+ this.log("HTTP/2 stream destroyed before writeHead — skip", "DEBUG");
28
+ return;
29
+ }
30
+ if (!this.stream.headersSent) {
31
+ if (this.statusCode) {
32
+ if (typeof this.statusCode === "string") this.statusCode = parseInt(this.statusCode, 10);
33
+ if (this.statusCode > 599) this.statusCode = 500;
34
+ }
35
+ this.statusMessage = this.getStatusMessage();
36
+ this.setLength();
37
+ this.ensureContentTypeHeader();
38
+ this.headers = extend(true, { "X-Status-Message": this.statusMessage }, this.getHeaders(), headers);
39
+ this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
40
+ const requestId = this.context.requestId;
41
+ if (requestId) this.headers["x-request-id"] = requestId;
42
+ const traceparent = this.context.traceparent;
43
+ if (traceparent) this.headers["traceparent"] = traceparent;
44
+ this.stream.respond(this.headers, { endStream: false });
45
+ } else this.log("Headers already sent !!", "WARNING");
46
+ } else return super.writeHead(statusCode, headers);
47
+ }
48
+ async send(chunk, encoding, _flush = false) {
49
+ if (this.context.isRedirect) {
50
+ if (this.stream && !this.stream.headersSent) this.writeHead();
51
+ await this.end();
52
+ return this;
53
+ }
54
+ const stream = this.stream;
55
+ if (!stream) return await super.send(chunk, encoding);
56
+ if (stream.destroyed || !stream.writable) {
57
+ this.log("HTTP/2 stream not writable before send — skip", "DEBUG");
58
+ return this;
59
+ }
60
+ if (chunk) this.setBody(chunk);
61
+ if (this.body === null) this.body = Buffer.alloc(0);
62
+ return new Promise((resolve, reject) => {
63
+ stream.write(this.body, encoding || this.encoding, (error) => {
64
+ if (error) return reject(error);
65
+ return resolve(this);
66
+ });
67
+ });
68
+ }
69
+ end(chunk, encoding) {
70
+ return new Promise((resolve, reject) => {
71
+ try {
72
+ if (this.stream) {
73
+ if (this.stream.destroyed || this.stream.closed) return resolve(this.stream);
74
+ return resolve(this.stream.end(chunk, encoding || this.encoding));
75
+ }
76
+ return resolve(super.end(chunk, encoding));
77
+ } catch (e) {
78
+ return reject(e);
79
+ }
80
+ });
81
+ }
82
+ getStatusMessage(code) {
83
+ if (code) {
84
+ if (this.response) return http.STATUS_CODES[code] || this.statusMessage;
85
+ }
86
+ if (this.response) return this.statusMessage || http.STATUS_CODES[this.statusCode];
87
+ return this.statusMessage || http.STATUS_CODES[this.statusCode];
88
+ }
89
+ getStatus() {
90
+ return {
91
+ code: this.getStatusCode(),
92
+ message: ""
93
+ };
94
+ }
95
+ };
96
+ //#endregion
97
+ export { Http2Response as default };
@@ -0,0 +1,47 @@
1
+ //#region nodefony/src/context/metaData.ts
2
+ /**
3
+ * Assemble l'enveloppe `metaData` d'une requête EN PLACE (builder monomorphe) :
4
+ * met à jour les champs de `target.nodefony` dans un ordre fixe → V8 garde une
5
+ * hidden class stable et inline les écritures. Remplace l'ancien
6
+ * `extend(true, …)` qui deep-clonait/dispatchait à chaque réponse JSON et chaque
7
+ * frame WS — inutile ici puisque `target` est l'objet metaData per-requête
8
+ * (jamais partagé).
9
+ *
10
+ * `route` est un **snapshot per-requête** `{ name, path, variablesMap }` : on ne
11
+ * diffuse jamais l'instance `Route` partagée (statique), dont les variables
12
+ * matchées seraient écrasées par toute requête/connexion concurrente (bleed).
13
+ *
14
+ * `override` (ex. frame WS `{ nodefony: { websocket } }`) est fusionné *shallow*
15
+ * dans l'enveloppe : le seul cas réel est l'ajout d'un sous-objet `nodefony`
16
+ * peu profond — pas besoin du deep-merge récursif générique. Toute clé top-level
17
+ * hors `nodefony` est préservée telle quelle.
18
+ *
19
+ * @param target - objet metaData per-requête à muter (et retourner)
20
+ * @param src - source structurelle (le `Context` réel, ou un faux en test)
21
+ * @param override - overrides optionnels de l'appelant
22
+ * @returns le même `target`, muté
23
+ */
24
+ function buildMetaData(target, src, override) {
25
+ const nf = target.nodefony;
26
+ nf.name = src.kernel?.projectName;
27
+ nf.version = src.kernel?.version;
28
+ nf.url = src.request?.href ?? src.request?.url;
29
+ nf.environment = src.kernel?.environment;
30
+ nf.debug = src.kernel?.debug;
31
+ nf.scheme = src.scheme;
32
+ nf.requestId = src.requestId;
33
+ const r = src.resolver;
34
+ nf.route = r && r.route ? {
35
+ name: r.route.name,
36
+ path: r.route.path,
37
+ variablesMap: r.getMatchedParams()
38
+ } : void 0;
39
+ if (override) {
40
+ const ovr = override.nodefony;
41
+ if (ovr) Object.assign(nf, ovr);
42
+ for (const k in override) if (k !== "nodefony") target[k] = override[k];
43
+ }
44
+ return target;
45
+ }
46
+ //#endregion
47
+ export { buildMetaData };
@@ -0,0 +1,41 @@
1
+ //#region nodefony/src/context/requestId.ts
2
+ /**
3
+ * Validation de l'identifiant de corrélation `X-Request-Id` fourni par le
4
+ * client, avant qu'il ne soit adopté comme `Context.requestId`.
5
+ *
6
+ * Le `requestId` finit (1) réfléchi dans la réponse (`X-Request-Id`),
7
+ * (2) écrit dans les logs (`ID : …`), (3) propagé en ALS à tout le pipeline.
8
+ * Une valeur cliente non assainie ouvre donc : log-injection (CR/LF dans les
9
+ * logs), forging de corrélation, et throw `setHeader` natif (→ 500/DoS) sur
10
+ * caractère de contrôle ou non-ASCII. Cf RFC 9110 §5.5 (field values) + Zero
11
+ * Trust (mémoire `feedback_security_rfc_rigor`).
12
+ */
13
+ /**
14
+ * Longueur maximale acceptée pour un `X-Request-Id` client. Borne anti-abus
15
+ * (log flooding / header oversize). 128 couvre largement un UUID (36), un
16
+ * nanoid, ou un `traceparent` (55).
17
+ */
18
+ const MAX_REQUEST_ID_LENGTH = 128;
19
+ /**
20
+ * Caractères sûrs : ASCII alphanumérique + `.`, `_`, `-`. Exclut tout caractère
21
+ * de contrôle (CR/LF → response splitting / log injection), espace, et
22
+ * non-ASCII (→ throw `setHeader` Node = DoS). Ancré + quantifieur borné : pas
23
+ * de backtracking catastrophique (ReDoS).
24
+ */
25
+ const SAFE_REQUEST_ID = /^[A-Za-z0-9._-]{1,128}$/u;
26
+ /**
27
+ * Valide un `X-Request-Id` entrant. Retourne la valeur si elle est sûre, sinon
28
+ * `null` (l'appelant conserve alors l'UUID généré côté serveur).
29
+ *
30
+ * On REJETTE (plutôt que tronquer/nettoyer) une valeur invalide : nettoyer
31
+ * donnerait au client un faux contrôle sur l'identifiant et masquerait l'abus.
32
+ *
33
+ * @param raw - valeur brute du header `x-request-id` (ou `undefined`/`null`).
34
+ * @returns la valeur si sûre, sinon `null`.
35
+ */
36
+ function sanitizeRequestId(raw) {
37
+ if (!raw || raw.length > 128) return null;
38
+ return SAFE_REQUEST_ID.test(raw) ? raw : null;
39
+ }
40
+ //#endregion
41
+ export { MAX_REQUEST_ID_LENGTH, sanitizeRequestId };
@@ -0,0 +1,167 @@
1
+ import { BlockList, isIPv4, isIPv6 } from "node:net";
2
+ //#region nodefony/src/context/trustProxy.ts
3
+ const PRESETS = {
4
+ loopback: [[
5
+ "127.0.0.0",
6
+ 8,
7
+ "ipv4"
8
+ ], [
9
+ "::1",
10
+ 128,
11
+ "ipv6"
12
+ ]],
13
+ linklocal: [[
14
+ "169.254.0.0",
15
+ 16,
16
+ "ipv4"
17
+ ], [
18
+ "fe80::",
19
+ 10,
20
+ "ipv6"
21
+ ]],
22
+ uniquelocal: [
23
+ [
24
+ "10.0.0.0",
25
+ 8,
26
+ "ipv4"
27
+ ],
28
+ [
29
+ "172.16.0.0",
30
+ 12,
31
+ "ipv4"
32
+ ],
33
+ [
34
+ "192.168.0.0",
35
+ 16,
36
+ "ipv4"
37
+ ],
38
+ [
39
+ "fc00::",
40
+ 7,
41
+ "ipv6"
42
+ ]
43
+ ]
44
+ };
45
+ /** Normalise une adresse IPv4-mapped IPv6 (`::ffff:127.0.0.1`) en IPv4. */
46
+ const normalize = (addr) => addr.startsWith("::ffff:") && isIPv4(addr.slice(7)) ? addr.slice(7) : addr;
47
+ const TRUST_ALL = { isTrusted: () => true };
48
+ const TRUST_NONE = { isTrusted: () => false };
49
+ /**
50
+ * Ajoute une entrée (IP, CIDR ou preset) à la `BlockList`.
51
+ *
52
+ * @throws {Error} si l'entrée n'est ni une IP, ni un CIDR, ni un preset connu.
53
+ */
54
+ function addEntry(list, raw) {
55
+ const entry = raw.trim();
56
+ if (!entry) return;
57
+ const preset = PRESETS[entry.toLowerCase()];
58
+ if (preset) {
59
+ for (const [net, prefix, family] of preset) list.addSubnet(net, prefix, family);
60
+ return;
61
+ }
62
+ const slash = entry.indexOf("/");
63
+ if (slash !== -1) {
64
+ const net = entry.slice(0, slash);
65
+ const prefix = Number(entry.slice(slash + 1));
66
+ const family = isIPv6(net) ? "ipv6" : "ipv4";
67
+ list.addSubnet(net, prefix, family);
68
+ return;
69
+ }
70
+ if (isIPv4(entry)) {
71
+ list.addAddress(entry, "ipv4");
72
+ return;
73
+ }
74
+ if (isIPv6(entry)) {
75
+ list.addAddress(entry, "ipv6");
76
+ return;
77
+ }
78
+ throw new Error(`trustProxy: invalid IP/CIDR/preset "${raw}"`);
79
+ }
80
+ /**
81
+ * Compile une config `trustProxy` en un {@link TrustProxyChecker} (une fois, au
82
+ * boot — pas par requête). Les entrées invalides lèvent à la compilation (fail
83
+ * fast) plutôt que silencieusement par requête.
84
+ *
85
+ * @param config - voir {@link TrustProxyConfig}. `undefined` → aucune confiance.
86
+ * @returns un checker `isTrusted(remoteAddress)`.
87
+ */
88
+ function buildTrustProxy(config) {
89
+ if (config === true) return TRUST_ALL;
90
+ if (!config) return TRUST_NONE;
91
+ const entries = Array.isArray(config) ? config : [config];
92
+ const list = new BlockList();
93
+ for (const entry of entries) addEntry(list, entry);
94
+ return { isTrusted(remoteAddress) {
95
+ if (!remoteAddress) return false;
96
+ const addr = normalize(remoteAddress);
97
+ const family = isIPv6(addr) ? "ipv6" : isIPv4(addr) ? "ipv4" : null;
98
+ return family ? list.check(addr, family) : false;
99
+ } };
100
+ }
101
+ /**
102
+ * Cœur de la résolution **from-right** (OWASP) partagé par `X-Forwarded-For`
103
+ * (de-facto) et le paramètre `for` du header `Forwarded` (RFC 7239 §5.2).
104
+ *
105
+ * `chain` = liste des maillons forwarded NORMALISÉS (IP nues, gauche→droite :
106
+ * `[client, proxy1, proxy2]`), telle qu'écrite par les proxies par **append à
107
+ * droite**. La partie gauche est forgeable par le client (cf {@link extractClientIp}).
108
+ * La résolution part de la **connexion réelle** (le socket, non forgeable) et
109
+ * remonte de DROITE à GAUCHE : tant que le maillon courant est un proxy de
110
+ * confiance, on passe au précédent ; le **premier maillon non fiable** est l'IP
111
+ * cliente réelle.
112
+ *
113
+ * Un maillon `null` (identifiant obfusqué `_secret`, `unknown`, ou node illisible
114
+ * — RFC 7239 §6.3/§8.3) est une **barrière non franchissable** : on ne peut pas
115
+ * en vérifier la confiance → on s'arrête et on retourne le dernier maillon de
116
+ * confiance connu (une vraie IP de l'infra), jamais `null` ni la valeur obfusquée.
117
+ *
118
+ * @param chain - maillons forwarded normalisés (IP ou `null` si obfusqué).
119
+ * @param socketAddress - `socket.remoteAddress` (connexion TCP réelle, fiable).
120
+ * @param checker - politique de confiance ({@link buildTrustProxy}).
121
+ * @returns l'IP cliente réelle résolue (toujours une valeur fiable).
122
+ */
123
+ function resolveFromRight(chain, socketAddress, checker) {
124
+ let candidate = socketAddress;
125
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
126
+ if (!checker.isTrusted(candidate)) return candidate;
127
+ const next = chain[i];
128
+ if (next === null) return candidate;
129
+ candidate = next;
130
+ }
131
+ return candidate;
132
+ }
133
+ /**
134
+ * Résout l'adresse IP cliente RÉELLE à partir de la connexion socket et de la
135
+ * chaîne `X-Forwarded-For`, en dépouillant les proxies de confiance **de droite
136
+ * à gauche** (algorithme OWASP).
137
+ *
138
+ * `X-Forwarded-For` (de-facto) est construit par **append de gauche à droite** :
139
+ * `XFF: client, proxy1, proxy2`. Chaque proxy ajoute À DROITE l'adresse qu'il a
140
+ * vue. La partie **gauche** est donc entièrement **forgeable** par le client
141
+ * (il peut envoyer `X-Forwarded-For: 1.2.3.4` ; le 1ᵉʳ proxy ne fait qu'append
142
+ * l'IP réelle après). Lire `XFF[0]` revient à lire la valeur du client →
143
+ * **IP spoofing** (contournement de ban / rate-limit / allow-list / audit).
144
+ *
145
+ * Hot path préservé : sans `X-Forwarded-For` (cas direct usuel) la fonction
146
+ * retourne immédiatement le socket — 0 allocation (pas de split). Pour le header
147
+ * standard `Forwarded` (RFC 7239), voir `resolveForwarded` (module `forwarded`).
148
+ *
149
+ * @param xff - valeur brute de l'en-tête `X-Forwarded-For` (string, ou string[]
150
+ * si l'en-tête est répété), ou `undefined`.
151
+ * @param socketAddress - `socket.remoteAddress` (la connexion TCP réelle).
152
+ * @param checker - politique de confiance ({@link buildTrustProxy}).
153
+ * @returns l'IP cliente réelle, ou `null` si aucune connexion socket fiable.
154
+ */
155
+ function extractClientIp(xff, socketAddress, checker) {
156
+ if (!socketAddress) return null;
157
+ if (!xff) return socketAddress;
158
+ const s = Array.isArray(xff) ? xff.join(",") : xff;
159
+ if (s.indexOf(",") === -1) {
160
+ const ip = s.trim();
161
+ if (!ip) return socketAddress;
162
+ return checker.isTrusted(socketAddress) ? ip : socketAddress;
163
+ }
164
+ return resolveFromRight(s.split(",").map((part) => part.trim()).filter(Boolean), socketAddress, checker);
165
+ }
166
+ //#endregion
167
+ export { buildTrustProxy, extractClientIp, resolveFromRight };
@@ -0,0 +1,181 @@
1
+ import Cookie from "../../cookies/cookie.js";
2
+ import { decideSend, readBackpressureOptions } from "./wsBackpressure.js";
3
+ import Ws from "ws";
4
+ import http from "node:http";
5
+ //#region nodefony/src/context/websocket/Response.ts
6
+ /**
7
+ * Codes d'erreur d'écriture « le client est PARTI » : reload de page, onglet
8
+ * fermé, HMR — la socket TCP est morte côté client pendant qu'une frame était
9
+ * en vol. Race réseau INÉVITABLE (la garde `readyState === OPEN` ne la couvre
10
+ * pas : l'état bascule après l'échec d'écriture) — c'est du réseau normal, pas
11
+ * une erreur serveur.
12
+ */
13
+ const PEER_GONE_CODES = /* @__PURE__ */ new Set([
14
+ "EPIPE",
15
+ "ECONNRESET",
16
+ "ECONNABORTED",
17
+ "ERR_STREAM_DESTROYED",
18
+ "ERR_STREAM_WRITE_AFTER_END"
19
+ ]);
20
+ /**
21
+ * `true` si l'erreur d'écriture signifie seulement que le client s'est
22
+ * déconnecté (frame perdue, connexion en cours de fermeture) — à logger DEBUG,
23
+ * jamais ERROR + stack (vécu : un simple reload de page pendant un ping WS
24
+ * remplissait le journal d'« Error: write EPIPE »).
25
+ */
26
+ function isPeerGoneError(error) {
27
+ const code = error?.code;
28
+ return typeof code === "string" && PEER_GONE_CODES.has(code);
29
+ }
30
+ const WS_CLOSE_DESCRIPTIONS = {
31
+ 1e3: "Normal Closure",
32
+ 1001: "Going Away",
33
+ 1002: "Protocol Error",
34
+ 1003: "Unsupported Data",
35
+ 1005: "No Status Received",
36
+ 1006: "Abnormal Closure",
37
+ 1007: "Invalid frame payload data",
38
+ 1008: "Policy Violation",
39
+ 1009: "Message too big",
40
+ 1010: "Missing Extension",
41
+ 1011: "Internal Error",
42
+ 1012: "Service Restart",
43
+ 1013: "Try Again Later",
44
+ 1015: "TLS Handshake"
45
+ };
46
+ var WebsocketResponse = class {
47
+ context;
48
+ statusCode = 1e3;
49
+ body = null;
50
+ encoding = "utf-8";
51
+ connection = null;
52
+ statusMessage = "";
53
+ webSocketVersion;
54
+ cookies = {};
55
+ constructor(connection, context) {
56
+ this.context = context;
57
+ this.connection = connection;
58
+ }
59
+ log(pci, severity, msgid, msg) {
60
+ const syslog = this.context?.container?.get("syslog");
61
+ if (!msgid) msgid = "WEBSOCKET RESPONSE";
62
+ return syslog?.log(pci, severity, msgid, msg);
63
+ }
64
+ setConnection(connection) {
65
+ this.connection = connection;
66
+ return connection;
67
+ }
68
+ async send(data, encoding) {
69
+ const payload = data ?? this.body;
70
+ if (payload == null) throw new Error("no data");
71
+ return new Promise((resolve, reject) => {
72
+ if (!this.connection || this.connection.readyState !== Ws.OPEN) return reject(/* @__PURE__ */ new Error("WebSocket not open"));
73
+ const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context?.server);
74
+ const decision = decideSend(this.connection, max, policy, closeAfterDrops);
75
+ if (decision !== "send") {
76
+ this.logFirstDrop(this.connection, decision, max);
77
+ return resolve(this);
78
+ }
79
+ const sendData = encoding === "binary" && Buffer.isBuffer(payload) ? payload : payload instanceof Buffer ? payload.toString(encoding ?? this.encoding) : payload;
80
+ this.connection.send(sendData, (error) => {
81
+ if (error) {
82
+ if (isPeerGoneError(error)) {
83
+ this.log(`WS send: client déconnecté pendant l'écriture (${error.code}) — frame perdue`, "DEBUG");
84
+ return resolve(this);
85
+ }
86
+ this.log(error, "ERROR");
87
+ return reject(error);
88
+ }
89
+ return resolve(this);
90
+ });
91
+ });
92
+ }
93
+ broadcast(data, type) {
94
+ const payload = data ?? this.body;
95
+ if (!payload) return;
96
+ const wss = this.context?.server;
97
+ if (!wss) return;
98
+ const sendData = type === "binary" && Buffer.isBuffer(payload) ? payload : payload instanceof Buffer ? payload.toString(this.encoding) : payload;
99
+ const { max, policy, closeAfterDrops } = readBackpressureOptions(wss);
100
+ const onWriteError = (error) => {
101
+ if (error && !isPeerGoneError(error)) this.log(error, "ERROR");
102
+ };
103
+ wss.clients.forEach((client) => {
104
+ if (client.readyState !== Ws.OPEN) return;
105
+ const decision = decideSend(client, max, policy, closeAfterDrops);
106
+ if (decision !== "send") {
107
+ this.logFirstDrop(client, decision, max);
108
+ return;
109
+ }
110
+ client.send(sendData, onWriteError);
111
+ });
112
+ }
113
+ /**
114
+ * Logge UNE seule fois par connexion (au 1er drop) qu'un client subit la
115
+ * backpressure — observabilité opérateur sans bruit dans le hot path.
116
+ */
117
+ logFirstDrop(ws, decision, max) {
118
+ if (ws._nfDrops === 1) this.log(`WS backpressure → ${decision} (bufferedAmount > ${max} o) — client lent à recevoir`, "WARNING");
119
+ }
120
+ setBody(ele, encoding) {
121
+ if (typeof ele === "string") this.body = Buffer.from(ele, encoding ?? this.encoding);
122
+ else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
123
+ else if ("buffer" in ele && ele.buffer instanceof ArrayBuffer) this.body = Buffer.from(ele.buffer);
124
+ return this.body;
125
+ }
126
+ drop(reasonCode, description) {
127
+ if (this.connection && this.connection.readyState === Ws.OPEN) return this.connection.close(reasonCode ?? this.statusCode, description);
128
+ throw new Error("Connection already closed");
129
+ }
130
+ close(reasonCode, description) {
131
+ if (this.connection && this.connection.readyState === Ws.OPEN) return this.connection.close(reasonCode ?? this.statusCode, description ?? "closed");
132
+ throw new Error("Connection already closed");
133
+ }
134
+ getStatus() {
135
+ return {
136
+ code: this.getStatusCode(),
137
+ message: this.getStatusMessage()
138
+ };
139
+ }
140
+ getStatusCode() {
141
+ return this.statusCode;
142
+ }
143
+ getStatusMessage() {
144
+ return this.statusMessage;
145
+ }
146
+ setStatusCode(status, message) {
147
+ if (typeof status !== "number") {
148
+ status = parseInt(status, 10);
149
+ if (isNaN(status)) status = 500;
150
+ }
151
+ if (!status) status = 500;
152
+ this.statusCode = status;
153
+ if (!message) message = WS_CLOSE_DESCRIPTIONS[this.statusCode] ?? http.STATUS_CODES[this.statusCode];
154
+ this.statusMessage = message ?? "";
155
+ return {
156
+ code: this.statusCode,
157
+ message: this.statusMessage
158
+ };
159
+ }
160
+ clean() {
161
+ this.connection = null;
162
+ this.body = null;
163
+ }
164
+ setEncoding(encoding) {
165
+ return this.encoding = encoding;
166
+ }
167
+ setHeader() {
168
+ return true;
169
+ }
170
+ setHeaders() {
171
+ return true;
172
+ }
173
+ addCookie(cookie) {
174
+ if (cookie instanceof Cookie) this.cookies[cookie.name] = cookie;
175
+ else throw new Error("Response addCookies not valid cookies");
176
+ }
177
+ setCookies() {}
178
+ setCookie(_cookie) {}
179
+ };
180
+ //#endregion
181
+ export { WebsocketResponse as default, isPeerGoneError };