@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,389 @@
|
|
|
1
|
+
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
2
|
+
import Context from "../Context.js";
|
|
3
|
+
import WebsocketResponse from "./Response.js";
|
|
4
|
+
import { sanitizeRequestId } from "../requestId.js";
|
|
5
|
+
import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
6
|
+
import { formatWsLogContent } from "./wsLogContent.js";
|
|
7
|
+
import { RequestContext, logColor } from "nodefony";
|
|
8
|
+
import Ws from "ws";
|
|
9
|
+
import { AsyncResource } from "node:async_hooks";
|
|
10
|
+
import url, { URL } from "node:url";
|
|
11
|
+
//#region nodefony/src/context/websocket/WebsocketContext.ts
|
|
12
|
+
/**
|
|
13
|
+
* Coerce un code (applicatif / HTTP / WS) en code de fermeture WebSocket VALIDE
|
|
14
|
+
* et conforme RFC 6455 §7.4, en PRÉFÉRANT les codes standard §7.4.1 quand le
|
|
15
|
+
* sens existe :
|
|
16
|
+
* - code déjà valide émissible (1000-1003, 1007-1011, 3000-4999) → conservé ;
|
|
17
|
+
* - HTTP 5xx / interne / code absent → **1011** (Internal Error) ;
|
|
18
|
+
* - HTTP 401 / 403 / 421 → **1008** (Policy Violation) ;
|
|
19
|
+
* - autre `< 1000` (ex. 404, sans équivalent RFC) → **4004**, plage privée
|
|
20
|
+
* 4000-4999 (§7.4.2, « undefined by this protocol » → convention applicative).
|
|
21
|
+
*
|
|
22
|
+
* Évite d'émettre un code de la plage 0-999 (« not used », rejeté par `ws`) ou
|
|
23
|
+
* un code réservé non émissible (1004/1005/1006/1015).
|
|
24
|
+
*
|
|
25
|
+
* @param code - code source (number, undefined…).
|
|
26
|
+
* @returns un code de fermeture WS valide.
|
|
27
|
+
*/
|
|
28
|
+
function toWsCloseCode(code) {
|
|
29
|
+
if (typeof code !== "number" || !Number.isInteger(code)) return 1011;
|
|
30
|
+
if (code === 1e3 || code >= 1001 && code <= 1003 || code >= 1007 && code <= 1011 || code >= 3e3 && code <= 4999) return code;
|
|
31
|
+
if (code >= 500 && code < 600) return 1011;
|
|
32
|
+
if (code === 401 || code === 403 || code === 421) return 1008;
|
|
33
|
+
if (code >= 400 && code < 500) return 4004;
|
|
34
|
+
return 1011;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Drapeau « logger le CONTENU des messages WS » — résolu **une seule fois** au
|
|
38
|
+
* premier message (kernel présent). `true` hors production : le Suivi de requête
|
|
39
|
+
* (Studio) affiche alors les frames RECEIVE/SEND/BROADCAST. En prod → `false` →
|
|
40
|
+
* le gate court-circuite AVANT toute allocation/concat (0 surcoût hot path).
|
|
41
|
+
*/
|
|
42
|
+
let wsContentLogging = null;
|
|
43
|
+
var WebsocketContext = class extends Context {
|
|
44
|
+
request;
|
|
45
|
+
response = null;
|
|
46
|
+
acceptedProtocol;
|
|
47
|
+
port;
|
|
48
|
+
rejected = false;
|
|
49
|
+
teardownWired = false;
|
|
50
|
+
connection = null;
|
|
51
|
+
origin;
|
|
52
|
+
proxy = null;
|
|
53
|
+
forwarded = null;
|
|
54
|
+
wsUrl = null;
|
|
55
|
+
queryGet = {};
|
|
56
|
+
queryRequest = {};
|
|
57
|
+
wsPath = "";
|
|
58
|
+
#frameSeq = 0;
|
|
59
|
+
constructor(scope, req, ws, type) {
|
|
60
|
+
super(scope, type);
|
|
61
|
+
this.webSocketState = "handshake";
|
|
62
|
+
this.request = req;
|
|
63
|
+
this.connection = ws;
|
|
64
|
+
this.response = new WebsocketResponse(ws, this);
|
|
65
|
+
this.method = this.getMethod();
|
|
66
|
+
this.origin = req.headers.origin ?? "";
|
|
67
|
+
const checker = this.httpKernel?.getTrustProxyChecker();
|
|
68
|
+
const socketAddress = req.socket?.remoteAddress;
|
|
69
|
+
if (checker && checker.isTrusted(socketAddress) && hasForwardingHeaders(req.headers)) this.forwarded = resolveForwarded(req.headers, socketAddress, checker);
|
|
70
|
+
this.remoteAddress = this.getRemoteAddress();
|
|
71
|
+
this.acceptedProtocol = req.headers["sec-websocket-protocol"];
|
|
72
|
+
this.scheme = type === "websocket-secure" ? "wss" : "ws";
|
|
73
|
+
const incomingId = sanitizeRequestId(req.headers["x-request-id"]);
|
|
74
|
+
if (incomingId) this.requestId = incomingId;
|
|
75
|
+
const host = req.headers.host ?? "localhost";
|
|
76
|
+
const rawUrl = req.url ?? "/";
|
|
77
|
+
this.wsUrl = new URL(`${this.scheme}://${host}${rawUrl}`);
|
|
78
|
+
this.queryGet = Object.fromEntries(this.wsUrl.searchParams.entries());
|
|
79
|
+
this.queryRequest = { ...this.queryGet };
|
|
80
|
+
this.wsPath = this.wsUrl.pathname + this.wsUrl.search;
|
|
81
|
+
this.url = url.format(this.wsUrl);
|
|
82
|
+
this.port = parseInt(this.wsUrl.port, 10) || (type === "websocket-secure" ? 443 : 80);
|
|
83
|
+
this.request.url = this.wsUrl;
|
|
84
|
+
this.request.queryGet = this.queryGet;
|
|
85
|
+
this.request.query = this.queryRequest;
|
|
86
|
+
this.request.path = this.wsPath;
|
|
87
|
+
try {
|
|
88
|
+
this.originUrl = new URL(this.origin);
|
|
89
|
+
} catch {
|
|
90
|
+
this.originUrl = new URL(this.url);
|
|
91
|
+
}
|
|
92
|
+
this.parseCookies();
|
|
93
|
+
this.cookieSession = this.getCookieSession(this.getSessionCookieName());
|
|
94
|
+
this.domain = this.getHostName();
|
|
95
|
+
this.validDomain = this.isValidDomain();
|
|
96
|
+
this.rejected = false;
|
|
97
|
+
const fwd = this.forwarded;
|
|
98
|
+
if (fwd) {
|
|
99
|
+
this.proxy = {
|
|
100
|
+
proxyServer: req.headers["x-forwarded-server"] ?? "unknown",
|
|
101
|
+
proxyProto: fwd.proto ?? req.headers["x-forwarded-proto"],
|
|
102
|
+
proxyPort: req.headers["x-forwarded-port"],
|
|
103
|
+
proxyFor: fwd.forwardedFor ?? req.headers["x-forwarded-for"],
|
|
104
|
+
proxyHost: fwd.host ?? req.headers["x-forwarded-host"],
|
|
105
|
+
proxyVia: req.headers.via
|
|
106
|
+
};
|
|
107
|
+
this.log(`PROXY WEBSOCKET REQUEST ${fwd.fromStandard ? "Forwarded (RFC 7239)" : "x-forwarded"} VIA : ${this.proxy?.proxyVia}`, "DEBUG");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
log(pci, severity, msgid, msg) {
|
|
111
|
+
if (!msgid) msgid = "WEBSOCKET CONTEXT";
|
|
112
|
+
return super.log(pci, severity, msgid, msg);
|
|
113
|
+
}
|
|
114
|
+
logRequest(httpError, acceptedProtocol) {
|
|
115
|
+
try {
|
|
116
|
+
const logger = this.httpKernel?.getRequestLogger();
|
|
117
|
+
if (!logger) return;
|
|
118
|
+
const entry = logger.renderWebsocket(this, httpError ?? null, acceptedProtocol ?? null);
|
|
119
|
+
const pdu = this.log(entry.text, entry.severity, entry.msgid);
|
|
120
|
+
if (pdu && pdu.requestId === void 0) pdu.requestId = this.requestId;
|
|
121
|
+
return pdu;
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
async connect() {
|
|
125
|
+
if (!this.response || !this.request || !this.connection) throw new Error("Nodefony Websocket Bad request/response/connection");
|
|
126
|
+
if (this.rejected) throw new Error("Nodefony Websocket rejected");
|
|
127
|
+
this.response.setConnection(this.connection);
|
|
128
|
+
this.connection.on("close", AsyncResource.bind(this.onClose.bind(this)));
|
|
129
|
+
this.connection.on("error", AsyncResource.bind(this.onConnectionError.bind(this)));
|
|
130
|
+
this.teardownWired = true;
|
|
131
|
+
await this.fireAsync("onConnect", this, this.connection);
|
|
132
|
+
this.requestEnded = true;
|
|
133
|
+
this.connection.on("message", AsyncResource.bind(this.handleMessage.bind(this)));
|
|
134
|
+
this.logRequest(null, this.acceptedProtocol ?? null);
|
|
135
|
+
this.webSocketState = "connected";
|
|
136
|
+
return this.connection;
|
|
137
|
+
}
|
|
138
|
+
async handle(data) {
|
|
139
|
+
if (this.rejected) {
|
|
140
|
+
this.webSocketState = "error";
|
|
141
|
+
throw new Error("Nodefony Websocket rejected");
|
|
142
|
+
}
|
|
143
|
+
if (!this.resolver) this.resolver = this.router?.resolve(this);
|
|
144
|
+
else try {
|
|
145
|
+
this.resolver.match(this.resolver.route, this);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (!this.rejected) this.reject(e.code ?? void 0, e.message);
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
await this.fireAsync("onRequest", this, this.resolver);
|
|
151
|
+
await this.kernel?.fireAsync("onRequest", this, this.resolver);
|
|
152
|
+
if (this.resolver && this.resolver.resolve) {
|
|
153
|
+
this.setMetaData({ nodefony: { websocket: {
|
|
154
|
+
state: this.webSocketState,
|
|
155
|
+
protocol: this.acceptedProtocol
|
|
156
|
+
} } });
|
|
157
|
+
await this.resolver.callController(data).then(async () => {
|
|
158
|
+
await this.saveSession().then((session) => {
|
|
159
|
+
if (session) this.log(`SAVE SESSION ID : ${session.id}`, "DEBUG");
|
|
160
|
+
}).catch((e) => {
|
|
161
|
+
throw e;
|
|
162
|
+
});
|
|
163
|
+
return this;
|
|
164
|
+
}).catch((error) => {
|
|
165
|
+
if (!this.rejected) {
|
|
166
|
+
if (this.requestEnded) throw this.close(error.code, error.message);
|
|
167
|
+
this.reject(error.code ?? void 0, error.message);
|
|
168
|
+
this.rejected = true;
|
|
169
|
+
this.webSocketState = "error";
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
} else if (!this.rejected) {
|
|
174
|
+
this.reject(4004, "Not Found");
|
|
175
|
+
this.rejected = true;
|
|
176
|
+
this.webSocketState = "error";
|
|
177
|
+
}
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
async render(chunk, encoding) {
|
|
181
|
+
let data = chunk;
|
|
182
|
+
if (this.isJson) data = JSON.stringify(chunk);
|
|
183
|
+
return this.send(data, encoding);
|
|
184
|
+
}
|
|
185
|
+
async send(data, encoding) {
|
|
186
|
+
if (this.response) {
|
|
187
|
+
const payload = data ?? this.response.body;
|
|
188
|
+
const sink = RequestContext.get()?.renderSink;
|
|
189
|
+
if (sink) {
|
|
190
|
+
sink.body = payload;
|
|
191
|
+
return this.response;
|
|
192
|
+
}
|
|
193
|
+
this.logMessageContent("SEND", payload);
|
|
194
|
+
this.fire("onMessage", payload, this, "SEND");
|
|
195
|
+
this.fire("onSend", payload, this);
|
|
196
|
+
return this.response.send(payload, encoding);
|
|
197
|
+
}
|
|
198
|
+
throw new Error("No response found");
|
|
199
|
+
}
|
|
200
|
+
broadcast(data, encoding) {
|
|
201
|
+
if (this.response) {
|
|
202
|
+
const payload = data ?? this.response.body;
|
|
203
|
+
if (payload) {
|
|
204
|
+
this.logMessageContent("BROADCAST", payload);
|
|
205
|
+
this.fire("onMessage", payload, this, "BROADCAST");
|
|
206
|
+
this.fire("onBroadcast", payload, this);
|
|
207
|
+
return this.response.broadcast(payload, encoding);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Logge le CONTENU d'un message WS (corrélé `requestId` via l'override `log`),
|
|
214
|
+
* gaté hors prod et borné — le Suivi de requête (Studio) le surface alors par
|
|
215
|
+
* direction. Hot path : le gate booléen court-circuite en prod AVANT toute
|
|
216
|
+
* construction de chaîne (0 allocation / 0 concat).
|
|
217
|
+
*
|
|
218
|
+
* @param dir - sens du message du point de vue serveur.
|
|
219
|
+
* @param data - charge utile (string, Buffer binaire, objet, ou null).
|
|
220
|
+
*/
|
|
221
|
+
logMessageContent(dir, data) {
|
|
222
|
+
if (wsContentLogging === null) wsContentLogging = this.kernel?.environment !== "production";
|
|
223
|
+
if (!wsContentLogging) return;
|
|
224
|
+
this.log(formatWsLogContent(data), "DEBUG", `WS ${dir}`);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Ouvre le profil d'**une invocation** du pont RPC (une frame `api.request`).
|
|
228
|
+
*
|
|
229
|
+
* Le contexte WS vit pour la CONNEXION : ses `phases` sont cumulatives et son
|
|
230
|
+
* `requestId` est unique pour toute la socket. Une frame reçoit donc son
|
|
231
|
+
* propre profil, identifié `<requestId de la connexion>.<n° de frame>` — le
|
|
232
|
+
* `id` JSON-RPC ne peut pas servir de clé : il est choisi par le client.
|
|
233
|
+
*
|
|
234
|
+
* @param method - méthode LOGIQUE de l'invocation (`GET`, `POST`…).
|
|
235
|
+
* @param framePath - chemin invoqué par la frame (jamais l'URL de la
|
|
236
|
+
* connexion) — nommé ainsi parce que `url` masquerait le module
|
|
237
|
+
* `node:url`, importé ici et utilisé par `connect()`.
|
|
238
|
+
* @returns le profil, ou `null` si profiler ET timing sont éteints (prod) —
|
|
239
|
+
* zéro allocation dans ce cas.
|
|
240
|
+
*/
|
|
241
|
+
beginFrame(method, framePath) {
|
|
242
|
+
if (!this.profiling && !this.timingEnabled) return null;
|
|
243
|
+
this.#frameSeq += 1;
|
|
244
|
+
return new FrameProfile({
|
|
245
|
+
requestId: `${this.requestId}.${this.#frameSeq}`,
|
|
246
|
+
type: this.type,
|
|
247
|
+
scheme: this.scheme,
|
|
248
|
+
method,
|
|
249
|
+
url: framePath,
|
|
250
|
+
remoteAddress: this.remoteAddress ?? null,
|
|
251
|
+
traceparent: this.traceparent,
|
|
252
|
+
security: this.security ?? null,
|
|
253
|
+
securityTrace: this.securityTrace,
|
|
254
|
+
timing: this.timingEnabled,
|
|
255
|
+
queries: this.profiling
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Enregistre le profil d'une invocation terminée dans le ring buffer du
|
|
260
|
+
* Profiler (no-op hors dev, ou si aucun profil n'a été ouvert).
|
|
261
|
+
*/
|
|
262
|
+
collectFrame(frame) {
|
|
263
|
+
if (frame === null || !this.profiling) return;
|
|
264
|
+
this.get("profiler")?.collect(frame);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Les phases émises pendant une invocation du pont (`initialize` par le
|
|
268
|
+
* Resolver, `render` par le Controller…) vont dans le profil de **la frame**,
|
|
269
|
+
* pas du contexte : sans cette redirection, `Context.phases` accumulerait deux
|
|
270
|
+
* entrées par message pour toute la vie de la socket (timeline cumulative
|
|
271
|
+
* ET croissance sans borne). Hors invocation (handshake), comportement de base.
|
|
272
|
+
*/
|
|
273
|
+
phaseStart(name) {
|
|
274
|
+
if (!this.timingEnabled) return;
|
|
275
|
+
const frame = RequestContext.get()?.invocation;
|
|
276
|
+
if (frame) {
|
|
277
|
+
frame.phaseStart(name);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
super.phaseStart(name);
|
|
281
|
+
}
|
|
282
|
+
phaseEnd(name) {
|
|
283
|
+
if (!this.timingEnabled) return;
|
|
284
|
+
const frame = RequestContext.get()?.invocation;
|
|
285
|
+
if (frame) {
|
|
286
|
+
frame.phaseEnd(name);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
super.phaseEnd(name);
|
|
290
|
+
}
|
|
291
|
+
async handleMessage(data, isBinary) {
|
|
292
|
+
this.webSocketState = "message";
|
|
293
|
+
const message = isBinary ? data : data.toString();
|
|
294
|
+
this.logMessageContent("RECEIVE", message);
|
|
295
|
+
if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data.toString());
|
|
296
|
+
try {
|
|
297
|
+
if (!this.resolver) this.resolver = this.router?.resolve(this);
|
|
298
|
+
else this.resolver.match(this.resolver?.route, this);
|
|
299
|
+
await this.fireAsync("onMessage", message, this, "RECEIVE");
|
|
300
|
+
if (this.resolver.resolve) {
|
|
301
|
+
this.setMetaData({ nodefony: { websocket: {
|
|
302
|
+
state: this.webSocketState,
|
|
303
|
+
protocol: this.acceptedProtocol
|
|
304
|
+
} } });
|
|
305
|
+
return this.resolver.callController([message]).catch((e) => {
|
|
306
|
+
throw e;
|
|
307
|
+
});
|
|
308
|
+
} else if (!this.rejected) {
|
|
309
|
+
this.reject(4004, "Not Found");
|
|
310
|
+
this.rejected = true;
|
|
311
|
+
}
|
|
312
|
+
} catch (e) {
|
|
313
|
+
this.reject(1011, "Internal Error");
|
|
314
|
+
throw e;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
onClose(code, reason) {
|
|
318
|
+
const description = reason.toString();
|
|
319
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("Description")} : ${description}`, "INFO", `${this.type} ${logColor.magenta(code)} CLOSE ${this.method}`);
|
|
320
|
+
if (this.connection?.readyState !== Ws.CLOSED) {
|
|
321
|
+
try {
|
|
322
|
+
this.response?.drop(code, description);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ORIGIN")} : ${this.originUrl?.host} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${e.message}`, "ERROR", `${this.type} CLOSE ${logColor.red(this.method ?? "")}`);
|
|
325
|
+
}
|
|
326
|
+
this.fire("onClose", code, description, this.connection);
|
|
327
|
+
} else this.fire("onClose", code, description, this.connection);
|
|
328
|
+
this.fire("onFinish", this, code, description);
|
|
329
|
+
this.webSocketState = "closed";
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Listener `error` de la socket ws (OBLIGATOIRE — sans lui, un 'error' émis
|
|
333
|
+
* sans listener crashe le process via EventEmitter). `ws` émet 'error' PUIS
|
|
334
|
+
* 'close' → le teardown se fait dans {@link onClose} ; ici on logge seulement
|
|
335
|
+
* (pas de double-close). Erreur transport (reset TCP, frame corrompue…).
|
|
336
|
+
*
|
|
337
|
+
* @param error - erreur émise par la socket.
|
|
338
|
+
*/
|
|
339
|
+
onConnectionError(error) {
|
|
340
|
+
this.webSocketState = "error";
|
|
341
|
+
this.log(`${logColor.cyan("URL")} : ${this.url} ${logColor.cyan("FROM")} : ${this.remoteAddress} ${logColor.cyan("ID")} : ${this.requestId} ${logColor.cyan("error")} : ${error?.message}`, "ERROR", `${this.type} ${logColor.red("SOCKET ERROR")} ${this.method}`);
|
|
342
|
+
this.fire("onError", error, this);
|
|
343
|
+
}
|
|
344
|
+
setScheme() {
|
|
345
|
+
return this.wsUrl?.protocol.replace(":", "");
|
|
346
|
+
}
|
|
347
|
+
getRemoteAddress() {
|
|
348
|
+
if (this.forwarded) return this.forwarded.clientIp;
|
|
349
|
+
return this.request?.socket?.remoteAddress ?? null;
|
|
350
|
+
}
|
|
351
|
+
getHost() {
|
|
352
|
+
return this.request?.headers.host;
|
|
353
|
+
}
|
|
354
|
+
getHostName() {
|
|
355
|
+
return this.wsUrl?.hostname;
|
|
356
|
+
}
|
|
357
|
+
getUserAgent() {
|
|
358
|
+
return this.request?.headers["user-agent"];
|
|
359
|
+
}
|
|
360
|
+
getMethod() {
|
|
361
|
+
return "WEBSOCKET";
|
|
362
|
+
}
|
|
363
|
+
setContextJson() {
|
|
364
|
+
this.isJson = true;
|
|
365
|
+
}
|
|
366
|
+
clean() {
|
|
367
|
+
this.request = null;
|
|
368
|
+
if (this.response) this.response.clean();
|
|
369
|
+
this.response = null;
|
|
370
|
+
this.connection = null;
|
|
371
|
+
this.container?.clean();
|
|
372
|
+
super.clean();
|
|
373
|
+
}
|
|
374
|
+
close(reasonCode, description) {
|
|
375
|
+
if (this.response) return this.response.close(toWsCloseCode(reasonCode), description);
|
|
376
|
+
}
|
|
377
|
+
drop(reasonCode, description) {
|
|
378
|
+
if (this.response) return this.response.drop(reasonCode, description);
|
|
379
|
+
}
|
|
380
|
+
reject(code, message) {
|
|
381
|
+
if (this.connection && this.connection.readyState === Ws.OPEN) {
|
|
382
|
+
const raw = typeof code === "string" ? parseInt(code, 10) : code;
|
|
383
|
+
this.connection.close(toWsCloseCode(raw), message ?? "Rejected");
|
|
384
|
+
}
|
|
385
|
+
this.rejected = true;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
//#endregion
|
|
389
|
+
export { WebsocketContext as default, toWsCloseCode };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region nodefony/src/context/websocket/wsBackpressure.ts
|
|
2
|
+
/**
|
|
3
|
+
* Relit le seuil + la politique depuis les options du `WebSocketServer`.
|
|
4
|
+
*
|
|
5
|
+
* `ws` préserve nos clés custom (`maxBackpressure`/`backpressurePolicy`) dans
|
|
6
|
+
* `wss.options` via le spread de son constructeur. Appelé UNE fois hors de la boucle
|
|
7
|
+
* `broadcast()` (pas par frame).
|
|
8
|
+
*
|
|
9
|
+
* @param wss - le serveur WebSocket (ou null/undefined → backpressure désactivée)
|
|
10
|
+
* @returns seuil résolu (`max`, 0 = off) + politique (`policy`, défaut `"drop"`)
|
|
11
|
+
*/
|
|
12
|
+
const readBackpressureOptions = (wss) => {
|
|
13
|
+
const opts = wss?.options ?? {};
|
|
14
|
+
return {
|
|
15
|
+
max: opts.maxBackpressure ?? 0,
|
|
16
|
+
policy: opts.backpressurePolicy ?? "drop",
|
|
17
|
+
closeAfterDrops: opts.backpressureCloseAfterDrops ?? 0
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Décide si une frame doit partir vers `ws` selon la backpressure SORTANTE
|
|
22
|
+
* (serveur → client). Protège la RAM d'envoi du serveur quand le client est lent à
|
|
23
|
+
* RECEVOIR (ne lit pas assez vite → `ws.bufferedAmount` gonfle → OOM). `broadcast()`
|
|
24
|
+
* amplifie : un seul client lent peut, sans borne, faire tomber la diffusion entière.
|
|
25
|
+
*
|
|
26
|
+
* PERF (hot path WS) : lecture O(1) de `ws.bufferedAmount`, **0 allocation sous le
|
|
27
|
+
* seuil** (chemin nominal = comportement inchangé). Au-delà : incrémente `_nfDrops`
|
|
28
|
+
* (lazy) et, si la politique est `close`, ferme la socket (RFC 6455 close 1013
|
|
29
|
+
* « Try Again Later » → le client peut back-off + reconnecter). NE FAIT PAS le `send()`
|
|
30
|
+
* — le caller émet uniquement si le retour vaut `"send"`.
|
|
31
|
+
*
|
|
32
|
+
* @param ws - la socket destinataire
|
|
33
|
+
* @param max - seuil d'octets (`<= 0` → désactivé)
|
|
34
|
+
* @param policy - action au dépassement
|
|
35
|
+
* @returns `"send"` (émettre) · `"drop"` (sauter cette frame) · `"close"` (socket fermée)
|
|
36
|
+
*/
|
|
37
|
+
const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
|
|
38
|
+
const sock = ws;
|
|
39
|
+
if (max <= 0 || (ws.bufferedAmount ?? 0) <= max) {
|
|
40
|
+
if (sock._nfDropStreak) sock._nfDropStreak -= 1;
|
|
41
|
+
return "send";
|
|
42
|
+
}
|
|
43
|
+
sock._nfDrops = (sock._nfDrops ?? 0) + 1;
|
|
44
|
+
const streak = sock._nfDropStreak = (sock._nfDropStreak ?? 0) + 1;
|
|
45
|
+
if (policy === "close") {
|
|
46
|
+
ws.close(1013, "backpressure");
|
|
47
|
+
return "close";
|
|
48
|
+
}
|
|
49
|
+
if (closeAfterDrops > 0 && streak >= closeAfterDrops) {
|
|
50
|
+
ws.close(1013, "backpressure");
|
|
51
|
+
return "close";
|
|
52
|
+
}
|
|
53
|
+
return "drop";
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { decideSend, readBackpressureOptions };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region nodefony/src/context/websocket/wsLogContent.ts
|
|
2
|
+
/**
|
|
3
|
+
* Formatage **pur** (0 état, 0 dépendance) du CONTENU d'un message WebSocket pour
|
|
4
|
+
* le log de trace (Suivi de requête Studio). Séparé de `WebsocketContext` pour
|
|
5
|
+
* être testable aux limites sans serveur.
|
|
6
|
+
*
|
|
7
|
+
* 🔒 Robustesse binaire (cf doc `ws` — `socket.send(data)` accepte
|
|
8
|
+
* `String | Number | Object | Buffer | ArrayBuffer | TypedArray | DataView |
|
|
9
|
+
* Buffer[] | Blob`, et l'event `message` livre `Buffer | ArrayBuffer | Buffer[]`).
|
|
10
|
+
* **Toute** charge binaire est résumée `[binary N B]` — JAMAIS sérialisée
|
|
11
|
+
* (`JSON.stringify(new Uint8Array(...))` produirait `{"0":..,"1":..}`, énorme et
|
|
12
|
+
* faux). Seules string et objets « JSON » sont rendus en texte (borné).
|
|
13
|
+
*/
|
|
14
|
+
/** Cap de troncature du contenu loggé (octets/chars). Borne ring + JSONL. */
|
|
15
|
+
const WS_LOG_CONTENT_CAP = 4096;
|
|
16
|
+
/**
|
|
17
|
+
* Taille en octets d'une charge **binaire** reconnue, ou `-1` si la valeur n'est
|
|
18
|
+
* pas binaire (→ à traiter en JSON/texte). Couvre tous les types binaires que
|
|
19
|
+
* `ws` accepte/livre : `Buffer`, `ArrayBuffer`, vues (`TypedArray`/`DataView`),
|
|
20
|
+
* `Blob` (Node ≥ 18), et `Buffer[]` (fragments — binaire ssi TOUS ses éléments
|
|
21
|
+
* le sont, sinon c'est un tableau JSON ordinaire).
|
|
22
|
+
*
|
|
23
|
+
* @param data - valeur à mesurer.
|
|
24
|
+
* @returns nombre d'octets, ou `-1` si non binaire.
|
|
25
|
+
*/
|
|
26
|
+
function binaryByteLength(data) {
|
|
27
|
+
if (Buffer.isBuffer(data)) return data.length;
|
|
28
|
+
if (data instanceof ArrayBuffer) return data.byteLength;
|
|
29
|
+
if (ArrayBuffer.isView(data)) return data.byteLength;
|
|
30
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return data.size;
|
|
31
|
+
if (Array.isArray(data)) {
|
|
32
|
+
let total = 0;
|
|
33
|
+
for (const part of data) {
|
|
34
|
+
const n = binaryByteLength(part);
|
|
35
|
+
if (n < 0) return -1;
|
|
36
|
+
total += n;
|
|
37
|
+
}
|
|
38
|
+
return total;
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Formate une charge utile WS en chaîne **bornée** et **sûre** pour le log :
|
|
44
|
+
* - `string` → tronquée à `cap` (+ ellipse) ;
|
|
45
|
+
* - binaire (cf {@link binaryByteLength}) → `[binary N B]` (jamais de dump) ;
|
|
46
|
+
* - `null`/`undefined` → `""` ;
|
|
47
|
+
* - objet « JSON » → `JSON.stringify` compact tronqué (cycle, `bigint`, valeur
|
|
48
|
+
* non sérialisable → repli `String(...)`).
|
|
49
|
+
*
|
|
50
|
+
* @param data - charge utile (RECEIVE/SEND/BROADCAST).
|
|
51
|
+
* @param cap - longueur max avant troncature (défaut {@link WS_LOG_CONTENT_CAP}).
|
|
52
|
+
*/
|
|
53
|
+
function formatWsLogContent(data, cap = WS_LOG_CONTENT_CAP) {
|
|
54
|
+
if (typeof data === "string") return data.length > cap ? `${data.slice(0, cap)}…` : data;
|
|
55
|
+
if (data === null || data === void 0) return "";
|
|
56
|
+
const bytes = binaryByteLength(data);
|
|
57
|
+
if (bytes >= 0) return `[binary ${bytes} B]`;
|
|
58
|
+
let s;
|
|
59
|
+
try {
|
|
60
|
+
s = JSON.stringify(data);
|
|
61
|
+
} catch {
|
|
62
|
+
return String(data);
|
|
63
|
+
}
|
|
64
|
+
if (typeof s !== "string") return String(data);
|
|
65
|
+
return s.length > cap ? `${s.slice(0, cap)}…` : s;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { WS_LOG_CONTENT_CAP, binaryByteLength, formatWsLogContent };
|