@nodefony/http 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +77 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +108 -0
- package/dist/nodefony/command/assetsPublishCommand.js +102 -0
- package/dist/nodefony/command/certificatesCommand.js +47 -0
- package/dist/nodefony/command/networkCommand.js +27 -0
- package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
- package/dist/nodefony/config/config.js +335 -0
- package/dist/nodefony/config/defineModuleConfig.js +93 -0
- package/dist/nodefony/interfaces/IContext.js +1 -0
- package/dist/nodefony/interfaces/ICookie.js +1 -0
- package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
- package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
- package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
- package/dist/nodefony/interfaces/IRequest.js +1 -0
- package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
- package/dist/nodefony/interfaces/IResponse.js +1 -0
- package/dist/nodefony/interfaces/ISession.js +1 -0
- package/dist/nodefony/interfaces/IUpload.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +376 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
- package/dist/nodefony/service/audit-logger.js +159 -0
- package/dist/nodefony/service/certificates.js +545 -0
- package/dist/nodefony/service/error-renderer.js +320 -0
- package/dist/nodefony/service/http-kernel.js +948 -0
- package/dist/nodefony/service/pretty-request-logger.js +72 -0
- package/dist/nodefony/service/request-logger.js +54 -0
- package/dist/nodefony/service/servers/clientError.js +20 -0
- package/dist/nodefony/service/servers/server-http.js +135 -0
- package/dist/nodefony/service/servers/server-https.js +204 -0
- package/dist/nodefony/service/servers/server-static.js +192 -0
- package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
- package/dist/nodefony/service/servers/server-websocket.js +104 -0
- package/dist/nodefony/service/servers/serverShutdown.js +31 -0
- package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
- package/dist/nodefony/service/sessions/sessions-service.js +580 -0
- package/dist/nodefony/service/trace.js +72 -0
- package/dist/nodefony/service/upload/upload-service.js +171 -0
- package/dist/nodefony/src/assets/collectAssets.js +34 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
- package/dist/nodefony/src/context/Context.js +415 -0
- package/dist/nodefony/src/context/domainMatcher.js +88 -0
- package/dist/nodefony/src/context/forwarded.js +185 -0
- package/dist/nodefony/src/context/http/HttpContext.js +309 -0
- package/dist/nodefony/src/context/http/Request.js +543 -0
- package/dist/nodefony/src/context/http/Response.js +368 -0
- package/dist/nodefony/src/context/http/parser.js +188 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
- package/dist/nodefony/src/context/http2/Request.js +29 -0
- package/dist/nodefony/src/context/http2/Response.js +97 -0
- package/dist/nodefony/src/context/metaData.js +47 -0
- package/dist/nodefony/src/context/requestId.js +41 -0
- package/dist/nodefony/src/context/trustProxy.js +167 -0
- package/dist/nodefony/src/context/websocket/Response.js +181 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
- package/dist/nodefony/src/cookies/cookie.js +258 -0
- package/dist/nodefony/src/errors/httpError.js +69 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
- package/dist/nodefony/src/profiler/Profiler.js +139 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
- package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
- package/dist/nodefony/src/servers/portBinder.js +114 -0
- package/dist/nodefony/src/session/session.js +390 -0
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
- package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
- package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
- package/dist/types/index.d.ts +83 -0
- package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
- package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
- package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
- package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
- package/dist/types/nodefony/config/config.d.ts +197 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
- package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
- package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
- package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
- package/dist/types/nodefony/service/certificates.d.ts +246 -0
- package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
- package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
- package/dist/types/nodefony/service/request-logger.d.ts +18 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
- package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
- package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
- package/dist/types/nodefony/service/trace.d.ts +39 -0
- package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
- package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
- package/dist/types/nodefony/src/context/Context.d.ts +195 -0
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
- package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
- package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
- package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
- package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
- package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
- package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
- package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
- package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
- package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
- package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
- package/dist/types/nodefony/src/session/session.d.ts +171 -0
- package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
- package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
- package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
- package/docs/cookies.md +365 -0
- package/docs/index.md +163 -0
- package/docs/observabilite.md +460 -0
- package/docs/rate-limit.md +372 -0
- package/docs/servers.md +935 -0
- package/docs/session.md +768 -0
- package/docs/upload.md +460 -0
- package/package.json +101 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { buildMetaData } from "./metaData.js";
|
|
2
|
+
import Cookie, { cookiesParser } from "../cookies/cookie.js";
|
|
3
|
+
import { RequestContext, Service, logColor } from "nodefony";
|
|
4
|
+
import { randomFillSync, randomUUID } from "node:crypto";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { AsyncResource } from "node:async_hooks";
|
|
7
|
+
//#region nodefony/src/context/Context.ts
|
|
8
|
+
const colorLogEvent = () => logColor.cyanBgBlack("EVENT CONTEXT");
|
|
9
|
+
const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
|
|
10
|
+
const EVENT_SEVERITY = {
|
|
11
|
+
onRequest: "INFO",
|
|
12
|
+
onSend: "INFO",
|
|
13
|
+
onConnect: "INFO",
|
|
14
|
+
onClose: "INFO"
|
|
15
|
+
};
|
|
16
|
+
let lifecycleEventLogging = null;
|
|
17
|
+
/**
|
|
18
|
+
* Bascule runtime du gate des logs d'events lifecycle. Réservé HORS hot path :
|
|
19
|
+
* tests et futur « Audit à chaud » (fenêtre bornée prod→verbeux, auto-revert
|
|
20
|
+
* serveur) — le canal Studio `nodefony:syslog` retrouve alors les events sans
|
|
21
|
+
* redémarrage. `null` → re-résolution depuis l'env au prochain event.
|
|
22
|
+
*/
|
|
23
|
+
function setLifecycleEventLogging(value) {
|
|
24
|
+
lifecycleEventLogging = value;
|
|
25
|
+
}
|
|
26
|
+
const EMPTY_PHASES = Object.freeze([]);
|
|
27
|
+
const NONCE_POOL_SIZE = 4096;
|
|
28
|
+
const NONCE_BYTES = 16;
|
|
29
|
+
const noncePool = Buffer.allocUnsafe(NONCE_POOL_SIZE);
|
|
30
|
+
let noncePoolOffset = NONCE_POOL_SIZE;
|
|
31
|
+
/**
|
|
32
|
+
* Rend le prochain nonce CSP (16 octets CSPRNG, base64) depuis le pool amorti.
|
|
33
|
+
*
|
|
34
|
+
* Exporté UNIQUEMENT pour le banc d'épuisement du pool (unicité à travers
|
|
35
|
+
* plusieurs refills) — le code produit passe par `context.cspNonce`.
|
|
36
|
+
*/
|
|
37
|
+
function nextCspNonce() {
|
|
38
|
+
if (noncePoolOffset + NONCE_BYTES > NONCE_POOL_SIZE) {
|
|
39
|
+
randomFillSync(noncePool);
|
|
40
|
+
noncePoolOffset = 0;
|
|
41
|
+
}
|
|
42
|
+
const nonce = noncePool.toString("base64", noncePoolOffset, noncePoolOffset + NONCE_BYTES);
|
|
43
|
+
noncePoolOffset += NONCE_BYTES;
|
|
44
|
+
return nonce;
|
|
45
|
+
}
|
|
46
|
+
var Context = class extends Service {
|
|
47
|
+
secure = false;
|
|
48
|
+
security = null;
|
|
49
|
+
cleaned = false;
|
|
50
|
+
isControlledAccess = false;
|
|
51
|
+
validDomain = false;
|
|
52
|
+
finished = false;
|
|
53
|
+
pushAllowed = false;
|
|
54
|
+
requestEnded = false;
|
|
55
|
+
requested = false;
|
|
56
|
+
sessionStarting = false;
|
|
57
|
+
domain = "";
|
|
58
|
+
type;
|
|
59
|
+
server;
|
|
60
|
+
httpKernel;
|
|
61
|
+
request = null;
|
|
62
|
+
response = null;
|
|
63
|
+
url = "";
|
|
64
|
+
method = null;
|
|
65
|
+
remoteAddress = null;
|
|
66
|
+
_originUrl = null;
|
|
67
|
+
get originUrl() {
|
|
68
|
+
return this._originUrl;
|
|
69
|
+
}
|
|
70
|
+
set originUrl(value) {
|
|
71
|
+
this._originUrl = value;
|
|
72
|
+
}
|
|
73
|
+
cookies = {};
|
|
74
|
+
error = null;
|
|
75
|
+
sessionService;
|
|
76
|
+
session = null;
|
|
77
|
+
cookieSession = null;
|
|
78
|
+
user = null;
|
|
79
|
+
waitAsync = false;
|
|
80
|
+
isJson = false;
|
|
81
|
+
_isHtml = null;
|
|
82
|
+
get isHtml() {
|
|
83
|
+
return this._isHtml ?? (this._isHtml = this.resolveIsHtml());
|
|
84
|
+
}
|
|
85
|
+
set isHtml(value) {
|
|
86
|
+
this._isHtml = value;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Défaut paresseux d'{@link isHtml} — overridé par HttpContext (négociation
|
|
90
|
+
* `Accept`). Base et WS : jamais HTML.
|
|
91
|
+
*/
|
|
92
|
+
resolveIsHtml() {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
crossDomain = false;
|
|
96
|
+
router = this.get("router");
|
|
97
|
+
resolver = null;
|
|
98
|
+
/**
|
|
99
|
+
* Intent de session de la route courante (posé par le Resolver depuis
|
|
100
|
+
* `@UseSession` / paramètre `@Session`). Pilote le point d'activation unique
|
|
101
|
+
* (HTTP + WS). `null` = aucune session sauf cookie existant (reprise L1).
|
|
102
|
+
*/
|
|
103
|
+
sessionIntent = null;
|
|
104
|
+
/**
|
|
105
|
+
* Directives CSP additionnelles de la route courante (`@Csp`), posées par le
|
|
106
|
+
* Resolver au match. `null` = la route n'en déclare pas (cas courant) → le
|
|
107
|
+
* firewall compose le CSP standard (0 surcoût). Lu par `applySecurityHeaders`
|
|
108
|
+
* APRÈS le resolve. Forme `directive → sources` (compatible `CspFragment`).
|
|
109
|
+
*/
|
|
110
|
+
cspDirectives = null;
|
|
111
|
+
/**
|
|
112
|
+
* CSRF per-route (`@CsrfProtect`/`@CsrfExempt`), posés par le Resolver au match,
|
|
113
|
+
* lus par `Firewall.enforceCsrf` (post-resolve). `csrfProtect` exige le
|
|
114
|
+
* synchronizer token sur la mutation ; `csrfExempt` sort la route de la défense
|
|
115
|
+
* CSRF (auth conservée). `csrfToken` = token émis par le firewall (cookie lisible
|
|
116
|
+
* + ce champ) sur une requête sûre vers une route protégée, à surfacer côté vue.
|
|
117
|
+
*/
|
|
118
|
+
csrfProtect = false;
|
|
119
|
+
csrfExempt = false;
|
|
120
|
+
csrfToken = null;
|
|
121
|
+
requestId = randomUUID();
|
|
122
|
+
#cspNonce = null;
|
|
123
|
+
get cspNonce() {
|
|
124
|
+
return this.#cspNonce ??= nextCspNonce();
|
|
125
|
+
}
|
|
126
|
+
traceparent = null;
|
|
127
|
+
profilerQueries = null;
|
|
128
|
+
profiling = false;
|
|
129
|
+
securityTrace = null;
|
|
130
|
+
_timingEnabled = false;
|
|
131
|
+
_timingVerbose = false;
|
|
132
|
+
phases = EMPTY_PHASES;
|
|
133
|
+
_phaseIndex = null;
|
|
134
|
+
_afterResponseFns = null;
|
|
135
|
+
_afterResponseFired = false;
|
|
136
|
+
_abortController = null;
|
|
137
|
+
metaData = {
|
|
138
|
+
nodefony: {},
|
|
139
|
+
result: null
|
|
140
|
+
};
|
|
141
|
+
scheme;
|
|
142
|
+
webSocketState = null;
|
|
143
|
+
constructor(container, type) {
|
|
144
|
+
super(`${type}`, container);
|
|
145
|
+
if (PERF_PROBE_SUB) {
|
|
146
|
+
const p = globalThis.__nfPerfProbe;
|
|
147
|
+
if (p && p.t0 !== 0n) p.svcNs += Number(process.hrtime.bigint() - p.t0);
|
|
148
|
+
}
|
|
149
|
+
this.type = type;
|
|
150
|
+
this.set("context", this);
|
|
151
|
+
this.httpKernel = this.get("HttpKernel");
|
|
152
|
+
this.sessionService = this.get("sessions");
|
|
153
|
+
this.setMetaData();
|
|
154
|
+
const explicit = (this.kernel?.options)?.timing?.enabled;
|
|
155
|
+
if (typeof explicit === "boolean") this._timingEnabled = explicit;
|
|
156
|
+
else this._timingEnabled = this.kernel?.environment !== "production";
|
|
157
|
+
if (this._timingEnabled) {
|
|
158
|
+
this.phases = [];
|
|
159
|
+
const verbose = (this.kernel?.options)?.timing?.verbose;
|
|
160
|
+
this._timingVerbose = verbose === true;
|
|
161
|
+
}
|
|
162
|
+
this.scheme = "https";
|
|
163
|
+
switch (this.type) {
|
|
164
|
+
case "http": {
|
|
165
|
+
this.scheme = "http";
|
|
166
|
+
const server = this.get("server-http");
|
|
167
|
+
this.server = server?.server;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "http2":
|
|
171
|
+
case "https": {
|
|
172
|
+
this.scheme = "https";
|
|
173
|
+
const server = this.get("server-https");
|
|
174
|
+
this.server = server?.server;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "http3": {
|
|
178
|
+
this.scheme = "https";
|
|
179
|
+
const server = this.get("server-http3");
|
|
180
|
+
this.server = server?.server ?? null;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case "websocket": {
|
|
184
|
+
this.scheme = "ws";
|
|
185
|
+
const server = this.get("server-websocket");
|
|
186
|
+
this.server = server?.server;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "websocket-secure": {
|
|
190
|
+
this.scheme = "wss";
|
|
191
|
+
const server = this.get("server-websocket-secure");
|
|
192
|
+
this.server = server?.server;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (PERF_PROBE_SUB) {
|
|
197
|
+
const p = globalThis.__nfPerfProbe;
|
|
198
|
+
if (p && p.t0 !== 0n) p.ctxBaseNs += Number(process.hrtime.bigint() - p.t0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Assemble l'enveloppe `metaData` de la requête (`nodefony.*` + `route`
|
|
203
|
+
* snapshot per-requête + overrides appelant). Délègue au builder monomorphe
|
|
204
|
+
* pur {@link buildMetaData} — pas de `extend(true, …)` (deep-clone + dispatch
|
|
205
|
+
* polymorphe inutiles : `this.metaData` est per-requête, jamais partagé). `this`
|
|
206
|
+
* satisfait structurellement {@link IMetaDataSource} (kernel/request/scheme/
|
|
207
|
+
* requestId/resolver) → zéro alloc d'objet intermédiaire.
|
|
208
|
+
*/
|
|
209
|
+
setMetaData(obj = {}) {
|
|
210
|
+
return buildMetaData(this.metaData, this, obj);
|
|
211
|
+
}
|
|
212
|
+
setScheme() {
|
|
213
|
+
return "https";
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Le timing est-il actif sur CETTE requête ? (dev/test par défaut, opt-out en
|
|
217
|
+
* production — cf constructeur.)
|
|
218
|
+
*
|
|
219
|
+
* Exposé pour que les appelants qui devraient **payer** pour mesurer (un
|
|
220
|
+
* `try/finally` autour d'une chaîne de promesses = une microtask de plus par
|
|
221
|
+
* requête) puissent garder leur chemin nominal INTACT quand la mesure est
|
|
222
|
+
* éteinte : `if (!ctx.timingEnabled) return fastPath()`. Les autres n'ont rien
|
|
223
|
+
* à tester — {@link phaseStart}/{@link phaseEnd} sont déjà des no-ops.
|
|
224
|
+
*/
|
|
225
|
+
get timingEnabled() {
|
|
226
|
+
return this._timingEnabled;
|
|
227
|
+
}
|
|
228
|
+
phaseStart(name) {
|
|
229
|
+
if (!this._timingEnabled) return;
|
|
230
|
+
if (this._phaseIndex === null) this._phaseIndex = /* @__PURE__ */ new Map();
|
|
231
|
+
const idx = this.phases.length;
|
|
232
|
+
this.phases.push({
|
|
233
|
+
name,
|
|
234
|
+
startMs: performance.now()
|
|
235
|
+
});
|
|
236
|
+
this._phaseIndex.set(name, idx);
|
|
237
|
+
}
|
|
238
|
+
phaseEnd(name) {
|
|
239
|
+
if (!this._timingEnabled || this._phaseIndex === null) return;
|
|
240
|
+
const idx = this._phaseIndex.get(name);
|
|
241
|
+
if (idx === void 0) return;
|
|
242
|
+
const p = this.phases[idx];
|
|
243
|
+
if (p.endMs !== void 0) return;
|
|
244
|
+
p.endMs = performance.now();
|
|
245
|
+
p.durationMs = p.endMs - p.startMs;
|
|
246
|
+
}
|
|
247
|
+
onAfterResponse(fn) {
|
|
248
|
+
const boundFn = AsyncResource.bind(fn);
|
|
249
|
+
if (this._afterResponseFired) {
|
|
250
|
+
Promise.resolve().then(() => boundFn(this)).catch((e) => this.log(e, "ERROR", "onAfterResponse(late)"));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (this._afterResponseFns === null) this._afterResponseFns = [];
|
|
254
|
+
this._afterResponseFns.push(boundFn);
|
|
255
|
+
}
|
|
256
|
+
async _runAfterResponse() {
|
|
257
|
+
if (this._afterResponseFired) return;
|
|
258
|
+
this._afterResponseFired = true;
|
|
259
|
+
const fns = this._afterResponseFns;
|
|
260
|
+
if (fns === null || fns.length === 0) return;
|
|
261
|
+
this._afterResponseFns = null;
|
|
262
|
+
for (const fn of fns) try {
|
|
263
|
+
await fn(this);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
this.log(e, "ERROR", "onAfterResponse");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
get signal() {
|
|
269
|
+
if (this._abortController === null) {
|
|
270
|
+
this._abortController = new AbortController();
|
|
271
|
+
const req = this.request;
|
|
272
|
+
if (req && typeof req.once === "function") {
|
|
273
|
+
const onAborted = () => {
|
|
274
|
+
if (this._abortController && !this._abortController.signal.aborted && req.complete === false) this._abortController.abort(/* @__PURE__ */ new Error("Request aborted by client"));
|
|
275
|
+
};
|
|
276
|
+
if (req.destroyed && req.complete === false) onAborted();
|
|
277
|
+
else req.once("close", onAborted);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return this._abortController.signal;
|
|
281
|
+
}
|
|
282
|
+
_abortIfPending(reason) {
|
|
283
|
+
if (this._abortController && !this._abortController.signal.aborted) this._abortController.abort(new Error(reason ?? "Context aborted"));
|
|
284
|
+
}
|
|
285
|
+
log(pci, severity, msgid, msg) {
|
|
286
|
+
if (!msgid) msgid = this.type;
|
|
287
|
+
if (this.requestId && RequestContext.getRequestId() === void 0) return RequestContext.run({ requestId: this.requestId }, () => super.log(pci, severity, msgid, msg));
|
|
288
|
+
return super.log(pci, severity, msgid, msg);
|
|
289
|
+
}
|
|
290
|
+
clean() {
|
|
291
|
+
this.cleaned = true;
|
|
292
|
+
this.httpKernel = null;
|
|
293
|
+
return super.clean();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Log d'un event du cycle de vie — gate boot-time (V2.1) : en production le
|
|
297
|
+
* Pdu n'est PAS construit (return avant toute allocation). Hors prod, jalon
|
|
298
|
+
* notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
|
|
299
|
+
*/
|
|
300
|
+
logEvent(event) {
|
|
301
|
+
if (lifecycleEventLogging === null) lifecycleEventLogging = this.kernel?.environment !== "production";
|
|
302
|
+
if (!lifecycleEventLogging) return;
|
|
303
|
+
this.log(`${colorLogEvent()} ${event}`, EVENT_SEVERITY[event] || "DEBUG");
|
|
304
|
+
}
|
|
305
|
+
fire(event, ...args) {
|
|
306
|
+
this.logEvent(event);
|
|
307
|
+
return super.fire(event, ...args);
|
|
308
|
+
}
|
|
309
|
+
emit(event, ...args) {
|
|
310
|
+
this.logEvent(event);
|
|
311
|
+
return super.emit(event, ...args);
|
|
312
|
+
}
|
|
313
|
+
emitAsync(event, ...args) {
|
|
314
|
+
this.logEvent(event);
|
|
315
|
+
return super.emitAsync(event, ...args);
|
|
316
|
+
}
|
|
317
|
+
fireAsync(event, ...args) {
|
|
318
|
+
this.logEvent(event);
|
|
319
|
+
return super.emitAsync(event, ...args);
|
|
320
|
+
}
|
|
321
|
+
logRequest(httpError) {
|
|
322
|
+
try {
|
|
323
|
+
const err = httpError ?? this.error ?? void 0;
|
|
324
|
+
if (err) this.error = err;
|
|
325
|
+
const logger = this.httpKernel?.getRequestLogger();
|
|
326
|
+
if (!logger) return;
|
|
327
|
+
if (logger.shouldSample && !logger.shouldSample(this, err)) return;
|
|
328
|
+
const entry = logger.renderHttp(this, err);
|
|
329
|
+
return this.log(entry.text, entry.severity, entry.msgid);
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* P3.7 — Émet au teardown un log DEBUG détaillant la durée de chaque phase du
|
|
334
|
+
* pipeline (`parse · action · firewall · …` + total). Opt-in via
|
|
335
|
+
* `kernel.options.timing.verbose`. Triple gate perf-first : `_timingVerbose`
|
|
336
|
+
* résolu 1× (false en prod/par défaut) → timing actif → phases non vides ;
|
|
337
|
+
* hors mode verbose, early-return AVANT toute allocation/`toFixed` (coût nul).
|
|
338
|
+
*/
|
|
339
|
+
logPhasesVerbose() {
|
|
340
|
+
if (!this._timingVerbose) return;
|
|
341
|
+
const phases = this.phases;
|
|
342
|
+
const n = phases.length;
|
|
343
|
+
if (n === 0) return;
|
|
344
|
+
let line = "";
|
|
345
|
+
let total = 0;
|
|
346
|
+
for (let i = 0; i < n; i++) {
|
|
347
|
+
const p = phases[i];
|
|
348
|
+
const d = p.durationMs ?? (p.endMs !== void 0 ? p.endMs - p.startMs : void 0);
|
|
349
|
+
if (i > 0) line += " · ";
|
|
350
|
+
if (d !== void 0) {
|
|
351
|
+
line += `${p.name}=${d.toFixed(2)}ms`;
|
|
352
|
+
total += d;
|
|
353
|
+
} else line += `${p.name}=…`;
|
|
354
|
+
}
|
|
355
|
+
this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.type} TIMING`);
|
|
356
|
+
}
|
|
357
|
+
addRequestCookie(cookie) {
|
|
358
|
+
if (cookie instanceof Cookie) return this.cookies[cookie.name] = cookie;
|
|
359
|
+
else {
|
|
360
|
+
const error = /* @__PURE__ */ new Error("addCookie cookie not valid !!");
|
|
361
|
+
this.log(cookie, "ERROR");
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
getRequestCookies(name) {
|
|
366
|
+
if (name) return this.cookies[name] || null;
|
|
367
|
+
return this.cookies;
|
|
368
|
+
}
|
|
369
|
+
setCookie(cookie) {
|
|
370
|
+
if (cookie) return this.response?.addCookie(cookie);
|
|
371
|
+
}
|
|
372
|
+
getRequest() {
|
|
373
|
+
return this.request;
|
|
374
|
+
}
|
|
375
|
+
getResponse() {
|
|
376
|
+
return this.response;
|
|
377
|
+
}
|
|
378
|
+
isValidDomain() {
|
|
379
|
+
if (!this.httpKernel) throw new Error(`Http Kernel not ready`);
|
|
380
|
+
return this.httpKernel.isValidDomain(this);
|
|
381
|
+
}
|
|
382
|
+
async saveSession() {
|
|
383
|
+
if (this.sessionService) return this.sessionService.saveSession(this);
|
|
384
|
+
throw new Error(`sessionService not found `);
|
|
385
|
+
}
|
|
386
|
+
hasSession() {
|
|
387
|
+
return Boolean(this.cookieSession);
|
|
388
|
+
}
|
|
389
|
+
getCookieSession(name) {
|
|
390
|
+
if (this.cookies[name]) return this.cookies[name];
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Nom effectif du cookie de session pour CE transport. Sur TLS (https/wss) on
|
|
395
|
+
* applique le préfixe **`__Host-`** (RFC 6265bis §4.1.3 / OWASP : recommandé
|
|
396
|
+
* pour les identifiants de session — impose Secure + Path=/ + interdit Domain,
|
|
397
|
+
* anti session-fixation cross-subdomain). En clair (http/ws) le préfixe est
|
|
398
|
+
* omis (le navigateur le rejetterait sans Secure) → dégradation gracieuse,
|
|
399
|
+
* notamment derrière un proxy qui termine le TLS. Lecture **et** écriture du
|
|
400
|
+
* cookie passent par ce nom unique → cohérence de la reprise (L1).
|
|
401
|
+
*/
|
|
402
|
+
getSessionCookieName() {
|
|
403
|
+
const base = this.sessionService?.defaultSessionName ?? "nodefony";
|
|
404
|
+
const mode = (this.sessionService?.options?.cookie)?.hostPrefix ?? "auto";
|
|
405
|
+
const tls = this.scheme === "https" || this.scheme === "wss";
|
|
406
|
+
return mode === true || mode === "auto" && tls ? `__Host-${base}` : base;
|
|
407
|
+
}
|
|
408
|
+
parseCookies() {
|
|
409
|
+
return cookiesParser(this);
|
|
410
|
+
}
|
|
411
|
+
setContextJson(_encoding = "utf-8") {}
|
|
412
|
+
setContextHtml(_encoding = "utf-8") {}
|
|
413
|
+
};
|
|
414
|
+
//#endregion
|
|
415
|
+
export { Context as default, nextCspNonce, setLifecycleEventLogging };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { escapeRegExp } from "nodefony";
|
|
2
|
+
//#region nodefony/src/context/domainMatcher.ts
|
|
3
|
+
/**
|
|
4
|
+
* Matching de domaine (`Host`) — fonctions PURES, politique UNIQUE partagée par
|
|
5
|
+
* les deux étages de Nodefony :
|
|
6
|
+
*
|
|
7
|
+
* - **`trustedHosts` (kernel, sécu)** : barrière testée AVANT le routing. Quels
|
|
8
|
+
* `Host` ce serveur accepte de traiter (anti Host-header injection). Host non
|
|
9
|
+
* trusté → 401 (`HttpKernel.checkValidDomain`). Grossier, optionnel (en prod,
|
|
10
|
+
* typiquement délégué au reverse-proxy via `trustedHosts: true`).
|
|
11
|
+
* - **`@Domain` (route, routing)** : restreint une route / un contrôleur à un
|
|
12
|
+
* vhost. Source de vérité du « qui sert quoi ». Testé PENDANT le routing
|
|
13
|
+
* (`Route.matchHostname`) → 403 si la route ne sert pas ce domaine.
|
|
14
|
+
*
|
|
15
|
+
* Politique de compilation (sûre, ancrée, ReDoS-free) :
|
|
16
|
+
* - string sans `*` → match EXACT ancré (`^...$`, le `.` est littéral).
|
|
17
|
+
* - string avec `*` → wildcard UN label (`*.example.com` → `^[^.]+\.example\.com$`,
|
|
18
|
+
* RFC 6125 TLS-wildcard : matche `img.example.com`, pas `a.b.example.com` ni `example.com`).
|
|
19
|
+
* - `RegExp` → reprise telle quelle (l'auteur assume l'ancrage).
|
|
20
|
+
*
|
|
21
|
+
* Compilation faite UNE fois (boot / enregistrement de route) ; le test par
|
|
22
|
+
* requête est un simple `RegExp.test` sur une liste pré-compilée (zéro alloc hot-path).
|
|
23
|
+
*/
|
|
24
|
+
const DEV_LOOPBACK = [
|
|
25
|
+
"localhost",
|
|
26
|
+
"127.0.0.1",
|
|
27
|
+
"[::1]"
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Compile UN pattern de domaine en `RegExp` selon la politique sûre.
|
|
31
|
+
*
|
|
32
|
+
* @param pattern - string (exact / `*`-wildcard) ou `RegExp` (reprise telle quelle).
|
|
33
|
+
* @returns une `RegExp` ancrée (pour les string) ou le `RegExp` fourni.
|
|
34
|
+
*/
|
|
35
|
+
function compileDomainPattern(pattern) {
|
|
36
|
+
if (pattern instanceof RegExp) return pattern;
|
|
37
|
+
const body = pattern.split("*").map(escapeRegExp).join("[^.]+");
|
|
38
|
+
return new RegExp(`^${body}$`, "u");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Compile une liste de patterns. Les string vides sont ignorées (un `^$` ne
|
|
42
|
+
* sert à rien et masque une coquille de config).
|
|
43
|
+
*
|
|
44
|
+
* @param patterns - un pattern ou un tableau de patterns.
|
|
45
|
+
* @returns liste de `RegExp` à tester contre le `Host` entrant.
|
|
46
|
+
*/
|
|
47
|
+
function compileDomainPatterns(patterns) {
|
|
48
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const p of list) if (p instanceof RegExp) out.push(p);
|
|
51
|
+
else if (typeof p === "string" && p) out.push(compileDomainPattern(p));
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Construit la liste `RegExp` de la barrière `trustedHosts` (kernel, avant routing).
|
|
56
|
+
*
|
|
57
|
+
* Toujours : le domaine canonique (`kernel.domain`). En development : + loopback
|
|
58
|
+
* (`localhost`/`127.0.0.1`/`[::1]`) pour que l'URL tapée (nom OU IP) passe.
|
|
59
|
+
* `trustedHosts: true` → bypass total (un seul `/^.*$/`).
|
|
60
|
+
*
|
|
61
|
+
* @param domain - domaine canonique du serveur (`kernel.domain`).
|
|
62
|
+
* @param trusted - config `http.trustedHosts` (optionnelle).
|
|
63
|
+
* @param isDev - vrai en environnement `development` (ajoute le loopback).
|
|
64
|
+
* @returns liste de `RegExp` pour {@link isDomainAllowed}.
|
|
65
|
+
*/
|
|
66
|
+
function compileTrustedHosts(domain, trusted, isDev) {
|
|
67
|
+
if (trusted === true) return [/^.*$/u];
|
|
68
|
+
const patterns = [domain];
|
|
69
|
+
if (isDev) patterns.push(...DEV_LOOPBACK);
|
|
70
|
+
if (trusted) {
|
|
71
|
+
if (Array.isArray(trusted)) patterns.push(...trusted);
|
|
72
|
+
else patterns.push(trusted);
|
|
73
|
+
}
|
|
74
|
+
return compileDomainPatterns(patterns);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Teste un `Host` entrant contre une liste de `RegExp` pré-compilée.
|
|
78
|
+
*
|
|
79
|
+
* @param regAlias - sortie de {@link compileTrustedHosts} ou {@link compileDomainPatterns}.
|
|
80
|
+
* @param domain - `Host` de la requête (`context.domain`, port déjà strippé).
|
|
81
|
+
* @returns `true` dès le premier match (court-circuit), `false` sinon.
|
|
82
|
+
*/
|
|
83
|
+
function isDomainAllowed(regAlias, domain) {
|
|
84
|
+
for (const reg of regAlias) if (reg.test(domain)) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { compileDomainPattern, compileDomainPatterns, compileTrustedHosts, isDomainAllowed };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { extractClientIp, resolveFromRight } from "./trustProxy.js";
|
|
2
|
+
import { isIPv4 } from "node:net";
|
|
3
|
+
//#region nodefony/src/context/forwarded.ts
|
|
4
|
+
/**
|
|
5
|
+
* Découpe une chaîne sur `sep` au **niveau supérieur uniquement**, en respectant
|
|
6
|
+
* les `quoted-string` RFC 7230 (`value = token / quoted-string`, §4) : un
|
|
7
|
+
* séparateur à l'intérieur de guillemets (ex. un host quoté, un IPv6) n'est PAS
|
|
8
|
+
* un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
|
|
9
|
+
*/
|
|
10
|
+
function splitTopLevel(input, sep) {
|
|
11
|
+
if (input.indexOf("\"") === -1) return input.split(sep);
|
|
12
|
+
const out = [];
|
|
13
|
+
let buf = "";
|
|
14
|
+
let inQuote = false;
|
|
15
|
+
let escaped = false;
|
|
16
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
17
|
+
const c = input[i];
|
|
18
|
+
if (escaped) {
|
|
19
|
+
buf += c;
|
|
20
|
+
escaped = false;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (c === "\\" && inQuote) {
|
|
24
|
+
buf += c;
|
|
25
|
+
escaped = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (c === "\"") {
|
|
29
|
+
inQuote = !inQuote;
|
|
30
|
+
buf += c;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (c === sep && !inQuote) {
|
|
34
|
+
out.push(buf);
|
|
35
|
+
buf = "";
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
buf += c;
|
|
39
|
+
}
|
|
40
|
+
out.push(buf);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
|
|
44
|
+
function unquote(value) {
|
|
45
|
+
const v = value.trim();
|
|
46
|
+
if (v.length >= 2 && v[0] === "\"" && v[v.length - 1] === "\"") return v.slice(1, -1).replace(/\\(.)/gu, "$1");
|
|
47
|
+
return v;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse le header `Forwarded` (RFC 7239 §4) en liste de `forwarded-element`,
|
|
51
|
+
* dans l'ordre **gauche→droite** (le plus à gauche = le plus proche du client).
|
|
52
|
+
*
|
|
53
|
+
* Grammaire honorée : `Forwarded = 1#forwarded-element`,
|
|
54
|
+
* `forwarded-element = [ forwarded-pair ] *( ";" [ forwarded-pair ] )`,
|
|
55
|
+
* `forwarded-pair = token "=" value`, `value = token / quoted-string`.
|
|
56
|
+
* Les noms de paramètres sont insensibles à la casse (§7.1) → normalisés.
|
|
57
|
+
*
|
|
58
|
+
* @param raw - valeur brute du header (string, ou string[] si répété), ou `undefined`.
|
|
59
|
+
* @returns les éléments parsés, ou `null` si le header est absent/vide.
|
|
60
|
+
*/
|
|
61
|
+
function parseForwarded(raw) {
|
|
62
|
+
if (!raw) return null;
|
|
63
|
+
const header = Array.isArray(raw) ? raw.join(",") : raw;
|
|
64
|
+
if (!header.trim()) return null;
|
|
65
|
+
const elements = [];
|
|
66
|
+
for (const part of splitTopLevel(header, ",")) {
|
|
67
|
+
if (!part.trim()) continue;
|
|
68
|
+
const element = {};
|
|
69
|
+
for (const pair of splitTopLevel(part, ";")) {
|
|
70
|
+
const eq = pair.indexOf("=");
|
|
71
|
+
if (eq === -1) continue;
|
|
72
|
+
const key = pair.slice(0, eq).trim().toLowerCase();
|
|
73
|
+
const rawValue = pair.slice(eq + 1);
|
|
74
|
+
switch (key) {
|
|
75
|
+
case "by":
|
|
76
|
+
element.by = unquote(rawValue);
|
|
77
|
+
break;
|
|
78
|
+
case "for":
|
|
79
|
+
element.for = unquote(rawValue);
|
|
80
|
+
break;
|
|
81
|
+
case "host":
|
|
82
|
+
element.host = unquote(rawValue);
|
|
83
|
+
break;
|
|
84
|
+
case "proto": element.proto = unquote(rawValue).toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
elements.push(element);
|
|
88
|
+
}
|
|
89
|
+
return elements.length ? elements : null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extrait l'**IP nue** d'un node identifier RFC 7239 §6
|
|
93
|
+
* (`node = nodename [ ":" node-port ]`).
|
|
94
|
+
*
|
|
95
|
+
* - IPv6 entre crochets (`[2001:db8::1]`, éventuel `]:port`) → l'IPv6 sans crochets.
|
|
96
|
+
* - IPv4 (`192.0.2.43`, éventuel `:port`) → l'IPv4 sans le port.
|
|
97
|
+
* - `unknown` et identifiants obfusqués (`obfnode` commençant par `_`, §6.3) →
|
|
98
|
+
* `null` : non comparables à une politique de confiance (barrière from-right).
|
|
99
|
+
*
|
|
100
|
+
* @param node - valeur d'un paramètre `for`/`by`, déjà déquotée.
|
|
101
|
+
* @returns l'IP exploitable, ou `null` si absente/obfusquée/illisible.
|
|
102
|
+
*/
|
|
103
|
+
function forwardedNodeIp(node) {
|
|
104
|
+
if (!node) return null;
|
|
105
|
+
const n = node.trim();
|
|
106
|
+
if (!n) return null;
|
|
107
|
+
if (n[0] === "[") {
|
|
108
|
+
const end = n.indexOf("]");
|
|
109
|
+
return end > 1 ? n.slice(1, end) : null;
|
|
110
|
+
}
|
|
111
|
+
if (n === "unknown" || n[0] === "_") return null;
|
|
112
|
+
const colon = n.indexOf(":");
|
|
113
|
+
const host = colon === -1 ? n : n.slice(0, colon);
|
|
114
|
+
return isIPv4(host) ? host : host || null;
|
|
115
|
+
}
|
|
116
|
+
/** Premier élément (le plus à gauche = côté client) définissant `key`. */
|
|
117
|
+
function firstDefined(elements, key) {
|
|
118
|
+
for (const element of elements) {
|
|
119
|
+
const value = element[key];
|
|
120
|
+
if (value !== void 0) return value;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Premier token d'un en-tête `X-Forwarded-*` multi-valeurs (`"a, b"` → `"a"`).
|
|
125
|
+
* `indexOf` + `slice` (pas de `split`) → 0 array alloué (hot path prod).
|
|
126
|
+
*/
|
|
127
|
+
function firstToken(value, lower = false) {
|
|
128
|
+
if (!value) return;
|
|
129
|
+
const s = Array.isArray(value) ? value[0] : value;
|
|
130
|
+
if (!s) return;
|
|
131
|
+
const comma = s.indexOf(",");
|
|
132
|
+
const first = (comma === -1 ? s : s.slice(0, comma)).trim();
|
|
133
|
+
if (!first) return;
|
|
134
|
+
return lower ? first.toLowerCase() : first;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Résout, en **une seule passe**, le scheme, le host et l'IP cliente effectifs à
|
|
138
|
+
* partir des en-têtes forwarded — header standard `Forwarded` (RFC 7239)
|
|
139
|
+
* **prioritaire**, repli sur les `X-Forwarded-*` de-facto.
|
|
140
|
+
*
|
|
141
|
+
* Le scheme/host effectifs sont pris sur l'élément le plus à GAUCHE (côté client) ;
|
|
142
|
+
* l'IP cliente est résolue **from-right** ({@link resolveFromRight}) en partant du
|
|
143
|
+
* socket réel, donc non spoofable. La priorité unique `Forwarded.proto` >
|
|
144
|
+
* `x-forwarded-proto` > `x-forwarded-scheme` supprime l'ancienne double source.
|
|
145
|
+
*
|
|
146
|
+
* À n'appeler que derrière un proxy de confiance (RFC 7239 §8.1) : la décision
|
|
147
|
+
* `isTrusted(socket)` est prise par l'appelant (lazy : pas d'allocation hors proxy).
|
|
148
|
+
*
|
|
149
|
+
* @param headers - en-têtes de la requête.
|
|
150
|
+
* @param socketAddress - `socket.remoteAddress` (connexion TCP réelle).
|
|
151
|
+
* @param checker - politique de confiance ({@link buildTrustProxy}).
|
|
152
|
+
* @returns la résolution canonique unifiée.
|
|
153
|
+
*/
|
|
154
|
+
function resolveForwarded(headers, socketAddress, checker) {
|
|
155
|
+
const elements = parseForwarded(headers.forwarded);
|
|
156
|
+
if (elements) {
|
|
157
|
+
const chain = elements.map((element) => forwardedNodeIp(element.for));
|
|
158
|
+
const forwardedFor = elements.map((element) => element.for).filter((value) => Boolean(value)).join(", ") || void 0;
|
|
159
|
+
return {
|
|
160
|
+
proto: firstDefined(elements, "proto"),
|
|
161
|
+
host: firstDefined(elements, "host"),
|
|
162
|
+
forwardedFor,
|
|
163
|
+
clientIp: socketAddress ? resolveFromRight(chain, socketAddress, checker) : null,
|
|
164
|
+
fromStandard: true
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const xff = headers["x-forwarded-for"];
|
|
168
|
+
return {
|
|
169
|
+
proto: firstToken(headers["x-forwarded-proto"], true) ?? firstToken(headers["x-forwarded-scheme"], true),
|
|
170
|
+
host: firstToken(headers["x-forwarded-host"]),
|
|
171
|
+
forwardedFor: typeof xff === "string" ? xff : xff?.join(", "),
|
|
172
|
+
clientIp: extractClientIp(xff, socketAddress, checker),
|
|
173
|
+
fromStandard: false
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Au moins un en-tête forwarded (standard ou de-facto) est-il présent ? Permet à
|
|
178
|
+
* l'appelant de n'allouer la résolution que quand c'est utile (hot path : 0 alloc
|
|
179
|
+
* sur une requête directe sans proxy).
|
|
180
|
+
*/
|
|
181
|
+
function hasForwardingHeaders(headers) {
|
|
182
|
+
return Boolean(headers.forwarded || headers["x-forwarded-for"] || headers["x-forwarded-proto"] || headers["x-forwarded-scheme"] || headers["x-forwarded-host"]);
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { forwardedNodeIp, hasForwardingHeaders, parseForwarded, resolveForwarded };
|