@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,68 @@
|
|
|
1
|
+
import http, { OutgoingHttpHeaders } from "node:http";
|
|
2
|
+
import http2 from "node:http2";
|
|
3
|
+
import HttpContext from "../http/HttpContext.js";
|
|
4
|
+
import { Pci, Pdu, Message, Severity, Msgid } from "nodefony";
|
|
5
|
+
import Cookie from "../../cookies/cookie.js";
|
|
6
|
+
declare class HttpResponse {
|
|
7
|
+
#private;
|
|
8
|
+
context: HttpContext;
|
|
9
|
+
response: http.ServerResponse | http2.Http2ServerResponse | null;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
statusMessage: string;
|
|
12
|
+
flushing: boolean;
|
|
13
|
+
encoding: BufferEncoding;
|
|
14
|
+
body: Buffer | null;
|
|
15
|
+
contentType: string;
|
|
16
|
+
headers: http.OutgoingHttpHeaders;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
cookies: Record<string, Cookie>;
|
|
19
|
+
constructor(response: http.ServerResponse | http2.Http2ServerResponse, context: HttpContext);
|
|
20
|
+
/**
|
|
21
|
+
* Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
|
|
22
|
+
* n'a été choisi (ni négociation, ni `render`, ni controller), émet le défaut
|
|
23
|
+
* `this.contentType` (application/octet-stream) — comportement identique à
|
|
24
|
+
* l'ancienne pose au constructeur, sans le ping-pong set/remove/re-set.
|
|
25
|
+
*/
|
|
26
|
+
protected ensureContentTypeHeader(): void;
|
|
27
|
+
clean(): void;
|
|
28
|
+
isHeaderSent(): boolean;
|
|
29
|
+
isHtml(): boolean;
|
|
30
|
+
setTimeout(ms: number): void;
|
|
31
|
+
addCookie(cookie: Cookie): Cookie;
|
|
32
|
+
deleteCookie(cookie: Cookie): boolean;
|
|
33
|
+
deleteCookieByName(name: string): boolean;
|
|
34
|
+
setCookies(): void | http.ServerResponse<http.IncomingMessage>;
|
|
35
|
+
setCookie(cookie: Cookie): void | http.ServerResponse<http.IncomingMessage>;
|
|
36
|
+
setHeader(name: string, value: number | string | readonly string[]): void | http.ServerResponse<http.IncomingMessage>;
|
|
37
|
+
setHeaders(obj: OutgoingHttpHeaders): OutgoingHttpHeaders;
|
|
38
|
+
setContentType(type?: string, encoding?: BufferEncoding): void | http.ServerResponse<http.IncomingMessage>;
|
|
39
|
+
setFileMimeType(type: string, encoding?: BufferEncoding): void | http.ServerResponse<http.IncomingMessage>;
|
|
40
|
+
setContentTypeByExtension(extention: string): void | http.ServerResponse<http.IncomingMessage>;
|
|
41
|
+
getMimeType(filenameOrExt: string): string | false;
|
|
42
|
+
setEncoding(encoding: BufferEncoding): BufferEncoding;
|
|
43
|
+
setStatusCode(status: number | string, message?: string): {
|
|
44
|
+
code: number;
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
getStatus(): {
|
|
48
|
+
code: number;
|
|
49
|
+
message: string;
|
|
50
|
+
};
|
|
51
|
+
getStatusCode(): number;
|
|
52
|
+
getStatusMessage(code?: number | string): string;
|
|
53
|
+
setBody(ele: unknown, encoding?: BufferEncoding | undefined): Buffer;
|
|
54
|
+
setLength(body?: string | NodeJS.ArrayBufferView | ArrayBuffer | SharedArrayBuffer): number;
|
|
55
|
+
writeHead(statusCode?: number, headers?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[]): void;
|
|
56
|
+
addTrailers(headers: http.OutgoingHttpHeaders): void;
|
|
57
|
+
flush(chunk: unknown, encoding: BufferEncoding): Promise<HttpResponse>;
|
|
58
|
+
send(chunk?: unknown, encoding?: BufferEncoding, _flush?: boolean): Promise<HttpResponse>;
|
|
59
|
+
write(chunk?: unknown, encoding?: BufferEncoding): Promise<HttpResponse>;
|
|
60
|
+
writeContinue(): void | undefined;
|
|
61
|
+
end(chunk?: string | Buffer, encoding?: BufferEncoding): Promise<http.ServerResponse | http2.ServerHttp2Stream>;
|
|
62
|
+
getHeader(name: string): string | number | string[] | undefined;
|
|
63
|
+
hasHeader(name: string): boolean;
|
|
64
|
+
getHeaders(): http.OutgoingHttpHeaders;
|
|
65
|
+
redirect(url: string, status?: number | string, headers?: Record<string, string | number>): this;
|
|
66
|
+
log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
|
|
67
|
+
}
|
|
68
|
+
export default HttpResponse;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import HttpRequest from "./Request.js";
|
|
2
|
+
import Http2Request from "../http2/Request.js";
|
|
3
|
+
import QS from "qs";
|
|
4
|
+
import xml2js from "xml2js";
|
|
5
|
+
declare class Parser {
|
|
6
|
+
request: HttpRequest | Http2Request;
|
|
7
|
+
chunks: Buffer[];
|
|
8
|
+
private received;
|
|
9
|
+
private aborted;
|
|
10
|
+
private overflow;
|
|
11
|
+
private onOverflow;
|
|
12
|
+
constructor(request: HttpRequest | Http2Request);
|
|
13
|
+
write(buffer: Buffer): Buffer;
|
|
14
|
+
/**
|
|
15
|
+
* Résout quand le flux requête est **entièrement reçu** (`end`). No-op si déjà
|
|
16
|
+
* terminé (corps vide / déjà drainé) → pas de hang d'un `once("end")` tardif.
|
|
17
|
+
* Indispensable avant de concaténer les chunks pour un corps à décoder
|
|
18
|
+
* (JSON…) : sinon on lit un buffer encore incomplet. (formidable drainait via
|
|
19
|
+
* son `await form.parse()` ; ses remplaçants doivent drainer explicitement.)
|
|
20
|
+
* Rejette en 413 si le corps dépasse `maxBodySize` (avant ou pendant l'attente).
|
|
21
|
+
*/
|
|
22
|
+
protected ended(): Promise<void>;
|
|
23
|
+
parse(): Promise<this>;
|
|
24
|
+
}
|
|
25
|
+
declare class ParserQs extends Parser {
|
|
26
|
+
parserOptions: QS.IParseOptions;
|
|
27
|
+
charset: BufferEncoding;
|
|
28
|
+
constructor(request: HttpRequest | Http2Request);
|
|
29
|
+
parse(): Promise<this>;
|
|
30
|
+
}
|
|
31
|
+
declare class ParserXml extends Parser {
|
|
32
|
+
xmlParser: xml2js.Parser;
|
|
33
|
+
charset: BufferEncoding;
|
|
34
|
+
constructor(request: HttpRequest | Http2Request, settingsXml?: xml2js.ParserOptions);
|
|
35
|
+
parse(): Promise<this>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse un corps `application/json` (ou `*+json`) en objet → `queryPost`
|
|
39
|
+
* (source lue par le décorateur `@Body`). Reprend le rôle de l'ancien plugin
|
|
40
|
+
* `json` de formidable, supprimé avec le passage à busboy (multipart seul).
|
|
41
|
+
* Lenient : un corps JSON malformé est ignoré (pas de throw — `parse()` est
|
|
42
|
+
* invoqué non-awaité par `initialize`, un rejet serait non géré) ; le brut
|
|
43
|
+
* reste disponible dans `request.data`.
|
|
44
|
+
*/
|
|
45
|
+
declare class ParserJson extends Parser {
|
|
46
|
+
charset: BufferEncoding;
|
|
47
|
+
constructor(request: HttpRequest | Http2Request);
|
|
48
|
+
parse(): Promise<this>;
|
|
49
|
+
}
|
|
50
|
+
/** Media-range « accepte tout » (wildcard type + sous-type) — repli sûr, jamais throw. */
|
|
51
|
+
/**
|
|
52
|
+
* Une entrée d'en-tête `Accept` analysée : le type et le sous-type compilés en
|
|
53
|
+
* expressions régulières, plus les paramètres bruts du segment (`q`, `charset`,
|
|
54
|
+
* `level`…). L'index est ouvert parce que le client choisit ces noms — mais ses
|
|
55
|
+
* valeurs, elles, sont connues : une regex pour le type, un nombre pour `q`, une
|
|
56
|
+
* chaîne pour le reste.
|
|
57
|
+
*/
|
|
58
|
+
type AcceptEntry = {
|
|
59
|
+
type: RegExp;
|
|
60
|
+
subtype: RegExp;
|
|
61
|
+
q?: number;
|
|
62
|
+
[key: string]: RegExp | number | string | undefined;
|
|
63
|
+
};
|
|
64
|
+
declare const acceptParser: (acc?: string) => AcceptEntry[];
|
|
65
|
+
export { Parser, ParserXml, ParserQs, ParserJson, acceptParser };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast-path d'analyse du request-target — évite le `new URL` (parse WHATWG
|
|
3
|
+
* complet) sur le chemin nominal, où le target est déjà sous sa forme
|
|
4
|
+
* canonique et où pathname/search s'extraient par simple découpe.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ SÉCURITÉ — le contrat qui rend la découpe légale : la normalisation
|
|
7
|
+
* WHATWG (dot-segments — y compris leurs formes percent-encodées `%2e` —,
|
|
8
|
+
* conversion `\` → `/`, percent-encoding, punycode/lowercase du host,
|
|
9
|
+
* normalisation des hosts « IPv4-like » type `127.1`/`0x7f.1`) PROTÈGE le
|
|
10
|
+
* routing et le matching de zones du firewall. La découpe n'est donc admise
|
|
11
|
+
* QUE si elle est prouvablement l'IDENTITÉ : tout caractère ou motif que le
|
|
12
|
+
* parseur WHATWG transformerait — ou dont l'innocuité n'est pas certaine —
|
|
13
|
+
* déclenche le bail-out (`null`) et le vrai `new URL` est construit, comme
|
|
14
|
+
* avant. Whitelist stricte : le doute coûte un parse, jamais un contournement.
|
|
15
|
+
*
|
|
16
|
+
* La preuve est portée par `tests/unit/urlFastPath.test.ts` : balayage
|
|
17
|
+
* exhaustif par caractère (0x00-0x7F, plus témoins > 0x7F) comparant chaque
|
|
18
|
+
* target ACCEPTÉ au résultat du vrai `new URL` — pathname, search et href
|
|
19
|
+
* doivent être STRICTEMENT identiques à la découpe.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Résultat d'une découpe acceptée : les deux composants, dans la même forme
|
|
23
|
+
* que `URL.pathname` / `URL.search` (le `?` inclus, `""` si query absente ou
|
|
24
|
+
* vide — `/x?` donne `search === ""` comme WHATWG).
|
|
25
|
+
*/
|
|
26
|
+
export interface ISplitTarget {
|
|
27
|
+
pathname: string;
|
|
28
|
+
search: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Découpe un request-target origin-form (`/path[?query]`) en
|
|
32
|
+
* pathname/search SI — et seulement si — le parse WHATWG serait l'identité.
|
|
33
|
+
*
|
|
34
|
+
* @param target - le request-target brut (`IncomingMessage.url` en HTTP/1,
|
|
35
|
+
* pseudo-header `:path` en HTTP/2).
|
|
36
|
+
* @returns les composants découpés, ou `null` (bail-out → vrai `new URL`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function splitTarget(target: unknown): ISplitTarget | null;
|
|
39
|
+
/**
|
|
40
|
+
* L'autorité (`host[:port]` brut de l'en-tête `Host` / `:authority`) est-elle
|
|
41
|
+
* déjà sous la forme exacte que le parseur WHATWG rendrait ?
|
|
42
|
+
*
|
|
43
|
+
* Refusé (→ bail-out) : majuscules (lowercase WHATWG), IPv6 (`[`), toute
|
|
44
|
+
* forme « IPv4-like » ambiguë (`127.1`, `0x7f.1`, `2130706433` — WHATWG les
|
|
45
|
+
* NORMALISE en dotted-quad, un matcher les lisant brut serait contournable),
|
|
46
|
+
* label vide (`a..b`, `.a`, `a.`), port par défaut du scheme (élidé par
|
|
47
|
+
* WHATWG) ou avec zéro de tête, percent-encoding, non-ASCII (punycode).
|
|
48
|
+
*
|
|
49
|
+
* @param host - autorité brute (peut contenir `:port`).
|
|
50
|
+
* @param scheme - scheme effectif (`http` | `https`) — décide du port par défaut.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isCanonicalAuthority(host: unknown, scheme: string): host is string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import http2 from "node:http2";
|
|
2
|
+
import HttpContext from "../http/HttpContext.js";
|
|
3
|
+
import HttpRequest from "../http/Request.js";
|
|
4
|
+
import { HTTPMethod } from "../Context.js";
|
|
5
|
+
declare class Http2Request extends HttpRequest {
|
|
6
|
+
request: http2.Http2ServerRequest;
|
|
7
|
+
constructor(request: http2.Http2ServerRequest, context: HttpContext);
|
|
8
|
+
getHost(): string | undefined;
|
|
9
|
+
getUserAgent(): string | undefined;
|
|
10
|
+
getMethod(): HTTPMethod;
|
|
11
|
+
getRawTarget(): string | undefined;
|
|
12
|
+
protected resolveScheme(): string;
|
|
13
|
+
}
|
|
14
|
+
export default Http2Request;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import http2 from "node:http2";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import HttpContext from "../http/HttpContext.js";
|
|
4
|
+
import HttpResponse from "../http/Response.js";
|
|
5
|
+
declare class Http2Response extends HttpResponse {
|
|
6
|
+
statusCode: number;
|
|
7
|
+
stream: http2.ServerHttp2Stream | null;
|
|
8
|
+
streamId?: number | undefined;
|
|
9
|
+
constructor(response: http2.Http2ServerResponse, context: HttpContext);
|
|
10
|
+
isHeaderSent(): boolean;
|
|
11
|
+
writeHead(statusCode?: number, headers?: http.OutgoingHttpHeaders | http.OutgoingHttpHeader[]): void;
|
|
12
|
+
send(chunk?: unknown, encoding?: BufferEncoding, _flush?: boolean): Promise<Http2Response>;
|
|
13
|
+
end(chunk?: string | Buffer, encoding?: BufferEncoding): Promise<http.ServerResponse | http2.ServerHttp2Stream>;
|
|
14
|
+
getStatusMessage(code?: number | string): string;
|
|
15
|
+
getStatus(): {
|
|
16
|
+
code: number;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export default Http2Response;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Data, MetaData } from "../../service/http-kernel.js";
|
|
2
|
+
/**
|
|
3
|
+
* Forme structurelle minimale d'une route nécessaire pour bâtir l'enveloppe
|
|
4
|
+
* `nodefony.route`. Découplée de la classe `Route` de `@nodefony/framework`
|
|
5
|
+
* (pas d'import, pas de cycle, pas de fuite d'instance partagée).
|
|
6
|
+
*/
|
|
7
|
+
export interface IMetaDataRouteSource {
|
|
8
|
+
name: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Contexte structurel minimal nécessaire pour assembler `Context.metaData`.
|
|
13
|
+
* Volontairement découplé de `Context` (typage par forme, comme
|
|
14
|
+
* `IParamArgContext`) → {@link buildMetaData} est une fonction pure, testable en
|
|
15
|
+
* unit avec une fausse source, sans démarrer de serveur. Le vrai `Context`
|
|
16
|
+
* satisfait cette forme.
|
|
17
|
+
*/
|
|
18
|
+
export interface IMetaDataSource {
|
|
19
|
+
kernel?: {
|
|
20
|
+
projectName?: string;
|
|
21
|
+
version?: string;
|
|
22
|
+
environment?: MetaData["environment"];
|
|
23
|
+
debug?: MetaData["debug"];
|
|
24
|
+
} | null;
|
|
25
|
+
request?: {
|
|
26
|
+
url?: URL;
|
|
27
|
+
href?: string;
|
|
28
|
+
} | null;
|
|
29
|
+
scheme: MetaData["scheme"];
|
|
30
|
+
requestId: string;
|
|
31
|
+
resolver?: {
|
|
32
|
+
route: IMetaDataRouteSource | null;
|
|
33
|
+
getMatchedParams(): Record<string, unknown>;
|
|
34
|
+
} | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Assemble l'enveloppe `metaData` d'une requête EN PLACE (builder monomorphe) :
|
|
38
|
+
* met à jour les champs de `target.nodefony` dans un ordre fixe → V8 garde une
|
|
39
|
+
* hidden class stable et inline les écritures. Remplace l'ancien
|
|
40
|
+
* `extend(true, …)` qui deep-clonait/dispatchait à chaque réponse JSON et chaque
|
|
41
|
+
* frame WS — inutile ici puisque `target` est l'objet metaData per-requête
|
|
42
|
+
* (jamais partagé).
|
|
43
|
+
*
|
|
44
|
+
* `route` est un **snapshot per-requête** `{ name, path, variablesMap }` : on ne
|
|
45
|
+
* diffuse jamais l'instance `Route` partagée (statique), dont les variables
|
|
46
|
+
* matchées seraient écrasées par toute requête/connexion concurrente (bleed).
|
|
47
|
+
*
|
|
48
|
+
* `override` (ex. frame WS `{ nodefony: { websocket } }`) est fusionné *shallow*
|
|
49
|
+
* dans l'enveloppe : le seul cas réel est l'ajout d'un sous-objet `nodefony`
|
|
50
|
+
* peu profond — pas besoin du deep-merge récursif générique. Toute clé top-level
|
|
51
|
+
* hors `nodefony` est préservée telle quelle.
|
|
52
|
+
*
|
|
53
|
+
* @param target - objet metaData per-requête à muter (et retourner)
|
|
54
|
+
* @param src - source structurelle (le `Context` réel, ou un faux en test)
|
|
55
|
+
* @param override - overrides optionnels de l'appelant
|
|
56
|
+
* @returns le même `target`, muté
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildMetaData(target: Data, src: IMetaDataSource, override?: Record<string, unknown>): Data;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation de l'identifiant de corrélation `X-Request-Id` fourni par le
|
|
3
|
+
* client, avant qu'il ne soit adopté comme `Context.requestId`.
|
|
4
|
+
*
|
|
5
|
+
* Le `requestId` finit (1) réfléchi dans la réponse (`X-Request-Id`),
|
|
6
|
+
* (2) écrit dans les logs (`ID : …`), (3) propagé en ALS à tout le pipeline.
|
|
7
|
+
* Une valeur cliente non assainie ouvre donc : log-injection (CR/LF dans les
|
|
8
|
+
* logs), forging de corrélation, et throw `setHeader` natif (→ 500/DoS) sur
|
|
9
|
+
* caractère de contrôle ou non-ASCII. Cf RFC 9110 §5.5 (field values) + Zero
|
|
10
|
+
* Trust (mémoire `feedback_security_rfc_rigor`).
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Longueur maximale acceptée pour un `X-Request-Id` client. Borne anti-abus
|
|
14
|
+
* (log flooding / header oversize). 128 couvre largement un UUID (36), un
|
|
15
|
+
* nanoid, ou un `traceparent` (55).
|
|
16
|
+
*/
|
|
17
|
+
export declare const MAX_REQUEST_ID_LENGTH = 128;
|
|
18
|
+
/**
|
|
19
|
+
* Valide un `X-Request-Id` entrant. Retourne la valeur si elle est sûre, sinon
|
|
20
|
+
* `null` (l'appelant conserve alors l'UUID généré côté serveur).
|
|
21
|
+
*
|
|
22
|
+
* On REJETTE (plutôt que tronquer/nettoyer) une valeur invalide : nettoyer
|
|
23
|
+
* donnerait au client un faux contrôle sur l'identifiant et masquerait l'abus.
|
|
24
|
+
*
|
|
25
|
+
* @param raw - valeur brute du header `x-request-id` (ou `undefined`/`null`).
|
|
26
|
+
* @returns la valeur si sûre, sinon `null`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sanitizeRequestId(raw: string | undefined | null): string | null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Politique de confiance envers les en-têtes `X-Forwarded-*` (et le scheme
|
|
3
|
+
* proxifié), pour décider si la connexion entrante provient d'un reverse-proxy
|
|
4
|
+
* légitime.
|
|
5
|
+
*
|
|
6
|
+
* Sans cette barrière, n'importe quel client peut envoyer
|
|
7
|
+
* `X-Forwarded-For: 1.2.3.4` → IP spoofée (contournement de rate-limit /
|
|
8
|
+
* d'allow-list IP, falsification des logs d'audit) et `X-Forwarded-Proto: https`
|
|
9
|
+
* → faux scheme. Cf RFC 7239 / OWASP « Host header & forwarded headers ».
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Valeur de configuration `trustProxy` :
|
|
13
|
+
* - `false` (défaut sûr) : ne JAMAIS faire confiance aux `X-Forwarded-*`.
|
|
14
|
+
* - `true` : confiance totale (déploiement où le LB est l'unique point d'entrée).
|
|
15
|
+
* - `string` / `string[]` : IP, CIDR (`10.0.0.0/8`, `::1/128`) ou presets
|
|
16
|
+
* `"loopback"`, `"linklocal"`, `"uniquelocal"`.
|
|
17
|
+
*/
|
|
18
|
+
export type TrustProxyConfig = boolean | string | string[];
|
|
19
|
+
/** Décide si l'adresse distante (socket réel) est un proxy de confiance. */
|
|
20
|
+
export interface TrustProxyChecker {
|
|
21
|
+
isTrusted(remoteAddress: string | undefined | null): boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compile une config `trustProxy` en un {@link TrustProxyChecker} (une fois, au
|
|
25
|
+
* boot — pas par requête). Les entrées invalides lèvent à la compilation (fail
|
|
26
|
+
* fast) plutôt que silencieusement par requête.
|
|
27
|
+
*
|
|
28
|
+
* @param config - voir {@link TrustProxyConfig}. `undefined` → aucune confiance.
|
|
29
|
+
* @returns un checker `isTrusted(remoteAddress)`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildTrustProxy(config: TrustProxyConfig | undefined): TrustProxyChecker;
|
|
32
|
+
/**
|
|
33
|
+
* Cœur de la résolution **from-right** (OWASP) partagé par `X-Forwarded-For`
|
|
34
|
+
* (de-facto) et le paramètre `for` du header `Forwarded` (RFC 7239 §5.2).
|
|
35
|
+
*
|
|
36
|
+
* `chain` = liste des maillons forwarded NORMALISÉS (IP nues, gauche→droite :
|
|
37
|
+
* `[client, proxy1, proxy2]`), telle qu'écrite par les proxies par **append à
|
|
38
|
+
* droite**. La partie gauche est forgeable par le client (cf {@link extractClientIp}).
|
|
39
|
+
* La résolution part de la **connexion réelle** (le socket, non forgeable) et
|
|
40
|
+
* remonte de DROITE à GAUCHE : tant que le maillon courant est un proxy de
|
|
41
|
+
* confiance, on passe au précédent ; le **premier maillon non fiable** est l'IP
|
|
42
|
+
* cliente réelle.
|
|
43
|
+
*
|
|
44
|
+
* Un maillon `null` (identifiant obfusqué `_secret`, `unknown`, ou node illisible
|
|
45
|
+
* — RFC 7239 §6.3/§8.3) est une **barrière non franchissable** : on ne peut pas
|
|
46
|
+
* en vérifier la confiance → on s'arrête et on retourne le dernier maillon de
|
|
47
|
+
* confiance connu (une vraie IP de l'infra), jamais `null` ni la valeur obfusquée.
|
|
48
|
+
*
|
|
49
|
+
* @param chain - maillons forwarded normalisés (IP ou `null` si obfusqué).
|
|
50
|
+
* @param socketAddress - `socket.remoteAddress` (connexion TCP réelle, fiable).
|
|
51
|
+
* @param checker - politique de confiance ({@link buildTrustProxy}).
|
|
52
|
+
* @returns l'IP cliente réelle résolue (toujours une valeur fiable).
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveFromRight(chain: ReadonlyArray<string | null>, socketAddress: string, checker: TrustProxyChecker): string;
|
|
55
|
+
/**
|
|
56
|
+
* Résout l'adresse IP cliente RÉELLE à partir de la connexion socket et de la
|
|
57
|
+
* chaîne `X-Forwarded-For`, en dépouillant les proxies de confiance **de droite
|
|
58
|
+
* à gauche** (algorithme OWASP).
|
|
59
|
+
*
|
|
60
|
+
* `X-Forwarded-For` (de-facto) est construit par **append de gauche à droite** :
|
|
61
|
+
* `XFF: client, proxy1, proxy2`. Chaque proxy ajoute À DROITE l'adresse qu'il a
|
|
62
|
+
* vue. La partie **gauche** est donc entièrement **forgeable** par le client
|
|
63
|
+
* (il peut envoyer `X-Forwarded-For: 1.2.3.4` ; le 1ᵉʳ proxy ne fait qu'append
|
|
64
|
+
* l'IP réelle après). Lire `XFF[0]` revient à lire la valeur du client →
|
|
65
|
+
* **IP spoofing** (contournement de ban / rate-limit / allow-list / audit).
|
|
66
|
+
*
|
|
67
|
+
* Hot path préservé : sans `X-Forwarded-For` (cas direct usuel) la fonction
|
|
68
|
+
* retourne immédiatement le socket — 0 allocation (pas de split). Pour le header
|
|
69
|
+
* standard `Forwarded` (RFC 7239), voir `resolveForwarded` (module `forwarded`).
|
|
70
|
+
*
|
|
71
|
+
* @param xff - valeur brute de l'en-tête `X-Forwarded-For` (string, ou string[]
|
|
72
|
+
* si l'en-tête est répété), ou `undefined`.
|
|
73
|
+
* @param socketAddress - `socket.remoteAddress` (la connexion TCP réelle).
|
|
74
|
+
* @param checker - politique de confiance ({@link buildTrustProxy}).
|
|
75
|
+
* @returns l'IP cliente réelle, ou `null` si aucune connexion socket fiable.
|
|
76
|
+
*/
|
|
77
|
+
export declare function extractClientIp(xff: string | string[] | undefined, socketAddress: string | undefined | null, checker: TrustProxyChecker): string | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Cookie from "../../cookies/cookie.js";
|
|
2
|
+
import { Message, Msgid, Pci, Severity } from "nodefony";
|
|
3
|
+
import WebsocketContext from "./WebsocketContext.js";
|
|
4
|
+
import Ws from "ws";
|
|
5
|
+
export type { IWsCookie } from "../../../interfaces/ICookie.js";
|
|
6
|
+
/**
|
|
7
|
+
* `true` si l'erreur d'écriture signifie seulement que le client s'est
|
|
8
|
+
* déconnecté (frame perdue, connexion en cours de fermeture) — à logger DEBUG,
|
|
9
|
+
* jamais ERROR + stack (vécu : un simple reload de page pendant un ping WS
|
|
10
|
+
* remplissait le journal d'« Error: write EPIPE »).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isPeerGoneError(error: unknown): boolean;
|
|
13
|
+
declare class WebsocketResponse {
|
|
14
|
+
private context;
|
|
15
|
+
statusCode: number;
|
|
16
|
+
body: Buffer | null;
|
|
17
|
+
encoding: BufferEncoding;
|
|
18
|
+
connection: Ws | null;
|
|
19
|
+
statusMessage: string;
|
|
20
|
+
webSocketVersion?: number;
|
|
21
|
+
cookies: Record<string, Cookie>;
|
|
22
|
+
constructor(connection: Ws | null, context: WebsocketContext);
|
|
23
|
+
log(pci: Pci, severity?: Severity, msgid?: Msgid, msg?: Message): import("nodefony").Pdu | undefined;
|
|
24
|
+
setConnection(connection: Ws): Ws;
|
|
25
|
+
send(data?: Buffer | string | null, encoding?: BufferEncoding): Promise<WebsocketResponse>;
|
|
26
|
+
broadcast(data?: Buffer | string | null, type?: BufferEncoding): void;
|
|
27
|
+
/**
|
|
28
|
+
* Logge UNE seule fois par connexion (au 1er drop) qu'un client subit la
|
|
29
|
+
* backpressure — observabilité opérateur sans bruit dans le hot path.
|
|
30
|
+
*/
|
|
31
|
+
private logFirstDrop;
|
|
32
|
+
setBody(ele: string | NodeJS.ArrayBufferView | ArrayBuffer | SharedArrayBuffer, encoding?: BufferEncoding): Buffer<ArrayBufferLike> | null;
|
|
33
|
+
drop(reasonCode: number, description: string): void;
|
|
34
|
+
close(reasonCode: number, description: string): void;
|
|
35
|
+
getStatus(): {
|
|
36
|
+
code: number;
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
getStatusCode(): number;
|
|
40
|
+
getStatusMessage(): string;
|
|
41
|
+
setStatusCode(status: number | string, message?: string): {
|
|
42
|
+
code: number;
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
clean(): void;
|
|
46
|
+
setEncoding(encoding: BufferEncoding): BufferEncoding;
|
|
47
|
+
setHeader(): boolean;
|
|
48
|
+
setHeaders(): boolean;
|
|
49
|
+
addCookie(cookie: Cookie): void;
|
|
50
|
+
setCookies(): void;
|
|
51
|
+
setCookie(_cookie: Cookie): void;
|
|
52
|
+
}
|
|
53
|
+
export default WebsocketResponse;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import Context from "../Context.js";
|
|
2
|
+
import { ServerType, SchemeType } from "../../../service/http-kernel.js";
|
|
3
|
+
import { Severity, Msgid, Message, nodefonyError, Scope } from "nodefony";
|
|
4
|
+
import Ws from "ws";
|
|
5
|
+
import type { IncomingMessage } from "node:http";
|
|
6
|
+
import WebsocketResponse from "./Response.js";
|
|
7
|
+
import { URL } from "node:url";
|
|
8
|
+
import { HTTPMethod } from "../Context.js";
|
|
9
|
+
import HttpError from "../../errors/httpError.js";
|
|
10
|
+
import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
11
|
+
import type { PhaseName } from "../../../interfaces/IContext.js";
|
|
12
|
+
import { ProxyType } from "../http/HttpContext.js";
|
|
13
|
+
import { type ResolvedProxy } from "../forwarded.js";
|
|
14
|
+
export interface IWsRequestExtension {
|
|
15
|
+
url: URL;
|
|
16
|
+
query: Record<string, string>;
|
|
17
|
+
queryGet: Record<string, string>;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
export type WsIncomingMessage = IncomingMessage & IWsRequestExtension;
|
|
21
|
+
import type { IWebsocketContext as IWebsocketContextInterface } from "../../../interfaces/IContext.js";
|
|
22
|
+
/**
|
|
23
|
+
* Coerce un code (applicatif / HTTP / WS) en code de fermeture WebSocket VALIDE
|
|
24
|
+
* et conforme RFC 6455 §7.4, en PRÉFÉRANT les codes standard §7.4.1 quand le
|
|
25
|
+
* sens existe :
|
|
26
|
+
* - code déjà valide émissible (1000-1003, 1007-1011, 3000-4999) → conservé ;
|
|
27
|
+
* - HTTP 5xx / interne / code absent → **1011** (Internal Error) ;
|
|
28
|
+
* - HTTP 401 / 403 / 421 → **1008** (Policy Violation) ;
|
|
29
|
+
* - autre `< 1000` (ex. 404, sans équivalent RFC) → **4004**, plage privée
|
|
30
|
+
* 4000-4999 (§7.4.2, « undefined by this protocol » → convention applicative).
|
|
31
|
+
*
|
|
32
|
+
* Évite d'émettre un code de la plage 0-999 (« not used », rejeté par `ws`) ou
|
|
33
|
+
* un code réservé non émissible (1004/1005/1006/1015).
|
|
34
|
+
*
|
|
35
|
+
* @param code - code source (number, undefined…).
|
|
36
|
+
* @returns un code de fermeture WS valide.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toWsCloseCode(code: number | undefined | null): number;
|
|
39
|
+
export default class WebsocketContext extends Context implements IWebsocketContextInterface {
|
|
40
|
+
#private;
|
|
41
|
+
request: WsIncomingMessage | null;
|
|
42
|
+
response: WebsocketResponse | null;
|
|
43
|
+
acceptedProtocol?: string;
|
|
44
|
+
port: number | string;
|
|
45
|
+
rejected: boolean;
|
|
46
|
+
teardownWired: boolean;
|
|
47
|
+
connection: Ws | null;
|
|
48
|
+
origin: string;
|
|
49
|
+
proxy: ProxyType | null;
|
|
50
|
+
forwarded: ResolvedProxy | null;
|
|
51
|
+
wsUrl: URL | null;
|
|
52
|
+
queryGet: Record<string, string>;
|
|
53
|
+
queryRequest: Record<string, string>;
|
|
54
|
+
wsPath: string;
|
|
55
|
+
constructor(scope: Scope, req: IncomingMessage, ws: Ws, type: ServerType);
|
|
56
|
+
log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message): import("nodefony").Pdu;
|
|
57
|
+
logRequest(httpError?: Error | HttpError | nodefonyError | null, acceptedProtocol?: string | null): import("nodefony").Pdu | undefined;
|
|
58
|
+
connect(): Promise<Ws>;
|
|
59
|
+
handle(data?: unknown[]): Promise<this>;
|
|
60
|
+
render(chunk: unknown, encoding?: BufferEncoding): Promise<WebsocketResponse>;
|
|
61
|
+
send(data?: string | Buffer | null, encoding?: BufferEncoding): Promise<WebsocketResponse>;
|
|
62
|
+
broadcast(data?: string | Buffer | null, encoding?: BufferEncoding): void | null;
|
|
63
|
+
/**
|
|
64
|
+
* Logge le CONTENU d'un message WS (corrélé `requestId` via l'override `log`),
|
|
65
|
+
* gaté hors prod et borné — le Suivi de requête (Studio) le surface alors par
|
|
66
|
+
* direction. Hot path : le gate booléen court-circuite en prod AVANT toute
|
|
67
|
+
* construction de chaîne (0 allocation / 0 concat).
|
|
68
|
+
*
|
|
69
|
+
* @param dir - sens du message du point de vue serveur.
|
|
70
|
+
* @param data - charge utile (string, Buffer binaire, objet, ou null).
|
|
71
|
+
*/
|
|
72
|
+
private logMessageContent;
|
|
73
|
+
/**
|
|
74
|
+
* Ouvre le profil d'**une invocation** du pont RPC (une frame `api.request`).
|
|
75
|
+
*
|
|
76
|
+
* Le contexte WS vit pour la CONNEXION : ses `phases` sont cumulatives et son
|
|
77
|
+
* `requestId` est unique pour toute la socket. Une frame reçoit donc son
|
|
78
|
+
* propre profil, identifié `<requestId de la connexion>.<n° de frame>` — le
|
|
79
|
+
* `id` JSON-RPC ne peut pas servir de clé : il est choisi par le client.
|
|
80
|
+
*
|
|
81
|
+
* @param method - méthode LOGIQUE de l'invocation (`GET`, `POST`…).
|
|
82
|
+
* @param framePath - chemin invoqué par la frame (jamais l'URL de la
|
|
83
|
+
* connexion) — nommé ainsi parce que `url` masquerait le module
|
|
84
|
+
* `node:url`, importé ici et utilisé par `connect()`.
|
|
85
|
+
* @returns le profil, ou `null` si profiler ET timing sont éteints (prod) —
|
|
86
|
+
* zéro allocation dans ce cas.
|
|
87
|
+
*/
|
|
88
|
+
beginFrame(method: string, framePath: string): FrameProfile | null;
|
|
89
|
+
/**
|
|
90
|
+
* Enregistre le profil d'une invocation terminée dans le ring buffer du
|
|
91
|
+
* Profiler (no-op hors dev, ou si aucun profil n'a été ouvert).
|
|
92
|
+
*/
|
|
93
|
+
collectFrame(frame: FrameProfile | null): void;
|
|
94
|
+
/**
|
|
95
|
+
* Les phases émises pendant une invocation du pont (`initialize` par le
|
|
96
|
+
* Resolver, `render` par le Controller…) vont dans le profil de **la frame**,
|
|
97
|
+
* pas du contexte : sans cette redirection, `Context.phases` accumulerait deux
|
|
98
|
+
* entrées par message pour toute la vie de la socket (timeline cumulative
|
|
99
|
+
* ET croissance sans borne). Hors invocation (handshake), comportement de base.
|
|
100
|
+
*/
|
|
101
|
+
phaseStart(name: PhaseName): void;
|
|
102
|
+
phaseEnd(name: PhaseName): void;
|
|
103
|
+
handleMessage(data: Buffer | string, isBinary: boolean): Promise<unknown>;
|
|
104
|
+
onClose(code: number, reason: Buffer): void;
|
|
105
|
+
/**
|
|
106
|
+
* Listener `error` de la socket ws (OBLIGATOIRE — sans lui, un 'error' émis
|
|
107
|
+
* sans listener crashe le process via EventEmitter). `ws` émet 'error' PUIS
|
|
108
|
+
* 'close' → le teardown se fait dans {@link onClose} ; ici on logge seulement
|
|
109
|
+
* (pas de double-close). Erreur transport (reset TCP, frame corrompue…).
|
|
110
|
+
*
|
|
111
|
+
* @param error - erreur émise par la socket.
|
|
112
|
+
*/
|
|
113
|
+
onConnectionError(error: Error): void;
|
|
114
|
+
setScheme(): SchemeType;
|
|
115
|
+
getRemoteAddress(): string | null;
|
|
116
|
+
getHost(): string | undefined;
|
|
117
|
+
getHostName(): string | undefined;
|
|
118
|
+
getUserAgent(): string;
|
|
119
|
+
getMethod(): HTTPMethod;
|
|
120
|
+
setContextJson(): void;
|
|
121
|
+
clean(): void;
|
|
122
|
+
close(reasonCode: number | undefined | null, description: string): void;
|
|
123
|
+
drop(reasonCode: number, description: string): void;
|
|
124
|
+
reject(code: number | string | undefined, message?: string): void;
|
|
125
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Ws, { WebSocketServer } from "ws";
|
|
2
|
+
/** Politique appliquée quand le buffer d'envoi d'un client dépasse le seuil. */
|
|
3
|
+
export type WsBackpressurePolicy = "drop" | "close";
|
|
4
|
+
/**
|
|
5
|
+
* Options de backpressure SORTANTE (serveur → client).
|
|
6
|
+
*
|
|
7
|
+
* Knobs Nodefony (pas des options `ws`) ; `ws` les conserve quand même dans
|
|
8
|
+
* `wss.options` car son constructeur fait `{ ...defaults, ...options }` → on les
|
|
9
|
+
* relit ici via {@link readBackpressureOptions}.
|
|
10
|
+
*/
|
|
11
|
+
export interface IWsBackpressureOptions {
|
|
12
|
+
/** Seuil (octets) de `ws.bufferedAmount` ; `0`/absent = désactivé. */
|
|
13
|
+
maxBackpressure?: number;
|
|
14
|
+
/** Action au dépassement du seuil. Défaut `"drop"`. */
|
|
15
|
+
backpressurePolicy?: WsBackpressurePolicy;
|
|
16
|
+
/** Refus consécutifs au-delà desquels on ferme (1013) ; `0`/absent = jamais. */
|
|
17
|
+
backpressureCloseAfterDrops?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ce dont la règle a besoin, et rien de plus : la taille de la file d'envoi et
|
|
21
|
+
* de quoi fermer. Typé STRUCTURELLEMENT pour que `@nodefony/realtime` applique
|
|
22
|
+
* la même règle sans importer `ws` (son transport type déjà sa connexion ainsi).
|
|
23
|
+
*/
|
|
24
|
+
export interface IBackpressureTarget {
|
|
25
|
+
readonly bufferedAmount?: number;
|
|
26
|
+
close(code?: number, reason?: string): void;
|
|
27
|
+
}
|
|
28
|
+
/** Socket augmentée d'un compteur de drops (number lazy — lu par la sonde socket). */
|
|
29
|
+
export interface IBackpressureSocket extends Ws {
|
|
30
|
+
/** Nombre cumulé de frames droppées/refusées pour backpressure sur cette socket. */
|
|
31
|
+
_nfDrops?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Frames refusées **d'affilée**, remis à zéro dès qu'une frame repart. C'est
|
|
34
|
+
* le signal d'une file qui ne se draine plus — le seul qui distingue un pic
|
|
35
|
+
* passager d'un client mort. Cf le palier 2 de {@link decideSend}.
|
|
36
|
+
*/
|
|
37
|
+
_nfDropStreak?: number;
|
|
38
|
+
}
|
|
39
|
+
/** Décision pour une frame : l'émettre, la sauter, ou (socket fermée) ne rien faire. */
|
|
40
|
+
export type WsSendDecision = "send" | "drop" | "close";
|
|
41
|
+
/**
|
|
42
|
+
* Relit le seuil + la politique depuis les options du `WebSocketServer`.
|
|
43
|
+
*
|
|
44
|
+
* `ws` préserve nos clés custom (`maxBackpressure`/`backpressurePolicy`) dans
|
|
45
|
+
* `wss.options` via le spread de son constructeur. Appelé UNE fois hors de la boucle
|
|
46
|
+
* `broadcast()` (pas par frame).
|
|
47
|
+
*
|
|
48
|
+
* @param wss - le serveur WebSocket (ou null/undefined → backpressure désactivée)
|
|
49
|
+
* @returns seuil résolu (`max`, 0 = off) + politique (`policy`, défaut `"drop"`)
|
|
50
|
+
*/
|
|
51
|
+
export declare const readBackpressureOptions: (wss: WebSocketServer | null | undefined) => {
|
|
52
|
+
max: number;
|
|
53
|
+
policy: WsBackpressurePolicy;
|
|
54
|
+
closeAfterDrops: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Décide si une frame doit partir vers `ws` selon la backpressure SORTANTE
|
|
58
|
+
* (serveur → client). Protège la RAM d'envoi du serveur quand le client est lent à
|
|
59
|
+
* RECEVOIR (ne lit pas assez vite → `ws.bufferedAmount` gonfle → OOM). `broadcast()`
|
|
60
|
+
* amplifie : un seul client lent peut, sans borne, faire tomber la diffusion entière.
|
|
61
|
+
*
|
|
62
|
+
* PERF (hot path WS) : lecture O(1) de `ws.bufferedAmount`, **0 allocation sous le
|
|
63
|
+
* seuil** (chemin nominal = comportement inchangé). Au-delà : incrémente `_nfDrops`
|
|
64
|
+
* (lazy) et, si la politique est `close`, ferme la socket (RFC 6455 close 1013
|
|
65
|
+
* « Try Again Later » → le client peut back-off + reconnecter). NE FAIT PAS le `send()`
|
|
66
|
+
* — le caller émet uniquement si le retour vaut `"send"`.
|
|
67
|
+
*
|
|
68
|
+
* @param ws - la socket destinataire
|
|
69
|
+
* @param max - seuil d'octets (`<= 0` → désactivé)
|
|
70
|
+
* @param policy - action au dépassement
|
|
71
|
+
* @returns `"send"` (émettre) · `"drop"` (sauter cette frame) · `"close"` (socket fermée)
|
|
72
|
+
*/
|
|
73
|
+
export declare const decideSend: (ws: IBackpressureTarget, max: number, policy: WsBackpressurePolicy, closeAfterDrops?: number) => WsSendDecision;
|