@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.6
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.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +29 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +4 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/error-renderer.js +6 -4
- package/dist/nodefony/service/http-kernel.js +112 -34
- package/dist/nodefony/service/pretty-request-logger.js +1 -1
- package/dist/nodefony/service/request-logger.js +1 -1
- package/dist/nodefony/service/reverse-proxy.js +7 -2
- package/dist/nodefony/service/servers/clientError.js +28 -4
- package/dist/nodefony/service/servers/server-h2c.js +5 -5
- package/dist/nodefony/service/servers/server-http.js +5 -3
- package/dist/nodefony/service/servers/server-https.js +12 -5
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
- package/dist/nodefony/service/servers/server-websocket.js +6 -10
- package/dist/nodefony/service/servers/serverShutdown.js +63 -4
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/http/ByteStream.js +359 -0
- package/dist/nodefony/src/context/http/HttpContext.js +55 -26
- package/dist/nodefony/src/context/http/Request.js +15 -6
- package/dist/nodefony/src/context/http/Response.js +354 -35
- package/dist/nodefony/src/context/http/SseStream.js +81 -146
- package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +8 -4
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +4 -2
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +71 -14
- 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 +54 -13
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +6 -0
- package/dist/types/nodefony/config/config.d.ts +4 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
- package/dist/types/nodefony/service/http-kernel.d.ts +15 -2
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +45 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
- package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +5 -3
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +40 -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/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +371 -0
- package/docs/cookies.md +13 -3
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +26 -15
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -43,6 +43,30 @@ Le serveur le pose dans le contexte asynchrone de la requête : `RequestContext.
|
|
|
43
43
|
depuis n'importe quel code, et les services `@injectable({ scope: "request" })` y vivent. Détail :
|
|
44
44
|
[Injection & portées](https://github.com/nodefony/nodefony-core/blob/main/docs/architecture/injection-portees.md).
|
|
45
45
|
|
|
46
|
+
## Réponses en flux
|
|
47
|
+
|
|
48
|
+
Deux formats de réponse qui ne finissent pas d'un coup, en HTTP/1.1 comme en HTTP/2, sur le même
|
|
49
|
+
cycle de vie (réponse prise, session sauvée avant les en-têtes, départ du client vu sur la réponse,
|
|
50
|
+
plafond par IP, révocation, coupure d'un client qui ne lit plus) :
|
|
51
|
+
|
|
52
|
+
| Besoin | Contrôleur | Contexte | Page |
|
|
53
|
+
| ---------------------------------------------------------- | ------------------------ | ------------------------- | -------------------------------------- |
|
|
54
|
+
| Pousser des **événements texte** lus par `EventSource` | `this.renderSse()` | `context.openSse()` | [Flux SSE](./docs/sse.md) |
|
|
55
|
+
| Servir des **octets** : processus, relais `fetch`, archive | `this.renderStream(src)` | `context.openStream(src)` | [Flux d'octets](./docs/byte-stream.md) |
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const tar = spawn("tar", ["-cz", "-C", "var", "logs"], {
|
|
59
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
const { reason, bytes } = await this.renderStream(tar.stdout, {
|
|
63
|
+
headers: { "content-type": "application/gzip" },
|
|
64
|
+
});
|
|
65
|
+
} finally {
|
|
66
|
+
tar.kill("SIGKILL");
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
46
70
|
## Rate-limit général par IP
|
|
47
71
|
|
|
48
72
|
Plafond de trafic **par IP cliente** sur toutes les routes HTTP — protège la capacité du serveur
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// \0@oxc-project+runtime@0.
|
|
1
|
+
// \0@oxc-project+runtime@0.157.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
import { normalizePrefix, parseProxyOrigin, proxyMountProblems } from "./nodefony/src/proxy/rules.js";
|
|
2
1
|
import config, { httpConfigSchema, httpOverlaySchema } from "./nodefony/config/config.js";
|
|
3
2
|
import { defineHttpConfig, httpConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
4
3
|
import { createHttpAdminApi } from "./nodefony/service/HttpAdminApi.js";
|
|
5
|
-
import { ACTION_SELF_PHASE, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, TOOLING_URL_PREFIX, aggregateProfiles, createProfileAggregator, isFailedProfile, sqlShape } from "./nodefony/src/profiler/aggregate.js";
|
|
6
4
|
import { createProfilerAdminApi } from "./nodefony/service/ProfilerAdminApi.js";
|
|
7
5
|
import { Profiler } from "./nodefony/src/profiler/Profiler.js";
|
|
8
|
-
import HttpError from "./nodefony/src/errors/httpError.js";
|
|
9
|
-
import { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
|
|
10
|
-
import { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "./nodefony/src/context/domainMatcher.js";
|
|
11
|
-
import { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
|
|
12
|
-
import Cookie from "./nodefony/src/cookies/cookie.js";
|
|
13
|
-
import Context from "./nodefony/src/context/Context.js";
|
|
14
|
-
import { decideSend, readBackpressureOptions } from "./nodefony/src/context/websocket/wsBackpressure.js";
|
|
15
|
-
import WebsocketResponse from "./nodefony/src/context/websocket/Response.js";
|
|
16
|
-
import { FrameProfile } from "./nodefony/src/profiler/FrameProfile.js";
|
|
17
|
-
import WebsocketContext from "./nodefony/src/context/websocket/WebsocketContext.js";
|
|
18
|
-
import HttpRequest from "./nodefony/src/context/http/Request.js";
|
|
19
|
-
import HttpResponse from "./nodefony/src/context/http/Response.js";
|
|
20
|
-
import Http2Request from "./nodefony/src/context/http2/Request.js";
|
|
21
|
-
import Http2Response from "./nodefony/src/context/http2/Response.js";
|
|
22
|
-
import { SSE_HEARTBEAT_MS, SseStream, acceptsEventStream, openSseStream } from "./nodefony/src/context/http/SseStream.js";
|
|
23
|
-
import HttpContext from "./nodefony/src/context/http/HttpContext.js";
|
|
24
|
-
import DefaultErrorRenderer, { schemaMismatchOf, toValidationFields } from "./nodefony/service/error-renderer.js";
|
|
25
|
-
import JsonAuditLogger from "./nodefony/service/audit-logger.js";
|
|
26
|
-
import DefaultRequestLogger from "./nodefony/service/request-logger.js";
|
|
27
|
-
import PrettyRequestLogger from "./nodefony/service/pretty-request-logger.js";
|
|
28
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
29
6
|
import http_kernel_default from "./nodefony/service/http-kernel.js";
|
|
30
7
|
import server_http_default from "./nodefony/service/servers/server-http.js";
|
|
31
8
|
import server_https_default from "./nodefony/service/servers/server-https.js";
|
|
@@ -38,13 +15,39 @@ import Network from "./nodefony/command/networkCommand.js";
|
|
|
38
15
|
import Certificates from "./nodefony/command/certificatesCommand.js";
|
|
39
16
|
import ProxyGenerate from "./nodefony/command/proxyGenerateCommand.js";
|
|
40
17
|
import AssetsPublish from "./nodefony/command/assetsPublishCommand.js";
|
|
41
|
-
import Session from "./nodefony/src/session/session.js";
|
|
42
|
-
import { SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS } from "./nodefony/src/session/storage/sessionSort.js";
|
|
43
18
|
import sessions_service_default from "./nodefony/service/sessions/sessions-service.js";
|
|
44
19
|
import Certificate, { trustOutboundAuthorities } from "./nodefony/service/certificates.js";
|
|
20
|
+
import Context from "./nodefony/src/context/Context.js";
|
|
21
|
+
import Session from "./nodefony/src/session/session.js";
|
|
22
|
+
import HttpError from "./nodefony/src/errors/httpError.js";
|
|
23
|
+
import HttpResponse, { isContentlessStatus } from "./nodefony/src/context/http/Response.js";
|
|
24
|
+
import Http2Response from "./nodefony/src/context/http2/Response.js";
|
|
25
|
+
import WebsocketResponse from "./nodefony/src/context/websocket/Response.js";
|
|
26
|
+
import Cookie from "./nodefony/src/cookies/cookie.js";
|
|
45
27
|
import upload_service_default from "./nodefony/service/upload/upload-service.js";
|
|
28
|
+
import DefaultErrorRenderer, { schemaMismatchOf, toValidationFields } from "./nodefony/service/error-renderer.js";
|
|
29
|
+
import DefaultRequestLogger from "./nodefony/service/request-logger.js";
|
|
30
|
+
import JsonAuditLogger from "./nodefony/service/audit-logger.js";
|
|
31
|
+
import PrettyRequestLogger from "./nodefony/service/pretty-request-logger.js";
|
|
32
|
+
import HttpContext from "./nodefony/src/context/http/HttpContext.js";
|
|
33
|
+
import WebsocketContext from "./nodefony/src/context/websocket/WebsocketContext.js";
|
|
34
|
+
import HttpRequest from "./nodefony/src/context/http/Request.js";
|
|
35
|
+
import Http2Request from "./nodefony/src/context/http2/Request.js";
|
|
36
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
|
|
37
|
+
import { parseRequestTarget } from "./nodefony/src/context/http/urlFastPath.js";
|
|
38
|
+
import { decideSend, readBackpressureOptions } from "./nodefony/src/context/websocket/wsBackpressure.js";
|
|
39
|
+
import { FRAME_RATE_WINDOW_MS, admitFrame, frameRateRejectedTotal, readFrameRateBound, readFrameRateCloseAfter } from "./nodefony/src/context/websocket/wsFrameRate.js";
|
|
46
40
|
import { isWebsocketContext } from "./nodefony/src/context/websocket/isWebsocketContext.js";
|
|
41
|
+
import { REQUEST_TRACE_CHANNEL } from "./nodefony/src/context/requestTrace.js";
|
|
42
|
+
import { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "./nodefony/src/context/domainMatcher.js";
|
|
43
|
+
import { MemoryRateLimitStore } from "./nodefony/src/rateLimit/MemoryRateLimitStore.js";
|
|
44
|
+
import { SSE_HEARTBEAT_MS, SseStream, acceptsEventStream, openSseStream } from "./nodefony/src/context/http/SseStream.js";
|
|
45
|
+
import { openByteStream } from "./nodefony/src/context/http/ByteStream.js";
|
|
46
|
+
import { ACTION_SELF_PHASE, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, TOOLING_URL_PREFIX, aggregateProfiles, createProfileAggregator, isFailedProfile, sqlShape } from "./nodefony/src/profiler/aggregate.js";
|
|
47
|
+
import { FrameProfile } from "./nodefony/src/profiler/FrameProfile.js";
|
|
48
|
+
import { normalizePrefix, parseProxyOrigin, proxyMountProblems } from "./nodefony/src/proxy/rules.js";
|
|
47
49
|
import { PrebuiltUi, resolveUiDelivery } from "./nodefony/src/assets/prebuiltUi.js";
|
|
50
|
+
import { SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS } from "./nodefony/src/session/storage/sessionSort.js";
|
|
48
51
|
import { Module, services } from "nodefony";
|
|
49
52
|
// index.ts
|
|
50
53
|
let Http = class Http extends Module {
|
|
@@ -116,4 +119,4 @@ Http = __decorate([services([
|
|
|
116
119
|
upload_service_default
|
|
117
120
|
])], Http);
|
|
118
121
|
var http_default = Http;
|
|
119
|
-
export { ACTION_SELF_PHASE, Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, PrebuiltUi, PrettyRequestLogger, Profiler, REQUEST_TRACE_CHANNEL, ReverseProxy, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, SSE_HEARTBEAT_MS, Session, sessions_service_default as SessionsService, SseStream, TOOLING_URL_PREFIX, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, acceptsEventStream, aggregateProfiles, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, createProfileAggregator, decideSend, http_default as default, defineHttpConfig, httpConfigJsonSchema, httpConfigSchema, isDomainAllowed, isFailedProfile, isWebsocketContext, normalizePrefix, openSseStream, parseProxyOrigin, proxyMountProblems, readBackpressureOptions, resolveUiDelivery, schemaMismatchOf, sqlShape, toValidationFields };
|
|
122
|
+
export { ACTION_SELF_PHASE, Context, Cookie, DefaultErrorRenderer, DefaultRequestLogger, FRAME_RATE_WINDOW_MS, FrameProfile, Http2Request, Http2Response, HttpContext, HttpError, http_kernel_default as HttpKernel, HttpRequest, HttpResponse, HttpResponse as Response, JsonAuditLogger, MemoryRateLimitStore, NO_ROUTE, N_PLUS_ONE_MIN_REPEATS, PrebuiltUi, PrettyRequestLogger, Profiler, REQUEST_TRACE_CHANNEL, ReverseProxy, SESSION_COLUMN_ALIASES, SESSION_DEFAULT_ORDER, SESSION_DEFAULT_ORDER_SQL, SESSION_SORTABLE_FIELDS, SSE_HEARTBEAT_MS, Session, sessions_service_default as SessionsService, SseStream, TOOLING_URL_PREFIX, WebsocketContext, WebsocketResponse, WebsocketResponse as wsResponse, acceptsEventStream, admitFrame, aggregateProfiles, compileDomainPattern, compileDomainPatterns, compileTrustedHosts, createProfileAggregator, decideSend, http_default as default, defineHttpConfig, frameRateRejectedTotal, httpConfigJsonSchema, httpConfigSchema, isContentlessStatus, isDomainAllowed, isFailedProfile, isWebsocketContext, normalizePrefix, openByteStream, openSseStream, parseProxyOrigin, parseRequestTarget, proxyMountProblems, readBackpressureOptions, readFrameRateBound, readFrameRateCloseAfter, resolveUiDelivery, schemaMismatchOf, sqlShape, toValidationFields };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { resolveTrustedHostNames } from "../src/context/domainMatcher.js";
|
|
2
|
-
import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
|
|
3
1
|
import { defaultIntrospection, generateHaproxyConfig, generateNginxConfig } from "../src/proxy/generateProxyConfig.js";
|
|
4
2
|
import { planAssetPublish } from "../src/assets/collectAssets.js";
|
|
3
|
+
import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
|
|
4
|
+
import { resolveTrustedHostNames } from "../src/context/domainMatcher.js";
|
|
5
5
|
import { Command } from "nodefony";
|
|
6
6
|
import fsp from "node:fs/promises";
|
|
7
7
|
import { isAbsolute, resolve } from "node:path";
|
|
@@ -101,7 +101,7 @@ const securityHeadersSchema = z.strictObject({
|
|
|
101
101
|
}),
|
|
102
102
|
frameOptions: z.string().nullable().default("DENY").meta({
|
|
103
103
|
runtimeMutable: true,
|
|
104
|
-
description: "X-Frame-Options
|
|
104
|
+
description: "Anti-clickjacking — pilote `X-Frame-Options` ET la directive CSP `frame-ancestors` (posée par @nodefony/security) : `DENY` (défaut, 'none') | `SAMEORIGIN` ('self') | `null` (aucun des deux). Encadrement par une origine tierce : `frame-ancestors <origine>` dans `security.headers.csp`, qui l’emporte. Éditable à chaud."
|
|
105
105
|
}),
|
|
106
106
|
strictTransportSecurity: strictTransportSecuritySchema.nullable().default(() => strictTransportSecuritySchema.parse({})).describe("HSTS — voir sous-schéma. `null` = header désactivé.")
|
|
107
107
|
}).describe("En-têtes de sécurité HTTP — defaults OWASP secure-by-default, posés en amont du pipeline (couvre aussi les statics).");
|
|
@@ -224,7 +224,9 @@ const websocketSchema = z.looseObject({
|
|
|
224
224
|
allowSynchronousEvents: z.boolean().default(true).describe("Émet les events ('message', 'ping'…) de plusieurs frames lues dans un même chunk réseau de façon synchrone (ws@8.18+). true = meilleur débit ; false = un event par tick d'event loop (latence plus régulière, plus équitable entre connexions)."),
|
|
225
225
|
maxBackpressure: z.number().int().nonnegative().default(4194304).describe("Seuil (octets) du buffer d'envoi par connexion (`ws.bufferedAmount`) au-delà duquel `backpressurePolicy` s'applique. Borne la RAM d'envoi face à un client LENT À RECEVOIR (anti-OOM) ; broadcast() amplifie (un seul lent peut plomber la diffusion). 0 = désactivé. Défaut 4 MiB."),
|
|
226
226
|
backpressureCloseAfterDrops: z.number().int().nonnegative().default(1e3).describe("SOLDE de frames refusées au-delà duquel la connexion est FERMÉE (1013), en politique 'drop' : +1 par frame refusée, −1 par frame envoyée. POURQUOI un solde et pas un second seuil d'octets : une fois qu'on jette, plus rien n'alimente la file — elle plafonne au seuil de drop et n'atteint jamais un seuil supérieur, qui serait donc inatteignable (mesuré). Et pas une série remise à zéro au premier envoi : la file d'un client bloqué oscille autour du seuil, chaque envoi effacerait la série et il ne serait jamais fermé. Le solde, lui, distingue le pic passager du client mort. 0 = ne jamais fermer. Défaut 1000."),
|
|
227
|
-
backpressurePolicy: z.enum(["drop", "close"]).default("drop").describe("Action quand `ws.bufferedAmount` dépasse `maxBackpressure`. 'drop' = sauter la frame pour ce client (il reste connecté, dégradable — idéal télémétrie/broadcast, défaut). 'close' = fermer le client (RFC 6455 close 1013 « Try Again Later »). La fusion (coalesce) relève de la couche canal realtime, pas du transport.")
|
|
227
|
+
backpressurePolicy: z.enum(["drop", "close"]).default("drop").describe("Action quand `ws.bufferedAmount` dépasse `maxBackpressure`. 'drop' = sauter la frame pour ce client (il reste connecté, dégradable — idéal télémétrie/broadcast, défaut). 'close' = fermer le client (RFC 6455 close 1013 « Try Again Later »). La fusion (coalesce) relève de la couche canal realtime, pas du transport."),
|
|
228
|
+
maxFramesPerSecond: z.number().int().nonnegative().default(0).describe("Trames REÇUES admises par seconde et par connexion (unité : une trame, texte ou binaire — la taille est bornée à part par `maxPayload`). Au-delà, la trame est écartée AVANT tout décodage et comptée (`frameRateRejectedTotal` sur la sonde du hub temps réel) ; la connexion reste ouverte. 0 = désactivée (défaut de toute la série 10). Repère : 50 trames/s d'audio passent avec marge sous une borne de 200."),
|
|
229
|
+
frameRateCloseAfterRejects: z.number().int().nonnegative().default(1e3).describe("SOLDE de trames écartées au-delà duquel la connexion est FERMÉE (1008, violation de politique) : +1 par trame écartée, −1 par trame admise — même règle que `backpressureCloseAfterDrops`. Un pic passager ne ferme pas ; seul un flux soutenu au-delà du DOUBLE de `maxFramesPerSecond` fait monter le solde. Sans effet tant que `maxFramesPerSecond` vaut 0. 0 = ne jamais fermer. Défaut 1000.")
|
|
228
230
|
}).describe("Serveur WebSocket (`ws@8`). Les options ci-dessus (+ loose : `verifyClient`, `handleProtocols`, `path`, `WebSocket`…) sont transmises à `ws`. GÉRÉ par Nodefony et donc NON exposé : `server`/`host`/`port`/`noServer` (forcé à `noServer` : l'upgrade du serveur HTTP passe d'abord par le proxy inverse, puis par `ws`), `clientTracking` (forcé true — requis par broadcast() + heartbeat), `backlog` (celui du serveur HTTP). `keepalive*`/`closeTimeout` sont des knobs Nodefony, pas des options `ws`.");
|
|
229
231
|
const staticOptionsSchema = z.looseObject({
|
|
230
232
|
cacheControl: z.boolean().optional().describe("Active le header Cache-Control."),
|
|
@@ -13,7 +13,8 @@ import { aggregateProfiles } from "../src/profiler/aggregate.js";
|
|
|
13
13
|
* `?connection=<requestId>` : les seules frames de cette socket WebSocket
|
|
14
14
|
* - `GET /nodefony/profiler/api/summary` → les profils de l'anneau agrégés par
|
|
15
15
|
* route (percentiles, phases, formes SQL, N+1, erreurs — `aggregateProfiles`)
|
|
16
|
-
* - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId
|
|
16
|
+
* - `GET /nodefony/profiler/api/{id}` → profil complet (phases) d'un requestId ;
|
|
17
|
+
* absent → 404 `{ startedAt, clearedAt }` : depuis quand l'anneau sait quelque chose
|
|
17
18
|
* - `DELETE /nodefony/profiler/api/recent` → vide le ring buffer
|
|
18
19
|
*
|
|
19
20
|
* La debug bar (toute page, dev) lit `X-Request-Id` de SON appel AJAX puis
|
|
@@ -68,7 +69,9 @@ function createProfilerAdminApi(profiler) {
|
|
|
68
69
|
status: 404,
|
|
69
70
|
body: {
|
|
70
71
|
error: "Profile not found",
|
|
71
|
-
requestId: params.id
|
|
72
|
+
requestId: params.id,
|
|
73
|
+
startedAt: profiler.startedAt,
|
|
74
|
+
clearedAt: profiler.clearedAt
|
|
72
75
|
}
|
|
73
76
|
};
|
|
74
77
|
return entry;
|
|
@@ -208,10 +208,11 @@ const PG_INVALID_INPUT = /^invalid input syntax for type (\w+): "(.*)"$/su;
|
|
|
208
208
|
/**
|
|
209
209
|
* Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
|
|
210
210
|
*
|
|
211
|
-
*
|
|
212
|
-
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier)
|
|
213
|
-
*
|
|
214
|
-
*
|
|
211
|
+
* Trois sources le disent : MongoDB par une `CastError` de Mongoose (la clé est
|
|
212
|
+
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier), et
|
|
213
|
+
* le kit de colonnes portable par une `ColumnValueError` de genre `uuid` ou
|
|
214
|
+
* `integer` — il refuse sur les trois moteurs la clé que PostgreSQL refuserait. Une clé rangée en
|
|
215
|
+
* texte NATIF ne lève rien, la lecture rend « rien » : le même 404.
|
|
215
216
|
*
|
|
216
217
|
* Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
|
|
217
218
|
*
|
|
@@ -223,6 +224,7 @@ function malformedIdentifierOf(error) {
|
|
|
223
224
|
for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
|
|
224
225
|
const candidate = current;
|
|
225
226
|
if (candidate.name === "CastError" && (candidate.kind === "ObjectId" || candidate.kind === "UUID") && (typeof candidate.value === "string" || typeof candidate.value === "number")) return String(candidate.value);
|
|
227
|
+
if (candidate.name === "ColumnValueError" && (candidate.kind === "uuid" || candidate.kind === "integer") && typeof candidate.value === "string") return candidate.value;
|
|
226
228
|
if (String(candidate.code) === "22P02") {
|
|
227
229
|
const match = PG_INVALID_INPUT.exec(candidate.message);
|
|
228
230
|
if (match) {
|
|
@@ -1,34 +1,41 @@
|
|
|
1
|
-
import { isProfiledUrl } from "../src/profiler/aggregate.js";
|
|
2
1
|
import HttpError from "../src/errors/httpError.js";
|
|
2
|
+
import { isUnsupportedTransferCoding } from "../src/proxy/rules.js";
|
|
3
|
+
import { hasConflictingAuthority } from "../src/context/transport.js";
|
|
3
4
|
import { buildTrustProxy } from "../src/context/trustProxy.js";
|
|
4
5
|
import { resolveForwarded } from "../src/context/forwarded.js";
|
|
5
6
|
import { MemoryRateLimitStore } from "../src/rateLimit/MemoryRateLimitStore.js";
|
|
6
7
|
import { WsConnectionCounter } from "../src/rateLimit/WsConnectionCounter.js";
|
|
7
8
|
import { compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "../src/context/domainMatcher.js";
|
|
9
|
+
import { isProfiledUrl } from "../src/profiler/aggregate.js";
|
|
8
10
|
import { responseEnded } from "../src/context/responseEnded.js";
|
|
9
11
|
import { unsentResponseWarning } from "../src/context/unsentResponse.js";
|
|
10
12
|
import { closeRequestTrace, hasRequestTraceSubscribers, openRequestTrace, runTraced } from "../src/context/requestTrace.js";
|
|
11
|
-
import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
|
|
12
13
|
import WebsocketContext, { toHandshakeStatus } from "../src/context/websocket/WebsocketContext.js";
|
|
13
14
|
import { trackPong } from "./servers/wsHeartbeat.js";
|
|
15
|
+
import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
|
|
14
16
|
import HttpContext from "../src/context/http/HttpContext.js";
|
|
15
17
|
import DefaultErrorRenderer from "./error-renderer.js";
|
|
16
|
-
import JsonAuditLogger from "./audit-logger.js";
|
|
17
18
|
import DefaultRequestLogger from "./request-logger.js";
|
|
18
19
|
import PrettyRequestLogger from "./pretty-request-logger.js";
|
|
20
|
+
import JsonAuditLogger from "./audit-logger.js";
|
|
19
21
|
import { resolveTraceparent } from "./trace.js";
|
|
20
22
|
import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
|
|
21
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
22
|
-
import { GcScheduler, IDENTITY_CHANGED_EVENT, RequestContext, RevocationWatch, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
|
|
23
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
|
|
24
|
+
import { BootConfigurationError, GcScheduler, IDENTITY_CHANGED_EVENT, RequestContext, RevocationWatch, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
|
|
23
25
|
import cluster from "node:cluster";
|
|
24
26
|
import http from "node:http";
|
|
25
27
|
// nodefony/service/http-kernel.ts
|
|
26
|
-
const HEALTH_HEADERS =
|
|
27
|
-
"content-type"
|
|
28
|
-
"
|
|
29
|
-
|
|
28
|
+
const HEALTH_HEADERS = [
|
|
29
|
+
"content-type",
|
|
30
|
+
"application/json",
|
|
31
|
+
"cache-control",
|
|
32
|
+
"no-store"
|
|
33
|
+
];
|
|
30
34
|
const HEALTH_BODY_OK = "{\"status\":\"ok\"}";
|
|
31
35
|
const HEALTH_BODY_UNREADY = "{\"status\":\"unready\"}";
|
|
36
|
+
/** Corps du 429 du limiteur de débit — figé, rien à sérialiser par refus. */
|
|
37
|
+
const TOO_MANY_REQUESTS_BODY = "{\"code\":429,\"message\":\"Too Many Requests\"}";
|
|
38
|
+
const TOO_MANY_REQUESTS_LENGTH = Buffer.byteLength(TOO_MANY_REQUESTS_BODY);
|
|
32
39
|
const perfProbe = process.env.NF_PERF_PROBE === "1" ? {
|
|
33
40
|
count: 0,
|
|
34
41
|
enterScopeNs: 0,
|
|
@@ -107,6 +114,20 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
107
114
|
secContentTypeOptions = null;
|
|
108
115
|
secFrameOptions = null;
|
|
109
116
|
secHsts = null;
|
|
117
|
+
/**
|
|
118
|
+
* En-têtes du transport, tableau plat `[nom, valeur, …]` aux noms en
|
|
119
|
+
* minuscules, par famille de serveur (HSTS sur TLS seulement). Figés et
|
|
120
|
+
* VALIDÉS au calcul : chaque réponse les recopie sans les revalider.
|
|
121
|
+
*/
|
|
122
|
+
#transportHeaders = {
|
|
123
|
+
http: [],
|
|
124
|
+
https: []
|
|
125
|
+
};
|
|
126
|
+
/** Réponse d'une sonde de santé : le transport, puis type et cache. */
|
|
127
|
+
#healthHeaders = {
|
|
128
|
+
http: [],
|
|
129
|
+
https: []
|
|
130
|
+
};
|
|
110
131
|
sessionService;
|
|
111
132
|
router;
|
|
112
133
|
firewall;
|
|
@@ -157,12 +178,49 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
157
178
|
if (hsts.preload) v += "; preload";
|
|
158
179
|
this.secHsts = v;
|
|
159
180
|
} else this.secHsts = null;
|
|
181
|
+
const headerServer = this.options.headerServer;
|
|
182
|
+
const http1 = [];
|
|
183
|
+
if (headerServer != null && headerServer !== "") http1.push("server", headerServer);
|
|
184
|
+
if (this.secContentTypeOptions !== null) http1.push("x-content-type-options", this.secContentTypeOptions);
|
|
185
|
+
if (this.secFrameOptions !== null) http1.push("x-frame-options", this.secFrameOptions);
|
|
186
|
+
const tls = this.secHsts === null ? http1 : [
|
|
187
|
+
...http1,
|
|
188
|
+
"strict-transport-security",
|
|
189
|
+
this.secHsts
|
|
190
|
+
];
|
|
191
|
+
for (let i = 0; i + 1 < tls.length; i += 2) {
|
|
192
|
+
const name = tls[i] ?? "";
|
|
193
|
+
try {
|
|
194
|
+
http.validateHeaderValue(name, tls[i + 1] ?? "");
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw new BootConfigurationError(`[@nodefony/http] en-tête « ${name} » refusé : ${error instanceof Error ? error.message : String(error)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.#transportHeaders = {
|
|
200
|
+
http: http1,
|
|
201
|
+
https: tls
|
|
202
|
+
};
|
|
203
|
+
this.#healthHeaders = {
|
|
204
|
+
http: [...http1, ...HEALTH_HEADERS],
|
|
205
|
+
https: [...tls, ...HEALTH_HEADERS]
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* En-têtes du transport d'une réponse, figés au démarrage — ce que
|
|
210
|
+
* `HttpResponse` place en tête de sa réserve.
|
|
211
|
+
*
|
|
212
|
+
* @param type - famille du serveur qui a reçu la requête.
|
|
213
|
+
* @returns la liste plate `[nom, valeur, …]`, noms en minuscules.
|
|
214
|
+
*/
|
|
215
|
+
transportHeaders(type) {
|
|
216
|
+
return type === "https" ? this.#transportHeaders.https : this.#transportHeaders.http;
|
|
160
217
|
}
|
|
161
218
|
/**
|
|
162
219
|
* Ce que ce kernel ÉMET réellement comme en-têtes de sécurité transport.
|
|
163
220
|
*
|
|
164
221
|
* Ces trois en-têtes (HSTS, X-Frame-Options, X-Content-Type-Options) sont posés
|
|
165
|
-
*
|
|
222
|
+
* par `@nodefony/http` sur TOUTE réponse (en tête de la réserve de `HttpResponse`),
|
|
223
|
+
* statiques et pages d'erreur comprises
|
|
166
224
|
* — pas par `@nodefony/security`, dont les clés homonymes sont inertes. Toute
|
|
167
225
|
* console d'administration doit donc lire CE point, sinon elle affiche une
|
|
168
226
|
* intention de configuration au lieu de l'état appliqué.
|
|
@@ -189,6 +247,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
189
247
|
*/
|
|
190
248
|
onConfigChanged() {
|
|
191
249
|
this.computeSecurityHeaderCaches();
|
|
250
|
+
this.firewall?.onTransportHeadersChanged?.();
|
|
192
251
|
this._trustProxyChecker = null;
|
|
193
252
|
this._wsOriginPolicy = Object.create(null);
|
|
194
253
|
this.trustedHosts = this.options.trustedHosts;
|
|
@@ -296,9 +355,9 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
296
355
|
* @param response - réponse serveur brute (HTTP/1 ou H2 compat)
|
|
297
356
|
* @param ok - `true` → 200, `false` → 503 (readiness pendant boot/shutdown)
|
|
298
357
|
*/
|
|
299
|
-
#respondHealth(response, ok) {
|
|
358
|
+
#respondHealth(response, ok, type) {
|
|
300
359
|
const res = response;
|
|
301
|
-
res.writeHead(ok ? 200 : 503,
|
|
360
|
+
res.writeHead(ok ? 200 : 503, type === "https" ? this.#healthHeaders.https : this.#healthHeaders.http);
|
|
302
361
|
res.end(ok ? HEALTH_BODY_OK : HEALTH_BODY_UNREADY);
|
|
303
362
|
}
|
|
304
363
|
async init() {
|
|
@@ -608,7 +667,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
608
667
|
if (!context.response.isHeaderSent()) {
|
|
609
668
|
if (context.isRedirect) {
|
|
610
669
|
context.isRedirect = false;
|
|
611
|
-
context.response.
|
|
670
|
+
context.response.removeHeader("Location");
|
|
612
671
|
}
|
|
613
672
|
try {
|
|
614
673
|
await context.render(result.body);
|
|
@@ -678,19 +737,14 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
678
737
|
* pipeline.
|
|
679
738
|
*/
|
|
680
739
|
dispatchHttpRequest(request, response, type) {
|
|
681
|
-
const headerServer = this.options.headerServer;
|
|
682
|
-
if (headerServer != null && headerServer !== "") response.setHeader("Server", headerServer);
|
|
683
|
-
if (this.secContentTypeOptions !== null) response.setHeader("X-Content-Type-Options", this.secContentTypeOptions);
|
|
684
|
-
if (this.secFrameOptions !== null) response.setHeader("X-Frame-Options", this.secFrameOptions);
|
|
685
|
-
if (this.secHsts !== null && type === "https") response.setHeader("Strict-Transport-Security", this.secHsts);
|
|
686
740
|
if (this.#healthPaths !== null) {
|
|
687
741
|
const url = request.url;
|
|
688
742
|
if (url === this.#healthPaths.liveness) {
|
|
689
|
-
this.#respondHealth(response, true);
|
|
743
|
+
this.#respondHealth(response, true, type);
|
|
690
744
|
return;
|
|
691
745
|
}
|
|
692
746
|
if (url === this.#healthPaths.readiness) {
|
|
693
|
-
this.#respondHealth(response, !this.#terminating && this.kernel?.servable === true);
|
|
747
|
+
this.#respondHealth(response, !this.#terminating && this.kernel?.servable === true, type);
|
|
694
748
|
return;
|
|
695
749
|
}
|
|
696
750
|
}
|
|
@@ -704,13 +758,19 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
704
758
|
if (verdict.limited) {
|
|
705
759
|
response.setHeader("Retry-After", verdict.retryAfterS);
|
|
706
760
|
const res = response;
|
|
707
|
-
|
|
708
|
-
|
|
761
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
762
|
+
response.setHeader("Content-Length", TOO_MANY_REQUESTS_LENGTH);
|
|
763
|
+
res.writeHead(429, type === "https" ? this.#transportHeaders.https : this.#transportHeaders.http);
|
|
764
|
+
res.end(TOO_MANY_REQUESTS_BODY);
|
|
709
765
|
return;
|
|
710
766
|
}
|
|
711
767
|
}
|
|
712
768
|
}
|
|
713
|
-
if (this.listenerCount("onServerRequest"))
|
|
769
|
+
if (this.listenerCount("onServerRequest")) {
|
|
770
|
+
const transport = type === "https" ? this.#transportHeaders.https : this.#transportHeaders.http;
|
|
771
|
+
for (let i = 0; i + 1 < transport.length; i += 2) response.setHeader(transport[i] ?? "", transport[i + 1] ?? "");
|
|
772
|
+
return this.fireAsync("onServerRequest", request, response, type).then(() => this.handle(request, response, type));
|
|
773
|
+
}
|
|
714
774
|
return this.handle(request, response, type);
|
|
715
775
|
}
|
|
716
776
|
async initServers() {
|
|
@@ -946,7 +1006,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
946
1006
|
if (!isPromise(served)) return served;
|
|
947
1007
|
return served.catch((e) => this.failHttp(e, created, scope));
|
|
948
1008
|
} catch (e) {
|
|
949
|
-
return this.failHttp(e, context, scope);
|
|
1009
|
+
return this.failHttp(e, context, scope, response);
|
|
950
1010
|
}
|
|
951
1011
|
}
|
|
952
1012
|
/**
|
|
@@ -986,9 +1046,19 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
986
1046
|
/**
|
|
987
1047
|
* Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
|
|
988
1048
|
* n'a pas pu se construire, puis rend l'erreur au client (`onError`).
|
|
1049
|
+
*
|
|
1050
|
+
* Sans contexte, `onError` n'a rien sur quoi écrire : la réponse BRUTE
|
|
1051
|
+
* reçoit alors un 500 nu. Sinon HTTP/1.1 coupait la connexion sans un octet
|
|
1052
|
+
* et HTTP/2 laissait le flux pendu.
|
|
989
1053
|
*/
|
|
990
|
-
failHttp(e, context, scope) {
|
|
991
|
-
if (context === null)
|
|
1054
|
+
failHttp(e, context, scope, response) {
|
|
1055
|
+
if (context === null) {
|
|
1056
|
+
this.container?.leaveScope(scope);
|
|
1057
|
+
if (response && !response.headersSent) {
|
|
1058
|
+
response.statusCode = 500;
|
|
1059
|
+
response.end();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
992
1062
|
return this.onError(e, context);
|
|
993
1063
|
}
|
|
994
1064
|
/**
|
|
@@ -1020,6 +1090,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1020
1090
|
*/
|
|
1021
1091
|
routeHttpRequest(httpContext, request, response) {
|
|
1022
1092
|
if (this.reverseProxy !== null && this.reverseProxy.mounts !== null && (httpContext.validDomain || !this.domainCheck)) {
|
|
1093
|
+
if (this.reverseProxy.match(request.url) !== void 0) httpContext.response.commitHeaders();
|
|
1023
1094
|
const relayed = this.reverseProxy.forward(request, response, httpContext.scheme);
|
|
1024
1095
|
if (relayed !== void 0) {
|
|
1025
1096
|
httpContext.relayAccessLog = this.reverseProxy.match(request.url)?.accessLog ?? "all";
|
|
@@ -1029,10 +1100,14 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1029
1100
|
httpContext.phaseStart("resolve");
|
|
1030
1101
|
httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
|
|
1031
1102
|
httpContext.phaseEnd("resolve");
|
|
1032
|
-
this.firewall
|
|
1103
|
+
if (this.firewall) httpContext.response.headerPolicy = this.firewall;
|
|
1033
1104
|
if (this.serverStatic && httpContext.resolver?.resolve !== true && !httpContext.resolver?.exception && (asServersConfig(this.kernel?.options.servers)?.statics || this.kernel?.options.statics || this.serverStatic.hasMounts())) {
|
|
1034
|
-
response.removeHeader("Content-Type");
|
|
1035
|
-
|
|
1105
|
+
httpContext.response.removeHeader("Content-Type");
|
|
1106
|
+
httpContext.response.commitBeforeNativeWrite();
|
|
1107
|
+
return this.serverStatic.handle(request, response).catch(() => void 0).then(() => {
|
|
1108
|
+
httpContext.response.disarmNativeWrite();
|
|
1109
|
+
return this.serveHttpRequest(httpContext);
|
|
1110
|
+
});
|
|
1036
1111
|
}
|
|
1037
1112
|
return this.serveHttpRequest(httpContext);
|
|
1038
1113
|
}
|
|
@@ -1041,6 +1116,9 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1041
1116
|
* ({@link onRequestEnd}) et l'action. Chacun n'est attendu que s'il attend.
|
|
1042
1117
|
*/
|
|
1043
1118
|
serveHttpRequest(httpContext) {
|
|
1119
|
+
const headers = httpContext.request.headers;
|
|
1120
|
+
if (isUnsupportedTransferCoding(headers["transfer-encoding"])) return Promise.reject(new HttpError("Transfer-Encoding not implemented", 501, httpContext));
|
|
1121
|
+
if (hasConflictingAuthority(headers)) return Promise.reject(new HttpError("Host differs from :authority", 400, httpContext));
|
|
1044
1122
|
const streamBody = httpContext.resolver?.resolve === true && httpContext.resolver.route?.bodyStream === true;
|
|
1045
1123
|
httpContext.phaseStart("parse");
|
|
1046
1124
|
const parsed = finallyMaybe(() => streamBody ? void 0 : httpContext.request.initialize(), () => httpContext.phaseEnd("parse"));
|
|
@@ -1098,8 +1176,8 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1098
1176
|
if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
|
|
1099
1177
|
} catch (authError) {
|
|
1100
1178
|
if (context.resolver?.exception) {
|
|
1101
|
-
context.response.
|
|
1102
|
-
context.response.
|
|
1179
|
+
context.response.removeHeader("Upgrade");
|
|
1180
|
+
context.response.removeHeader("Connection");
|
|
1103
1181
|
}
|
|
1104
1182
|
await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
|
|
1105
1183
|
throw authError;
|
|
@@ -1424,12 +1502,12 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1424
1502
|
return true;
|
|
1425
1503
|
}
|
|
1426
1504
|
/**
|
|
1427
|
-
* Inscrit un flux
|
|
1505
|
+
* Inscrit un flux (événements ou octets) à identité révocable : il sera fermé dans la fenêtre de
|
|
1428
1506
|
* re-validation qui suit la mort de son identité (déconnexion, session
|
|
1429
1507
|
* révoquée, jeton expiré ou annulé) — le même registre et la même règle que
|
|
1430
1508
|
* les sockets WebSocket.
|
|
1431
1509
|
*
|
|
1432
|
-
* @internal — appelé par le flux
|
|
1510
|
+
* @internal — appelé par le cycle de vie du flux (`StreamLifecycle`).
|
|
1433
1511
|
*/
|
|
1434
1512
|
watchStream(stream) {
|
|
1435
1513
|
(this.#streams ??= new RevocationWatch({
|
|
@@ -1443,7 +1521,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1443
1521
|
this.#streams?.unregister(stream);
|
|
1444
1522
|
}
|
|
1445
1523
|
/**
|
|
1446
|
-
* Re-valide tous les flux
|
|
1524
|
+
* Re-valide tous les flux en cours et ferme ceux dont l'identité est morte.
|
|
1447
1525
|
* Publique pour qu'un banc la déclenche sans attendre le tick.
|
|
1448
1526
|
*/
|
|
1449
1527
|
revalidateStreams() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
|
|
2
1
|
import { severityFromStatus, statusOf } from "./audit-logger.js";
|
|
2
|
+
import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
|
|
3
3
|
import { accessStatusFamily, logColor } from "nodefony";
|
|
4
4
|
// nodefony/service/pretty-request-logger.ts
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
|
|
2
1
|
import { severityFromStatus } from "./audit-logger.js";
|
|
2
|
+
import { handshakeRefusalStatus } from "../src/context/websocket/WebsocketContext.js";
|
|
3
3
|
import { logColor } from "nodefony";
|
|
4
4
|
// nodefony/service/request-logger.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { forwardRequest, forwardUpgrade, refuseMalformedHandshake, refuseUpgrade } from "../src/proxy/forward.js";
|
|
1
2
|
import { isAmbiguousPath, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem } from "../src/proxy/rules.js";
|
|
2
3
|
import { requestAuthority } from "../src/context/transport.js";
|
|
3
|
-
import { forwardRequest, forwardUpgrade, refuseUpgrade } from "../src/proxy/forward.js";
|
|
4
4
|
import { Service } from "nodefony";
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
@@ -275,10 +275,15 @@ var ReverseProxy = class extends Service {
|
|
|
275
275
|
refuseUpgrade(socket, 421, "Misdirected Request");
|
|
276
276
|
return true;
|
|
277
277
|
}
|
|
278
|
-
if (hostname === null || isAmbiguousPath(req.url ?? "/")
|
|
278
|
+
if (hostname === null || isAmbiguousPath(req.url ?? "/")) {
|
|
279
279
|
refuseUpgrade(socket, 400, "Bad Request");
|
|
280
280
|
return true;
|
|
281
281
|
}
|
|
282
|
+
const problem = websocketHandshakeProblem(req.method, req.headers);
|
|
283
|
+
if (problem !== null) {
|
|
284
|
+
refuseMalformedHandshake(socket, problem);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
282
287
|
const secure = req.socket instanceof TLSSocket;
|
|
283
288
|
if (rules !== void 0 && !rules.isWebsocketOriginAllowed(req.headers.origin, hostname, secure)) {
|
|
284
289
|
refuseUpgrade(socket, 403, "Forbidden");
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isPeerGoneError } from "../../src/context/websocket/Response.js";
|
|
2
2
|
// nodefony/service/servers/clientError.ts
|
|
3
3
|
/**
|
|
4
|
-
* Gère l'event `clientError` d'un `http.Server` / `https.Server
|
|
4
|
+
* Gère l'event `clientError` d'un `http.Server` / `https.Server`, et des connexions
|
|
5
|
+
* HTTP/1.1 d'un serveur HTTP/2 à `allowHTTP1`.
|
|
5
6
|
*
|
|
6
7
|
* ⚠️ Doc Node (`http.Server` event `'clientError'`) : dès qu'un listener
|
|
7
8
|
* `clientError` est attaché, Node **cesse de fermer le socket automatiquement**
|
|
@@ -13,9 +14,32 @@ import { isPeerGoneError } from "../../src/context/websocket/Response.js";
|
|
|
13
14
|
* @param socket - flux client brut (l'event `clientError` fournit un `Duplex`).
|
|
14
15
|
*/
|
|
15
16
|
function handleClientError(error, socket) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const code = "code" in error ? error.code : void 0;
|
|
18
|
+
if (code === "ECONNRESET" || !socket.writable) return;
|
|
19
|
+
const statusLine = code === "HPE_HEADER_OVERFLOW" ? overflowStatusLine(error) : "400 Bad Request";
|
|
20
|
+
socket.end(`HTTP/1.1 ${statusLine}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Le statut d'un `HPE_HEADER_OVERFLOW` : Node lève la même erreur pour une
|
|
24
|
+
* cible trop longue et pour des en-têtes trop longs, alors que la norme les
|
|
25
|
+
* distingue — 414 pour la cible (RFC 9112 §3), 431 pour les en-têtes
|
|
26
|
+
* (RFC 6585 §5).
|
|
27
|
+
*
|
|
28
|
+
* Le parseur ne constate le débordement qu'à la FIN d'un élément (la cible,
|
|
29
|
+
* une valeur d'en-tête), au point `bytesParsed` du paquet reçu (`rawPacket`).
|
|
30
|
+
* Si la ligne de requête était finie, son `CRLF` — ou celui d'un en-tête —
|
|
31
|
+
* précède ce point : ce sont les en-têtes qui débordent. Sinon, c'est la cible.
|
|
32
|
+
* Constaté sur Node 26, en un paquet comme en plusieurs. Faute de ces deux
|
|
33
|
+
* champs, 431 : le statut d'avant.
|
|
34
|
+
*/
|
|
35
|
+
function overflowStatusLine(error) {
|
|
36
|
+
const raw = "rawPacket" in error ? error.rawPacket : void 0;
|
|
37
|
+
const parsed = "bytesParsed" in error ? error.bytesParsed : void 0;
|
|
38
|
+
if (Buffer.isBuffer(raw) && typeof parsed === "number") {
|
|
39
|
+
const eol = raw.indexOf("\r\n");
|
|
40
|
+
if (eol === -1 || eol >= parsed) return "414 URI Too Long";
|
|
41
|
+
}
|
|
42
|
+
return "431 Request Header Fields Too Large";
|
|
19
43
|
}
|
|
20
44
|
/**
|
|
21
45
|
* Gère l'event `sessionError` d'un serveur HTTP/2 (h2c ou TLS) : un pair PARTI
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { asServersConfig, configuredServerPort, isH2cEnabled } from "../../src/servers/kernelServers.js";
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
3
|
-
import { handleSessionError } from "./clientError.js";
|
|
4
1
|
import { createDrainTerminator } from "./serverShutdown.js";
|
|
5
|
-
import {
|
|
6
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
|
|
2
|
+
import { handleSessionError } from "./clientError.js";
|
|
7
3
|
import { http2ServerLimits, setHttp2SessionTimeout } from "../../src/servers/http2Limits.js";
|
|
4
|
+
import { bindWithFallback, buildBindPlan, h2cPortConflict } from "../../src/servers/portBinder.js";
|
|
5
|
+
import { asServersConfig, configuredServerPort, isH2cEnabled } from "../../src/servers/kernelServers.js";
|
|
6
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
|
|
7
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
|
|
8
8
|
import { BootConfigurationError, Inject, Service, nodefonyError } from "nodefony";
|
|
9
9
|
import http2 from "node:http2";
|
|
10
10
|
// nodefony/service/servers/server-h2c.ts
|