@nodefony/http 10.0.0-beta.2 → 10.0.0-beta.4
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/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +19 -11
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +45 -6
- package/dist/nodefony/config/defineModuleConfig.js +2 -3
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -3
- package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
- package/dist/nodefony/service/audit-logger.js +22 -16
- package/dist/nodefony/service/certificates.js +228 -19
- package/dist/nodefony/service/error-renderer.js +30 -13
- package/dist/nodefony/service/http-kernel.js +331 -82
- package/dist/nodefony/service/pretty-request-logger.js +5 -4
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +7 -10
- package/dist/nodefony/service/servers/server-websocket-secure.js +18 -18
- package/dist/nodefony/service/servers/server-websocket.js +18 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +32 -17
- package/dist/nodefony/service/sessions/sessions-service.js +34 -20
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +51 -32
- package/dist/nodefony/src/context/domainMatcher.js +21 -4
- package/dist/nodefony/src/context/forwarded.js +3 -4
- package/dist/nodefony/src/context/http/HttpContext.js +98 -46
- package/dist/nodefony/src/context/http/Request.js +34 -38
- package/dist/nodefony/src/context/http/Response.js +39 -7
- package/dist/nodefony/src/context/http/SseStream.js +349 -0
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +22 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/responseEnded.js +26 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/websocket/Response.js +42 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +94 -20
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +20 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
- package/dist/nodefony/src/profiler/Profiler.js +108 -30
- package/dist/nodefony/src/profiler/aggregate.js +422 -0
- package/dist/nodefony/src/proxy/forward.js +500 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +188 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +128 -0
- package/dist/nodefony/src/session/session.js +30 -15
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +12 -3
- package/dist/types/nodefony/config/config.d.ts +16 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +65 -4
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +147 -5
- package/dist/types/nodefony/service/error-renderer.d.ts +32 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +157 -15
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +2 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +2 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/src/context/Context.d.ts +35 -6
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +48 -1
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +14 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +69 -15
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +168 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +88 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +69 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +85 -78
- package/docs/index.md +5 -1
- package/docs/observabilite.md +81 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +378 -230
- package/docs/session.md +180 -180
- package/docs/sse.md +287 -0
- package/docs/upload.md +120 -117
- package/package.json +10 -11
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { isLoopbackHostname } from "nodefony";
|
|
2
|
+
// nodefony/src/proxy/rules.ts
|
|
3
|
+
/**
|
|
4
|
+
* Règles d'un montage du proxy inverse — UNIQUE source, lue par le schéma de
|
|
5
|
+
* configuration (`proxy.mounts`) ET par `ReverseProxy.mount()` : un montage
|
|
6
|
+
* écrit dans la config et un montage posé par un module obéissent aux mêmes
|
|
7
|
+
* refus, avec les mêmes mots.
|
|
8
|
+
*
|
|
9
|
+
* Pur : n'importe que le cœur, importable depuis `config/config.ts` sans
|
|
10
|
+
* cycle.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Préfixes que le framework se réserve : un montage n'y est JAMAIS accepté.
|
|
14
|
+
* `/nodefony/` porte la console d'administration et les API de Studio — les
|
|
15
|
+
* relayer ailleurs détournerait une surface protégée par le pare-feu.
|
|
16
|
+
*/
|
|
17
|
+
const RESERVED_PROXY_PREFIXES = ["/nodefony/"];
|
|
18
|
+
/**
|
|
19
|
+
* Préfixes réservés aux montages POSÉS PAR UN MODULE : la configuration ne
|
|
20
|
+
* peut pas les prendre. `/_vite/` appartient à `@nodefony/frontend`, qui y
|
|
21
|
+
* relaie ses serveurs Vite de développement.
|
|
22
|
+
*/
|
|
23
|
+
const MODULE_PROXY_PREFIXES = ["/_vite/"];
|
|
24
|
+
/** Jeton de méthode HTTP (RFC 9110 §9.1 → `token`, §5.6.2). */
|
|
25
|
+
const METHOD_TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
26
|
+
/** Nom de champ d'en-tête (RFC 9110 §5.1 → `token`). */
|
|
27
|
+
const HEADER_TOKEN = METHOD_TOKEN;
|
|
28
|
+
/**
|
|
29
|
+
* Normalise un préfixe d'URL : `/` en tête et en fin, sans `/` doublé. Règle
|
|
30
|
+
* UNIQUE des montages statiques et du proxy inverse — deux écritures du même
|
|
31
|
+
* préfixe doivent désigner la même entrée.
|
|
32
|
+
*
|
|
33
|
+
* @param prefix - préfixe tel qu'écrit (`"svc"`, `"/svc"`, `"/svc/"`)
|
|
34
|
+
* @returns le préfixe normalisé (`"/svc/"`)
|
|
35
|
+
*/
|
|
36
|
+
function normalizePrefix(prefix) {
|
|
37
|
+
let p = prefix.trim();
|
|
38
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
39
|
+
if (!p.endsWith("/")) p = `${p}/`;
|
|
40
|
+
return p.replace(/\/{2,}/g, "/");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Analyse une origine d'amont. Seule une origine NUE est acceptée : un chemin,
|
|
44
|
+
* une requête, un fragment ou des identifiants dans la cible déplaceraient en
|
|
45
|
+
* silence toutes les URL relayées, ou feraient voyager un secret dans la
|
|
46
|
+
* configuration.
|
|
47
|
+
*
|
|
48
|
+
* @param origin - `scheme://hôte[:port]`
|
|
49
|
+
* @returns l'URL analysée, ou `null` si ce n'est pas une origine http(s) nue
|
|
50
|
+
*/
|
|
51
|
+
function parseProxyOrigin(origin) {
|
|
52
|
+
let url;
|
|
53
|
+
try {
|
|
54
|
+
url = new URL(origin);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
59
|
+
if (!/^https?:\/\/[^/?#]+\/?$/i.test(origin.trim())) return null;
|
|
60
|
+
if (url.search !== "" || url.hash !== "") return null;
|
|
61
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
62
|
+
return url;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Examine un montage. Rend TOUS les refus, pas le premier : une configuration
|
|
66
|
+
* se corrige en une passe.
|
|
67
|
+
*
|
|
68
|
+
* @param prefix - préfixe tel qu'écrit
|
|
69
|
+
* @param mount - réglages du montage
|
|
70
|
+
* @param source - `"config"` (déclaré dans `proxy.mounts`) ou `"module"`
|
|
71
|
+
* (posé par un module) — seuls les préfixes réservés aux modules diffèrent
|
|
72
|
+
* @returns la liste des refus, vide si le montage est sain
|
|
73
|
+
*/
|
|
74
|
+
function proxyMountProblems(prefix, mount, source) {
|
|
75
|
+
const problems = [];
|
|
76
|
+
const raw = prefix.trim();
|
|
77
|
+
const p = normalizePrefix(raw);
|
|
78
|
+
if (p === "/") problems.push({
|
|
79
|
+
field: "prefix",
|
|
80
|
+
message: "le préfixe « / » relaierait TOUTE l'application, routes et pare-feu compris — monter un préfixe dédié (`/svc/`)"
|
|
81
|
+
});
|
|
82
|
+
else if (/[?#\s\\%]/.test(raw) || p.split("/").some((s) => s === "." || s === "..")) problems.push({
|
|
83
|
+
field: "prefix",
|
|
84
|
+
message: `préfixe « ${raw} » invalide — un chemin littéral, sans « ? », « # », « % », « \\ », espace ni segment « . » / « .. »`
|
|
85
|
+
});
|
|
86
|
+
else if (RESERVED_PROXY_PREFIXES.some((r) => p.startsWith(r))) problems.push({
|
|
87
|
+
field: "prefix",
|
|
88
|
+
message: `le préfixe « ${p} » est réservé au framework (console d'administration)`
|
|
89
|
+
});
|
|
90
|
+
else if (source === "config" && MODULE_PROXY_PREFIXES.some((r) => p.startsWith(r))) problems.push({
|
|
91
|
+
field: "prefix",
|
|
92
|
+
message: `le préfixe « ${p} » est réservé à @nodefony/frontend (serveurs Vite de développement), qui le monte lui-même`
|
|
93
|
+
});
|
|
94
|
+
let target = null;
|
|
95
|
+
if (typeof mount.target === "string") {
|
|
96
|
+
target = parseProxyOrigin(mount.target);
|
|
97
|
+
if (target === null) problems.push({
|
|
98
|
+
field: "target",
|
|
99
|
+
message: `cible « ${mount.target} » invalide — attendu une origine http(s) nue (\`scheme://hôte[:port]\`), sans chemin, requête ni identifiants`
|
|
100
|
+
});
|
|
101
|
+
} else if (typeof mount.target !== "function") problems.push({
|
|
102
|
+
field: "target",
|
|
103
|
+
message: "cible absente — une origine `scheme://hôte[:port]`"
|
|
104
|
+
});
|
|
105
|
+
if (mount.secure === false && target !== null) {
|
|
106
|
+
if (target.protocol !== "https:") problems.push({
|
|
107
|
+
field: "secure",
|
|
108
|
+
message: "`secure: false` ne concerne qu'une cible https: — le retirer"
|
|
109
|
+
});
|
|
110
|
+
else if (!isLoopbackHostname(target.hostname)) problems.push({
|
|
111
|
+
field: "secure",
|
|
112
|
+
message: `\`secure: false\` refusé vers « ${target.hostname} » : sans vérification de certificat, n'importe qui sur le chemin réseau se fait passer pour l'amont. Réservé à la boucle locale — sinon, fournir l'autorité de certification de l'amont au processus (\`http.certificates.extraCaFiles\`, ou NODE_EXTRA_CA_CERTS)`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (mount.methods !== void 0) {
|
|
116
|
+
if (mount.methods.length === 0) problems.push({
|
|
117
|
+
field: "methods",
|
|
118
|
+
message: "liste de méthodes vide : rien ne serait relayé — retirer `methods` pour toutes"
|
|
119
|
+
});
|
|
120
|
+
for (const m of mount.methods) if (!METHOD_TOKEN.test(m)) problems.push({
|
|
121
|
+
field: "methods",
|
|
122
|
+
message: `méthode « ${m} » invalide (RFC 9110 §9.1)`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
for (const h of mount.stripHeaders ?? []) if (!HEADER_TOKEN.test(h)) problems.push({
|
|
126
|
+
field: "stripHeaders",
|
|
127
|
+
message: `nom d'en-tête « ${h} » invalide`
|
|
128
|
+
});
|
|
129
|
+
else if (h.toLowerCase() === "host") problems.push({
|
|
130
|
+
field: "stripHeaders",
|
|
131
|
+
message: "« host » ne se retire pas : il est toujours posé — voir `preserveHost`"
|
|
132
|
+
});
|
|
133
|
+
if (mount.timeoutMs !== void 0 && (!Number.isInteger(mount.timeoutMs) || mount.timeoutMs <= 0)) problems.push({
|
|
134
|
+
field: "timeoutMs",
|
|
135
|
+
message: `délai « ${mount.timeoutMs} » invalide — un entier de millisecondes > 0`
|
|
136
|
+
});
|
|
137
|
+
return problems;
|
|
138
|
+
}
|
|
139
|
+
/** Encodages qui changent le SENS d'un chemin selon qui le décode. */
|
|
140
|
+
const AMBIGUOUS_ENCODING = /%(?:2e|2f|5c|00|25)/i;
|
|
141
|
+
/**
|
|
142
|
+
* Le chemin d'une requête relayée est-il ambigu ? Un chemin que le proxy et
|
|
143
|
+
* l'amont pourraient lire différemment — segment `.`/`..`, barre inverse,
|
|
144
|
+
* octet nul, ou leur forme encodée (`%2e`, `%2f`, `%5c`, `%00`, `%25`) — est
|
|
145
|
+
* REFUSÉ (400), jamais normalisé en silence : c'est le défaut de Traefik, et
|
|
146
|
+
* la parade aux contournements de préfixe (`/svc/..%2fadmin`).
|
|
147
|
+
*
|
|
148
|
+
* @param url - cible brute de la requête (chemin + requête éventuelle)
|
|
149
|
+
* @returns `true` si le chemin doit être refusé
|
|
150
|
+
*/
|
|
151
|
+
function isAmbiguousPath(url) {
|
|
152
|
+
const q = url.indexOf("?");
|
|
153
|
+
const path = q === -1 ? url : url.slice(0, q);
|
|
154
|
+
if (path.includes("\\") || path.includes("\0")) return true;
|
|
155
|
+
if (AMBIGUOUS_ENCODING.test(path)) return true;
|
|
156
|
+
for (const segment of path.split("/")) if (segment === "." || segment === "..") return true;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
/** Clé de handshake : 16 octets en base64 (RFC 6455 §4.1, §4.2.1). */
|
|
160
|
+
const WS_KEY = /^\s*[A-Za-z0-9+/]{22}==\s*$/;
|
|
161
|
+
/** `Upgrade: websocket`, casse et blancs indifférents. */
|
|
162
|
+
const UPGRADE_WEBSOCKET = /^\s*websocket\s*$/i;
|
|
163
|
+
/** Le jeton `upgrade` dans une liste `Connection` (RFC 9110 §7.6.1). */
|
|
164
|
+
const CONNECTION_UPGRADE = /(?:^|,)\s*upgrade\s*(?:,|$)/i;
|
|
165
|
+
/**
|
|
166
|
+
* Un upgrade est-il un handshake WebSocket conforme (RFC 6455 §4.2.1) ? Seul
|
|
167
|
+
* un tel handshake est relayé : un `Upgrade` quelconque (`h2c`…) raccordé
|
|
168
|
+
* octet pour octet ouvrirait un tunnel que rien n'inspecte.
|
|
169
|
+
*
|
|
170
|
+
* Appelé à CHAQUE handshake (proxy inverse et routes de Nodefony) : aucune
|
|
171
|
+
* allocation — des expressions testées, jamais de découpe de chaîne.
|
|
172
|
+
*
|
|
173
|
+
* @param method - méthode de la requête
|
|
174
|
+
* @param headers - en-têtes reçus
|
|
175
|
+
* @returns la raison du refus, ou `null` si le handshake est conforme
|
|
176
|
+
*/
|
|
177
|
+
function websocketHandshakeProblem(method, headers) {
|
|
178
|
+
if (method !== "GET") return "méthode autre que GET";
|
|
179
|
+
const upgrade = headers.upgrade;
|
|
180
|
+
if (typeof upgrade !== "string" || !UPGRADE_WEBSOCKET.test(upgrade)) return "Upgrade autre que websocket";
|
|
181
|
+
const connection = headers.connection;
|
|
182
|
+
if (!(typeof connection === "string" ? CONNECTION_UPGRADE.test(connection) : Array.isArray(connection) && connection.some((token) => CONNECTION_UPGRADE.test(token)))) return "Connection sans upgrade";
|
|
183
|
+
if (headers["sec-websocket-version"] !== "13") return "Sec-WebSocket-Version ≠ 13";
|
|
184
|
+
const key = headers["sec-websocket-key"];
|
|
185
|
+
if (typeof key !== "string" || !WS_KEY.test(key)) return "Sec-WebSocket-Key invalide";
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
export { MODULE_PROXY_PREFIXES, RESERVED_PROXY_PREFIXES, isAmbiguousPath, normalizePrefix, parseProxyOrigin, proxyMountProblems, websocketHandshakeProblem };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertPageQuery } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/rateLimit/MemoryRateLimitStore.ts
|
|
3
3
|
/**
|
|
4
4
|
* Store de rate-limit **en mémoire** — algorithme *fixed window* par clé (IP).
|
|
5
5
|
*
|
|
@@ -142,5 +142,4 @@ var MemoryRateLimitStore = class {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
|
-
//#endregion
|
|
146
145
|
export { MemoryRateLimitStore };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/rateLimit/WsConnectionCounter.ts
|
|
2
2
|
/**
|
|
3
3
|
* Compteur de connexions WebSocket CONCURRENTES par IP — backstop opt-in
|
|
4
4
|
* (F6c, revue 0.6). Distinct du {@link MemoryRateLimitStore} : celui-ci compte un
|
|
@@ -60,5 +60,4 @@ var WsConnectionCounter = class {
|
|
|
60
60
|
return this.#rejectedTotal;
|
|
61
61
|
}
|
|
62
62
|
};
|
|
63
|
-
//#endregion
|
|
64
63
|
export { WsConnectionCounter, WsConnectionCounter as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/rateLimit/rateLimitFilters.ts
|
|
2
2
|
/**
|
|
3
3
|
* **Le vocabulaire de filtre du registre de rate-limit**, en noms PUBLICS —
|
|
4
4
|
* celui que la console écrit dans l'URL (`?limited=true`).
|
|
@@ -16,5 +16,4 @@
|
|
|
16
16
|
const RATE_LIMIT_FILTERS = {
|
|
17
17
|
/** `true` = seulement les clés au plafond, `false` = seulement les autres. */
|
|
18
18
|
limited: "boolean" };
|
|
19
|
-
//#endregion
|
|
20
19
|
export { RATE_LIMIT_FILTERS };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// nodefony/src/servers/http2Limits.ts
|
|
2
|
+
/**
|
|
3
|
+
* Limites anti-DoS d'un serveur HTTP/2, lues dans la section `http2` du module :
|
|
4
|
+
* `maxSessionMemory` (amplification mémoire) et `maxConcurrentStreams` dans les
|
|
5
|
+
* SETTINGS (CVE-2023-44487, Rapid Reset). Règle UNIQUE des serveurs HTTP/2 — TLS
|
|
6
|
+
* (5152) comme h2c — : une limite oubliée sur l'un d'eux en ferait la porte de
|
|
7
|
+
* l'attaque.
|
|
8
|
+
*
|
|
9
|
+
* Une limite absente garde le défaut de Node (aucune régression).
|
|
10
|
+
*
|
|
11
|
+
* @param limits - `module.options.http2`.
|
|
12
|
+
* @returns les options à fusionner dans celles de `createServer`/`createSecureServer`.
|
|
13
|
+
*/
|
|
14
|
+
function http2ServerLimits(limits) {
|
|
15
|
+
const options = {};
|
|
16
|
+
if (limits?.maxSessionMemory) options.maxSessionMemory = limits.maxSessionMemory;
|
|
17
|
+
if (limits?.maxConcurrentStreams) options.settings = { maxConcurrentStreams: limits.maxConcurrentStreams };
|
|
18
|
+
return options;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Borne l'inactivité des sessions d'un serveur HTTP/2 — TLS (5152) comme h2c —
|
|
22
|
+
* et contourne un défaut de Node qui garde chaque session fermée en vie jusqu'à
|
|
23
|
+
* l'échéance de ce délai.
|
|
24
|
+
*
|
|
25
|
+
* Le défaut (Node 24, 26) : `closeSession` (`internal/http2/core`) marque la
|
|
26
|
+
* session détruite PUIS appelle `session.setTimeout(0)` ; or `setStreamTimeout`
|
|
27
|
+
* sort aussitôt sur un objet détruit. Le minuteur n'est jamais désarmé et retient
|
|
28
|
+
* la session (~2,4 Ko) pendant `timeoutMs` : sous un flux de connexions neuves,
|
|
29
|
+
* connexions/s × délai × 2,4 Ko. Signalé à Node : https://github.com/nodejs/node/issues/66885
|
|
30
|
+
*
|
|
31
|
+
* Le contournement : désarmer à la réception du GOAWAY, AVANT la destruction —
|
|
32
|
+
* c'est ainsi que ferment un proxy et un client Node, même par `destroy()`. Rien
|
|
33
|
+
* n'est retiré à la protection : Node n'agit déjà plus à l'échéance sur une
|
|
34
|
+
* session fermée (`sessionOnTimeout` sort sur `closed`). Une coupure TCP sans
|
|
35
|
+
* GOAWAY garde la rétention, bornée au délai.
|
|
36
|
+
*
|
|
37
|
+
* @param server - le serveur HTTP/2.
|
|
38
|
+
* @param timeoutMs - délai d'inactivité ; 0 ou absent le laisse désactivé.
|
|
39
|
+
* @param onTimeout - appelé à l'échéance (événement `timeout` du serveur).
|
|
40
|
+
*/
|
|
41
|
+
function setHttp2SessionTimeout(server, timeoutMs, onTimeout) {
|
|
42
|
+
if (!timeoutMs) return;
|
|
43
|
+
server.setTimeout(timeoutMs, onTimeout);
|
|
44
|
+
server.on("session", (session) => {
|
|
45
|
+
session.once("goaway", () => {
|
|
46
|
+
session.setTimeout(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export { http2ServerLimits, setHttp2SessionTimeout };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/servers/kernelServers.ts
|
|
2
2
|
/**
|
|
3
3
|
* Rétrécit la section `servers` de la configuration du kernel (lue dans le sac
|
|
4
4
|
* de config non typé du cœur) en {@link ServersConfig}, sans propager `any`.
|
|
@@ -13,6 +13,15 @@ function asServersConfig(servers) {
|
|
|
13
13
|
return servers !== null && typeof servers === "object" ? servers : void 0;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
+
* Le serveur h2c démarre-t-il ? Seul un `enabled: true` EXPLICITE l'allume : la
|
|
17
|
+
* section existe toujours (défauts), un port seul ne vaut pas activation.
|
|
18
|
+
*
|
|
19
|
+
* @param entry - `servers.h2c`.
|
|
20
|
+
*/
|
|
21
|
+
function isH2cEnabled(entry) {
|
|
22
|
+
return entry?.enabled === true;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
16
25
|
* Port DEMANDÉ pour un serveur (`servers.<type>.port`), ou `undefined` si le
|
|
17
26
|
* serveur est désactivé, absent, ou sans port numérique.
|
|
18
27
|
*
|
|
@@ -22,8 +31,8 @@ function asServersConfig(servers) {
|
|
|
22
31
|
function configuredServerPort(servers, type) {
|
|
23
32
|
const entry = servers?.[type];
|
|
24
33
|
if (!entry) return void 0;
|
|
34
|
+
if (type === "h2c" && !isH2cEnabled(servers.h2c)) return void 0;
|
|
25
35
|
const port = entry.port;
|
|
26
36
|
return typeof port === "number" ? port : void 0;
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
export { asServersConfig, configuredServerPort };
|
|
38
|
+
export { asServersConfig, configuredServerPort, isH2cEnabled };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isH2cEnabled } from "./kernelServers.js";
|
|
1
2
|
import { isPortListening, resolvePortPolicy } from "nodefony";
|
|
2
3
|
import cluster from "node:cluster";
|
|
3
|
-
|
|
4
|
+
// nodefony/src/servers/portBinder.ts
|
|
4
5
|
/**
|
|
5
6
|
* **portBinder** — écoute sur le port désiré, ou sur le prochain port libre.
|
|
6
7
|
*
|
|
@@ -48,27 +49,58 @@ import cluster from "node:cluster";
|
|
|
48
49
|
*/
|
|
49
50
|
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
50
51
|
const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
52
|
+
const BOUND_SERVERS = [
|
|
53
|
+
"http",
|
|
54
|
+
"https",
|
|
55
|
+
"h2c"
|
|
56
|
+
];
|
|
51
57
|
/** Port configuré d'un serveur, ou `0` (désactivé / non précisé → choix noyau). */
|
|
52
|
-
function configuredPort(
|
|
58
|
+
function configuredPort(servers, which) {
|
|
59
|
+
if (which === "h2c") {
|
|
60
|
+
const entry = servers?.h2c;
|
|
61
|
+
return isH2cEnabled(entry) ? entry?.port ?? 0 : 0;
|
|
62
|
+
}
|
|
63
|
+
const entry = servers?.[which];
|
|
53
64
|
if (!entry) return 0;
|
|
54
65
|
return entry.port ?? 0;
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
68
|
+
* Serveur actif qui convoite le MÊME port que h2c, ou `null`.
|
|
69
|
+
*
|
|
70
|
+
* Un port partagé avec h2c n'est pas un conflit à contourner : sur ce port, le
|
|
71
|
+
* client HTTP/1.1 et le proxy h2c se marchent dessus, et le repli `auto` ferait
|
|
72
|
+
* glisser l'un des deux là où personne ne l'attend. Le boot le refuse.
|
|
73
|
+
*
|
|
74
|
+
* @param servers - `kernel.options.servers`.
|
|
75
|
+
* @returns `"http"` ou `"https"` en conflit, sinon `null` (h2c inactif, port 0).
|
|
76
|
+
*/
|
|
77
|
+
function h2cPortConflict(servers) {
|
|
78
|
+
const port = configuredPort(servers, "h2c");
|
|
79
|
+
if (port === 0) return null;
|
|
80
|
+
if (configuredPort(servers, "http") === port) return "http";
|
|
81
|
+
if (configuredPort(servers, "https") === port) return "https";
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
57
85
|
* Compose le plan de bind d'un serveur depuis la config du kernel — source UNIQUE
|
|
58
|
-
* (les
|
|
86
|
+
* (les serveurs l'appellent ; la politique n'est décidée qu'ici).
|
|
59
87
|
*
|
|
60
88
|
* @param which - le serveur qu'on borne.
|
|
61
89
|
* @param servers - `kernel.options.servers`.
|
|
62
90
|
* @param environment - `kernel.environment` (arbitre le défaut de la politique).
|
|
63
91
|
*/
|
|
64
92
|
function buildBindPlan(which, servers, environment) {
|
|
65
|
-
const desired = configuredPort(servers
|
|
66
|
-
const
|
|
67
|
-
const
|
|
93
|
+
const desired = configuredPort(servers, which);
|
|
94
|
+
const reserved = [];
|
|
95
|
+
for (const other of BOUND_SERVERS) {
|
|
96
|
+
if (other === which) continue;
|
|
97
|
+
const port = configuredPort(servers, other);
|
|
98
|
+
if (port > 0 && port !== desired) reserved.push(port);
|
|
99
|
+
}
|
|
68
100
|
return {
|
|
69
101
|
desired,
|
|
70
|
-
reserved
|
|
71
|
-
attempts:
|
|
102
|
+
reserved,
|
|
103
|
+
attempts: resolvePortPolicy(environment, servers?.portPolicy) === "auto" ? servers?.portRetryAttempts ?? 20 : 0
|
|
72
104
|
};
|
|
73
105
|
}
|
|
74
106
|
/** Adresses qui désignent « toutes les interfaces » — rien ne s'y connecte. */
|
|
@@ -224,5 +256,4 @@ async function bindWithFallback(server, host, plan, probe = isPortListening, sha
|
|
|
224
256
|
candidate = next;
|
|
225
257
|
}
|
|
226
258
|
}
|
|
227
|
-
|
|
228
|
-
export { DEFAULT_PORT_RETRY_ATTEMPTS, bindWithFallback, buildBindPlan, conflictProbeTargets, detectPortConflict, resolvePortPolicy };
|
|
259
|
+
export { DEFAULT_PORT_RETRY_ATTEMPTS, bindWithFallback, buildBindPlan, conflictProbeTargets, detectPortConflict, h2cPortConflict, resolvePortPolicy };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { websocketHandshakeProblem } from "../proxy/rules.js";
|
|
2
|
+
import { refuseUpgrade } from "../proxy/forward.js";
|
|
3
|
+
import { STATUS_CODES } from "node:http";
|
|
4
|
+
// nodefony/src/servers/upgradeDispatch.ts
|
|
5
|
+
/** Défaut sans noyau : tout upgrade conforme est accepté puis émis en `connection`. */
|
|
6
|
+
function acceptAll(wss) {
|
|
7
|
+
return (req, admission) => {
|
|
8
|
+
const accepted = admission.accept();
|
|
9
|
+
if (accepted instanceof Promise) accepted.then((ws) => {
|
|
10
|
+
if (ws !== null) wss.emit("connection", ws, req);
|
|
11
|
+
});
|
|
12
|
+
else if (accepted !== null) wss.emit("connection", accepted, req);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Le verdict d'UN upgrade. Pendant que le noyau décide, le socket n'a plus
|
|
17
|
+
* d'écouteur `error` (`node:http` retire les siens à l'émission d'`upgrade`,
|
|
18
|
+
* `ws` ne pose les siens qu'à `handleUpgrade`) : une coupure du client
|
|
19
|
+
* (`ECONNRESET`) y deviendrait une erreur non gérée qui abat le process. D'où
|
|
20
|
+
* l'écouteur de la fenêtre d'attente, retiré quand le verdict tombe.
|
|
21
|
+
*
|
|
22
|
+
* Une classe, pas un objet de fermetures : une allocation par handshake (plus
|
|
23
|
+
* l'écouteur), méthodes partagées par le prototype.
|
|
24
|
+
*/
|
|
25
|
+
var UpgradeAdmission = class {
|
|
26
|
+
wss;
|
|
27
|
+
req;
|
|
28
|
+
socket;
|
|
29
|
+
head;
|
|
30
|
+
#accepted = void 0;
|
|
31
|
+
#settled = false;
|
|
32
|
+
#onPendingError;
|
|
33
|
+
constructor(wss, req, socket, head) {
|
|
34
|
+
this.wss = wss;
|
|
35
|
+
this.req = req;
|
|
36
|
+
this.socket = socket;
|
|
37
|
+
this.head = head;
|
|
38
|
+
this.#onPendingError = () => socket.destroy();
|
|
39
|
+
socket.on("error", this.#onPendingError);
|
|
40
|
+
}
|
|
41
|
+
#settle() {
|
|
42
|
+
if (this.#settled) return false;
|
|
43
|
+
this.#settled = true;
|
|
44
|
+
this.socket.removeListener("error", this.#onPendingError);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
accept() {
|
|
48
|
+
if (this.#accepted !== void 0) return this.#accepted;
|
|
49
|
+
if (!this.#settle()) return this.#accepted = null;
|
|
50
|
+
const { socket } = this;
|
|
51
|
+
if (socket.destroyed || !socket.writable) {
|
|
52
|
+
socket.destroy();
|
|
53
|
+
return this.#accepted = null;
|
|
54
|
+
}
|
|
55
|
+
let ws;
|
|
56
|
+
let late = null;
|
|
57
|
+
const onClose = () => late?.(null);
|
|
58
|
+
this.wss.handleUpgrade(this.req, socket, this.head, (accepted) => {
|
|
59
|
+
if (late === null) {
|
|
60
|
+
ws = accepted;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
socket.removeListener("close", onClose);
|
|
64
|
+
late(accepted);
|
|
65
|
+
});
|
|
66
|
+
if (ws !== void 0) return this.#accepted = ws;
|
|
67
|
+
socket.once("close", onClose);
|
|
68
|
+
return this.#accepted = new Promise((resolve) => {
|
|
69
|
+
late = resolve;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
refuse(status, headers) {
|
|
73
|
+
if (!this.#settle()) return;
|
|
74
|
+
refuseUpgrade(this.socket, status, STATUS_CODES[status] ?? "Refused", headers);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Pose l'UNIQUE écouteur `upgrade` d'un serveur : le proxy inverse d'abord,
|
|
79
|
+
* puis le serveur WebSocket de Nodefony.
|
|
80
|
+
*
|
|
81
|
+
* Pourquoi un seul écouteur : `ws` attaché par son option `server` prend TOUS
|
|
82
|
+
* les upgrades et répond `400` à ceux qu'il ne reconnaît pas. Un second
|
|
83
|
+
* écouteur, même posé avant, ne l'empêcherait pas de répondre sur un socket
|
|
84
|
+
* déjà relayé. Le serveur `ws` est donc créé en `noServer`, et c'est ici que
|
|
85
|
+
* l'upgrade lui est confié — avec ses propres contrôles intacts (`path`,
|
|
86
|
+
* `verifyClient`, `maxPayload`…), que `handleUpgrade` applique.
|
|
87
|
+
*
|
|
88
|
+
* Entre les deux, `refusal` tranche ce qui doit être refusé AVANT le `101`
|
|
89
|
+
* (RFC 6455 §4.2.2) : un chemin sans route WebSocket reçoit un statut HTTP,
|
|
90
|
+
* jamais une ouverture suivie d'une fermeture. Puis la FORME du handshake est
|
|
91
|
+
* contrôlée (`websocketHandshakeProblem`, la règle du proxy inverse) : un
|
|
92
|
+
* handshake malformé reçoit `400` sans jamais atteindre le noyau. Enfin
|
|
93
|
+
* `admit` décide — pare-feu, garde de l'action — et seul son verdict ouvre.
|
|
94
|
+
*
|
|
95
|
+
* Sans montage, le coût par connexion est une lecture de champ.
|
|
96
|
+
*
|
|
97
|
+
* @param server - serveur HTTP(S) qui reçoit les upgrades
|
|
98
|
+
* @param wss - serveur WebSocket de Nodefony (`noServer: true`)
|
|
99
|
+
* @param relay - proxy inverse, lu à chaque upgrade (il peut naître après)
|
|
100
|
+
* @param refusal - statut HTTP qui refuse l'upgrade, `null` pour l'admettre
|
|
101
|
+
* (cf `HttpKernel.websocketUpgradeRefusal`)
|
|
102
|
+
* @param admit - décide d'un upgrade admis (cf `HttpKernel.onWebsocketUpgrade`) ;
|
|
103
|
+
* par défaut, tout upgrade conforme est accepté et émis en `connection`
|
|
104
|
+
* @returns la fonction qui retire l'écouteur — à appeler à l'arrêt
|
|
105
|
+
*/
|
|
106
|
+
function attachUpgradeDispatch(server, wss, relay, refusal, admit = acceptAll(wss)) {
|
|
107
|
+
const onUpgrade = (req, socket, head) => {
|
|
108
|
+
const proxy = relay();
|
|
109
|
+
if (proxy !== null && proxy.mounts !== null) {
|
|
110
|
+
if (proxy.handleUpgrade(req, socket, head)) return;
|
|
111
|
+
}
|
|
112
|
+
const status = refusal(req);
|
|
113
|
+
if (status !== null) {
|
|
114
|
+
refuseUpgrade(socket, status, STATUS_CODES[status] ?? "Refused");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (websocketHandshakeProblem(req.method, req.headers) !== null) {
|
|
118
|
+
refuseUpgrade(socket, 400, STATUS_CODES[400] ?? "Bad Request");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
admit(req, new UpgradeAdmission(wss, req, socket, head));
|
|
122
|
+
};
|
|
123
|
+
server.on("upgrade", onUpgrade);
|
|
124
|
+
return () => {
|
|
125
|
+
server.removeListener("upgrade", onUpgrade);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export { attachUpgradeDispatch };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Cookie from "../cookies/cookie.js";
|
|
2
2
|
import { extend } from "nodefony";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
|
|
4
|
+
// nodefony/src/session/session.ts
|
|
5
5
|
const defaultSessionOptions = {
|
|
6
6
|
name: "nodefony",
|
|
7
7
|
strictMode: true,
|
|
@@ -9,6 +9,20 @@ const defaultSessionOptions = {
|
|
|
9
9
|
};
|
|
10
10
|
/** Taille de l'identifiant opaque (octets CSPRNG → base64url, 43 chars). */
|
|
11
11
|
const SESSION_ID_BYTES = 32;
|
|
12
|
+
/** Un sac de session relu du storage : objet, ni `null` ni tableau. */
|
|
13
|
+
function isBag(v) {
|
|
14
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Une date relue du storage : `Date` d'un store mémoire, chaîne ou nombre d'un
|
|
18
|
+
* store sérialisé. Toute autre forme → `undefined` (la valeur courante reste).
|
|
19
|
+
* Convertie UNE fois à la lecture : `created`/`updated` sont alors toujours des
|
|
20
|
+
* `Date`, et les contrôles du chemin chaud lisent `getTime()` sans allouer.
|
|
21
|
+
*/
|
|
22
|
+
function toDate(v) {
|
|
23
|
+
if (v instanceof Date) return v;
|
|
24
|
+
return typeof v === "string" || typeof v === "number" ? new Date(v) : void 0;
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* Session serveur Nodefony — état persistant lié à une identité, indexé par un
|
|
14
28
|
* identifiant **opaque** (porté par le cookie) dans un {@link ISessionStorage}
|
|
@@ -157,16 +171,19 @@ var Session = class {
|
|
|
157
171
|
* `onSaveSession`.
|
|
158
172
|
*
|
|
159
173
|
* @param user - principal authentifié (string) lié au blob ; défaut courant.
|
|
174
|
+
* Fourni, il DEVIENT le principal de la session : une sauvegarde suivante,
|
|
175
|
+
* dans la même requête, le garde sans qu'on le lui redonne.
|
|
160
176
|
*/
|
|
161
177
|
async save(user) {
|
|
178
|
+
if (user !== void 0) this.user = user;
|
|
162
179
|
if (this.readOnly) {
|
|
163
180
|
if (this.mutated) this.log(`READONLY SESSION mutated — write skipped : ${this.name}`, "WARNING");
|
|
164
181
|
return this;
|
|
165
182
|
}
|
|
166
183
|
if (!this.mutated) return this;
|
|
167
184
|
const stored = await this.storage.write(this.id, this.serialize(user));
|
|
168
|
-
this.created = stored.createdAt ?? this.created;
|
|
169
|
-
this.updated = stored.updatedAt ?? this.updated;
|
|
185
|
+
this.created = toDate(stored.createdAt) ?? this.created;
|
|
186
|
+
this.updated = toDate(stored.updatedAt) ?? this.updated;
|
|
170
187
|
this.mutated = false;
|
|
171
188
|
this.saved = true;
|
|
172
189
|
if (this.context) await this.context.fireAsync("onSaveSession", this);
|
|
@@ -221,7 +238,7 @@ var Session = class {
|
|
|
221
238
|
}
|
|
222
239
|
isValidSession(_data, context) {
|
|
223
240
|
if (this.options.refererCheck) {
|
|
224
|
-
let sameHost
|
|
241
|
+
let sameHost;
|
|
225
242
|
try {
|
|
226
243
|
sameHost = this.checkSecureReferer(context);
|
|
227
244
|
} catch {
|
|
@@ -234,15 +251,14 @@ var Session = class {
|
|
|
234
251
|
}
|
|
235
252
|
const now = Date.now();
|
|
236
253
|
if (this.options.absoluteTimeoutS && this.created) {
|
|
237
|
-
if (now -
|
|
254
|
+
if (!(now - this.created.getTime() <= this.options.absoluteTimeoutS * 1e3)) {
|
|
238
255
|
this.log(`SESSION EXPIRED (absolute) ==> ${this.name} : ${this.id}`, "WARNING");
|
|
239
256
|
return false;
|
|
240
257
|
}
|
|
241
258
|
}
|
|
242
259
|
const idleS = this.options.idleTimeoutS;
|
|
243
260
|
if (idleS && idleS > 0 && this.updated) {
|
|
244
|
-
|
|
245
|
-
if (lastUsed && lastUsed + idleS * 1e3 < now) {
|
|
261
|
+
if (!(this.updated.getTime() + idleS * 1e3 >= now)) {
|
|
246
262
|
this.log(`SESSION EXPIRED (idle) ==> ${this.name} : ${this.id}`, "WARNING");
|
|
247
263
|
return false;
|
|
248
264
|
}
|
|
@@ -270,7 +286,7 @@ var Session = class {
|
|
|
270
286
|
if (typeof storage.touch !== "function") return;
|
|
271
287
|
if (!this.updated) return;
|
|
272
288
|
const now = Date.now();
|
|
273
|
-
const lastUsed =
|
|
289
|
+
const lastUsed = this.updated.getTime();
|
|
274
290
|
if (lastUsed && now - lastUsed < idleS * 1e3 / 2) return;
|
|
275
291
|
await storage.touch(this.id, idleS);
|
|
276
292
|
this.updated = new Date(now);
|
|
@@ -356,12 +372,12 @@ var Session = class {
|
|
|
356
372
|
}
|
|
357
373
|
deSerialize(serialized) {
|
|
358
374
|
const data = serialized;
|
|
359
|
-
if (data.Attributes) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
360
|
-
if (data.metaBag) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
361
|
-
if (data.flashBag) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
362
|
-
this.created = data.createdAt ?? this.created;
|
|
363
|
-
this.updated = data.updatedAt ?? this.updated;
|
|
364
|
-
if (data.user) this.user = data.user;
|
|
375
|
+
if (isBag(data.Attributes)) for (const k in data.Attributes) this.attributesBag[k] = data.Attributes[k];
|
|
376
|
+
if (isBag(data.metaBag)) for (const k in data.metaBag) this.metaBagStore[k] = data.metaBag[k];
|
|
377
|
+
if (isBag(data.flashBag)) for (const k in data.flashBag) this.flashBag[k] = data.flashBag[k];
|
|
378
|
+
this.created = toDate(data.createdAt) ?? this.created;
|
|
379
|
+
this.updated = toDate(data.updatedAt) ?? this.updated;
|
|
380
|
+
if (typeof data.user === "string" && data.user) this.user = data.user;
|
|
365
381
|
}
|
|
366
382
|
/** Réinitialise les trois sacs (attributs, métas, flash) — état vide. */
|
|
367
383
|
clear() {
|
|
@@ -394,5 +410,4 @@ var Session = class {
|
|
|
394
410
|
}
|
|
395
411
|
}
|
|
396
412
|
};
|
|
397
|
-
//#endregion
|
|
398
413
|
export { Session as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { thrownMessage } from "../errors/thrownMessage.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/session/sessionStoreFailure.ts
|
|
3
3
|
/**
|
|
4
4
|
* Reconnaît une table de session absente, quel que soit le dialecte.
|
|
5
5
|
*
|
|
@@ -26,5 +26,4 @@ function describeSessionStoreFailure(error) {
|
|
|
26
26
|
message: `La session n'a pas pu être persistée — la requête est servie en 500 plutôt que laissée sans réponse. Cause : ${cause}.${looksLikeMissingTable(cause) ? " La table de session est probablement absente : appliquer les migrations (`nodefony orm:migrate`) avant de servir, ou choisir un autre stockage (`http.session.store`)." : " Vérifier que le stockage de session déclaré sous `http.session.store` est joignable ; à défaut, appliquer les migrations (`nodefony orm:migrate`)."}`
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
-
//#endregion
|
|
30
29
|
export { describeSessionStoreFailure };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SESSION_DEFAULT_ORDER, SESSION_SORTABLE_FIELDS } from "./sessionSort.js";
|
|
2
2
|
import { assertPageQuery, compareByOrder, pickOrder } from "nodefony";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/session/storage/MemorySessionStorage.ts
|
|
4
4
|
/**
|
|
5
5
|
* Store de sessions **en mémoire** (Map process) — implémentation de référence
|
|
6
6
|
* d'{@link ISessionStorage}, pendant `session` des `Memory*Store` de sécurité.
|
|
@@ -181,5 +181,4 @@ var MemorySessionStorage = class {
|
|
|
181
181
|
return Promise.resolve(users.size);
|
|
182
182
|
}
|
|
183
183
|
};
|
|
184
|
-
//#endregion
|
|
185
184
|
export { MemorySessionStorage as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/session/storage/RevocationGuardStorage.ts
|
|
2
2
|
/**
|
|
3
3
|
* Durée de vie (ms) d'une pierre tombale de révocation. Couvre la fenêtre
|
|
4
4
|
* pendant laquelle une requête « en vol » — qui avait chargé la session AVANT
|
|
@@ -148,5 +148,4 @@ var RevocationGuardStorage = class {
|
|
|
148
148
|
if (t.size === 0) this.#tombstones = null;
|
|
149
149
|
}
|
|
150
150
|
};
|
|
151
|
-
//#endregion
|
|
152
151
|
export { RevocationGuardStorage as default };
|