@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7
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/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +30 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +29 -4
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +47 -13
- package/dist/nodefony/service/http-kernel.js +306 -49
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +8 -3
- package/dist/nodefony/service/servers/clientError.js +53 -4
- package/dist/nodefony/service/servers/server-h2c.js +6 -6
- package/dist/nodefony/service/servers/server-http.js +6 -4
- package/dist/nodefony/service/servers/server-https.js +14 -7
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
- package/dist/nodefony/service/servers/server-websocket.js +7 -11
- package/dist/nodefony/service/servers/serverShutdown.js +43 -6
- package/dist/nodefony/service/sessions/sessions-service.js +7 -7
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +364 -0
- package/dist/nodefony/src/context/http/HttpContext.js +139 -32
- package/dist/nodefony/src/context/http/Request.js +111 -22
- package/dist/nodefony/src/context/http/Response.js +459 -37
- package/dist/nodefony/src/context/http/SseStream.js +83 -148
- package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +55 -12
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +15 -3
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +57 -14
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +11 -2
- package/dist/types/nodefony/config/config.d.ts +14 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
- package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
- package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +406 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +45 -1
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +67 -43
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/docs/upload.md +43 -17
- package/package.json +38 -18
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { isLoopbackHostname } from "nodefony";
|
|
1
|
+
import { isLoopbackHostname, parseBareOrigin } from "nodefony";
|
|
2
2
|
// nodefony/src/proxy/rules.ts
|
|
3
|
+
/** Schémas d’amont d’un relais : un proxy inverse ne parle que HTTP. */
|
|
4
|
+
const PROXY_ORIGIN_PROTOCOLS = ["http:", "https:"];
|
|
3
5
|
/**
|
|
4
6
|
* Règles d'un montage du proxy inverse — UNIQUE source, lue par le schéma de
|
|
5
7
|
* configuration (`proxy.mounts`) ET par `ReverseProxy.mount()` : un montage
|
|
@@ -49,17 +51,7 @@ function normalizePrefix(prefix) {
|
|
|
49
51
|
* @returns l'URL analysée, ou `null` si ce n'est pas une origine http(s) nue
|
|
50
52
|
*/
|
|
51
53
|
function parseProxyOrigin(origin) {
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
url = new URL(origin);
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
59
|
-
if (!/^https?:\/\/[^/?#]+\/?$/i.test(origin.trim())) return null;
|
|
60
|
-
if (url.search !== "" || url.hash !== "") return null;
|
|
61
|
-
if (url.username !== "" || url.password !== "") return null;
|
|
62
|
-
return url;
|
|
54
|
+
return parseBareOrigin(origin, PROXY_ORIGIN_PROTOCOLS);
|
|
63
55
|
}
|
|
64
56
|
/**
|
|
65
57
|
* Examine un montage. Rend TOUS les refus, pas le premier : une configuration
|
|
@@ -163,6 +155,29 @@ const UPGRADE_WEBSOCKET = /^\s*websocket\s*$/i;
|
|
|
163
155
|
/** Le jeton `upgrade` dans une liste `Connection` (RFC 9110 §7.6.1). */
|
|
164
156
|
const CONNECTION_UPGRADE = /(?:^|,)\s*upgrade\s*(?:,|$)/i;
|
|
165
157
|
/**
|
|
158
|
+
* Le corps reçu porte-t-il un codage de transfert que le serveur ne sait pas
|
|
159
|
+
* défaire ? Seul `chunked` est compris : Node le retire, mais laisse en place
|
|
160
|
+
* tout codage qui le précède (`gzip, chunked`) — le corps livré serait encore
|
|
161
|
+
* compressé. RFC 9112 §6.1 : un tel message reçoit 501. Un `chunked` qui n'est
|
|
162
|
+
* pas le DERNIER codage est déjà refusé par Node en 400 (§6.3).
|
|
163
|
+
*
|
|
164
|
+
* Seule règle, pour le serveur d'origine et pour le proxy inverse. Chemin de
|
|
165
|
+
* chaque requête : une lecture d'en-tête, rien d'autre sans `Transfer-Encoding`.
|
|
166
|
+
*
|
|
167
|
+
* @param coding - valeur de l'en-tête `Transfer-Encoding`, absente le plus souvent
|
|
168
|
+
* @returns `true` quand le corps ne peut pas être lu tel qu'annoncé
|
|
169
|
+
*/
|
|
170
|
+
function isUnsupportedTransferCoding(coding) {
|
|
171
|
+
if (coding === void 0 || coding === "chunked") return false;
|
|
172
|
+
return coding.trim().toLowerCase() !== "chunked";
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Raison rendue par {@link websocketHandshakeProblem} quand le client parle une
|
|
176
|
+
* autre version du protocole : la seule qui appelle une réponse distincte —
|
|
177
|
+
* le client doit apprendre vers quelle version se rabattre (RFC 6455 §4.4).
|
|
178
|
+
*/
|
|
179
|
+
const WS_UNSUPPORTED_VERSION = "Sec-WebSocket-Version ≠ 13";
|
|
180
|
+
/**
|
|
166
181
|
* Un upgrade est-il un handshake WebSocket conforme (RFC 6455 §4.2.1) ? Seul
|
|
167
182
|
* un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
|
|
168
183
|
* octet pour octet ouvrirait un tunnel que rien n'inspecte.
|
|
@@ -180,9 +195,9 @@ function websocketHandshakeProblem(method, headers) {
|
|
|
180
195
|
if (typeof upgrade !== "string" || !UPGRADE_WEBSOCKET.test(upgrade)) return "Upgrade autre que websocket";
|
|
181
196
|
const connection = headers.connection;
|
|
182
197
|
if (!(typeof connection === "string" ? CONNECTION_UPGRADE.test(connection) : Array.isArray(connection) && connection.some((token) => CONNECTION_UPGRADE.test(token)))) return "Connection sans upgrade";
|
|
183
|
-
if (headers["sec-websocket-version"] !== "13") return
|
|
198
|
+
if (headers["sec-websocket-version"] !== "13") return WS_UNSUPPORTED_VERSION;
|
|
184
199
|
const key = headers["sec-websocket-key"];
|
|
185
200
|
if (typeof key !== "string" || !WS_KEY.test(key)) return "Sec-WebSocket-Key invalide";
|
|
186
201
|
return null;
|
|
187
202
|
}
|
|
188
|
-
export { MODULE_PROXY_PREFIXES, RESERVED_PROXY_PREFIXES, isAmbiguousPath, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem };
|
|
203
|
+
export { MODULE_PROXY_PREFIXES, RESERVED_PROXY_PREFIXES, WS_UNSUPPORTED_VERSION, isAmbiguousPath, isUnsupportedTransferCoding, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { constants } from "node:http2";
|
|
2
|
+
// nodefony/src/servers/http2Goaway.ts
|
|
3
|
+
/** Plus long délai d'annonce (ms) : un pair qui ne répond pas au PING ne retient pas la fermeture. */
|
|
4
|
+
const ANNOUNCE_CAP_MS = 1e3;
|
|
5
|
+
/** Dernier identifiant de flux possible (RFC 9113 §6.8) : « aucun flux n'est encore écarté ». */
|
|
6
|
+
const LAST_POSSIBLE_STREAM = 2 ** 31 - 1;
|
|
7
|
+
/**
|
|
8
|
+
* Annonce à UNE session HTTP/2 qu'elle va fermer, SANS écarter un flux déjà en route.
|
|
9
|
+
*
|
|
10
|
+
* Un PING mesure l'aller-retour, puis GOAWAY à `2^31-1`, puis l'attente de cet
|
|
11
|
+
* aller-retour : après elle, tout flux ouvert par le client avant l'annonce est
|
|
12
|
+
* arrivé, et le GOAWAY définitif (`session.close()`, dernier flux traité) peut
|
|
13
|
+
* suivre. RFC 9113 §6.8 : « SHOULD send an initial GOAWAY frame with the last
|
|
14
|
+
* stream identifier set to 2^31-1 […] After allowing time for any in-flight
|
|
15
|
+
* stream creation (at least one round-trip time) ». Sans annonce, un flux croisé
|
|
16
|
+
* avec le GOAWAY est refusé alors qu'il aurait pu être servi.
|
|
17
|
+
*
|
|
18
|
+
* L'aller-retour se MESURE avant l'annonce : après un GOAWAY, un client Node ne
|
|
19
|
+
* répond plus aux PING (constaté sur Node 26.13).
|
|
20
|
+
*
|
|
21
|
+
* @param session - la session à annoncer, ni fermée ni détruite
|
|
22
|
+
* @param settle - appelé UNE fois : après l'attente, à la fermeture de la
|
|
23
|
+
* session, ou aussitôt si le PING ne part pas. Un pair muet ne l'appelle
|
|
24
|
+
* jamais — le délai maximal appartient à l'appelant.
|
|
25
|
+
*/
|
|
26
|
+
function announceSessionClose(session, settle) {
|
|
27
|
+
let settled = false;
|
|
28
|
+
let wait = null;
|
|
29
|
+
const done = () => {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
if (wait !== null) clearTimeout(wait);
|
|
33
|
+
session.removeListener("close", done);
|
|
34
|
+
settle();
|
|
35
|
+
};
|
|
36
|
+
session.once("close", done);
|
|
37
|
+
if (!session.ping((error, rttMs) => {
|
|
38
|
+
if (error !== null || settled || session.closed) return done();
|
|
39
|
+
session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
|
|
40
|
+
wait = setTimeout(done, Math.max(1, Math.ceil(rttMs)));
|
|
41
|
+
})) {
|
|
42
|
+
session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
|
|
43
|
+
done();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ferme EN DOUCEUR une session HTTP/2 restée inactive au-delà du délai du serveur.
|
|
48
|
+
*
|
|
49
|
+
* RFC 9113 §9.1 : un serveur est « permitted to terminate idle connections »,
|
|
50
|
+
* et « SHOULD first send a GOAWAY ». La fermeture suit l'annonce en deux temps
|
|
51
|
+
* ({@link announceSessionClose}), puis `session.close()` : aucun nouveau flux,
|
|
52
|
+
* mais les flux en cours vont à leur terme — un flux long et muet (abonnement
|
|
53
|
+
* gRPC) n'est pas coupé, seuls les NOUVEAUX flux repartent sur une autre
|
|
54
|
+
* connexion, ce que tout client HTTP/2 fait sur GOAWAY.
|
|
55
|
+
*
|
|
56
|
+
* Le délai de la session est désarmé d'abord : Node (26.13) marque une session
|
|
57
|
+
* détruite AVANT de désarmer son minuteur, qui la retiendrait alors jusqu'à son
|
|
58
|
+
* échéance (nodejs/node#66885).
|
|
59
|
+
*
|
|
60
|
+
* @param session - la session inactive
|
|
61
|
+
* @param capMs - délai maximal d'attente de l'écho du PING (ms)
|
|
62
|
+
*/
|
|
63
|
+
function closeIdleSession(session, capMs = ANNOUNCE_CAP_MS) {
|
|
64
|
+
if (session.closed || session.destroyed) return;
|
|
65
|
+
session.setTimeout(0);
|
|
66
|
+
let finished = false;
|
|
67
|
+
const finish = () => {
|
|
68
|
+
if (finished) return;
|
|
69
|
+
finished = true;
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
if (!session.closed && !session.destroyed) session.close();
|
|
72
|
+
};
|
|
73
|
+
const timer = setTimeout(finish, capMs);
|
|
74
|
+
announceSessionClose(session, finish);
|
|
75
|
+
}
|
|
76
|
+
export { ANNOUNCE_CAP_MS, announceSessionClose, closeIdleSession };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeIdleSession } from "./http2Goaway.js";
|
|
1
2
|
// nodefony/src/servers/http2Limits.ts
|
|
2
3
|
/**
|
|
3
4
|
* Limites anti-DoS d'un serveur HTTP/2, lues dans la section `http2` du module :
|
|
@@ -18,29 +19,54 @@ function http2ServerLimits(limits) {
|
|
|
18
19
|
return options;
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Une session HTTP/2, et non une socket HTTP/1.1 : sur 5152 (`allowHTTP1`),
|
|
23
|
+
* l'événement `timeout` du serveur reçoit les deux. `node:http2` n'exporte pas
|
|
24
|
+
* la classe de session — elle se reconnaît à sa capacité (`goaway`).
|
|
25
|
+
*/
|
|
26
|
+
function isHttp2Session(target) {
|
|
27
|
+
return typeof target === "object" && target !== null && "goaway" in target;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ferme les sessions d'un serveur HTTP/2 — TLS (5152) comme h2c — restées
|
|
31
|
+
* inactives au-delà de `timeoutMs`, et contourne un défaut de Node qui garde
|
|
32
|
+
* chaque session fermée en vie jusqu'à l'échéance de ce délai.
|
|
24
33
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* **La fermeture est À NOUS.** Node arme chaque session par
|
|
35
|
+
* `session.setTimeout(server.timeout, sessionOnTimeout)` — un écouteur `once` —
|
|
36
|
+
* et `sessionOnTimeout` ne détruit la session que si le serveur n'a AUCUN
|
|
37
|
+
* écouteur `timeout` ; s'il en a un, il lui passe la main, UNE fois par session
|
|
38
|
+
* (`internal/http2/core`, Node 26.13). Le serveur 5152 en a toujours un
|
|
39
|
+
* (`routeSocketTimeout`, pour ses sockets HTTP/1.1), et ce délai doit prévenir
|
|
40
|
+
* l'application (`onTimeout`) : sans fermeture ici, une session inactive vivait
|
|
41
|
+
* jusqu'à ce que le CLIENT la ferme (#763). À l'échéance, la session est fermée
|
|
42
|
+
* en douceur — GOAWAY annoncé puis définitif, les flux en cours vont à leur
|
|
43
|
+
* terme ({@link closeIdleSession}, RFC 9113 §9.1 et §6.8).
|
|
30
44
|
*
|
|
31
|
-
* Le contournement
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
45
|
+
* **Le contournement** (Node 24, 26) : `closeSession` marque la session détruite
|
|
46
|
+
* PUIS appelle `session.setTimeout(0)`, qui sort aussitôt sur un objet détruit ;
|
|
47
|
+
* le minuteur retient alors la session (~2,4 Ko) pendant `timeoutMs`. Il est
|
|
48
|
+
* désarmé à la réception du GOAWAY, AVANT la destruction — c'est ainsi que
|
|
49
|
+
* ferment un proxy et un client Node, même par `destroy()` ; et
|
|
50
|
+
* `closeIdleSession` le désarme avant de fermer. Signalé à Node :
|
|
51
|
+
* https://github.com/nodejs/node/issues/66885 (corrigé par nodejs/node#66886,
|
|
52
|
+
* absent de la 26.13). Une coupure TCP sans GOAWAY garde la rétention, bornée
|
|
53
|
+
* au délai.
|
|
54
|
+
*
|
|
55
|
+
* Coût par requête : nul — un écouteur par serveur, et la fermeture n'agit qu'à
|
|
56
|
+
* l'échéance.
|
|
36
57
|
*
|
|
37
58
|
* @param server - le serveur HTTP/2.
|
|
38
59
|
* @param timeoutMs - délai d'inactivité ; 0 ou absent le laisse désactivé.
|
|
39
|
-
* @param onTimeout - appelé à l'échéance (événement `timeout` du
|
|
60
|
+
* @param onTimeout - appelé à l'échéance d'une session (événement `timeout` du
|
|
61
|
+
* serveur), après le début de sa fermeture.
|
|
40
62
|
*/
|
|
41
63
|
function setHttp2SessionTimeout(server, timeoutMs, onTimeout) {
|
|
42
64
|
if (!timeoutMs) return;
|
|
43
|
-
server.setTimeout(timeoutMs,
|
|
65
|
+
server.setTimeout(timeoutMs, (target) => {
|
|
66
|
+
if (!isHttp2Session(target)) return;
|
|
67
|
+
closeIdleSession(target);
|
|
68
|
+
onTimeout();
|
|
69
|
+
});
|
|
44
70
|
server.on("session", (session) => {
|
|
45
71
|
session.once("goaway", () => {
|
|
46
72
|
session.setTimeout(0);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import http2 from "node:http2";
|
|
2
|
+
// nodefony/src/servers/http2StreamGrace.ts
|
|
3
|
+
/**
|
|
4
|
+
* Délai avant de retenter un `PING` que Node a refusé (plafond de `PING` en vol
|
|
5
|
+
* atteint par d'autres émetteurs de la session) : ceux-là reviennent en un
|
|
6
|
+
* aller-retour, une boucle `setImmediate` brûlerait le processeur pendant ce
|
|
7
|
+
* temps sur un lien lent.
|
|
8
|
+
*/
|
|
9
|
+
const PING_RETRY_MS = 10;
|
|
10
|
+
/**
|
|
11
|
+
* Les flux d'une session qui attendent leur grâce. `inFlight` : couverts par le
|
|
12
|
+
* `PING` en vol ; `next` : arrivés après son envoi, donc couverts par le
|
|
13
|
+
* suivant — un `PING` envoyé AVANT leurs en-têtes ne prouve rien pour eux.
|
|
14
|
+
*/
|
|
15
|
+
var SessionGrace = class {
|
|
16
|
+
inFlight = null;
|
|
17
|
+
next = null;
|
|
18
|
+
};
|
|
19
|
+
/** Chemin d'erreur seulement : aucune session n'a d'entrée hors refus en cours. */
|
|
20
|
+
const graces = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
/** Coupe le flux en `NO_ERROR` s'il est encore ouvert et que le client émet encore. */
|
|
22
|
+
function decide(stream) {
|
|
23
|
+
if (stream.closed || stream.destroyed || stream.state.remoteClose) return;
|
|
24
|
+
stream.close(http2.constants.NGHTTP2_NO_ERROR);
|
|
25
|
+
}
|
|
26
|
+
function sendPing(session, grace) {
|
|
27
|
+
if (session.destroyed) {
|
|
28
|
+
graces.delete(session);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (session.closed) {
|
|
32
|
+
const pending = grace.inFlight;
|
|
33
|
+
const later = grace.next;
|
|
34
|
+
grace.inFlight = null;
|
|
35
|
+
grace.next = null;
|
|
36
|
+
graces.delete(session);
|
|
37
|
+
setImmediate(() => {
|
|
38
|
+
if (pending !== null) for (const stream of pending) decide(stream);
|
|
39
|
+
if (later !== null) for (const stream of later) decide(stream);
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!session.ping(() => settle(session, grace))) setTimeout(() => sendPing(session, grace), PING_RETRY_MS).unref();
|
|
44
|
+
}
|
|
45
|
+
function settle(session, grace) {
|
|
46
|
+
const batch = grace.inFlight;
|
|
47
|
+
grace.inFlight = grace.next;
|
|
48
|
+
grace.next = null;
|
|
49
|
+
if (batch !== null) for (const stream of batch) decide(stream);
|
|
50
|
+
if (grace.inFlight !== null) sendPing(session, grace);
|
|
51
|
+
else graces.delete(session);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Ferme en `NO_ERROR` un flux HTTP/2 dont la réponse est partie mais dont le
|
|
55
|
+
* client émet encore, après la grâce d'un aller-retour (`PING`, RFC 9113 §6.7).
|
|
56
|
+
*
|
|
57
|
+
* RFC 9113 §8.1 : après une réponse complète, le serveur PEUT demander au
|
|
58
|
+
* client d'arrêter d'émettre par un `RST_STREAM` `NO_ERROR`. La coupe ne doit
|
|
59
|
+
* jamais précéder l'envoi de la réponse : un `RST_STREAM` soumis pendant que
|
|
60
|
+
* les en-têtes attendent encore dans la file de nghttp2 les fait JETER, et le
|
|
61
|
+
* client reçoit une fin de flux sans statut (un client gRPC la lit INTERNAL).
|
|
62
|
+
* Le `PING` part derrière la réponse ; son écho prouve qu'elle est arrivée.
|
|
63
|
+
*
|
|
64
|
+
* **Un seul `PING` par session** couvre tous les flux qui attendent : un flux
|
|
65
|
+
* arrivé pendant qu'un `PING` est en vol attend le suivant. Le plafond de
|
|
66
|
+
* `PING` en vol de Node (10 par session) n'est donc jamais atteint par ce
|
|
67
|
+
* chemin, et aucun flux n'est coupé sans grâce — ce que faisait la coupe
|
|
68
|
+
* immédiate quand `ping()` rendait `false`, au-delà de 10 refus simultanés.
|
|
69
|
+
*
|
|
70
|
+
* Session en fermeture GRACIEUSE (délai d'inactivité, arrêt) : Node n'envoie
|
|
71
|
+
* plus de `PING`, et la session attend la fin de ses flux — un client muet la
|
|
72
|
+
* retiendrait. La coupe part alors au tour de boucle suivant, le temps que les
|
|
73
|
+
* réponses déjà soumises quittent la file de nghttp2.
|
|
74
|
+
*
|
|
75
|
+
* Après la grâce : client qui a fini d'émettre → rien (le flux se ferme seul,
|
|
76
|
+
* sans `RST_STREAM`) ; sinon → coupé. L'écho d'un `PING` passe avant les autres
|
|
77
|
+
* trames (§6.7) : une trame DATA encore en route peut arriver après lui, la
|
|
78
|
+
* coupe reste conforme (§8.1).
|
|
79
|
+
*
|
|
80
|
+
* Coût : chemin d'erreur seulement — une entrée de `WeakMap` par session tant
|
|
81
|
+
* que des flux attendent, un `PING` par aller-retour et par session.
|
|
82
|
+
*
|
|
83
|
+
* @param stream - flux dont la réponse est ÉCRITE (`finish` passé).
|
|
84
|
+
*/
|
|
85
|
+
function closeStreamAfterGrace(stream) {
|
|
86
|
+
if (stream.closed || stream.destroyed || stream.state.remoteClose) return;
|
|
87
|
+
const session = stream.session;
|
|
88
|
+
if (session === void 0) return;
|
|
89
|
+
let grace = graces.get(session);
|
|
90
|
+
if (grace === void 0) {
|
|
91
|
+
grace = new SessionGrace();
|
|
92
|
+
graces.set(session, grace);
|
|
93
|
+
}
|
|
94
|
+
if (grace.inFlight === null) {
|
|
95
|
+
grace.inFlight = [stream];
|
|
96
|
+
sendPing(session, grace);
|
|
97
|
+
} else if (grace.next === null) grace.next = [stream];
|
|
98
|
+
else grace.next.push(stream);
|
|
99
|
+
}
|
|
100
|
+
export { closeStreamAfterGrace };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// nodefony/src/servers/receiveDeadline.ts
|
|
2
|
+
/** Capacité de départ de l'anneau, et celle où il revient une fois vide. */
|
|
3
|
+
const INITIAL_CAPACITY = 64;
|
|
4
|
+
/**
|
|
5
|
+
* Une échéance ABSOLUE pour des réceptions en cours, sans un minuteur par
|
|
6
|
+
* réception : un seul balayage périodique pour toutes.
|
|
7
|
+
*
|
|
8
|
+
* ## Pourquoi
|
|
9
|
+
*
|
|
10
|
+
* `node:http2` ne borne pas la réception d'une requête : il n'a que des délais
|
|
11
|
+
* d'INACTIVITÉ (`http2stream.setTimeout`, `http2session.setTimeout`), qu'un
|
|
12
|
+
* client tient en vie avec un octet avant chaque échéance. `requestTimeout`,
|
|
13
|
+
* la borne absolue anti « slow-loris », n'existe que pour HTTP/1.1.
|
|
14
|
+
*
|
|
15
|
+
* ## Le coût, et pourquoi cette forme
|
|
16
|
+
*
|
|
17
|
+
* C'est la forme que Node donne lui-même à `requestTimeout` : aucun minuteur
|
|
18
|
+
* par requête, un contrôle périodique (`connectionsCheckingInterval`). Les
|
|
19
|
+
* réceptions suivies vivent dans un ANNEAU — un tableau des éléments et un
|
|
20
|
+
* `Int32Array` des tours d'entrée, indexés par un numéro de séquence :
|
|
21
|
+
* - suivre = deux écritures ; libérer = une comparaison et une écriture. Ni
|
|
22
|
+
* minuteur, ni fermeture, ni lecture d'horloge (le temps se compte en TOURS),
|
|
23
|
+
* ni champ ajouté à l'élément suivi ;
|
|
24
|
+
* - mesuré contre une `Map` (hachage d'identité de la clé) : ~26–32 ns par
|
|
25
|
+
* réception contre ~90–150 ns, de 1 à 10 000 réceptions simultanées.
|
|
26
|
+
*
|
|
27
|
+
* Toutes les entrées ont le même délai : l'ordre d'insertion EST l'ordre
|
|
28
|
+
* d'échéance. Un tour avance la tête et s'arrête à la première entrée vivante
|
|
29
|
+
* non échue — il ne coûte que ce qu'il expire ou saute.
|
|
30
|
+
*
|
|
31
|
+
* Contrepartie, bornée : une case libérée DERRIÈRE une entrée vivante attend
|
|
32
|
+
* que la tête la dépasse, soit au plus un délai de réception — 12 octets par
|
|
33
|
+
* réception commencée dans cette fenêtre. Sous charge, l'anneau garde sa
|
|
34
|
+
* capacité d'un tour à l'autre (jamais rétréci puis recrû) ; il la rend au
|
|
35
|
+
* premier tour sans réception nouvelle. Le minuteur unique est `unref`, et s'arrête au
|
|
36
|
+
* même moment.
|
|
37
|
+
*
|
|
38
|
+
* ## Précision
|
|
39
|
+
*
|
|
40
|
+
* Une entrée expire entre `timeoutMs` et `timeoutMs + periodMs` après son
|
|
41
|
+
* suivi — jamais AVANT le délai configuré.
|
|
42
|
+
*
|
|
43
|
+
* @typeParam T - l'élément suivi (un contexte de requête)
|
|
44
|
+
*/
|
|
45
|
+
var ReceiveDeadline = class {
|
|
46
|
+
#items = new Array(INITIAL_CAPACITY).fill(null);
|
|
47
|
+
#ticks = new Int32Array(INITIAL_CAPACITY);
|
|
48
|
+
#mask = 63;
|
|
49
|
+
/** Numéro de séquence de la plus ancienne case non balayée. */
|
|
50
|
+
#head = 0;
|
|
51
|
+
/** Numéro de séquence de la prochaine entrée. */
|
|
52
|
+
#tail = 0;
|
|
53
|
+
#tick = 0;
|
|
54
|
+
#timer = null;
|
|
55
|
+
#onExpire;
|
|
56
|
+
#periodMs;
|
|
57
|
+
/** Tours pleins qu'une entrée doit voir passer avant d'expirer. */
|
|
58
|
+
#ttl;
|
|
59
|
+
/**
|
|
60
|
+
* @param timeoutMs - le délai de réception (> 0)
|
|
61
|
+
* @param onExpire - appelé une fois par entrée échue, déjà retirée
|
|
62
|
+
* @param periodMs - la période du balayage, bornée par le délai (défaut 1 s)
|
|
63
|
+
* @throws RangeError si `timeoutMs` ou `periodMs` n'est pas un entier > 0
|
|
64
|
+
*/
|
|
65
|
+
constructor(timeoutMs, onExpire, periodMs = 1e3) {
|
|
66
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) throw new RangeError(`délai de réception invalide : ${timeoutMs}`);
|
|
67
|
+
if (!Number.isInteger(periodMs) || periodMs <= 0) throw new RangeError(`période de balayage invalide : ${periodMs}`);
|
|
68
|
+
this.#onExpire = onExpire;
|
|
69
|
+
this.#periodMs = Math.min(periodMs, timeoutMs);
|
|
70
|
+
this.#ttl = Math.ceil(timeoutMs / this.#periodMs);
|
|
71
|
+
}
|
|
72
|
+
/** Cases occupées, libérées ou non, entre la tête et la queue. */
|
|
73
|
+
get size() {
|
|
74
|
+
return this.#tail - this.#head;
|
|
75
|
+
}
|
|
76
|
+
/** Capacité courante de l'anneau. */
|
|
77
|
+
get capacity() {
|
|
78
|
+
return this.#mask + 1;
|
|
79
|
+
}
|
|
80
|
+
/** Vrai tant que le balayage tourne. */
|
|
81
|
+
get running() {
|
|
82
|
+
return this.#timer !== null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Suit une réception qui commence ; démarre le balayage s'il dormait.
|
|
86
|
+
*
|
|
87
|
+
* @param item - la réception
|
|
88
|
+
* @returns le numéro à rendre à {@link ReceiveDeadline.release}
|
|
89
|
+
*/
|
|
90
|
+
track(item) {
|
|
91
|
+
if (this.#tail - this.#head > this.#mask) this.#grow();
|
|
92
|
+
const seq = this.#tail++;
|
|
93
|
+
const index = seq & this.#mask;
|
|
94
|
+
this.#items[index] = item;
|
|
95
|
+
this.#ticks[index] = this.#tick;
|
|
96
|
+
if (this.#timer === null) {
|
|
97
|
+
this.#timer = setInterval(() => this.#sweep(), this.#periodMs);
|
|
98
|
+
this.#timer.unref();
|
|
99
|
+
}
|
|
100
|
+
return seq;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Cesse de suivre une réception terminée (reçue, refusée ou échue). Sans
|
|
104
|
+
* effet si la case a déjà été balayée ou réattribuée : l'identité de
|
|
105
|
+
* l'élément est vérifiée. Le balayage ne s'arrête pas ici mais au tour
|
|
106
|
+
* suivant s'il ne reste rien — jamais un minuteur armé puis désarmé par
|
|
107
|
+
* requête.
|
|
108
|
+
*
|
|
109
|
+
* @param seq - le numéro rendu par {@link ReceiveDeadline.track}
|
|
110
|
+
* @param item - la réception suivie
|
|
111
|
+
*/
|
|
112
|
+
release(seq, item) {
|
|
113
|
+
const index = seq & this.#mask;
|
|
114
|
+
if (this.#items[index] === item) this.#items[index] = null;
|
|
115
|
+
}
|
|
116
|
+
/** Arrête le balayage et oublie toutes les réceptions (arrêt du serveur). */
|
|
117
|
+
close() {
|
|
118
|
+
if (this.#timer !== null) {
|
|
119
|
+
clearInterval(this.#timer);
|
|
120
|
+
this.#timer = null;
|
|
121
|
+
}
|
|
122
|
+
this.#reset();
|
|
123
|
+
}
|
|
124
|
+
#sweep() {
|
|
125
|
+
const tick = ++this.#tick;
|
|
126
|
+
const items = this.#items;
|
|
127
|
+
const carried = this.#tail - this.#head;
|
|
128
|
+
while (this.#head < this.#tail) {
|
|
129
|
+
const index = this.#head & this.#mask;
|
|
130
|
+
const item = items[index] ?? null;
|
|
131
|
+
if (item !== null && tick - (this.#ticks[index] ?? 0) <= this.#ttl) break;
|
|
132
|
+
items[index] = null;
|
|
133
|
+
this.#head++;
|
|
134
|
+
if (item !== null) this.#onExpire(item);
|
|
135
|
+
}
|
|
136
|
+
if (this.#head !== this.#tail) return;
|
|
137
|
+
if (carried > 0) {
|
|
138
|
+
this.#head = 0;
|
|
139
|
+
this.#tail = 0;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (this.#timer !== null) {
|
|
143
|
+
clearInterval(this.#timer);
|
|
144
|
+
this.#timer = null;
|
|
145
|
+
}
|
|
146
|
+
this.#reset();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Anneau au repos ou fermé : numéros remis à zéro et capacité de départ
|
|
150
|
+
* rendue. Un numéro encore détenu par un appelant ne désigne plus son
|
|
151
|
+
* élément : `release` le reconnaît à l'identité.
|
|
152
|
+
*/
|
|
153
|
+
#reset() {
|
|
154
|
+
this.#head = 0;
|
|
155
|
+
this.#tail = 0;
|
|
156
|
+
if (this.#mask + 1 !== INITIAL_CAPACITY) {
|
|
157
|
+
this.#items = new Array(INITIAL_CAPACITY).fill(null);
|
|
158
|
+
this.#ticks = new Int32Array(INITIAL_CAPACITY);
|
|
159
|
+
this.#mask = 63;
|
|
160
|
+
} else this.#items.fill(null);
|
|
161
|
+
}
|
|
162
|
+
/** Double la capacité, les entrées gardant leur numéro de séquence. */
|
|
163
|
+
#grow() {
|
|
164
|
+
const capacity = (this.#mask + 1) * 2;
|
|
165
|
+
const mask = capacity - 1;
|
|
166
|
+
const items = new Array(capacity).fill(null);
|
|
167
|
+
const ticks = new Int32Array(capacity);
|
|
168
|
+
for (let seq = this.#head; seq < this.#tail; seq++) {
|
|
169
|
+
items[seq & mask] = this.#items[seq & this.#mask] ?? null;
|
|
170
|
+
ticks[seq & mask] = this.#ticks[seq & this.#mask] ?? 0;
|
|
171
|
+
}
|
|
172
|
+
this.#items = items;
|
|
173
|
+
this.#ticks = ticks;
|
|
174
|
+
this.#mask = mask;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
export { ReceiveDeadline };
|
|
@@ -1,7 +1,60 @@
|
|
|
1
|
+
import { refuseMalformedHandshake, refuseUpgrade } from "../proxy/forward.js";
|
|
1
2
|
import { websocketHandshakeProblem } from "../proxy/rules.js";
|
|
2
|
-
import { refuseUpgrade } from "../proxy/forward.js";
|
|
3
3
|
import { STATUS_CODES } from "node:http";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
4
5
|
// nodefony/src/servers/upgradeDispatch.ts
|
|
6
|
+
/**
|
|
7
|
+
* Sous-protocole choisi par handshake, entre {@link IUpgradeAdmission.selectSubprotocol}
|
|
8
|
+
* et le rappel `handleProtocols` de `ws` — qui ne reçoit que la requête. Une
|
|
9
|
+
* table faible plutôt qu'une variable « en cours » : un `verifyClient`
|
|
10
|
+
* asynchrone sépare les deux instants, et deux handshakes s'y croiseraient.
|
|
11
|
+
* Une entrée n'existe que pour un handshake qui a choisi.
|
|
12
|
+
*/
|
|
13
|
+
const chosenSubprotocols = /* @__PURE__ */ new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* Fabrique l'option `handleProtocols` du serveur `ws` de Nodefony : le choix
|
|
16
|
+
* retenu par l'admission ({@link IUpgradeAdmission.selectSubprotocol}) s'il y
|
|
17
|
+
* en a un, sinon `fallback` (celle de la configuration), sinon le premier
|
|
18
|
+
* offert — le défaut de `ws` 8, conservé pour tout contrôleur qui ne choisit
|
|
19
|
+
* pas.
|
|
20
|
+
*
|
|
21
|
+
* @param fallback - l'option `handleProtocols` de la configuration, s'il y en a une
|
|
22
|
+
* @returns le rappel à passer à `new WebSocketServer({ handleProtocols })`
|
|
23
|
+
*/
|
|
24
|
+
function createSubprotocolHandler(fallback) {
|
|
25
|
+
return (protocols, request) => {
|
|
26
|
+
const chosen = chosenSubprotocols.get(request);
|
|
27
|
+
if (chosen !== void 0) {
|
|
28
|
+
chosenSubprotocols.delete(request);
|
|
29
|
+
return chosen !== false && protocols.has(chosen) ? chosen : false;
|
|
30
|
+
}
|
|
31
|
+
if (fallback !== void 0) return fallback(protocols, request);
|
|
32
|
+
const first = protocols.values().next();
|
|
33
|
+
return first.done === true ? false : first.value;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Construit le serveur `ws` de Nodefony à partir des options de la
|
|
38
|
+
* configuration, en forçant ce que Nodefony gère — seul endroit où ces
|
|
39
|
+
* options sont posées, pour le serveur clair comme pour le sécurisé :
|
|
40
|
+
*
|
|
41
|
+
* - `noServer` : l'upgrade passe d'abord par le proxy inverse, puis par
|
|
42
|
+
* {@link attachUpgradeDispatch} ;
|
|
43
|
+
* - `clientTracking` : requis par `broadcast()` et le heartbeat ;
|
|
44
|
+
* - `handleProtocols` : le sous-protocole CHOISI par le contrôleur
|
|
45
|
+
* ({@link createSubprotocolHandler}), celui de la configuration à défaut.
|
|
46
|
+
*
|
|
47
|
+
* @param options - options `ws` issues de la configuration du serveur WebSocket
|
|
48
|
+
* @returns le serveur, sans écoute propre
|
|
49
|
+
*/
|
|
50
|
+
function createNodefonyWebSocketServer(options) {
|
|
51
|
+
return new WebSocketServer({
|
|
52
|
+
...options,
|
|
53
|
+
noServer: true,
|
|
54
|
+
clientTracking: true,
|
|
55
|
+
handleProtocols: createSubprotocolHandler(options.handleProtocols)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
5
58
|
/** Défaut sans noyau : tout upgrade conforme est accepté puis émis en `connection`. */
|
|
6
59
|
function acceptAll(wss) {
|
|
7
60
|
return (req, admission) => {
|
|
@@ -38,6 +91,9 @@ var UpgradeAdmission = class {
|
|
|
38
91
|
this.#onPendingError = () => socket.destroy();
|
|
39
92
|
socket.on("error", this.#onPendingError);
|
|
40
93
|
}
|
|
94
|
+
selectSubprotocol(protocol) {
|
|
95
|
+
if (!this.#settled) chosenSubprotocols.set(this.req, protocol);
|
|
96
|
+
}
|
|
41
97
|
#settle() {
|
|
42
98
|
if (this.#settled) return false;
|
|
43
99
|
this.#settled = true;
|
|
@@ -71,6 +127,7 @@ var UpgradeAdmission = class {
|
|
|
71
127
|
}
|
|
72
128
|
refuse(status, headers) {
|
|
73
129
|
if (!this.#settle()) return;
|
|
130
|
+
chosenSubprotocols.delete(this.req);
|
|
74
131
|
refuseUpgrade(this.socket, status, STATUS_CODES[status] ?? "Refused", headers);
|
|
75
132
|
}
|
|
76
133
|
};
|
|
@@ -114,8 +171,9 @@ function attachUpgradeDispatch(server, wss, relay, refusal, admit = acceptAll(ws
|
|
|
114
171
|
refuseUpgrade(socket, status, STATUS_CODES[status] ?? "Refused");
|
|
115
172
|
return;
|
|
116
173
|
}
|
|
117
|
-
|
|
118
|
-
|
|
174
|
+
const problem = websocketHandshakeProblem(req.method, req.headers);
|
|
175
|
+
if (problem !== null) {
|
|
176
|
+
refuseMalformedHandshake(socket, problem);
|
|
119
177
|
return;
|
|
120
178
|
}
|
|
121
179
|
admit(req, new UpgradeAdmission(wss, req, socket, head));
|
|
@@ -125,4 +183,4 @@ function attachUpgradeDispatch(server, wss, relay, refusal, admit = acceptAll(ws
|
|
|
125
183
|
server.removeListener("upgrade", onUpgrade);
|
|
126
184
|
};
|
|
127
185
|
}
|
|
128
|
-
export { attachUpgradeDispatch };
|
|
186
|
+
export { attachUpgradeDispatch, createNodefonyWebSocketServer, createSubprotocolHandler };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -68,12 +68,19 @@ declare class Http extends Module<IHttpConfig> {
|
|
|
68
68
|
}
|
|
69
69
|
export default Http;
|
|
70
70
|
export { defineHttpConfig, httpConfigJsonSchema, } from "./nodefony/config/defineModuleConfig.js";
|
|
71
|
+
export { parseRequestTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
72
|
+
export { isContentlessStatus } from "./nodefony/src/context/http/Response.js";
|
|
73
|
+
export type { ISplitTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
71
74
|
export { httpConfigSchema } from "./nodefony/config/config.js";
|
|
72
75
|
export type { IHttpConfig, IHttpConfigInput, } from "./nodefony/interfaces/IHttpConfig.js";
|
|
73
76
|
export { decideSend, readBackpressureOptions, type WsBackpressurePolicy, type WsSendDecision, type IWsBackpressureOptions, type IBackpressureSocket, type IBackpressureTarget, } from "./nodefony/src/context/websocket/wsBackpressure.js";
|
|
77
|
+
export { admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter, FRAME_RATE_WINDOW_MS, type IFrameRateWindow, type IWsFrameRateOptions, } from "./nodefony/src/context/websocket/wsFrameRate.js";
|
|
74
78
|
export { isWebsocketContext } from "./nodefony/src/context/websocket/isWebsocketContext.js";
|
|
75
79
|
export { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
|
|
80
|
+
export { rpcCorsLists } from "./nodefony/src/grpc/cors.js";
|
|
81
|
+
export type { IRpcCorsLists } from "./nodefony/src/grpc/cors.js";
|
|
76
82
|
export type { IRequestTrace, RequestTraceProtocol, } from "./nodefony/interfaces/IRequestTrace.js";
|
|
83
|
+
export type { IRpcCall, IRpcOutcome, RpcSystem, } from "./nodefony/interfaces/IRpcCall.js";
|
|
77
84
|
export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed, } from "./nodefony/src/context/domainMatcher.js";
|
|
78
85
|
export type { DomainPattern, ITrustedHostsConfig, } from "./nodefony/src/context/domainMatcher.js";
|
|
79
86
|
export { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
|
|
@@ -81,6 +88,8 @@ export type { IRateLimitStore, IRateLimitOptions, RateLimitVerdict, } from "./no
|
|
|
81
88
|
export { Context, Session, SessionsService, HttpError, HttpKernel, Response, Http2Response, Response as HttpResponse, wsResponse, wsResponse as WebsocketResponse, Cookie, HttpContext, WebsocketContext, HttpRequest, Http2Request, DefaultErrorRenderer, DefaultRequestLogger, JsonAuditLogger, PrettyRequestLogger, Profiler, };
|
|
82
89
|
export { SseStream, openSseStream, acceptsEventStream, SSE_HEARTBEAT_MS, } from "./nodefony/src/context/http/SseStream.js";
|
|
83
90
|
export type { ISseStreamOptions, ISseSendOptions, } from "./nodefony/src/context/http/SseStream.js";
|
|
91
|
+
export { openByteStream } from "./nodefony/src/context/http/ByteStream.js";
|
|
92
|
+
export type { ByteStreamEndReason, ByteStreamSource, IByteStreamOptions, IByteStreamResult, } from "./nodefony/src/context/http/ByteStream.js";
|
|
84
93
|
export { schemaMismatchOf, type SchemaMismatchConfidence, } from "./nodefony/service/error-renderer.js";
|
|
85
94
|
export { toValidationFields, type IValidationField, } from "./nodefony/service/error-renderer.js";
|
|
86
95
|
export type { ProfileEntry, ProfileSummary, ProfilePhase, ProfileQuery, ProfileSecurity, IProfileBatch, IProfilerReader, } from "./nodefony/src/profiler/Profiler.js";
|
|
@@ -102,8 +111,8 @@ export type { IHttpResponse, IWebsocketResponse, } from "./nodefony/interfaces/I
|
|
|
102
111
|
export type { IHttpKernel } from "./nodefony/interfaces/IHttpKernel.js";
|
|
103
112
|
export type { IResolvedRoute, IRouteResolver, IRequestRouter, } from "./nodefony/interfaces/IRouting.js";
|
|
104
113
|
export type { ISecurityZone, IFirewallGate, } from "./nodefony/interfaces/ISecurity.js";
|
|
105
|
-
export type { IUploadedFile, IUploadService, IParsedUploadFile, IUploadOptions, } from "./nodefony/interfaces/IUpload.js";
|
|
106
|
-
export type { IErrorRenderer, IErrorHttpResult, IErrorWebsocketResult, } from "./nodefony/interfaces/IErrorRenderer.js";
|
|
114
|
+
export type { IUploadedFile, IUploadService, IParsedUploadFile, IUploadOptions, IRouteUploadOptions, } from "./nodefony/interfaces/IUpload.js";
|
|
115
|
+
export type { IErrorRenderer, IErrorHttpResult, IErrorWebsocketResult, IErrorDescription, } from "./nodefony/interfaces/IErrorRenderer.js";
|
|
107
116
|
export type { IRequestLogger, IRequestLogEntry, } from "./nodefony/interfaces/IRequestLogger.js";
|
|
108
117
|
export type { AuditLogEntry, AuditErrorEntry, JsonAuditLoggerOptions, } from "./nodefony/service/audit-logger.js";
|
|
109
118
|
export type { ContextType, httpRequest, httpResponse, ProtocolType, } from "./nodefony/service/http-kernel.js";
|