@nodefony/http 10.0.0-beta.1 → 10.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +48 -7
- package/dist/nodefony/config/defineModuleConfig.js +1 -1
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +0 -1
- package/dist/nodefony/service/audit-logger.js +2 -3
- package/dist/nodefony/service/certificates.js +214 -158
- package/dist/nodefony/service/error-renderer.js +1 -1
- package/dist/nodefony/service/http-kernel.js +221 -47
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-static.js +4 -7
- package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
- package/dist/nodefony/service/servers/server-websocket.js +12 -4
- package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
- package/dist/nodefony/service/sessions/sessions-service.js +30 -9
- package/dist/nodefony/service/trace.js +2 -1
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/context/Context.js +28 -1
- package/dist/nodefony/src/context/domainMatcher.js +20 -2
- package/dist/nodefony/src/context/forwarded.js +2 -2
- package/dist/nodefony/src/context/http/HttpContext.js +91 -40
- package/dist/nodefony/src/context/http/Request.js +9 -4
- package/dist/nodefony/src/context/http/Response.js +46 -7
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/context/responseEnded.js +27 -0
- package/dist/nodefony/src/context/websocket/Response.js +9 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
- package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
- package/dist/nodefony/src/profiler/Profiler.js +6 -10
- package/dist/nodefony/src/proxy/forward.js +476 -0
- package/dist/nodefony/src/proxy/rules.js +182 -0
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
- package/dist/nodefony/src/session/session.js +28 -12
- package/dist/types/index.d.ts +5 -0
- package/dist/types/nodefony/config/config.d.ts +17 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +126 -50
- package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
- 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/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +22 -2
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +13 -13
- package/docs/index.md +9 -3
- package/docs/observabilite.md +18 -18
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +115 -78
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +37 -37
- package/package.json +10 -13
|
@@ -1,15 +1,125 @@
|
|
|
1
1
|
import { asServersConfig } from "../src/servers/kernelServers.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { SHA1_WITH_RSA_OID, createSelfSignedCertificate, distinguishedNameField, parseSubjectAltName, signatureAlgorithmName, signatureAlgorithmOid } from "./x509.js";
|
|
3
|
+
import { BootConfigurationError, Service, extend } from "nodefony";
|
|
4
|
+
import { X509Certificate, generateKeyPair, randomBytes } from "node:crypto";
|
|
4
5
|
import fsp from "node:fs/promises";
|
|
5
6
|
import path, { resolve } from "node:path";
|
|
7
|
+
import tls from "node:tls";
|
|
6
8
|
import { promisify } from "node:util";
|
|
7
9
|
import { execFile } from "node:child_process";
|
|
8
10
|
//#region nodefony/service/certificates.ts
|
|
9
11
|
const execFileAsync = promisify(execFile);
|
|
12
|
+
const generateKeyPairAsync = promisify(generateKeyPair);
|
|
13
|
+
/** Compare deux PEM sans dépendre des fins de ligne ni des espaces. */
|
|
14
|
+
function normalizePem(pem) {
|
|
15
|
+
return pem.replace(/\s+/g, "");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ajoute une autorité aux ancres TLS par défaut du processus (requêtes sortantes
|
|
19
|
+
* `fetch`, `https`, `tls.connect`), sans en retirer aucune.
|
|
20
|
+
*
|
|
21
|
+
* C'est l'équivalent en cours d'exécution de `NODE_EXTRA_CA_CERTS`, qui n'est lu
|
|
22
|
+
* qu'au lancement du processus — trop tôt pour une CA que le boot vient de
|
|
23
|
+
* générer, et que seul un script de banc posait. La capacité se CONSTATE
|
|
24
|
+
* (`tls.setDefaultCACertificates` n'existe qu'à partir de Node 24.5), elle ne se
|
|
25
|
+
* déduit pas d'un numéro de version. Idempotent : une autorité déjà présente
|
|
26
|
+
* n'est pas ajoutée deux fois.
|
|
27
|
+
*
|
|
28
|
+
* @param pem - autorité au format PEM.
|
|
29
|
+
* @param store - accès aux ancres ; `node:tls` par défaut.
|
|
30
|
+
* @returns `added`, `present` (déjà de confiance), ou `unsupported`.
|
|
31
|
+
*/
|
|
32
|
+
function addDefaultCaCertificate(pem, store = tls) {
|
|
33
|
+
const { getCACertificates, setDefaultCACertificates } = store;
|
|
34
|
+
if (typeof getCACertificates !== "function" || typeof setDefaultCACertificates !== "function") return "unsupported";
|
|
35
|
+
const current = getCACertificates("default");
|
|
36
|
+
const wanted = normalizePem(pem);
|
|
37
|
+
if (current.some((c) => normalizePem(c) === wanted)) return "present";
|
|
38
|
+
setDefaultCACertificates([...current, pem]);
|
|
39
|
+
return "added";
|
|
40
|
+
}
|
|
41
|
+
const PEM_CERTIFICATE = /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g;
|
|
42
|
+
/**
|
|
43
|
+
* Découpe un fichier PEM en ses certificats — une chaîne d'entreprise est
|
|
44
|
+
* souvent livrée en un seul fichier (racine + intermédiaires).
|
|
45
|
+
*
|
|
46
|
+
* Chaque certificat est ajouté SÉPARÉMENT : c'est ce qui rend l'ajout
|
|
47
|
+
* idempotent certificat par certificat, la liste par défaut du processus
|
|
48
|
+
* n'étant faite que de certificats isolés.
|
|
49
|
+
*
|
|
50
|
+
* @param pem - contenu d'un fichier PEM (un ou plusieurs certificats)
|
|
51
|
+
* @returns les certificats, dans l'ordre du fichier ; vide s'il n'y en a aucun
|
|
52
|
+
*/
|
|
53
|
+
function splitPemCertificates(pem) {
|
|
54
|
+
return pem.match(PEM_CERTIFICATE) ?? [];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ajoute les autorités d'un fichier PEM aux ancres TLS du processus
|
|
58
|
+
* (`certificates.extraCaFiles`) — l'équivalent, porté par l'APPLICATION, de
|
|
59
|
+
* `NODE_EXTRA_CA_CERTS`.
|
|
60
|
+
*
|
|
61
|
+
* `NODE_EXTRA_CA_CERTS` n'est lu qu'au lancement de Node, et se pose dans la
|
|
62
|
+
* syntaxe du shell — `VAR=… npm run dev` n'existe pas sous Windows. Une autorité
|
|
63
|
+
* d'entreprise (Keycloak interne, proxy sortant qui ré-signe) se déclare donc
|
|
64
|
+
* dans la configuration, et sert à TOUTES les requêtes sortantes du processus :
|
|
65
|
+
* découverte OpenID, point de jeton, jeu de clés, appels d'administration.
|
|
66
|
+
*
|
|
67
|
+
* @param file - chemin du fichier, absolu ou relatif à `root`
|
|
68
|
+
* @param root - racine de l'application
|
|
69
|
+
* @param store - accès aux ancres ; `node:tls` par défaut
|
|
70
|
+
* @returns le bilan de l'ajout
|
|
71
|
+
* @throws BootConfigurationError - fichier illisible, sans certificat, ou Node
|
|
72
|
+
* incapable d'élargir ses ancres en cours d'exécution
|
|
73
|
+
*/
|
|
74
|
+
async function trustCaFile(file, root, store = tls) {
|
|
75
|
+
const absolute = path.resolve(root, file);
|
|
76
|
+
let content;
|
|
77
|
+
try {
|
|
78
|
+
content = await fsp.readFile(absolute, "utf8");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
81
|
+
const reason = typeof code === "string" ? code : error instanceof Error ? error.message : String(error);
|
|
82
|
+
throw new BootConfigurationError(`http.certificates.extraCaFiles : « ${absolute} » est illisible (${reason}).`);
|
|
83
|
+
}
|
|
84
|
+
const certificates = splitPemCertificates(content);
|
|
85
|
+
if (certificates.length === 0) throw new BootConfigurationError(`http.certificates.extraCaFiles : « ${absolute} » ne contient aucun certificat PEM (-----BEGIN CERTIFICATE-----).`);
|
|
86
|
+
let added = 0;
|
|
87
|
+
let present = 0;
|
|
88
|
+
for (const pem of certificates) {
|
|
89
|
+
const verdict = addDefaultCaCertificate(pem, store);
|
|
90
|
+
if (verdict === "unsupported") throw new BootConfigurationError(`http.certificates.extraCaFiles : ce Node ne sait pas élargir ses ancres TLS en cours d'exécution (tls.setDefaultCACertificates, Node ≥ 24.5). Mettre Node à jour, ou lancer avec NODE_EXTRA_CA_CERTS=${absolute}.`);
|
|
91
|
+
if (verdict === "added") added += 1;
|
|
92
|
+
else present += 1;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
file: absolute,
|
|
96
|
+
added,
|
|
97
|
+
present
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Ajoute aux ancres TLS du processus les autorités déclarées
|
|
102
|
+
* (`certificates.extraCaFiles`), au plus tôt : appelé à l'enregistrement du
|
|
103
|
+
* module, avant tout service — toute requête sortante, commande console
|
|
104
|
+
* comprise, les trouve en place.
|
|
105
|
+
*
|
|
106
|
+
* La CA de DÉVELOPPEMENT n'y figure pas : le service de certificats la pose
|
|
107
|
+
* lui-même ({@link Certificate.generateServerCertificates}).
|
|
108
|
+
*
|
|
109
|
+
* @param files - `certificates.extraCaFiles`
|
|
110
|
+
* @param root - racine de l'application
|
|
111
|
+
* @param log - journal du module
|
|
112
|
+
* @throws BootConfigurationError - cf {@link trustCaFile}
|
|
113
|
+
*/
|
|
114
|
+
async function trustOutboundAuthorities(files, root, log) {
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const trust = await trustCaFile(file, root);
|
|
117
|
+
log(`autorité sortante ${trust.file} : ${trust.added} ajoutée(s), ${trust.present} déjà présente(s)`, "INFO");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
10
120
|
/**
|
|
11
121
|
* Vrai si `host` est une IP littérale (IPv4 `n.n.n.n` ou IPv6 — contient `:`).
|
|
12
|
-
* Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC
|
|
122
|
+
* Une IP doit aller en SAN `iPAddress`, jamais en `dNSName` (RFC 9525).
|
|
13
123
|
*/
|
|
14
124
|
function isIpLiteral(host) {
|
|
15
125
|
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
|
@@ -32,49 +142,30 @@ const defaultOptions = {
|
|
|
32
142
|
}
|
|
33
143
|
};
|
|
34
144
|
/**
|
|
35
|
-
* Valeur texte d'un champ de certificat node-forge (`getField` rend `any`),
|
|
36
|
-
* ou `null` si le champ est absent.
|
|
37
|
-
*/
|
|
38
|
-
function certFieldValue(field) {
|
|
39
|
-
if (field === null || typeof field !== "object") return null;
|
|
40
|
-
const value = field.value;
|
|
41
|
-
return typeof value === "string" ? value : String(value);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
145
|
* Service de fourniture du certificat TLS du serveur HTTPS/WSS.
|
|
45
146
|
*
|
|
46
147
|
* Trois stratégies : `explicit` (certificat fourni en config — le cas de
|
|
47
148
|
* PRODUCTION), `mkcert` (CA locale trustée, confort de dev) et `selfsigned`
|
|
48
|
-
* (auto-
|
|
149
|
+
* (auto-signé, secours). La génération est réservée au
|
|
49
150
|
* DÉVELOPPEMENT : en production sans certificat fourni, le service crie un
|
|
50
151
|
* avertissement (Nodefony n'est pas une autorité de certification).
|
|
51
152
|
*
|
|
52
153
|
* Conformité (génération auto-signée) : signature SHA-256 (jamais SHA-1,
|
|
53
154
|
* RFC 5280 / CA-B Forum), numéro de série aléatoire 128 bits unique
|
|
54
|
-
* (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC
|
|
155
|
+
* (RFC 5280 §4.1.2.2), SAN qui fait foi (RFC 9525), `notBefore` reculé,
|
|
55
156
|
* clé privée écrite en `0600`.
|
|
56
157
|
*/
|
|
57
158
|
var Certificate = class Certificate extends Service {
|
|
58
159
|
module;
|
|
59
160
|
files = [];
|
|
60
161
|
keysPair = null;
|
|
61
|
-
certForge = null;
|
|
62
162
|
ca = "";
|
|
63
163
|
key = "";
|
|
64
164
|
cert = "";
|
|
65
165
|
fullchainPem = "";
|
|
66
166
|
publicKeyPem = "";
|
|
67
|
-
rootCertPem = "";
|
|
68
|
-
intermediateCertPem = "";
|
|
69
167
|
/** CAROOT résolu de mkcert (null tant que non détecté / indisponible). */
|
|
70
168
|
mkcertCaRoot = null;
|
|
71
|
-
/**
|
|
72
|
-
* Backend crypto node-forge, chargé PARESSEUSEMENT. La génération de
|
|
73
|
-
* certificat est un confort de DÉVELOPPEMENT : en production avec un
|
|
74
|
-
* certificat fourni (`strategy: "explicit"`), cette grosse dépendance n'est
|
|
75
|
-
* jamais importée (règle perf-mémoire — pas de dep chargée « au cas où »).
|
|
76
|
-
*/
|
|
77
|
-
forge = null;
|
|
78
169
|
path = resolve(".", "nodefony", "config", "certificates");
|
|
79
170
|
serverPath = resolve(this.path, "server");
|
|
80
171
|
caPath = resolve(this.path, "ca", "nodefony-root-ca.crt.pem");
|
|
@@ -95,20 +186,6 @@ var Certificate = class Certificate extends Service {
|
|
|
95
186
|
return this.kernel?.environment === "development";
|
|
96
187
|
}
|
|
97
188
|
/**
|
|
98
|
-
* Charge node-forge à la demande (idempotent) — uniquement sur le chemin de
|
|
99
|
-
* GÉNÉRATION (selfsigned / mkcert / inspection). Le chemin `explicit` (prod)
|
|
100
|
-
* ne l'appelle jamais.
|
|
101
|
-
*/
|
|
102
|
-
async loadForge() {
|
|
103
|
-
this.forge ??= (await import("node-forge")).default;
|
|
104
|
-
return this.forge;
|
|
105
|
-
}
|
|
106
|
-
/** Accès au backend forge déjà chargé (lève si `loadForge` n'a pas été appelé). */
|
|
107
|
-
get forgeLib() {
|
|
108
|
-
if (!this.forge) throw new Error("node-forge non chargé — appeler loadForge() avant toute génération.");
|
|
109
|
-
return this.forge;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
189
|
* Fabrique-t-on un certificat au démarrage ?
|
|
113
190
|
*
|
|
114
191
|
* 🔴 Seulement si un serveur TLS est ACTIF. Sans cette question, le hook
|
|
@@ -199,7 +276,7 @@ var Certificate = class Certificate extends Service {
|
|
|
199
276
|
/**
|
|
200
277
|
* Génère (ou recharge) le certificat serveur selon la stratégie résolue :
|
|
201
278
|
* `explicit` (fourni en config / prod), `mkcert` (dev, CA trustée) ou
|
|
202
|
-
* `selfsigned` (auto-
|
|
279
|
+
* `selfsigned` (auto-signé, secours). Régénère automatiquement si
|
|
203
280
|
* le certificat présent sur disque n'est pas adéquat pour la stratégie active
|
|
204
281
|
* (expiré, SHA-1, SAN incomplet).
|
|
205
282
|
*
|
|
@@ -210,17 +287,17 @@ var Certificate = class Certificate extends Service {
|
|
|
210
287
|
await this.ensureDirectoriesExist();
|
|
211
288
|
const strategy = await this.resolveStrategy();
|
|
212
289
|
if (strategy === "explicit") return this.loadExplicitCert();
|
|
213
|
-
await this.
|
|
214
|
-
|
|
290
|
+
if (await this.checkCertificates() && !force && await this.isCertAdequate(strategy)) {
|
|
291
|
+
await this.readCerticates();
|
|
292
|
+
this.trustDevelopmentAuthority();
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
215
295
|
if (strategy === "mkcert") await this.generateWithMkcert();
|
|
216
296
|
else {
|
|
217
|
-
this.keysPair = this.generateKeys();
|
|
218
|
-
this.certForge = this.createCertificate();
|
|
219
|
-
this.setExtension();
|
|
220
|
-
this.sign();
|
|
297
|
+
this.keysPair = await this.generateKeys();
|
|
221
298
|
this.key = this.generatePrivateKeyPem();
|
|
222
299
|
this.publicKeyPem = this.generatePublickeyPem();
|
|
223
|
-
const certPem = this.
|
|
300
|
+
const certPem = await this.createCertificate();
|
|
224
301
|
this.cert = certPem;
|
|
225
302
|
this.ca = certPem;
|
|
226
303
|
this.fullchainPem = this.createFullChain();
|
|
@@ -228,7 +305,26 @@ var Certificate = class Certificate extends Service {
|
|
|
228
305
|
await this.writeCertificates(true);
|
|
229
306
|
await fsp.writeFile(this.caPath, certPem.toString(), "utf8");
|
|
230
307
|
}
|
|
231
|
-
|
|
308
|
+
await this.readCerticates();
|
|
309
|
+
this.trustDevelopmentAuthority();
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* En développement, fait confiance à la CA que Nodefony vient de générer ou de
|
|
314
|
+
* relire, pour les requêtes SORTANTES du processus.
|
|
315
|
+
*
|
|
316
|
+
* Sans elle, un service du décor qui sert le certificat de l'application
|
|
317
|
+
* (Keycloak, l'auto-vérification d'un jeton MCP) est injoignable depuis
|
|
318
|
+
* `nodefony development` — `fetch failed` / `SELF_SIGNED_CERT_IN_CHAIN` — alors
|
|
319
|
+
* que `curl -k` répond : la confiance n'existait que sous le script de banc.
|
|
320
|
+
* Réservé au DÉVELOPPEMENT et aux certificats que Nodefony fabrique : un
|
|
321
|
+
* certificat `explicit` (production) n'élargit jamais la confiance du processus.
|
|
322
|
+
*/
|
|
323
|
+
trustDevelopmentAuthority() {
|
|
324
|
+
if (!this.isDev() || !this.ca || this.ca.length === 0) return;
|
|
325
|
+
const verdict = addDefaultCaCertificate(this.ca.toString());
|
|
326
|
+
if (verdict === "added") this.log(`CA de développement ajoutée aux ancres TLS du processus (${this.caPath})`, "DEBUG");
|
|
327
|
+
else if (verdict === "unsupported") this.log(`Ce Node ne sait pas élargir ses ancres TLS en cours d'exécution (tls.setDefaultCACertificates, Node ≥ 24.5) : les requêtes sortantes vers un service qui sert le certificat de développement échoueront. Relancer avec NODE_EXTRA_CA_CERTS=${this.caPath}`, "WARNING");
|
|
232
328
|
}
|
|
233
329
|
/**
|
|
234
330
|
* Résout la stratégie effective à partir de `certificates.strategy` :
|
|
@@ -236,7 +332,7 @@ var Certificate = class Certificate extends Service {
|
|
|
236
332
|
* erreur si absents.
|
|
237
333
|
* - `mkcert` : dev + binaire mkcert + CA locale présents. Forcé hors dev →
|
|
238
334
|
* retombe sur `selfsigned` avec avertissement.
|
|
239
|
-
* - `selfsigned` : auto-signé
|
|
335
|
+
* - `selfsigned` : auto-signé (fallback). En PRODUCTION, crie un
|
|
240
336
|
* avertissement : la génération n'est pas le rôle d'un serveur de prod.
|
|
241
337
|
*/
|
|
242
338
|
async resolveStrategy() {
|
|
@@ -253,7 +349,7 @@ var Certificate = class Certificate extends Service {
|
|
|
253
349
|
this.mkcertCaRoot = caRoot;
|
|
254
350
|
return "mkcert";
|
|
255
351
|
}
|
|
256
|
-
this.log((requested === "mkcert" ? "strategy='mkcert' mais mkcert introuvable — " : "mkcert introuvable — ") + "fallback certificat auto-signé
|
|
352
|
+
this.log((requested === "mkcert" ? "strategy='mkcert' mais mkcert introuvable — " : "mkcert introuvable — ") + "fallback certificat auto-signé (non trusté). `brew install mkcert nss && mkcert -install` pour un HTTPS dev sans erreur (HMR cross-origin/WSS).", "WARNING");
|
|
257
353
|
} else if (requested === "mkcert") this.log("strategy='mkcert' ignoré hors development → certificat auto-signé.", "WARNING");
|
|
258
354
|
}
|
|
259
355
|
if (!this.isDev()) this.log("Aucun certificat TLS fourni en PRODUCTION : génération d'un auto-signé NON trusté (secours). Nodefony n'est PAS une autorité de certification de production — fournissez un vrai certificat (Let's Encrypt, ingress k8s, reverse-proxy edge) via certificates.{ key, cert, ca } (strategy='explicit').", "WARNING");
|
|
@@ -282,7 +378,7 @@ var Certificate = class Certificate extends Service {
|
|
|
282
378
|
/**
|
|
283
379
|
* SAN effectif : config explicite si fournie, sinon dérivé du kernel
|
|
284
380
|
* (localhost + domain en DNS ; loopback en IP). Une IP littérale (ex. domain
|
|
285
|
-
* `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC
|
|
381
|
+
* `127.0.0.1` en dev) est classée en `ip`, pas en `dns` (RFC 9525).
|
|
286
382
|
*/
|
|
287
383
|
derivedSan() {
|
|
288
384
|
const san = this.certOptions.san;
|
|
@@ -330,8 +426,10 @@ var Certificate = class Certificate extends Service {
|
|
|
330
426
|
await this.restrictPrivateKey();
|
|
331
427
|
const rootCaPem = await fsp.readFile(path.join(caRoot, "rootCA.pem"), "utf8");
|
|
332
428
|
const certPem = await fsp.readFile(this.certPath, "utf8");
|
|
333
|
-
const
|
|
334
|
-
|
|
429
|
+
const publicKeyPem = new X509Certificate(certPem).publicKey.export({
|
|
430
|
+
type: "spki",
|
|
431
|
+
format: "pem"
|
|
432
|
+
});
|
|
335
433
|
await fsp.writeFile(this.publicKeyPath, publicKeyPem, "utf8");
|
|
336
434
|
await fsp.writeFile(this.fullchainPath, `${certPem}${rootCaPem}`, "utf8");
|
|
337
435
|
await fsp.writeFile(this.caPath, rootCaPem, "utf8");
|
|
@@ -348,25 +446,21 @@ var Certificate = class Certificate extends Service {
|
|
|
348
446
|
*/
|
|
349
447
|
async isCertAdequate(strategy) {
|
|
350
448
|
try {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (cert.validity.notAfter.getTime() <= Date.now()) return false;
|
|
355
|
-
const ext = cert.getExtension("subjectAltName");
|
|
356
|
-
if (!ext || !this.sanCovers(ext.altNames ?? [])) return false;
|
|
449
|
+
const cert = new X509Certificate(await fsp.readFile(this.certPath));
|
|
450
|
+
if (cert.validToDate.getTime() <= Date.now()) return false;
|
|
451
|
+
if (!this.sanCovers(parseSubjectAltName(cert.subjectAltName).dns)) return false;
|
|
357
452
|
if (strategy === "mkcert") {
|
|
358
|
-
const org =
|
|
453
|
+
const org = distinguishedNameField(cert.issuer, "O");
|
|
359
454
|
return org !== null && /mkcert/i.test(org);
|
|
360
455
|
}
|
|
361
|
-
return cert.
|
|
456
|
+
return signatureAlgorithmOid(cert.raw) !== SHA1_WITH_RSA_OID;
|
|
362
457
|
} catch {
|
|
363
458
|
return false;
|
|
364
459
|
}
|
|
365
460
|
}
|
|
366
|
-
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC
|
|
367
|
-
sanCovers(
|
|
368
|
-
|
|
369
|
-
return this.sanDnsNames().every((name) => presentDns.has(name));
|
|
461
|
+
/** Le SAN présent couvre-t-il tous les noms DNS requis (RFC 9525) ? */
|
|
462
|
+
sanCovers(presentDns) {
|
|
463
|
+
return this.sanDnsNames().every((name) => presentDns.includes(name));
|
|
370
464
|
}
|
|
371
465
|
/** Charge un certificat fourni en config (chemin fichier ou Buffer). */
|
|
372
466
|
async loadExplicitCert() {
|
|
@@ -386,11 +480,7 @@ var Certificate = class Certificate extends Service {
|
|
|
386
480
|
return Buffer.from(await fsp.readFile(value, "utf8"));
|
|
387
481
|
}
|
|
388
482
|
createFullChain() {
|
|
389
|
-
return
|
|
390
|
-
this.cert,
|
|
391
|
-
this.intermediateCertPem,
|
|
392
|
-
this.rootCertPem
|
|
393
|
-
].map((part) => part?.toString().trim()).filter(Boolean).join("\n");
|
|
483
|
+
return this.cert?.toString().trim() ?? "";
|
|
394
484
|
}
|
|
395
485
|
async readCerticates() {
|
|
396
486
|
for (const file of this.files) try {
|
|
@@ -452,86 +542,58 @@ var Certificate = class Certificate extends Service {
|
|
|
452
542
|
this.log(err, "DEBUG");
|
|
453
543
|
}
|
|
454
544
|
}
|
|
455
|
-
|
|
456
|
-
|
|
545
|
+
/** Génère la paire RSA (taille configurée) hors de la boucle d'événements. */
|
|
546
|
+
async generateKeys() {
|
|
547
|
+
return generateKeyPairAsync("rsa", { modulusLength: this.certOptions.selfSigned.size });
|
|
457
548
|
}
|
|
549
|
+
/** Clé privée en PEM PKCS#1 (`BEGIN RSA PRIVATE KEY`). */
|
|
458
550
|
generatePrivateKeyPem() {
|
|
459
|
-
if (this.keysPair) return Buffer.from(this.
|
|
460
|
-
|
|
551
|
+
if (this.keysPair) return Buffer.from(this.keysPair.privateKey.export({
|
|
552
|
+
type: "pkcs1",
|
|
553
|
+
format: "pem"
|
|
554
|
+
}));
|
|
555
|
+
throw new Error(`KeyPair not found`);
|
|
461
556
|
}
|
|
557
|
+
/** Clé publique en PEM SPKI (`BEGIN PUBLIC KEY`). */
|
|
462
558
|
generatePublickeyPem() {
|
|
463
|
-
if (this.keysPair) return Buffer.from(this.
|
|
464
|
-
|
|
559
|
+
if (this.keysPair) return Buffer.from(this.keysPair.publicKey.export({
|
|
560
|
+
type: "spki",
|
|
561
|
+
format: "pem"
|
|
562
|
+
}));
|
|
563
|
+
throw new Error(`KeyPair not found`);
|
|
465
564
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
565
|
+
/**
|
|
566
|
+
* Fabrique et signe le certificat auto-signé avec la paire courante.
|
|
567
|
+
*
|
|
568
|
+
* Série aléatoire unique (RFC 5280 §4.1.2.2), `notBefore` reculé (décalage
|
|
569
|
+
* d'horloge du client), SAN DNS + IP (config explicite sinon dérivé), signé
|
|
570
|
+
* avec le hachage configuré — jamais SHA-1 (collision SHAttered 2017,
|
|
571
|
+
* CA/Browser Forum depuis 2016).
|
|
572
|
+
*
|
|
573
|
+
* @returns le certificat en PEM
|
|
574
|
+
* @throws Si la paire de clés n'a pas été générée
|
|
575
|
+
*/
|
|
576
|
+
async createCertificate() {
|
|
471
577
|
if (!this.keysPair) throw new Error(`KeyPair not found`);
|
|
472
578
|
const o = this.certOptions.selfSigned;
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
/** subjectAltName de l'auto-signé : DNS + IP (config explicite sinon dérivé). */
|
|
485
|
-
altNames() {
|
|
486
|
-
const out = [];
|
|
487
|
-
for (const dns of this.sanDnsNames()) out.push({
|
|
488
|
-
type: 2,
|
|
489
|
-
value: dns
|
|
490
|
-
});
|
|
491
|
-
for (const ip of this.sanIps()) out.push({
|
|
492
|
-
type: 7,
|
|
493
|
-
ip
|
|
579
|
+
const start = Date.now() - o.backdateMinutes * 6e4;
|
|
580
|
+
const pem = await createSelfSignedCertificate({
|
|
581
|
+
privateKey: this.keysPair.privateKey,
|
|
582
|
+
publicKey: this.keysPair.publicKey,
|
|
583
|
+
serialHex: Certificate.generateSerialHex(),
|
|
584
|
+
notBefore: new Date(start),
|
|
585
|
+
notAfter: new Date(start + o.validityDays * 864e5),
|
|
586
|
+
attributes: o.attrs,
|
|
587
|
+
dns: this.sanDnsNames(),
|
|
588
|
+
ip: this.sanIps(),
|
|
589
|
+
hash: this.allowedHash(o.hash)
|
|
494
590
|
});
|
|
495
|
-
return
|
|
496
|
-
}
|
|
497
|
-
setExtension() {
|
|
498
|
-
if (!this.certForge) throw new Error(`pki.Certificate not found`);
|
|
499
|
-
this.certForge.setExtensions([
|
|
500
|
-
{
|
|
501
|
-
name: "basicConstraints",
|
|
502
|
-
cA: false
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
name: "keyUsage",
|
|
506
|
-
digitalSignature: true,
|
|
507
|
-
keyEncipherment: true
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
name: "extKeyUsage",
|
|
511
|
-
serverAuth: true,
|
|
512
|
-
clientAuth: true
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
name: "subjectAltName",
|
|
516
|
-
altNames: this.altNames()
|
|
517
|
-
},
|
|
518
|
-
{ name: "subjectKeyIdentifier" }
|
|
519
|
-
]);
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* Signe le certificat avec le hachage configuré (SHA-256 par défaut).
|
|
523
|
-
* SHA-1 est INTERDIT (collision SHAttered 2017, CA/Browser Forum depuis 2016) :
|
|
524
|
-
* `node-forge` signe en SHA-1 par défaut si on ne passe pas de digest → on en
|
|
525
|
-
* passe toujours un.
|
|
526
|
-
*/
|
|
527
|
-
sign() {
|
|
528
|
-
if (!this.certForge || !this.keysPair) throw new Error(`pki.rsa.KeyPair or pki.Certificate not found`);
|
|
529
|
-
this.certForge.sign(this.keysPair.privateKey, this.digestFor(this.certOptions.selfSigned.hash));
|
|
591
|
+
return Buffer.from(pem);
|
|
530
592
|
}
|
|
531
593
|
/**
|
|
532
594
|
* Résumé introspectable du certificat serveur courant — réutilisé par la
|
|
533
595
|
* commande CLI `certificates` et un futur endpoint d'admin Studio (parité
|
|
534
|
-
* CLI ↔ Web).
|
|
596
|
+
* CLI ↔ Web).
|
|
535
597
|
*/
|
|
536
598
|
async describe() {
|
|
537
599
|
const info = {
|
|
@@ -545,38 +607,32 @@ var Certificate = class Certificate extends Service {
|
|
|
545
607
|
info.caPath = this.caPath;
|
|
546
608
|
} catch {}
|
|
547
609
|
if (!this.cert) return info;
|
|
548
|
-
const { pki } = await this.loadForge();
|
|
549
610
|
try {
|
|
550
|
-
const cert =
|
|
551
|
-
info.serial = cert.serialNumber;
|
|
552
|
-
info.validFrom = cert.
|
|
553
|
-
info.validTo = cert.
|
|
554
|
-
info.signatureAlgorithm =
|
|
555
|
-
const cn =
|
|
611
|
+
const cert = new X509Certificate(this.cert);
|
|
612
|
+
info.serial = cert.serialNumber.toLowerCase();
|
|
613
|
+
info.validFrom = cert.validFromDate.toISOString();
|
|
614
|
+
info.validTo = cert.validToDate.toISOString();
|
|
615
|
+
info.signatureAlgorithm = signatureAlgorithmName(signatureAlgorithmOid(cert.raw));
|
|
616
|
+
const cn = cert.subject ? distinguishedNameField(cert.subject, "CN") : null;
|
|
556
617
|
if (cn !== null) info.commonName = cn;
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
618
|
+
const san = parseSubjectAltName(cert.subjectAltName);
|
|
619
|
+
if (san.dns.length > 0 || san.ip.length > 0) info.san = [...san.dns, ...san.ip];
|
|
559
620
|
} catch {}
|
|
560
621
|
return info;
|
|
561
622
|
}
|
|
562
|
-
/**
|
|
563
|
-
|
|
564
|
-
return pki.oids[oid] ?? oid;
|
|
565
|
-
}
|
|
566
|
-
/** Digest node-forge correspondant au hachage configuré (jamais SHA-1). */
|
|
567
|
-
digestFor(hash) {
|
|
568
|
-
const { md } = this.forgeLib;
|
|
623
|
+
/** Hachage configuré, ramené à SHA-256 s'il sort du contrat (jamais SHA-1). */
|
|
624
|
+
allowedHash(hash) {
|
|
569
625
|
switch (hash) {
|
|
570
|
-
case "sha512":
|
|
571
|
-
case "sha384":
|
|
572
|
-
case "sha256": return
|
|
626
|
+
case "sha512":
|
|
627
|
+
case "sha384":
|
|
628
|
+
case "sha256": return hash;
|
|
573
629
|
default: {
|
|
574
630
|
const refused = hash;
|
|
575
631
|
this.log(`Hachage '${refused}' refusé (SHA-1 interdit) → SHA-256.`, "WARNING");
|
|
576
|
-
return
|
|
632
|
+
return "sha256";
|
|
577
633
|
}
|
|
578
634
|
}
|
|
579
635
|
}
|
|
580
636
|
};
|
|
581
637
|
//#endregion
|
|
582
|
-
export { Certificate as default };
|
|
638
|
+
export { addDefaultCaCertificate, Certificate as default, splitPemCertificates, trustCaFile, trustOutboundAuthorities };
|
|
@@ -348,7 +348,7 @@ const SCHEMA_MISMATCH_HINT = {
|
|
|
348
348
|
* reason: error.message
|
|
349
349
|
*
|
|
350
350
|
* Stateless singleton — zero per-request allocation. Override via
|
|
351
|
-
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC
|
|
351
|
+
* `httpKernel.setErrorRenderer(custom)` for prod hardening or RFC 9457.
|
|
352
352
|
*/
|
|
353
353
|
var DefaultErrorRenderer = class {
|
|
354
354
|
renderHttp(error, context) {
|