@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +19 -11
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +45 -6
- package/dist/nodefony/config/defineModuleConfig.js +2 -3
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -3
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +22 -16
- package/dist/nodefony/service/certificates.js +228 -19
- package/dist/nodefony/service/error-renderer.js +30 -13
- package/dist/nodefony/service/http-kernel.js +331 -82
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +7 -10
- package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
- package/dist/nodefony/service/servers/server-websocket.js +18 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
- package/dist/nodefony/service/sessions/sessions-service.js +34 -20
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +51 -32
- package/dist/nodefony/src/context/domainMatcher.js +21 -4
- package/dist/nodefony/src/context/forwarded.js +3 -4
- package/dist/nodefony/src/context/http/HttpContext.js +98 -46
- package/dist/nodefony/src/context/http/Request.js +34 -38
- package/dist/nodefony/src/context/http/Response.js +39 -7
- package/dist/nodefony/src/context/http/SseStream.js +349 -0
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +26 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +42 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +108 -30
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +500 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +188 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
- package/dist/nodefony/src/session/session.js +30 -15
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +12 -3
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +147 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/src/context/Context.d.ts +35 -6
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +85 -78
- package/docs/index.md +5 -1
- package/docs/observabilite.md +81 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +378 -230
- package/docs/session.md +180 -180
- package/docs/sse.md +287 -0
- package/docs/upload.md +120 -117
- package/package.json +10 -11
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import type http2 from "node:http2";
|
|
3
|
+
import type { Duplex } from "node:stream";
|
|
4
|
+
import type { IProxyMount } from "../../interfaces/IReverseProxy.js";
|
|
5
|
+
/** Requête entrante relayable : HTTP/1.1 ou HTTP/2 (API de compatibilité). */
|
|
6
|
+
export type ProxiedRequest = http.IncomingMessage | http2.Http2ServerRequest;
|
|
7
|
+
/** Réponse vers le client : HTTP/1.1 ou HTTP/2 (API de compatibilité). */
|
|
8
|
+
export type ProxiedResponse = http.ServerResponse | http2.Http2ServerResponse;
|
|
9
|
+
/**
|
|
10
|
+
* Ce que le proxy sait du client, et qu'il annonce à l'amont
|
|
11
|
+
* (`X-Forwarded-*`, `Via`).
|
|
12
|
+
*/
|
|
13
|
+
export interface IForwardedFrom {
|
|
14
|
+
/** Adresse du pair TCP immédiat — ajoutée à la chaîne `X-Forwarded-For`. */
|
|
15
|
+
remoteAddress: string | undefined;
|
|
16
|
+
/** Scheme par lequel le pair est arrivé (`http` | `https`). */
|
|
17
|
+
scheme: string;
|
|
18
|
+
/** Autorité demandée (`Host` ou `:authority`). */
|
|
19
|
+
host: string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Le pair est-il un relais de confiance (`trustProxy`) ? Seul un tel pair
|
|
22
|
+
* peut faire passer des `Forwarded`/`X-Forwarded-*`/`X-Real-IP` : venus
|
|
23
|
+
* d'ailleurs, ils sont forgeables (RFC 7239 §8.1) et sont écartés.
|
|
24
|
+
*/
|
|
25
|
+
trustedPeer: boolean;
|
|
26
|
+
/** Version du message reçu, pour `Via` (`"1.1"`, `"2.0"`). */
|
|
27
|
+
httpVersion: string;
|
|
28
|
+
}
|
|
29
|
+
/** Réglages du relais, fixés par le service (identiques pour tous les montages). */
|
|
30
|
+
export interface IRelaySettings {
|
|
31
|
+
/** Pseudonyme de CE processus dans `Via` (RFC 9110 §7.6.3) — base de la garde de boucle. */
|
|
32
|
+
pseudonym: string;
|
|
33
|
+
/** Délai d'établissement de la connexion amont (ms). Dépassé : 504. */
|
|
34
|
+
connectTimeoutMs: number;
|
|
35
|
+
/**
|
|
36
|
+
* Pool de connexions du montage (keep-alive borné) ; `false` = connexion à
|
|
37
|
+
* usage unique (arrêt engagé : plus de pool à créer).
|
|
38
|
+
*/
|
|
39
|
+
agent: http.Agent | false | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Sauts `Via` au-delà desquels un message est tenu pour une boucle. Notre
|
|
43
|
+
* pseudonyme ne repère une boucle qu'au RETOUR dans ce processus : derrière un
|
|
44
|
+
* répartiteur qui tourne sur N instances, cela peut prendre N+1 sauts, chacun
|
|
45
|
+
* tenant une connexion. Une chaîne légitime (CDN, répartiteur, ingress) en
|
|
46
|
+
* compte rarement plus de cinq.
|
|
47
|
+
*/
|
|
48
|
+
export declare const MAX_VIA_HOPS = 10;
|
|
49
|
+
/**
|
|
50
|
+
* Le message est-il déjà passé par CE processus ? Un `Via` qui porte notre
|
|
51
|
+
* pseudonyme signe une boucle (RFC 9110 §7.6 : un proxy ne se renvoie pas un
|
|
52
|
+
* message sans s'en protéger).
|
|
53
|
+
*
|
|
54
|
+
* @param via - en-tête `Via` reçu
|
|
55
|
+
* @param pseudonym - pseudonyme de ce processus
|
|
56
|
+
*/
|
|
57
|
+
export declare function viaLoops(via: string | string[] | undefined, pseudonym: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Cadrage du corps transmis à l'amont (RFC 9112 §6). `unsupported` : un
|
|
60
|
+
* codage de transfert autre que `chunked` seul — le proxy ne sait pas le
|
|
61
|
+
* transmettre fidèlement.
|
|
62
|
+
*/
|
|
63
|
+
export type BodyFraming = "none" | "length" | "chunked" | "unsupported";
|
|
64
|
+
/**
|
|
65
|
+
* Cadrage du corps reçu — décidé UNE fois, pour que l'amont lise exactement
|
|
66
|
+
* les octets que le proxy lui envoie (RFC 9112 §6.3). Un `Transfer-Encoding`
|
|
67
|
+
* l'emporte sur un `Content-Length` (que l'on retire alors) ; en HTTP/2, un
|
|
68
|
+
* flux sans fin à la tête porte un corps même sans longueur annoncée.
|
|
69
|
+
*
|
|
70
|
+
* @param req - requête reçue
|
|
71
|
+
* @returns le cadrage à appliquer vers l'amont
|
|
72
|
+
*/
|
|
73
|
+
export declare function bodyFramingOf(req: ProxiedRequest): BodyFraming;
|
|
74
|
+
/**
|
|
75
|
+
* En-têtes de la requête à transmettre à l'amont.
|
|
76
|
+
*
|
|
77
|
+
* - écartés : pseudo-en-têtes HTTP/2, en-têtes de connexion et ceux que nomme
|
|
78
|
+
* `Connection`, `Expect` (le serveur a déjà répondu `100`), en-têtes retirés
|
|
79
|
+
* par le montage, chaîne de relais venue d'un pair NON fiable ;
|
|
80
|
+
* - `Host` réécrit sur la cible (sauf `preserveHost`) ;
|
|
81
|
+
* - `X-Forwarded-For` prolongé (pair fiable) ou recommencé ;
|
|
82
|
+
* `X-Forwarded-Proto`/`-Host` hérités d'un pair fiable, posés sinon ;
|
|
83
|
+
* `X-Forwarded-Prefix` quand le préfixe est retiré — à la suite de celui
|
|
84
|
+
* d'un pair fiable ; `Forwarded` d'un pair fiable prolongé de ce saut ;
|
|
85
|
+
* - `Via` prolongé de ce saut ;
|
|
86
|
+
* - cadrage du corps rendu cohérent ({@link bodyFramingOf}).
|
|
87
|
+
*
|
|
88
|
+
* @returns un objet neuf, prêt pour `http.request`
|
|
89
|
+
*/
|
|
90
|
+
export declare function requestHeadersFor(headers: http.IncomingHttpHeaders, mount: IProxyMount, target: URL, from: IForwardedFrom, pseudonym: string, framing: BodyFraming): http.OutgoingHttpHeaders;
|
|
91
|
+
/**
|
|
92
|
+
* Ramène sous le préfixe un `Location` émis par un amont qui ignore son
|
|
93
|
+
* montage (`stripPrefix`) — l'équivalent de `proxy_redirect default` de
|
|
94
|
+
* nginx. Un chemin absolu (`/login`) et une URL sur l'origine de l'amont sont
|
|
95
|
+
* réécrits ; toute autre URL (un tiers) part intacte.
|
|
96
|
+
*
|
|
97
|
+
* @returns la valeur réécrite, ou celle reçue
|
|
98
|
+
*/
|
|
99
|
+
export declare function rewriteLocation(location: string, mount: IProxyMount, target: URL): string;
|
|
100
|
+
/**
|
|
101
|
+
* En-têtes de la réponse de l'amont à renvoyer au client : en-têtes de
|
|
102
|
+
* connexion écartés — y compris ceux que nomme `Connection` —, `Via`
|
|
103
|
+
* prolongé, `Location` ramené sous le préfixe (`stripPrefix`) ; le reste
|
|
104
|
+
* intact (un `set-cookie` multiple reste un tableau).
|
|
105
|
+
*
|
|
106
|
+
* @returns un objet neuf, valable en HTTP/1.1 comme en HTTP/2
|
|
107
|
+
*/
|
|
108
|
+
export declare function responseHeadersFor(headers: http.IncomingHttpHeaders, mount: IProxyMount, target: URL, hop: string): http.OutgoingHttpHeaders;
|
|
109
|
+
/**
|
|
110
|
+
* Chemin transmis à l'amont : tel quel, ou sans le préfixe (`stripPrefix`).
|
|
111
|
+
*
|
|
112
|
+
* @param url - cible de la requête (chemin + requête), qui commence par le préfixe
|
|
113
|
+
* @param mount - montage qui relaie
|
|
114
|
+
* @returns le chemin, toujours avec un `/` en tête
|
|
115
|
+
*/
|
|
116
|
+
export declare function upstreamPath(url: string, mount: IProxyMount): string;
|
|
117
|
+
/**
|
|
118
|
+
* Relaie une requête HTTP vers l'amont et renvoie sa réponse au client.
|
|
119
|
+
*
|
|
120
|
+
* Le corps est transmis avec un cadrage cohérent ({@link bodyFramingOf}). Un
|
|
121
|
+
* message qui porte déjà notre `Via` rend 508 sans contacter l'amont. Un
|
|
122
|
+
* client parti coupe l'échange amont ; un amont injoignable rend 502 ; une
|
|
123
|
+
* connexion non établie à temps ou un amont muet rendent 504 — à condition
|
|
124
|
+
* que rien n'ait encore été envoyé, sinon la réponse est interrompue (un
|
|
125
|
+
* statut ne se rattrape pas). Un amont coupé en plein corps interrompt la
|
|
126
|
+
* réponse au client au lieu de la laisser pendre.
|
|
127
|
+
*
|
|
128
|
+
* @returns une promesse résolue à la fermeture de la réponse — jamais rejetée
|
|
129
|
+
*/
|
|
130
|
+
export declare function forwardRequest(req: ProxiedRequest, res: ProxiedResponse, mount: IProxyMount, target: URL, from: IForwardedFrom, relay: IRelaySettings): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Sérialise une tête de réponse HTTP/1.1 depuis des en-têtes BRUTS (paires
|
|
133
|
+
* nom/valeur), sans réordonner ni fusionner, en écartant les noms donnés —
|
|
134
|
+
* une réponse `101` doit rendre au client exactement ce que l'amont a négocié.
|
|
135
|
+
*
|
|
136
|
+
* @param status - ligne de statut, sans CRLF
|
|
137
|
+
* @param rawHeaders - `IncomingMessage.rawHeaders`
|
|
138
|
+
* @param skip - noms à écarter (minuscules)
|
|
139
|
+
* @param extra - lignes ajoutées à la fin (`Nom: valeur`)
|
|
140
|
+
* @returns la tête complète, CRLF final compris
|
|
141
|
+
*/
|
|
142
|
+
export declare function rawHead(status: string, rawHeaders: readonly string[], skip?: ReadonlySet<string> | null, extra?: readonly string[]): string;
|
|
143
|
+
/**
|
|
144
|
+
* Réponse brute minimale sur un socket d'upgrade, puis fermeture.
|
|
145
|
+
*
|
|
146
|
+
* Les en-têtes passent par la validation de `node:http` : un nom ou une valeur
|
|
147
|
+
* invalide — un saut de ligne surtout, qui forgerait un en-tête de plus — est
|
|
148
|
+
* OMIS, jamais écrit. `Connection` et `Content-Length` sont posés ici et ne se
|
|
149
|
+
* remplacent pas.
|
|
150
|
+
*
|
|
151
|
+
* @param socket - socket du client, encore en attente de sa réponse
|
|
152
|
+
* @param status - statut HTTP du refus
|
|
153
|
+
* @param reason - phrase de raison
|
|
154
|
+
* @param headers - en-têtes du refus (`WWW-Authenticate`, `Retry-After`…)
|
|
155
|
+
*/
|
|
156
|
+
export declare function refuseUpgrade(socket: Duplex, status: number, reason: string, headers?: Readonly<Record<string, string | number>> | null): void;
|
|
157
|
+
/**
|
|
158
|
+
* Relaie un upgrade WebSocket DÉJÀ validé (méthode, `Upgrade`, clé, version,
|
|
159
|
+
* `Origin` — cf `ReverseProxy.handleUpgrade`) : rejoue le handshake vers
|
|
160
|
+
* l'amont, renvoie sa réponse `101` au client puis raccorde les deux sockets
|
|
161
|
+
* octet pour octet. Un refus de l'amont (`400`, `403`…) est rendu avec un
|
|
162
|
+
* cadrage refait (fin de corps = fermeture) ; un amont injoignable rend `502`,
|
|
163
|
+
* une connexion non établie à temps — ou un amont qui accepte puis se tait
|
|
164
|
+
* au-delà de `timeoutMs` — `504`, une boucle `508`. Le délai d'inactivité
|
|
165
|
+
* cesse au `101` : un tunnel établi peut rester muet. Une erreur d'un côté
|
|
166
|
+
* détruit l'autre.
|
|
167
|
+
*/
|
|
168
|
+
export declare function forwardUpgrade(req: http.IncomingMessage, socket: Duplex, head: Buffer, mount: IProxyMount, target: URL, from: IForwardedFrom, relay: IRelaySettings): void;
|
|
@@ -42,6 +42,12 @@ export interface ProxyIntrospection {
|
|
|
42
42
|
httpPort: number;
|
|
43
43
|
/** Port HTTPS/2 du backend (re-encrypt). */
|
|
44
44
|
httpsPort: number;
|
|
45
|
+
/**
|
|
46
|
+
* Port h2c du backend (`servers.h2c`), ou `null` s'il n'est pas activé. Le
|
|
47
|
+
* générateur haproxy y envoie le trafic en HTTP/2 (`proto h2`) ; nginx, qui
|
|
48
|
+
* ne parle pas HTTP/2 à ses amonts, l'ignore.
|
|
49
|
+
*/
|
|
50
|
+
h2cPort?: number | null;
|
|
45
51
|
/** Dossiers statiques servis à la racine `/` (ordre = priorité). */
|
|
46
52
|
staticRoots: string[];
|
|
47
53
|
/** Montages statiques préfixés (servis tels quels). */
|
|
@@ -71,6 +77,16 @@ export interface ProxyIntrospection {
|
|
|
71
77
|
* saines.
|
|
72
78
|
*/
|
|
73
79
|
keepaliveIntervalMs: number;
|
|
80
|
+
/**
|
|
81
|
+
* Chemin de la sonde de disponibilité (`health.readinessPath`), ou `null`
|
|
82
|
+
* si les sondes sont désactivées.
|
|
83
|
+
*
|
|
84
|
+
* HAProxy la sonde (`option httpchk`) au lieu d'un simple contrôle de
|
|
85
|
+
* connexion : un exemplaire qui draine garde son port ouvert, mais `/readyz`
|
|
86
|
+
* répond déjà `503` — c'est ce qui le retire du répartiteur AVANT qu'il
|
|
87
|
+
* cesse d'accepter. Un contrôle TCP le garderait jusqu'à la fermeture du port.
|
|
88
|
+
*/
|
|
89
|
+
readinessPath?: string | null;
|
|
74
90
|
/**
|
|
75
91
|
* Terminaison TLS au frontal, ou `null` pour n'écouter qu'en clair.
|
|
76
92
|
*
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Règles d'un montage du proxy inverse — UNIQUE source, lue par le schéma de
|
|
3
|
+
* configuration (`proxy.mounts`) ET par `ReverseProxy.mount()` : un montage
|
|
4
|
+
* écrit dans la config et un montage posé par un module obéissent aux mêmes
|
|
5
|
+
* refus, avec les mêmes mots.
|
|
6
|
+
*
|
|
7
|
+
* Pur : n'importe que le cœur, importable depuis `config/config.ts` sans
|
|
8
|
+
* cycle.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Préfixes que le framework se réserve : un montage n'y est JAMAIS accepté.
|
|
12
|
+
* `/nodefony/` porte la console d'administration et les API de Studio — les
|
|
13
|
+
* relayer ailleurs détournerait une surface protégée par le pare-feu.
|
|
14
|
+
*/
|
|
15
|
+
export declare const RESERVED_PROXY_PREFIXES: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* Préfixes réservés aux montages POSÉS PAR UN MODULE : la configuration ne
|
|
18
|
+
* peut pas les prendre. `/_vite/` appartient à `@nodefony/frontend`, qui y
|
|
19
|
+
* relaie ses serveurs Vite de développement.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MODULE_PROXY_PREFIXES: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Normalise un préfixe d'URL : `/` en tête et en fin, sans `/` doublé. Règle
|
|
24
|
+
* UNIQUE des montages statiques et du proxy inverse — deux écritures du même
|
|
25
|
+
* préfixe doivent désigner la même entrée.
|
|
26
|
+
*
|
|
27
|
+
* @param prefix - préfixe tel qu'écrit (`"svc"`, `"/svc"`, `"/svc/"`)
|
|
28
|
+
* @returns le préfixe normalisé (`"/svc/"`)
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizePrefix(prefix: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Analyse une origine d'amont. Seule une origine NUE est acceptée : un chemin,
|
|
33
|
+
* une requête, un fragment ou des identifiants dans la cible déplaceraient en
|
|
34
|
+
* silence toutes les URL relayées, ou feraient voyager un secret dans la
|
|
35
|
+
* configuration.
|
|
36
|
+
*
|
|
37
|
+
* @param origin - `scheme://hôte[:port]`
|
|
38
|
+
* @returns l'URL analysée, ou `null` si ce n'est pas une origine http(s) nue
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseProxyOrigin(origin: string): URL | null;
|
|
41
|
+
/** Ce qu'une règle de montage examine — la forme commune config / code. */
|
|
42
|
+
export interface IProxyMountShape {
|
|
43
|
+
target: unknown;
|
|
44
|
+
methods?: readonly string[] | undefined;
|
|
45
|
+
stripHeaders?: readonly string[] | undefined;
|
|
46
|
+
timeoutMs?: number | undefined;
|
|
47
|
+
secure?: boolean | undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Un refus, nommé : le champ fautif et pourquoi. */
|
|
50
|
+
export interface IProxyMountProblem {
|
|
51
|
+
field: "prefix" | "target" | "methods" | "stripHeaders" | "timeoutMs" | "secure";
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Examine un montage. Rend TOUS les refus, pas le premier : une configuration
|
|
56
|
+
* se corrige en une passe.
|
|
57
|
+
*
|
|
58
|
+
* @param prefix - préfixe tel qu'écrit
|
|
59
|
+
* @param mount - réglages du montage
|
|
60
|
+
* @param source - `"config"` (déclaré dans `proxy.mounts`) ou `"module"`
|
|
61
|
+
* (posé par un module) — seuls les préfixes réservés aux modules diffèrent
|
|
62
|
+
* @returns la liste des refus, vide si le montage est sain
|
|
63
|
+
*/
|
|
64
|
+
export declare function proxyMountProblems(prefix: string, mount: IProxyMountShape, source: "config" | "module"): IProxyMountProblem[];
|
|
65
|
+
/**
|
|
66
|
+
* Le chemin d'une requête relayée est-il ambigu ? Un chemin que le proxy et
|
|
67
|
+
* l'amont pourraient lire différemment — segment `.`/`..`, barre inverse,
|
|
68
|
+
* octet nul, ou leur forme encodée (`%2e`, `%2f`, `%5c`, `%00`, `%25`) — est
|
|
69
|
+
* REFUSÉ (400), jamais normalisé en silence : c'est le défaut de Traefik, et
|
|
70
|
+
* la parade aux contournements de préfixe (`/svc/..%2fadmin`).
|
|
71
|
+
*
|
|
72
|
+
* @param url - cible brute de la requête (chemin + requête éventuelle)
|
|
73
|
+
* @returns `true` si le chemin doit être refusé
|
|
74
|
+
*/
|
|
75
|
+
export declare function isAmbiguousPath(url: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Un upgrade est-il un handshake WebSocket conforme (RFC 6455 §4.2.1) ? Seul
|
|
78
|
+
* un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
|
|
79
|
+
* octet pour octet ouvrirait un tunnel que rien n'inspecte.
|
|
80
|
+
*
|
|
81
|
+
* Appelé à CHAQUE handshake (proxy inverse et routes de Nodefony) : aucune
|
|
82
|
+
* allocation — des expressions testées, jamais de découpe de chaîne.
|
|
83
|
+
*
|
|
84
|
+
* @param method - méthode de la requête
|
|
85
|
+
* @param headers - en-têtes reçus
|
|
86
|
+
* @returns la raison du refus, ou `null` si le handshake est conforme
|
|
87
|
+
*/
|
|
88
|
+
export declare function websocketHandshakeProblem(method: string | undefined, headers: Record<string, string | string[] | undefined>): string | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Http2SecureServer, Http2Server, ServerOptions } from "node:http2";
|
|
2
|
+
import type { IHttpConfig } from "../../config/config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Limites anti-DoS d'un serveur HTTP/2, lues dans la section `http2` du module :
|
|
5
|
+
* `maxSessionMemory` (amplification mémoire) et `maxConcurrentStreams` dans les
|
|
6
|
+
* SETTINGS (CVE-2023-44487, Rapid Reset). Règle UNIQUE des serveurs HTTP/2 — TLS
|
|
7
|
+
* (5152) comme h2c — : une limite oubliée sur l'un d'eux en ferait la porte de
|
|
8
|
+
* l'attaque.
|
|
9
|
+
*
|
|
10
|
+
* Une limite absente garde le défaut de Node (aucune régression).
|
|
11
|
+
*
|
|
12
|
+
* @param limits - `module.options.http2`.
|
|
13
|
+
* @returns les options à fusionner dans celles de `createServer`/`createSecureServer`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function http2ServerLimits(limits: Partial<IHttpConfig["http2"]> | undefined): Pick<ServerOptions, "maxSessionMemory" | "settings">;
|
|
16
|
+
/**
|
|
17
|
+
* Borne l'inactivité des sessions d'un serveur HTTP/2 — TLS (5152) comme h2c —
|
|
18
|
+
* et contourne un défaut de Node qui garde chaque session fermée en vie jusqu'à
|
|
19
|
+
* l'échéance de ce délai.
|
|
20
|
+
*
|
|
21
|
+
* Le défaut (Node 24, 26) : `closeSession` (`internal/http2/core`) marque la
|
|
22
|
+
* session détruite PUIS appelle `session.setTimeout(0)` ; or `setStreamTimeout`
|
|
23
|
+
* sort aussitôt sur un objet détruit. Le minuteur n'est jamais désarmé et retient
|
|
24
|
+
* la session (~2,4 Ko) pendant `timeoutMs` : sous un flux de connexions neuves,
|
|
25
|
+
* connexions/s × délai × 2,4 Ko. Signalé à Node : https://github.com/nodejs/node/issues/66885
|
|
26
|
+
*
|
|
27
|
+
* Le contournement : désarmer à la réception du GOAWAY, AVANT la destruction —
|
|
28
|
+
* c'est ainsi que ferment un proxy et un client Node, même par `destroy()`. Rien
|
|
29
|
+
* n'est retiré à la protection : Node n'agit déjà plus à l'échéance sur une
|
|
30
|
+
* session fermée (`sessionOnTimeout` sort sur `closed`). Une coupure TCP sans
|
|
31
|
+
* GOAWAY garde la rétention, bornée au délai.
|
|
32
|
+
*
|
|
33
|
+
* @param server - le serveur HTTP/2.
|
|
34
|
+
* @param timeoutMs - délai d'inactivité ; 0 ou absent le laisse désactivé.
|
|
35
|
+
* @param onTimeout - appelé à l'échéance (événement `timeout` du serveur).
|
|
36
|
+
*/
|
|
37
|
+
export declare function setHttp2SessionTimeout(server: Http2Server | Http2SecureServer, timeoutMs: number | undefined, onTimeout: () => void): void;
|
|
@@ -10,6 +10,15 @@ import type { ServersConfig } from "nodefony";
|
|
|
10
10
|
* @returns la section typée, ou `undefined` si elle est absente ou n'est pas un objet.
|
|
11
11
|
*/
|
|
12
12
|
export declare function asServersConfig(servers: unknown): ServersConfig | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Le serveur h2c démarre-t-il ? Seul un `enabled: true` EXPLICITE l'allume : la
|
|
15
|
+
* section existe toujours (défauts), un port seul ne vaut pas activation.
|
|
16
|
+
*
|
|
17
|
+
* @param entry - `servers.h2c`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isH2cEnabled(entry: {
|
|
20
|
+
enabled?: boolean | undefined;
|
|
21
|
+
} | undefined): boolean;
|
|
13
22
|
/**
|
|
14
23
|
* Port DEMANDÉ pour un serveur (`servers.<type>.port`), ou `undefined` si le
|
|
15
24
|
* serveur est désactivé, absent, ou sans port numérique.
|
|
@@ -17,4 +26,4 @@ export declare function asServersConfig(servers: unknown): ServersConfig | undef
|
|
|
17
26
|
* @param servers - section `servers` déjà rétrécie ({@link asServersConfig}).
|
|
18
27
|
* @param type - le serveur visé.
|
|
19
28
|
*/
|
|
20
|
-
export declare function configuredServerPort(servers: ServersConfig | undefined, type: "http" | "https"): number | undefined;
|
|
29
|
+
export declare function configuredServerPort(servers: ServersConfig | undefined, type: "http" | "https" | "h2c"): number | undefined;
|
|
@@ -3,12 +3,19 @@ import { resolvePortPolicy } from "nodefony";
|
|
|
3
3
|
import type { PortPolicy } from "nodefony";
|
|
4
4
|
export { resolvePortPolicy };
|
|
5
5
|
export type { PortPolicy };
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Serveur écoutable (surface minimale commune `http`/`https`/`http2`).
|
|
8
|
+
*
|
|
9
|
+
* Les écouteurs sont typés événement par événement : c'est la forme que les
|
|
10
|
+
* surcharges de Node acceptent telle quelle, sans conversion chez l'appelant.
|
|
11
|
+
*/
|
|
7
12
|
export interface Listenable {
|
|
8
13
|
listen(port: number, host?: string): unknown;
|
|
9
14
|
address(): AddressInfo | string | null;
|
|
10
|
-
once(event:
|
|
11
|
-
|
|
15
|
+
once(event: "error", listener: (error: NodeJS.ErrnoException) => void): unknown;
|
|
16
|
+
once(event: "listening", listener: () => void): unknown;
|
|
17
|
+
removeListener(event: "error", listener: (error: NodeJS.ErrnoException) => void): unknown;
|
|
18
|
+
removeListener(event: "listening", listener: () => void): unknown;
|
|
12
19
|
}
|
|
13
20
|
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
14
21
|
export declare const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
@@ -34,18 +41,35 @@ export interface ServersPortConfig {
|
|
|
34
41
|
https?: {
|
|
35
42
|
port?: number | undefined;
|
|
36
43
|
} | false;
|
|
44
|
+
h2c?: {
|
|
45
|
+
enabled?: boolean | undefined;
|
|
46
|
+
port?: number | undefined;
|
|
47
|
+
};
|
|
37
48
|
portPolicy?: PortPolicy;
|
|
38
49
|
portRetryAttempts?: number;
|
|
39
50
|
}
|
|
51
|
+
/** Serveur dont `buildBindPlan` compose le plan. */
|
|
52
|
+
export type BoundServer = "http" | "https" | "h2c";
|
|
53
|
+
/**
|
|
54
|
+
* Serveur actif qui convoite le MÊME port que h2c, ou `null`.
|
|
55
|
+
*
|
|
56
|
+
* Un port partagé avec h2c n'est pas un conflit à contourner : sur ce port, le
|
|
57
|
+
* client HTTP/1.1 et le proxy h2c se marchent dessus, et le repli `auto` ferait
|
|
58
|
+
* glisser l'un des deux là où personne ne l'attend. Le boot le refuse.
|
|
59
|
+
*
|
|
60
|
+
* @param servers - `kernel.options.servers`.
|
|
61
|
+
* @returns `"http"` ou `"https"` en conflit, sinon `null` (h2c inactif, port 0).
|
|
62
|
+
*/
|
|
63
|
+
export declare function h2cPortConflict(servers: ServersPortConfig | undefined): "http" | "https" | null;
|
|
40
64
|
/**
|
|
41
65
|
* Compose le plan de bind d'un serveur depuis la config du kernel — source UNIQUE
|
|
42
|
-
* (les
|
|
66
|
+
* (les serveurs l'appellent ; la politique n'est décidée qu'ici).
|
|
43
67
|
*
|
|
44
68
|
* @param which - le serveur qu'on borne.
|
|
45
69
|
* @param servers - `kernel.options.servers`.
|
|
46
70
|
* @param environment - `kernel.environment` (arbitre le défaut de la politique).
|
|
47
71
|
*/
|
|
48
|
-
export declare function buildBindPlan(which:
|
|
72
|
+
export declare function buildBindPlan(which: BoundServer, servers: ServersPortConfig | undefined, environment: string | undefined): BindPlan;
|
|
49
73
|
/**
|
|
50
74
|
* Sonde « un serveur répond-il déjà ici ? ».
|
|
51
75
|
*
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type IncomingMessage } from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
import type { WebSocket, WebSocketServer } from "ws";
|
|
4
|
+
/** Serveur qui émet `upgrade` : HTTP/1.1, HTTPS ou HTTP/2 sécurisé (`allowHTTP1`). */
|
|
5
|
+
interface IUpgradeEmitter {
|
|
6
|
+
on(event: "upgrade", listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void): unknown;
|
|
7
|
+
removeListener(event: "upgrade", listener: (req: IncomingMessage, socket: Duplex, head: Buffer) => void): unknown;
|
|
8
|
+
}
|
|
9
|
+
/** Ce que le répartiteur lit du proxy inverse (cf `ReverseProxy`). */
|
|
10
|
+
interface IUpgradeRelay {
|
|
11
|
+
readonly mounts: readonly unknown[] | null;
|
|
12
|
+
handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Le verdict d'un upgrade, rendu par le noyau APRÈS sa décision — qui peut
|
|
16
|
+
* attendre (session, pare-feu, garde de l'action). Un seul verdict compte : le
|
|
17
|
+
* premier appel l'emporte, les suivants sont ignorés.
|
|
18
|
+
*/
|
|
19
|
+
export interface IUpgradeAdmission {
|
|
20
|
+
/**
|
|
21
|
+
* Termine le handshake (`101`) et rend la socket ouverte.
|
|
22
|
+
*
|
|
23
|
+
* @returns la socket DIRECTEMENT quand `ws` la rend tout de suite (le cas
|
|
24
|
+
* nominal : aucune promesse, aucune micro-tâche), sinon sa promesse ;
|
|
25
|
+
* `null` si le client est parti pendant la décision (ou si `ws` a rejeté
|
|
26
|
+
* le handshake) — il n'y a alors rien à servir
|
|
27
|
+
*/
|
|
28
|
+
accept(): WebSocket | null | Promise<WebSocket | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Refuse par un statut HTTP, AVANT le `101` (RFC 6455 §4.2.2).
|
|
31
|
+
*
|
|
32
|
+
* @param status - statut du refus (`401`, `403`, `429`…)
|
|
33
|
+
* @param headers - en-têtes du refus (`WWW-Authenticate`, `Retry-After`)
|
|
34
|
+
*/
|
|
35
|
+
refuse(status: number, headers?: Readonly<Record<string, string | number>> | null): void;
|
|
36
|
+
}
|
|
37
|
+
/** Ce qui décide d'un upgrade : reçoit la requête et rend son verdict, plus tard s'il le faut. */
|
|
38
|
+
export type UpgradeAdmit = (req: IncomingMessage, admission: IUpgradeAdmission) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Pose l'UNIQUE écouteur `upgrade` d'un serveur : le proxy inverse d'abord,
|
|
41
|
+
* puis le serveur WebSocket de Nodefony.
|
|
42
|
+
*
|
|
43
|
+
* Pourquoi un seul écouteur : `ws` attaché par son option `server` prend TOUS
|
|
44
|
+
* les upgrades et répond `400` à ceux qu'il ne reconnaît pas. Un second
|
|
45
|
+
* écouteur, même posé avant, ne l'empêcherait pas de répondre sur un socket
|
|
46
|
+
* déjà relayé. Le serveur `ws` est donc créé en `noServer`, et c'est ici que
|
|
47
|
+
* l'upgrade lui est confié — avec ses propres contrôles intacts (`path`,
|
|
48
|
+
* `verifyClient`, `maxPayload`…), que `handleUpgrade` applique.
|
|
49
|
+
*
|
|
50
|
+
* Entre les deux, `refusal` tranche ce qui doit être refusé AVANT le `101`
|
|
51
|
+
* (RFC 6455 §4.2.2) : un chemin sans route WebSocket reçoit un statut HTTP,
|
|
52
|
+
* jamais une ouverture suivie d'une fermeture. Puis la FORME du handshake est
|
|
53
|
+
* contrôlée (`websocketHandshakeProblem`, la règle du proxy inverse) : un
|
|
54
|
+
* handshake malformé reçoit `400` sans jamais atteindre le noyau. Enfin
|
|
55
|
+
* `admit` décide — pare-feu, garde de l'action — et seul son verdict ouvre.
|
|
56
|
+
*
|
|
57
|
+
* Sans montage, le coût par connexion est une lecture de champ.
|
|
58
|
+
*
|
|
59
|
+
* @param server - serveur HTTP(S) qui reçoit les upgrades
|
|
60
|
+
* @param wss - serveur WebSocket de Nodefony (`noServer: true`)
|
|
61
|
+
* @param relay - proxy inverse, lu à chaque upgrade (il peut naître après)
|
|
62
|
+
* @param refusal - statut HTTP qui refuse l'upgrade, `null` pour l'admettre
|
|
63
|
+
* (cf `HttpKernel.websocketUpgradeRefusal`)
|
|
64
|
+
* @param admit - décide d'un upgrade admis (cf `HttpKernel.onWebsocketUpgrade`) ;
|
|
65
|
+
* par défaut, tout upgrade conforme est accepté et émis en `connection`
|
|
66
|
+
* @returns la fonction qui retire l'écouteur — à appeler à l'arrêt
|
|
67
|
+
*/
|
|
68
|
+
export declare function attachUpgradeDispatch(server: IUpgradeEmitter, wss: WebSocketServer, relay: () => IUpgradeRelay | null, refusal: (req: IncomingMessage) => number | null, admit?: UpgradeAdmit): () => void;
|
|
69
|
+
export {};
|
|
@@ -118,6 +118,8 @@ declare class Session implements ISession {
|
|
|
118
118
|
* `onSaveSession`.
|
|
119
119
|
*
|
|
120
120
|
* @param user - principal authentifié (string) lié au blob ; défaut courant.
|
|
121
|
+
* Fourni, il DEVIENT le principal de la session : une sauvegarde suivante,
|
|
122
|
+
* dans la même requête, le garde sans qu'on le lui redonne.
|
|
121
123
|
*/
|
|
122
124
|
save(user?: string): Promise<this>;
|
|
123
125
|
/**
|