@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4

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 (139) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +19 -11
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +45 -6
  9. package/dist/nodefony/config/defineModuleConfig.js +2 -3
  10. package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
  11. package/dist/nodefony/service/HttpAdminApi.js +2 -3
  12. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  13. package/dist/nodefony/service/audit-logger.js +22 -16
  14. package/dist/nodefony/service/certificates.js +228 -19
  15. package/dist/nodefony/service/error-renderer.js +30 -13
  16. package/dist/nodefony/service/http-kernel.js +331 -82
  17. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  18. package/dist/nodefony/service/request-logger.js +24 -16
  19. package/dist/nodefony/service/reverse-proxy.js +336 -0
  20. package/dist/nodefony/service/servers/clientError.js +22 -3
  21. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  22. package/dist/nodefony/service/servers/server-http.js +5 -8
  23. package/dist/nodefony/service/servers/server-https.js +12 -24
  24. package/dist/nodefony/service/servers/server-static.js +7 -10
  25. package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
  26. package/dist/nodefony/service/servers/server-websocket.js +18 -18
  27. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  28. package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
  29. package/dist/nodefony/service/sessions/sessions-service.js +34 -20
  30. package/dist/nodefony/service/trace.js +1 -2
  31. package/dist/nodefony/service/upload/upload-service.js +5 -8
  32. package/dist/nodefony/service/x509.js +1 -2
  33. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  34. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  35. package/dist/nodefony/src/context/Context.js +51 -32
  36. package/dist/nodefony/src/context/domainMatcher.js +21 -4
  37. package/dist/nodefony/src/context/forwarded.js +3 -4
  38. package/dist/nodefony/src/context/http/HttpContext.js +98 -46
  39. package/dist/nodefony/src/context/http/Request.js +34 -38
  40. package/dist/nodefony/src/context/http/Response.js +39 -7
  41. package/dist/nodefony/src/context/http/SseStream.js +349 -0
  42. package/dist/nodefony/src/context/http/parser.js +36 -10
  43. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  44. package/dist/nodefony/src/context/http2/Request.js +4 -8
  45. package/dist/nodefony/src/context/http2/Response.js +22 -9
  46. package/dist/nodefony/src/context/metaData.js +1 -2
  47. package/dist/nodefony/src/context/requestId.js +1 -2
  48. package/dist/nodefony/src/context/responseEnded.js +26 -0
  49. package/dist/nodefony/src/context/transport.js +45 -0
  50. package/dist/nodefony/src/context/trustProxy.js +1 -2
  51. package/dist/nodefony/src/context/websocket/Response.js +42 -6
  52. package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
  53. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  54. package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
  55. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  57. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  58. package/dist/nodefony/src/cookies/cookie.js +3 -7
  59. package/dist/nodefony/src/errors/httpError.js +1 -2
  60. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  61. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  62. package/dist/nodefony/src/profiler/Profiler.js +108 -30
  63. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  64. package/dist/nodefony/src/proxy/forward.js +500 -0
  65. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  66. package/dist/nodefony/src/proxy/rules.js +188 -0
  67. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  68. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  69. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  70. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  71. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  72. package/dist/nodefony/src/servers/portBinder.js +41 -10
  73. package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
  74. package/dist/nodefony/src/session/session.js +30 -15
  75. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  76. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  79. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  80. package/dist/types/index.d.ts +12 -3
  81. package/dist/types/nodefony/config/config.d.ts +16 -0
  82. package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
  83. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
  84. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  85. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
  86. package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
  87. package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
  88. package/dist/types/nodefony/interfaces/index.d.ts +3 -2
  89. package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
  90. package/dist/types/nodefony/service/certificates.d.ts +147 -5
  91. package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
  92. package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
  93. package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
  94. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  95. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  96. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  97. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  98. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
  99. package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
  100. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  101. package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
  102. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
  103. package/dist/types/nodefony/src/context/Context.d.ts +35 -6
  104. package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
  105. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
  106. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  107. package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
  108. package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
  109. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  110. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  111. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  112. package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
  113. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  114. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  115. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  116. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  117. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
  118. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  119. package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
  120. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  121. package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
  122. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  123. package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
  124. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  125. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  126. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  127. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
  128. package/dist/types/nodefony/src/session/session.d.ts +2 -0
  129. package/docs/cookies.md +85 -78
  130. package/docs/index.md +5 -1
  131. package/docs/observabilite.md +81 -81
  132. package/docs/rate-limit.md +83 -83
  133. package/docs/reverse-proxy.md +522 -0
  134. package/docs/servers.md +378 -230
  135. package/docs/session.md +180 -180
  136. package/docs/sse.md +287 -0
  137. package/docs/upload.md +120 -117
  138. package/package.json +10 -11
  139. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -7,14 +7,17 @@ import type { IRateLimitStore } from "../src/rateLimit/IRateLimitStore.js";
7
7
  import { type ITrustedHostsConfig } from "../src/context/domainMatcher.js";
8
8
  import http from "node:http";
9
9
  import http2 from "node:http2";
10
- import type { IncomingMessage } from "node:http";
10
+ import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
11
11
  import Ws from "ws";
12
12
  import httpServer from "../service/servers/server-http.js";
13
13
  import httpsServer from "../service/servers/server-https.js";
14
+ import h2cServer from "../service/servers/server-h2c.js";
14
15
  import websocketServer from "../service/servers/server-websocket.js";
15
16
  import websocketSecureServer from "../service/servers/server-websocket-secure.js";
16
17
  import Statics from "./servers/server-static.js";
18
+ import type ReverseProxy from "./reverse-proxy.js";
17
19
  import WebsocketContext from "../src/context/websocket/WebsocketContext.js";
20
+ import type { IUpgradeAdmission } from "../src/servers/upgradeDispatch.js";
18
21
  import HttpContext from "../src/context/http/HttpContext.js";
19
22
  import Context, { HTTPMethod, WebSocketState } from "../src/context/Context.js";
20
23
  import Certicates from "./certificates.js";
@@ -25,13 +28,24 @@ import type { IErrorRenderer } from "../interfaces/IErrorRenderer.js";
25
28
  import type { IRequestLogger } from "../interfaces/IRequestLogger.js";
26
29
  import type { DefaultOptionsService } from "nodefony";
27
30
  import type { IHttpConfig } from "../config/config.js";
31
+ /**
32
+ * Config interne (shape déclarée par `config/config.ts`). Locale au module —
33
+ * pas exportée tant que la surface publique n'a pas vocation à être étendue.
34
+ */
35
+ /** Ce qu'il faut d'une requête pour en compter la connexion par IP. */
36
+ export interface IConnectionRequest {
37
+ readonly headers: IncomingHttpHeaders;
38
+ readonly socket: {
39
+ readonly remoteAddress?: string | undefined;
40
+ };
41
+ }
28
42
  export type ProtocolType = "1.1" | "2.0" | "3.0";
29
43
  export type httpRequest = http.IncomingMessage | http2.Http2ServerRequest;
30
44
  export type httpResponse = http.ServerResponse | http2.Http2ServerResponse;
45
+ export { responseEnded } from "../src/context/responseEnded.js";
31
46
  export type ContextType = WebsocketContext | HttpContext | Context;
32
- export type ServerType = "http" | "https" | "http2" | "http3" | "websocket" | "websocket-secure";
33
- export type responseTimeoutType = "http" | "https" | "http2" | "http3";
34
- export type SchemeType = "http" | "https" | "ws" | "wss";
47
+ export type { ServerType, SchemeType } from "../interfaces/IContext.js";
48
+ import type { ServerType, SchemeType } from "../interfaces/IContext.js";
35
49
  export interface WsMetaData {
36
50
  type?: "message" | "handshake" | undefined;
37
51
  state?: WebSocketState | undefined;
@@ -81,18 +95,31 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
81
95
  cert: string;
82
96
  ca: string;
83
97
  serverStatic: Statics | null;
98
+ /**
99
+ * Proxy inverse (`reverse-proxy`) — lu à chaque requête avant le routage ;
100
+ * `mounts === null` tant que personne n'a monté de préfixe.
101
+ */
102
+ reverseProxy: ReverseProxy | null;
84
103
  domain: string;
85
104
  trustedHosts?: ITrustedHostsConfig | undefined;
86
- domainCheck: boolean;
105
+ /**
106
+ * Contrôle de domaine actif (`domainCheck` de la config d'application) —
107
+ * lu à la source, pour le HTTP comme pour le handshake WebSocket.
108
+ */
109
+ get domainCheck(): boolean;
87
110
  regAlias: RegExp[];
88
111
  module: Module;
89
- httpsPort?: number | undefined;
90
- httpPort?: number | undefined;
112
+ /**
113
+ * Port du serveur HTTPS effectivement lié, `undefined` s'il est inactif —
114
+ * lu sur le serveur, jamais recopié (lu par `HttpContext.redirectHttps`).
115
+ */
116
+ get httpsPort(): number | undefined;
117
+ /** Port du serveur HTTP effectivement lié, `undefined` s'il est inactif. */
118
+ get httpPort(): number | undefined;
119
+ private boundPort;
91
120
  responseTimeout: {
92
121
  http: number;
93
122
  https: number;
94
- http2: number;
95
- http3: number;
96
123
  };
97
124
  closeTimeOutWs: {
98
125
  ws: number;
@@ -196,6 +223,22 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
196
223
  * @returns la politique mémoïsée (0 alloc après le 1er handshake).
197
224
  */
198
225
  private getWsOriginPolicy;
226
+ /**
227
+ * L'`Origin` d'un handshake WebSocket est-elle acceptée ? Règle UNIQUE,
228
+ * interrogeable SANS contexte : le serveur WebSocket de Nodefony
229
+ * ({@link checkWebsocketOrigin}) et le proxy inverse, qui relaie un upgrade
230
+ * avant qu'aucun contexte n'existe, décident par elle.
231
+ *
232
+ * Same-origin (nom de l'`Origin` = nom servi, port ignoré), loopback toléré
233
+ * en development, allowlist `allowedOrigins` ; sans `Origin` (client
234
+ * non-navigateur) → acceptée. Doctrine complète : {@link checkWebsocketOrigin}.
235
+ *
236
+ * @param origin - valeur brute de l'en-tête `Origin`
237
+ * @param hostname - nom d'hôte servi, sans port (`Context.domain`)
238
+ * @param secure - handshake reçu en TLS (politique `websocketSecure`)
239
+ * @returns `true` si le handshake peut se poursuivre
240
+ */
241
+ isWebsocketOriginAllowed(origin: string | undefined, hostname: string, secure: boolean): boolean;
199
242
  /**
200
243
  * B4 — Validation d'`Origin` au handshake WebSocket (anti-CSWSH, OWASP
201
244
  * WSTG-CLNT-10). Les navigateurs n'appliquent PAS CORS aux WebSockets : sans
@@ -304,7 +347,7 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
304
347
  */
305
348
  /**
306
349
  * Override the default error renderer — e.g. to hide stack traces in prod
307
- * or emit RFC 7807 problem+json. Stateless singleton expected.
350
+ * or emit RFC 9457 problem+json. Stateless singleton expected.
308
351
  */
309
352
  setErrorRenderer(renderer: IErrorRenderer): void;
310
353
  getErrorRenderer(): IErrorRenderer;
@@ -339,7 +382,7 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
339
382
  * pipeline.
340
383
  */
341
384
  private dispatchHttpRequest;
342
- initServers(): Promise<(httpServer | httpsServer | websocketServer | websocketSecureServer)[]>;
385
+ initServers(): Promise<(httpServer | httpsServer | h2cServer | websocketServer | websocketSecureServer)[]>;
343
386
  /**
344
387
  * Publie les ports EFFECTIFS du runtime (state file), une fois l'écoute établie.
345
388
  *
@@ -498,7 +541,7 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
498
541
  * @throws (rejet) le refus du pare-feu, après `onAuthFailure`.
499
542
  */
500
543
  private runFirewall;
501
- createWebsocketContext(scope: Scope, req: IncomingMessage, ws: Ws, type: ServerType): WebsocketContext;
544
+ createWebsocketContext(scope: Scope, req: IncomingMessage, ws: Ws | null, type: ServerType): WebsocketContext;
502
545
  /**
503
546
  * Teardown d'une connexion WS à sa fermeture : signal d'abandon, hooks
504
547
  * post-réponse, persistance de la session, puis libération du scope.
@@ -506,10 +549,47 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
506
549
  * écouteur d'événement synchrone, qui n'attend rien).
507
550
  */
508
551
  private finishWebsocket;
509
- onWebsocketRequest(ws: Ws, req: IncomingMessage, type: ServerType): Promise<unknown>;
510
- handleWebsocket(scope: Scope, ws: Ws, req: IncomingMessage, type: ServerType): Promise<unknown>;
552
+ /**
553
+ * Point d'entrée d'un upgrade vers une route WebSocket de Nodefony, appelé
554
+ * par le répartiteur d'upgrade AVANT le `101` (#596) : rien ne s'ouvre tant
555
+ * que le pipeline n'a pas dit oui.
556
+ *
557
+ * Les bornes par IP passent en premier, avant tout scope, ALS ou pipeline
558
+ * (symétrie avec le 429 HTTP et avec le proxy inverse) : le vrai vecteur DoS
559
+ * est d'accumuler des sockets ouvertes. La place est rendue à la fermeture du
560
+ * socket du client — même règle que pour un tunnel relayé.
561
+ *
562
+ * @param req - requête d'upgrade
563
+ * @param admission - le verdict à rendre : accepter (`101`) ou refuser (HTTP)
564
+ * @param type - serveur qui a reçu l'upgrade
565
+ * @returns la promesse du traitement de la connexion
566
+ */
567
+ onWebsocketUpgrade(req: IncomingMessage, admission: IUpgradeAdmission, type: ServerType): Promise<unknown>;
568
+ /**
569
+ * Pipeline d'un handshake WebSocket, dans la bulle ALS de la connexion :
570
+ * jugement (`onConnect`), pare-feu, gardes de l'action — PUIS seulement
571
+ * l'acceptation, la socket et l'action de handshake. Tout refus antérieur à
572
+ * l'acceptation part en statut HTTP (`onError` → `refuseHandshake`).
573
+ *
574
+ * @param scope - scope DI de la connexion
575
+ * @param req - requête d'upgrade
576
+ * @param admission - le verdict à rendre
577
+ * @param type - serveur qui a reçu l'upgrade
578
+ * @returns ce que rend l'action de handshake
579
+ */
580
+ handleWebsocket(scope: Scope, req: IncomingMessage, admission: IUpgradeAdmission, type: ServerType): Promise<unknown>;
511
581
  private releaseOrphanWsScope;
512
- onConnect(context: WebsocketContext | null, error?: unknown): Promise<Ws | number>;
582
+ /**
583
+ * Juge un handshake WebSocket AVANT son acceptation : hôte, origine,
584
+ * `beforeResolve`, contrôleur (protocole négocié), session. N'ouvre rien —
585
+ * l'acceptation n'a lieu qu'après le pare-feu et les gardes de l'action
586
+ * (cf {@link handleWebsocket}).
587
+ *
588
+ * @param context - contexte du handshake, encore sans socket
589
+ * @param error - erreur levée à la construction du contexte, relancée telle quelle
590
+ * @throws la première erreur du jugement — rendue en statut HTTP par `onError`
591
+ */
592
+ onConnect(context: WebsocketContext | null, error?: unknown): Promise<void>;
513
593
  checkValidDomain(context: ContextType): number;
514
594
  isValidDomain(context: ContextType): boolean;
515
595
  /**
@@ -527,5 +607,67 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
527
607
  * couvre ce nom.
528
608
  */
529
609
  isTrustedHostname(hostname: string): boolean;
610
+ /**
611
+ * Refus d'un upgrade WebSocket AVANT le `101`, par un statut HTTP
612
+ * (RFC 6455 §4.2.2) — appelé par le répartiteur d'upgrade, après le proxy
613
+ * inverse et avant le serveur `ws`.
614
+ *
615
+ * Pourquoi avant : accepter puis fermer fait croire au client que le serveur
616
+ * a répondu. Le client Vite sonde « le serveur est-il revenu ? » en ouvrant
617
+ * une socket ; une ouverture suivie d'une fermeture `4004` lui faisait
618
+ * recharger la page avant que Vite soit prêt — page blanche.
619
+ *
620
+ * Ne juge que l'existence d'une route WebSocket pour le chemin
621
+ * ({@link IRequestRouter.servesWebsocket}) : hôte, origine, sous-protocole,
622
+ * quotas et pare-feu restent jugés après l'ouverture, comme avant. Un refus
623
+ * compte au limiteur de débit comme une requête HTTP refusée — sans quoi un
624
+ * flot d'upgrades vers des chemins inexistants y échapperait.
625
+ *
626
+ * @param req - requête d'upgrade
627
+ * @returns le statut du refus — `404` chemin sans route WebSocket, `400` URL
628
+ * inanalysable, `429` débit dépassé, `503` routeur pas encore posé (boot) —
629
+ * ou `null` si l'upgrade est admis
630
+ */
631
+ websocketUpgradeRefusal(req: IncomingMessage): 400 | 404 | 429 | 503 | null;
632
+ /**
633
+ * Bornes par IP d'un upgrade WebSocket : débit de handshakes (le MÊME
634
+ * compteur que les requêtes HTTP — un upgrade EST un GET) puis connexions
635
+ * simultanées (`wsMaxConnectionsPerIp`). Seule implémentation, appelée par
636
+ * le serveur WebSocket de Nodefony ET par le proxy inverse pour un upgrade
637
+ * qu'il relaie : un tunnel relayé n'échappe pas aux quotas d'une connexion
638
+ * servie.
639
+ *
640
+ * L'IP est résolue comme en HTTP (RFC 7239 + `trustProxy`), et seulement si
641
+ * un limiteur est armé — 0 coût quand tout est désactivé (défaut). Aucun log
642
+ * par refus : sous un flood, il serait lui-même un amplificateur.
643
+ *
644
+ * @param req - requête d'upgrade
645
+ * @param connection - ce dont la fermeture libère la place comptée (le `ws`
646
+ * accepté, ou le socket du client pour un tunnel relayé)
647
+ * @returns `null` si l'upgrade est admis — sa place est alors rendue à la
648
+ * fermeture de `connection` —, sinon la raison du refus
649
+ */
650
+ websocketQuotaRefusal(req: IncomingMessage, connection: {
651
+ once(event: "close", listener: () => void): unknown;
652
+ }): "rate limit" | "too many connections" | null;
653
+ /**
654
+ * Prend une place de connexion LONGUE pour l'IP de la requête — le plafond
655
+ * `wsMaxConnectionsPerIp`, partagé par les sockets WebSocket ET les flux SSE :
656
+ * un seul budget par IP, sinon N sockets plus N flux doubleraient le plafond.
657
+ * Le débit n'est pas recompté ici : la requête qui ouvre un flux l'a déjà été
658
+ * à l'entrée du pipeline.
659
+ *
660
+ * @param req - la requête qui ouvre le flux (en-têtes + socket du pair)
661
+ * @param connection - ce dont la fermeture rend la place (la réponse)
662
+ * @returns `true` si la place est prise — ou si aucun plafond n'est armé
663
+ */
664
+ acquireConnectionSlot(req: IConnectionRequest, connection: {
665
+ once(event: "close", listener: () => void): unknown;
666
+ }): boolean;
667
+ /**
668
+ * Re-valide tous les flux SSE et ferme ceux dont l'identité est morte.
669
+ * Publique pour qu'un banc la déclenche sans attendre le tick.
670
+ */
671
+ revalidateStreams(): Promise<void>;
530
672
  }
531
673
  export default HttpKernel;
@@ -0,0 +1,156 @@
1
+ import http from "node:http";
2
+ import type { Duplex } from "node:stream";
3
+ import { Service, Module } from "nodefony";
4
+ import type { IProxyMount, IProxyMountOptions } from "../interfaces/IReverseProxy.js";
5
+ import { type ProxiedRequest, type ProxiedResponse } from "../src/proxy/forward.js";
6
+ /** Un montage tel que le service le garde : la dernière cible analysée y est mise en cache. */
7
+ interface IMountRecord extends IProxyMount {
8
+ /** D'où vient le montage : la configuration de l'application, ou un module. */
9
+ readonly source: "config" | "module";
10
+ /** Dernière origine rendue par `target`, et son analyse. */
11
+ lastOrigin: string | undefined;
12
+ lastUrl: URL | undefined;
13
+ /** Pool de connexions vers l'amont, créé au premier relais (scheme de la cible). */
14
+ agent: http.Agent | null;
15
+ /** Tunnels WebSocket ouverts vers l'amont — bornés par `maxSockets`, hors pool. */
16
+ tunnels: number;
17
+ }
18
+ /**
19
+ * Proxy inverse de Nodefony : relaie un préfixe d'URL vers un autre serveur
20
+ * HTTP — requêtes ET, sur demande, upgrades WebSocket — sur la MÊME origine
21
+ * que la page. Le client ne voit qu'un serveur, un certificat ; l'amont peut
22
+ * rester sur la boucle locale.
23
+ *
24
+ * Deux sources de montages, mêmes règles (`src/proxy/rules.ts`) : la section
25
+ * `proxy.mounts` de la configuration, lue à la construction, et
26
+ * {@link ReverseProxy.mount}, appelé par un module (`@nodefony/frontend` y
27
+ * relaie `/_vite/<famille>/` vers ses serveurs Vite de développement). Un
28
+ * module se résout ce service par NOM (`container.get("reverse-proxy")`) :
29
+ * il n'a pas à importer `@nodefony/http`.
30
+ *
31
+ * Consulté AVANT le routage : une route attrape-tout de l'application ne peut
32
+ * pas avaler un préfixe relayé. Sans montage, le pipeline ne paie qu'une
33
+ * lecture de champ (`mounts === null`) — c'est le cas de toute application
34
+ * qui ne s'en sert pas.
35
+ *
36
+ * Règles communes à tout montage : en-têtes de connexion jamais transmis
37
+ * (RFC 9110 §7.6.1), `X-Forwarded-For/-Proto/-Host` et `Via` posés par le
38
+ * proxy, Host hors `trustedHosts` jamais relayé (même barrière, même 421 que
39
+ * toute requête), amont injoignable → 502, amont muet → 504, boucle → 508.
40
+ */
41
+ declare class ReverseProxy extends Service {
42
+ #private;
43
+ module: Module;
44
+ /**
45
+ * Montages actifs, du préfixe le PLUS LONG au plus court : le premier qui
46
+ * couvre une URL est le plus spécifique. `null` tant qu'il n'y en a aucun.
47
+ */
48
+ mounts: IMountRecord[] | null;
49
+ /** Délai d'inactivité appliqué aux montages qui n'en fixent pas (`proxy.timeoutMs`). */
50
+ readonly defaultTimeoutMs: number;
51
+ /** Délai d'établissement de la connexion amont (`proxy.connectTimeoutMs`). */
52
+ readonly connectTimeoutMs: number;
53
+ /** Connexions simultanées par amont (`proxy.maxSockets`) : pool ET tunnels WebSocket. */
54
+ readonly maxSockets: number;
55
+ /**
56
+ * Nom de CE processus dans `Via` (RFC 9110 §7.6.3) : tiré au hasard au
57
+ * démarrage, il ne dit rien de la machine et distingue deux instances —
58
+ * un message qui le porte déjà est une boucle (508).
59
+ */
60
+ readonly pseudonym: string;
61
+ constructor(module: Module);
62
+ /**
63
+ * Relaie un préfixe d'URL vers un amont. Idempotent pour un même module :
64
+ * un même préfixe est remplacé. Un module qui recouvre un préfixe de la
65
+ * CONFIGURATION le remplace aussi, mais le dit — l'auteur de l'application
66
+ * doit pouvoir lire pourquoi son montage ne sert plus.
67
+ *
68
+ * @param prefix - préfixe d'URL (normalisé : `/` en tête et en fin)
69
+ * @param options - cible et réglages du montage
70
+ * @throws Error qui nomme chaque refus (préfixe `/` ou réservé, cible qui
71
+ * n'est pas une origine http(s) nue, `secure: false` hors boucle locale…)
72
+ */
73
+ mount(prefix: string, options: IProxyMountOptions): void;
74
+ /**
75
+ * Retire un montage. Le dernier retiré rend `mounts` à `null` : le pipeline
76
+ * cesse alors de le consulter.
77
+ *
78
+ * @param prefix - préfixe tel que monté (normalisé de la même façon)
79
+ */
80
+ unmount(prefix: string): void;
81
+ /**
82
+ * Ferme tous les pools (arrêt du noyau). Les montages restent déclarés ; un
83
+ * relais encore en vol part ensuite sur une connexion à usage unique.
84
+ */
85
+ closeAll(): void;
86
+ /**
87
+ * Montage le plus spécifique qui couvre une URL, s'il y en a un.
88
+ *
89
+ * @param url - cible brute de la requête (chemin + requête)
90
+ * @returns le montage au préfixe le plus long qui couvre l'URL
91
+ */
92
+ match(url: string | undefined): IProxyMount | undefined;
93
+ /**
94
+ * Origine courante d'un montage, analysée — mise en cache tant que la cible
95
+ * rend la même chaîne.
96
+ *
97
+ * @returns l'URL de l'amont, ou `undefined` si rien n'est relayable
98
+ */
99
+ private targetOf;
100
+ /**
101
+ * Pool de connexions d'un montage, créé au premier relais et recréé si la
102
+ * cible change de scheme.
103
+ *
104
+ * Un `Agent` DÉDIÉ, jamais l'agent global : sous `NODE_USE_ENV_PROXY`, cas
105
+ * courant d'un poste d'entreprise, l'agent global envoie tout vers
106
+ * `HTTP_PROXY` — un amont sur la boucle locale serait demandé au proxy de
107
+ * l'entreprise. Un agent construit ici n'a pas de `proxyEnv` : il va droit
108
+ * à la cible. Keep-alive borné (`maxSockets`), `lifo` : le socket le plus
109
+ * récent, le moins susceptible d'avoir été fermé par l'amont.
110
+ */
111
+ private agentFor;
112
+ /** Réglages de relais d'un montage. */
113
+ private relayFor;
114
+ /** Règles du noyau HTTP (résolues par nom), `undefined` hors serveur. */
115
+ private rules;
116
+ /** Ce que le proxy annonce à l'amont sur le client. */
117
+ private fromOf;
118
+ /**
119
+ * Relaie une requête HTTP si un montage la couvre — méthode comprise. Cible
120
+ * pas encore connue → `503` + `Retry-After`, sans aller jusqu'au routage.
121
+ *
122
+ * L'appelant a déjà tranché la barrière d'hôte : c'est le pipeline HTTP, qui
123
+ * la calcule pour toute requête (`Context.validDomain`). Un chemin ambigu
124
+ * sous un préfixe monté rend 400, sans contacter l'amont.
125
+ *
126
+ * ⚠️ Un préfixe monté est servi AVANT le pare-feu, la CSRF et les en-têtes
127
+ * de sécurité applicatifs : c'est une surface publique pour qui atteint
128
+ * Nodefony. L'amont porte sa propre politique d'accès.
129
+ *
130
+ * @param req - requête du client
131
+ * @param res - réponse au client
132
+ * @param scheme - scheme par lequel le client est arrivé
133
+ * @returns la promesse de fin d'échange, ou `undefined` si la requête n'est
134
+ * pas relayée (hors préfixe ou méthode non relayée : elle suit alors son
135
+ * chemin normal)
136
+ */
137
+ forward(req: ProxiedRequest, res: ProxiedResponse, scheme: string): Promise<void> | undefined;
138
+ /**
139
+ * Relaie un upgrade WebSocket si un montage `websocket` couvre son URL.
140
+ * Appelé par le répartiteur d'`upgrade` des serveurs WebSocket, AVANT le
141
+ * serveur WebSocket de Nodefony.
142
+ *
143
+ * Contrôles, dans l'ordre, chacun avec la réponse que rendrait le serveur
144
+ * WebSocket de Nodefony : cible pas encore connue → 503 ; `Host` hors `trustedHosts` (si `domainCheck`) →
145
+ * 421 ; chemin ambigu ou handshake non conforme (RFC 6455 §4.2.1 — seul
146
+ * `Upgrade: websocket` est relayé, jamais un tunnel `h2c`) → 400 ;
147
+ * `Origin` refusée par la MÊME règle que le serveur WebSocket (anti-CSWSH,
148
+ * RFC 6455 §10.2) → 403 ; `maxSockets` tunnels déjà ouverts vers l'amont →
149
+ * 503 ; quotas par IP du noyau (débit de handshakes, connexions
150
+ * simultanées) → 429. Le socket est alors consommé.
151
+ *
152
+ * @returns `true` si le proxy a pris le socket en charge
153
+ */
154
+ handleUpgrade(req: http.IncomingMessage, socket: Duplex, head: Buffer): boolean;
155
+ }
156
+ export default ReverseProxy;
@@ -12,3 +12,19 @@ import type { Duplex } from "node:stream";
12
12
  * @param socket - flux client brut (l'event `clientError` fournit un `Duplex`).
13
13
  */
14
14
  export declare function handleClientError(error: NodeJS.ErrnoException, socket: Duplex): void;
15
+ /**
16
+ * Gère l'event `sessionError` d'un serveur HTTP/2 (h2c ou TLS) : un pair PARTI
17
+ * se tait, toute autre erreur est rapportée.
18
+ *
19
+ * Une session réinitialisée par le pair n'a rien à diagnostiquer. C'est la
20
+ * signature d'une sonde de port — `nodefony status`, `doctor`, le superviseur
21
+ * de développement, la sonde `tcpSocket` d'un orchestrateur — qui se connecte
22
+ * puis ferme sans lire le `SETTINGS` que le serveur envoie dès l'ouverture : le
23
+ * système ferme alors par un RST plutôt qu'un FIN, et Node lève `ECONNRESET`.
24
+ * La rapporter imprimait une pile entière par sonde. La règle « client parti »
25
+ * est celle des frames WebSocket ({@link isPeerGoneError}), pas une copie.
26
+ *
27
+ * @param error - erreur de session émise par `node:http2`
28
+ * @param report - rapporte une erreur réelle, à la sévérité choisie par le serveur
29
+ */
30
+ export declare function handleSessionError(error: Error, report: (error: Error) => void): void;
@@ -0,0 +1,51 @@
1
+ import { Service, type Module, FamilyType } from "nodefony";
2
+ import HttpKernel, { ProtocolType, ServerType, SchemeType } from "../http-kernel.js";
3
+ import http2 from "node:http2";
4
+ import { AddressInfo } from "node:net";
5
+ import { type IDrainTerminator } from "./serverShutdown.js";
6
+ import type { IHttpConfig } from "../../config/config.js";
7
+ import type { DefaultOptionsService } from "nodefony";
8
+ /**
9
+ * Serveur HTTP/2 en clair (h2c, connaissance préalable — RFC 9113 §3.3), sur un
10
+ * port DÉDIÉ (`servers.h2c`, 5153 par défaut), pour un proxy inverse qui parle
11
+ * HTTP/2 à ses amonts.
12
+ *
13
+ * Il partage le chemin HTTP/2 du 5152 — même `onHttpRequest`, mêmes limites
14
+ * anti-DoS (`http2ServerLimits`), même drain par GOAWAY — sans TLS : `serverType`
15
+ * vaut `"http"`, le schéma vient du transport ou d'un proxy de confiance, jamais
16
+ * du pseudo-en-tête `:scheme`. Le protocole du saut (`"h2c"`) se constate sur la
17
+ * requête (`transport.ts#alpnProtocolOf`).
18
+ *
19
+ * Hors de son périmètre, délibérément : HTTP/1.1 (refusé par la préface HTTP/2 —
20
+ * sondes et clients HTTP/1.1 restent sur le port HTTP), WebSocket (aucun CONNECT
21
+ * étendu, RFC 8441 : un proxy route `Upgrade: websocket` vers le port HTTP) et la
22
+ * bascule `Upgrade: h2c` (retirée par RFC 9113 §3.1).
23
+ */
24
+ declare class ServerH2c extends Service {
25
+ private readonly httpKernel;
26
+ options: IHttpConfig["http"] & DefaultOptionsService;
27
+ module: Module<IHttpConfig>;
28
+ server: http2.Http2Server | null;
29
+ httpTerminator: IDrainTerminator | null;
30
+ port: number;
31
+ protocol: ProtocolType;
32
+ ready: boolean;
33
+ type: ServerType;
34
+ domain: string | null;
35
+ scheme: SchemeType;
36
+ address: string | null;
37
+ family: FamilyType | null;
38
+ active: boolean;
39
+ infos: AddressInfo | null;
40
+ constructor(module: Module<IHttpConfig>, httpKernel: HttpKernel);
41
+ /**
42
+ * Crée et met en écoute le serveur h2c.
43
+ *
44
+ * @returns le serveur à l'écoute
45
+ * @throws BootConfigurationError si `servers.h2c.port` est celui du serveur
46
+ * HTTP ou HTTPS — un port partagé n'est pas un conflit à contourner
47
+ */
48
+ createServer(): Promise<http2.Http2Server>;
49
+ showBanner(): void;
50
+ }
51
+ export default ServerH2c;
@@ -1,9 +1,9 @@
1
- import { Service, Module, FamilyType } from "nodefony";
1
+ import { Service, type Module, FamilyType } from "nodefony";
2
2
  import HttpKernel, { ProtocolType, ServerType, SchemeType } from "../http-kernel.js";
3
3
  import http from "node:http";
4
4
  import http2 from "node:http2";
5
5
  import { AddressInfo } from "node:net";
6
- import { HttpTerminator } from "./serverShutdown.js";
6
+ import { type IDrainTerminator } from "./serverShutdown.js";
7
7
  import type { IHttpConfig } from "../../config/config.js";
8
8
  import type { DefaultOptionsService } from "nodefony";
9
9
  declare class ServerHttp extends Service {
@@ -11,7 +11,7 @@ declare class ServerHttp extends Service {
11
11
  options: IHttpConfig["http"] & DefaultOptionsService;
12
12
  module: Module;
13
13
  server: http.Server | http2.Http2Server | null;
14
- httpTerminator: HttpTerminator | null;
14
+ httpTerminator: IDrainTerminator | null;
15
15
  port: number;
16
16
  protocol: ProtocolType;
17
17
  ready: boolean;
@@ -1,15 +1,15 @@
1
- import { Service, Module, FamilyType } from "nodefony";
1
+ import { Service, type Module, FamilyType } from "nodefony";
2
2
  import HttpKernel, { ProtocolType, ServerType, SchemeType } from "../http-kernel.js";
3
3
  import https from "node:https";
4
4
  import http2 from "node:http2";
5
- import { HttpTerminator } from "./serverShutdown.js";
5
+ import { type IDrainTerminator } from "./serverShutdown.js";
6
6
  import { AddressInfo } from "node:net";
7
7
  import type { IHttpConfig } from "../../config/config.js";
8
8
  import type { DefaultOptionsService } from "nodefony";
9
9
  declare class ServerHttps extends Service {
10
10
  private readonly httpKernel;
11
11
  options: IHttpConfig["https"] & DefaultOptionsService;
12
- httpTerminator: HttpTerminator | null;
12
+ httpTerminator: IDrainTerminator | null;
13
13
  module: Module;
14
14
  server: https.Server | http2.Http2SecureServer | null;
15
15
  port: number;
@@ -23,7 +23,7 @@ declare class ServerHttps extends Service {
23
23
  active: boolean;
24
24
  infos: AddressInfo | null;
25
25
  constructor(module: Module, httpKernel: HttpKernel);
26
- terminator(): HttpTerminator;
26
+ terminator(): IDrainTerminator;
27
27
  setPort(): number;
28
28
  createServer(): Promise<https.Server | http2.Http2SecureServer>;
29
29
  createServerH2(): Promise<http2.Http2SecureServer>;
@@ -1,12 +1,12 @@
1
- import Ws, { WebSocketServer } from "ws";
1
+ import { WebSocketServer } from "ws";
2
2
  import { Service, Module, FamilyType } from "nodefony";
3
3
  import type { DefaultOptionsService } from "nodefony";
4
4
  import type { IHttpConfig } from "../../config/config.js";
5
5
  import HttpKernel, { ProtocolType, ServerType, SchemeType } from "../http-kernel.js";
6
6
  import { AddressInfo } from "node:net";
7
- import type { IncomingMessage } from "node:http";
8
7
  import httpsServers from "./server-https.js";
9
8
  declare class WebsocketSecure extends Service {
9
+ #private;
10
10
  private readonly httpKernel;
11
11
  options: IHttpConfig["websocketSecure"] & DefaultOptionsService;
12
12
  module: Module;
@@ -25,7 +25,6 @@ declare class WebsocketSecure extends Service {
25
25
  constructor(module: Module, httpKernel: HttpKernel);
26
26
  setPort(): number;
27
27
  createServer(serverHttps: httpsServers): Promise<WebSocketServer>;
28
- onConnection(ws: Ws, req: IncomingMessage): void;
29
28
  terminate(): Promise<boolean>;
30
29
  showBanner(): void;
31
30
  }
@@ -1,12 +1,12 @@
1
- import Ws, { WebSocketServer } from "ws";
1
+ import { WebSocketServer } from "ws";
2
2
  import { Service, Module, FamilyType } from "nodefony";
3
3
  import type { DefaultOptionsService } from "nodefony";
4
4
  import type { IHttpConfig } from "../../config/config.js";
5
5
  import HttpKernel, { ProtocolType, ServerType, SchemeType } from "../http-kernel.js";
6
6
  import { AddressInfo } from "node:net";
7
- import type { IncomingMessage } from "node:http";
8
7
  import httpServer from "./server-http.js";
9
8
  declare class Websocket extends Service {
9
+ #private;
10
10
  private readonly httpKernel;
11
11
  options: IHttpConfig["websocket"] & DefaultOptionsService;
12
12
  module: Module;
@@ -25,7 +25,6 @@ declare class Websocket extends Service {
25
25
  constructor(module: Module, httpKernel: HttpKernel);
26
26
  setPort(): number;
27
27
  createServer(serverHttp: httpServer): Promise<WebSocketServer>;
28
- onConnection(ws: Ws, req: IncomingMessage): void;
29
28
  terminate(): Promise<boolean>;
30
29
  showBanner(): void;
31
30
  }
@@ -1,27 +1,41 @@
1
- import { HttpTerminator } from "http-terminator";
2
1
  import type { Server as HttpServer } from "node:http";
3
2
  import type { Server as HttpsServer } from "node:https";
4
- import type { Http2SecureServer } from "node:http2";
3
+ import type { Http2SecureServer, Http2Server } from "node:http2";
4
+ /** Serveur que `createDrainTerminator` sait drainer. */
5
+ export type DrainableServer = HttpServer | HttpsServer | Http2Server | Http2SecureServer;
6
+ /** Arrêt gracieux d'un serveur, rendu par `createDrainTerminator`. */
7
+ export interface IDrainTerminator {
8
+ /**
9
+ * Draine puis ferme le serveur ; idempotent (le deuxième appel rend la même promesse).
10
+ *
11
+ * @returns une promesse résolue quand toutes les connexions sont fermées
12
+ */
13
+ terminate(): Promise<void>;
14
+ }
5
15
  /**
6
- * Fabrique le terminator de drain graceful d'un serveur HTTP/HTTPS/HTTP2.
16
+ * Fabrique l'arrêt gracieux d'un serveur HTTP/1.1, HTTPS, HTTP/2 sur TLS ou h2c.
7
17
  *
8
- * Au `terminate()` (shutdown SIGTERM/docker stop) : les requêtes in-flight se
9
- * terminent (header `connection: close` injecté sur les réponses en cours), les
10
- * sockets idle sont fermées immédiatement, et tout ce qui reste après
11
- * `shutdownTimeout` ms est détruit de force. Le terminator appelle lui-même
12
- * `server.close()` — ne pas le rappeler derrière.
18
+ * Au `terminate()` (SIGTERM, `docker stop`), le serveur cesse d'accepter, puis :
19
+ * - **HTTP/2** : chaque session reçoit un GOAWAY (`session.close()`) — les flux
20
+ * en vol se terminent, les nouveaux sont refusés, et le client sait qu'il doit
21
+ * rouvrir ailleurs au lieu de constater une coupure ;
22
+ * - **HTTP/1.1** : la réponse en vol part avec `Connection: close`, la socket
23
+ * inactive est fermée tout de suite, une requête qui arrive encore sur une
24
+ * socket gardée ouverte reçoit aussi `Connection: close`.
25
+ *
26
+ * Ce qui reste après `shutdownTimeout` ms est détruit. `server.close()` est
27
+ * appelé ici : ne pas le rappeler derrière.
13
28
  *
14
29
  * ⚠️ Les sockets WebSocket upgradées (sans réponse HTTP en cours) sont détruites
15
- * SANS frame Close par le terminator → les serveurs WS doivent fermer leurs
16
- * clients (close 1001) AVANT ce drain. Garanti par l'ordre des listeners
17
- * `onTerminate` : WS/WSS s'attachent en `prependOnceListener`, les serveurs
18
- * HTTP en `once`.
30
+ * SANS frame Close → les serveurs WS doivent fermer leurs clients (close 1001)
31
+ * AVANT ce drain. Garanti par l'ordre des listeners `onTerminate` : WS/WSS
32
+ * s'attachent en `prependOnceListener`, les serveurs HTTP en `once`.
19
33
  *
20
- * @param server - serveur Node à drainer (créé, pas forcément listening)
34
+ * @param server - serveur Node à drainer, créé AVANT son `listen` (le suivi des
35
+ * connexions commence ici)
21
36
  * @param shutdownTimeout - délai de drain en ms avant destruction forcée
22
- * @returns le terminator à invoquer au shutdown
37
+ * @returns l'arrêt à invoquer au shutdown
23
38
  */
24
- export declare function createDrainTerminator(server: HttpServer | HttpsServer | Http2SecureServer, shutdownTimeout?: number): HttpTerminator;
39
+ export declare function createDrainTerminator(server: DrainableServer, shutdownTimeout?: number): IDrainTerminator;
25
40
  /** Drain par défaut (ms) — même valeur que le défaut Zod `servers.*.shutdownTimeout`. */
26
41
  export declare const DEFAULT_SHUTDOWN_TIMEOUT = 5000;
27
- export type { HttpTerminator };