@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.2
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 +12 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +6 -4
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -2
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +26 -4
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +87 -144
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +342 -165
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +23 -23
- package/dist/nodefony/service/servers/server-https.js +41 -42
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
- package/dist/nodefony/service/servers/server-websocket.js +11 -10
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +22 -15
- package/dist/nodefony/service/trace.js +3 -2
- package/dist/nodefony/service/upload/upload-service.js +11 -10
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +108 -46
- package/dist/nodefony/src/context/http/Response.js +134 -54
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -11
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +65 -23
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +23 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +42 -57
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +16 -65
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +25 -25
- package/docs/index.md +4 -2
- package/docs/observabilite.md +27 -27
- package/docs/rate-limit.md +20 -20
- package/docs/servers.md +82 -79
- package/docs/session.md +74 -69
- package/docs/upload.md +49 -43
- package/package.json +9 -12
|
@@ -29,7 +29,7 @@ function parseTraceparent(header) {
|
|
|
29
29
|
if (typeof header !== "string") return null;
|
|
30
30
|
const m = TRACEPARENT_RE.exec(header.trim().toLowerCase());
|
|
31
31
|
if (!m) return null;
|
|
32
|
-
const [, version, traceId, parentId, flags] = m;
|
|
32
|
+
const [, version = "", traceId = "", parentId = "", flags = ""] = m;
|
|
33
33
|
if (version === "ff") return null;
|
|
34
34
|
if (/^0+$/.test(traceId)) return null;
|
|
35
35
|
if (/^0+$/.test(parentId)) return null;
|
|
@@ -66,7 +66,8 @@ function resolveTraceparent(header) {
|
|
|
66
66
|
const newSpanId = randomHex(8);
|
|
67
67
|
return `${parsed.version}-${parsed.traceId}-${newSpanId}-${parsed.flags}`;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
const hex = randomHex(24);
|
|
70
|
+
return `00-${hex.slice(0, 32)}-${hex.slice(32)}-01`;
|
|
70
71
|
}
|
|
71
72
|
//#endregion
|
|
72
73
|
export { parseTraceparent, resolveTraceparent };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
3
3
|
import http_kernel_default from "../http-kernel.js";
|
|
4
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateParam.js";
|
|
5
5
|
import { Cli, FileClass, Module, Service, inject } from "nodefony";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import fsp from "node:fs/promises";
|
|
@@ -21,18 +21,19 @@ let upload = class upload extends Service {
|
|
|
21
21
|
path;
|
|
22
22
|
module;
|
|
23
23
|
constructor(module, httpKernel) {
|
|
24
|
-
super("upload", httpKernel
|
|
24
|
+
super("upload", httpKernel.container, httpKernel.notificationsCenter);
|
|
25
25
|
this.httpKernel = httpKernel;
|
|
26
26
|
this.module = module;
|
|
27
27
|
this.kernel?.once("onBoot", async () => {
|
|
28
28
|
this.options = this.httpKernel.options;
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const uploadCfg = this.httpKernel.options.upload;
|
|
30
|
+
if (path.isAbsolute(uploadCfg.uploadDir)) this.path = uploadCfg.uploadDir;
|
|
31
|
+
else this.path = path.resolve(`${this.kernel?.path}/${uploadCfg.uploadDir}`);
|
|
31
32
|
try {
|
|
32
33
|
await fsp.mkdir(this.path, { recursive: true });
|
|
33
34
|
} catch (e) {
|
|
34
35
|
this.path = "/tmp";
|
|
35
|
-
|
|
36
|
+
uploadCfg.uploadDir = this.path;
|
|
36
37
|
this.log(e, "DEBUG");
|
|
37
38
|
}
|
|
38
39
|
});
|
|
@@ -102,8 +103,7 @@ var UploadedFile = class UploadedFile extends FileClass {
|
|
|
102
103
|
return this.parsedFile.originalFilename || name || this.parsedFile.newFilename;
|
|
103
104
|
}
|
|
104
105
|
getMimeType() {
|
|
105
|
-
|
|
106
|
-
return super.getMimeType();
|
|
106
|
+
return this.parsedFile.mimetype || super.getMimeType(this.filename);
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* Nom de fichier SÛR à poser dans un dossier de destination.
|
|
@@ -141,7 +141,8 @@ var UploadedFile = class UploadedFile extends FileClass {
|
|
|
141
141
|
return super.move(name);
|
|
142
142
|
} else return super.move(target);
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
fs.lstatSync(dirname);
|
|
145
|
+
throw new Error(`Upload target directory not found: ${dirname}`);
|
|
145
146
|
}
|
|
146
147
|
/**
|
|
147
148
|
* Variante **async** de `move()` — déplace le fichier uploadé sans bloquer
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { isIPv4, isIPv6 } from "node:net";
|
|
2
|
+
import { createHash, sign } from "node:crypto";
|
|
3
|
+
//#region nodefony/service/x509.ts
|
|
4
|
+
/**
|
|
5
|
+
* Fabrique et lecture de certificats X.509 par `node:crypto` seul — aucune
|
|
6
|
+
* dépendance tierce.
|
|
7
|
+
*
|
|
8
|
+
* `node:crypto` sait LIRE un certificat (`X509Certificate`) et SIGNER, pas
|
|
9
|
+
* en FABRIQUER un : ce module encode le `TBSCertificate` en DER (RFC 5280
|
|
10
|
+
* §4.1), le signe avec `crypto.sign`, puis l'assemble. Seul le sous-ensemble
|
|
11
|
+
* nécessaire à un certificat serveur auto-signé est couvert. Les fonctions
|
|
12
|
+
* sont PURES (aucun disque, aucun kernel) : elles s'éprouvent sans décor.
|
|
13
|
+
*
|
|
14
|
+
* Pourquoi pas node-forge : sa vérification de signature RSA PKCS#1 v1.5
|
|
15
|
+
* accepte des signatures forgées (GHSA-86w9-cpqp-85rv), sans aucune version
|
|
16
|
+
* corrigée. Nous ne vérifions aucune signature, mais `npm audit` signale le
|
|
17
|
+
* paquet pour tous ceux qui installent `@nodefony/http`.
|
|
18
|
+
*/
|
|
19
|
+
const SIGNATURE_OIDS = {
|
|
20
|
+
sha1: "1.2.840.113549.1.1.5",
|
|
21
|
+
sha256: "1.2.840.113549.1.1.11",
|
|
22
|
+
sha384: "1.2.840.113549.1.1.12",
|
|
23
|
+
sha512: "1.2.840.113549.1.1.13"
|
|
24
|
+
};
|
|
25
|
+
/** OID de la signature RSA-SHA1, que le service régénère. */
|
|
26
|
+
const SHA1_WITH_RSA_OID = SIGNATURE_OIDS.sha1;
|
|
27
|
+
/** Noms lisibles des algorithmes de signature courants (rendu de `describe`). */
|
|
28
|
+
const SIGNATURE_NAMES = Object.assign(Object.create(null), {
|
|
29
|
+
[SIGNATURE_OIDS.sha1]: "sha1WithRSAEncryption",
|
|
30
|
+
[SIGNATURE_OIDS.sha256]: "sha256WithRSAEncryption",
|
|
31
|
+
[SIGNATURE_OIDS.sha384]: "sha384WithRSAEncryption",
|
|
32
|
+
[SIGNATURE_OIDS.sha512]: "sha512WithRSAEncryption",
|
|
33
|
+
"1.2.840.113549.1.1.10": "RSASSA-PSS",
|
|
34
|
+
"1.2.840.10045.4.3.2": "ecdsa-with-SHA256",
|
|
35
|
+
"1.2.840.10045.4.3.3": "ecdsa-with-SHA384",
|
|
36
|
+
"1.2.840.10045.4.3.4": "ecdsa-with-SHA512",
|
|
37
|
+
"1.3.101.112": "Ed25519"
|
|
38
|
+
});
|
|
39
|
+
const ATTRIBUTE_OIDS = Object.assign(Object.create(null), {
|
|
40
|
+
commonName: "2.5.4.3",
|
|
41
|
+
CN: "2.5.4.3",
|
|
42
|
+
surname: "2.5.4.4",
|
|
43
|
+
SN: "2.5.4.4",
|
|
44
|
+
serialNumber: "2.5.4.5",
|
|
45
|
+
countryName: "2.5.4.6",
|
|
46
|
+
C: "2.5.4.6",
|
|
47
|
+
localityName: "2.5.4.7",
|
|
48
|
+
L: "2.5.4.7",
|
|
49
|
+
stateOrProvinceName: "2.5.4.8",
|
|
50
|
+
ST: "2.5.4.8",
|
|
51
|
+
streetAddress: "2.5.4.9",
|
|
52
|
+
street: "2.5.4.9",
|
|
53
|
+
organizationName: "2.5.4.10",
|
|
54
|
+
O: "2.5.4.10",
|
|
55
|
+
organizationalUnitName: "2.5.4.11",
|
|
56
|
+
OU: "2.5.4.11",
|
|
57
|
+
title: "2.5.4.12",
|
|
58
|
+
givenName: "2.5.4.42",
|
|
59
|
+
emailAddress: "1.2.840.113549.1.9.1",
|
|
60
|
+
E: "1.2.840.113549.1.9.1"
|
|
61
|
+
});
|
|
62
|
+
const OID_COUNTRY = "2.5.4.6";
|
|
63
|
+
const OID_EMAIL = "1.2.840.113549.1.9.1";
|
|
64
|
+
const OID_BASIC_CONSTRAINTS = "2.5.29.19";
|
|
65
|
+
const OID_KEY_USAGE = "2.5.29.15";
|
|
66
|
+
const OID_EXT_KEY_USAGE = "2.5.29.37";
|
|
67
|
+
const OID_SUBJECT_ALT_NAME = "2.5.29.17";
|
|
68
|
+
const OID_SUBJECT_KEY_ID = "2.5.29.14";
|
|
69
|
+
const OID_SERVER_AUTH = "1.3.6.1.5.5.7.3.1";
|
|
70
|
+
const OID_CLIENT_AUTH = "1.3.6.1.5.5.7.3.2";
|
|
71
|
+
function derLength(length) {
|
|
72
|
+
if (length < 128) return Buffer.from([length]);
|
|
73
|
+
const bytes = [];
|
|
74
|
+
for (let rest = length; rest > 0; rest = Math.floor(rest / 256)) bytes.unshift(rest & 255);
|
|
75
|
+
return Buffer.from([128 | bytes.length, ...bytes]);
|
|
76
|
+
}
|
|
77
|
+
function tlv(tag, content) {
|
|
78
|
+
return Buffer.concat([
|
|
79
|
+
Buffer.from([tag]),
|
|
80
|
+
derLength(content.length),
|
|
81
|
+
content
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
const sequence = (...items) => tlv(48, Buffer.concat(items));
|
|
85
|
+
const set = (...items) => tlv(49, Buffer.concat(items));
|
|
86
|
+
const explicit = (n, content) => tlv(160 | n, content);
|
|
87
|
+
const octetString = (content) => tlv(4, content);
|
|
88
|
+
const bitString = (content) => tlv(3, Buffer.concat([Buffer.from([0]), content]));
|
|
89
|
+
const DER_NULL = Buffer.from([5, 0]);
|
|
90
|
+
const DER_TRUE = tlv(1, Buffer.from([255]));
|
|
91
|
+
function objectIdentifier(dotted) {
|
|
92
|
+
const [first = 0, second = 0, ...rest] = dotted.split(".").map(Number);
|
|
93
|
+
const bytes = [first * 40 + second];
|
|
94
|
+
for (const arc of rest) {
|
|
95
|
+
const chunk = [arc & 127];
|
|
96
|
+
for (let value = Math.floor(arc / 128); value > 0; value = Math.floor(value / 128)) chunk.unshift(value & 127 | 128);
|
|
97
|
+
bytes.push(...chunk);
|
|
98
|
+
}
|
|
99
|
+
return tlv(6, Buffer.from(bytes));
|
|
100
|
+
}
|
|
101
|
+
/** Entier positif depuis sa forme hexadécimale (octet 0x00 ajouté si besoin). */
|
|
102
|
+
function positiveInteger(hex) {
|
|
103
|
+
let bytes = Buffer.from(hex.length % 2 === 0 ? hex : `0${hex}`, "hex");
|
|
104
|
+
let start = 0;
|
|
105
|
+
while (start < bytes.length - 1 && bytes[start] === 0) start++;
|
|
106
|
+
bytes = bytes.subarray(start);
|
|
107
|
+
if (bytes.length === 0) bytes = Buffer.from([0]);
|
|
108
|
+
if ((bytes[0] ?? 0) & 128) bytes = Buffer.concat([Buffer.from([0]), bytes]);
|
|
109
|
+
return tlv(2, bytes);
|
|
110
|
+
}
|
|
111
|
+
/** RFC 5280 §4.1.2.5 : UTCTime jusqu'en 2049, GeneralizedTime au-delà. */
|
|
112
|
+
function time(date) {
|
|
113
|
+
const iso = date.toISOString();
|
|
114
|
+
const digits = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
|
|
115
|
+
const year = date.getUTCFullYear();
|
|
116
|
+
if (year >= 1950 && year < 2050) return tlv(23, Buffer.from(digits.slice(2), "ascii"));
|
|
117
|
+
return tlv(24, Buffer.from(digits, "ascii"));
|
|
118
|
+
}
|
|
119
|
+
function attributeOid(attribute) {
|
|
120
|
+
if (attribute.type && /^\d+(\.\d+)+$/.test(attribute.type)) return attribute.type;
|
|
121
|
+
const key = attribute.name ?? attribute.shortName ?? attribute.type ?? "";
|
|
122
|
+
const oid = ATTRIBUTE_OIDS[key];
|
|
123
|
+
if (!oid) throw new Error(`Attribut de certificat inconnu : '${key}'. Noms reconnus : ${Object.keys(ATTRIBUTE_OIDS).join(", ")}.`);
|
|
124
|
+
return oid;
|
|
125
|
+
}
|
|
126
|
+
/** Nom distinctif : un RDN par attribut (RFC 5280 §4.1.2.4). */
|
|
127
|
+
function distinguishedName(attributes) {
|
|
128
|
+
const rdns = attributes.map((attribute) => {
|
|
129
|
+
const oid = attributeOid(attribute);
|
|
130
|
+
if (typeof attribute.value !== "string") throw new Error(`Attribut de certificat '${oid}' sans valeur.`);
|
|
131
|
+
const value = Buffer.from(attribute.value, "utf8");
|
|
132
|
+
const tag = oid === OID_COUNTRY ? 19 : oid === OID_EMAIL ? 22 : 12;
|
|
133
|
+
return set(sequence(objectIdentifier(oid), tlv(tag, value)));
|
|
134
|
+
});
|
|
135
|
+
return sequence(...rdns);
|
|
136
|
+
}
|
|
137
|
+
/** Octets d'une adresse IP pour un SAN `iPAddress` (4 ou 16 octets). */
|
|
138
|
+
function ipAddressBytes(ip) {
|
|
139
|
+
if (isIPv4(ip)) return Buffer.from(ip.split(".").map(Number));
|
|
140
|
+
if (!isIPv6(ip) || ip.includes("%")) throw new Error(`Adresse IP invalide pour un SAN : '${ip}'.`);
|
|
141
|
+
const toGroups = (part) => {
|
|
142
|
+
if (part === "") return [];
|
|
143
|
+
const groups = [];
|
|
144
|
+
for (const piece of part.split(":")) if (piece.includes(".")) {
|
|
145
|
+
const [a = 0, b = 0, c = 0, d = 0] = piece.split(".").map(Number);
|
|
146
|
+
groups.push(a << 8 | b, c << 8 | d);
|
|
147
|
+
} else groups.push(parseInt(piece, 16));
|
|
148
|
+
return groups;
|
|
149
|
+
};
|
|
150
|
+
const [head = "", tail] = ip.split("::");
|
|
151
|
+
const left = toGroups(head);
|
|
152
|
+
const right = tail === void 0 ? [] : toGroups(tail);
|
|
153
|
+
const zeros = new Array(8 - left.length - right.length).fill(0);
|
|
154
|
+
const out = Buffer.alloc(16);
|
|
155
|
+
[
|
|
156
|
+
...left,
|
|
157
|
+
...zeros,
|
|
158
|
+
...right
|
|
159
|
+
].forEach((group, i) => out.writeUInt16BE(group, i * 2));
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Octets d'un SAN `dNSName` — IA5String (RFC 5280 §4.2.1.6).
|
|
164
|
+
*
|
|
165
|
+
* 🔴 Jamais `Buffer.from(nom, "ascii")` : Node y garde l'octet BAS de chaque
|
|
166
|
+
* caractère, et `evil\u012Elocalhost` devient `evil.localhost` — un certificat
|
|
167
|
+
* qui couvre un nom que personne n'a configuré. Hors ASCII imprimable, le nom
|
|
168
|
+
* est REFUSÉ (un IDN s'écrit en punycode, `xn--…`).
|
|
169
|
+
*/
|
|
170
|
+
function dnsNameBytes(dns) {
|
|
171
|
+
if (!/^[\x20-\x7e]+$/u.test(dns)) throw new Error(`Nom DNS invalide pour un SAN : ${JSON.stringify(dns)} — ASCII imprimable seulement (un nom international s'écrit en punycode, xn--…).`);
|
|
172
|
+
return Buffer.from(dns, "ascii");
|
|
173
|
+
}
|
|
174
|
+
function extension(oid, critical, value) {
|
|
175
|
+
return critical ? sequence(objectIdentifier(oid), DER_TRUE, octetString(value)) : sequence(objectIdentifier(oid), octetString(value));
|
|
176
|
+
}
|
|
177
|
+
function readNode(der, offset) {
|
|
178
|
+
const tag = der[offset];
|
|
179
|
+
let length = der[offset + 1];
|
|
180
|
+
if (tag === void 0 || length === void 0) throw new Error("DER tronqué.");
|
|
181
|
+
let start = offset + 2;
|
|
182
|
+
if (length & 128) {
|
|
183
|
+
const count = length & 127;
|
|
184
|
+
length = 0;
|
|
185
|
+
for (let i = 0; i < count; i++) length = length * 256 + (der[start + i] ?? 0);
|
|
186
|
+
start += count;
|
|
187
|
+
}
|
|
188
|
+
const end = start + length;
|
|
189
|
+
if (end > der.length) throw new Error("DER tronqué.");
|
|
190
|
+
return {
|
|
191
|
+
tag,
|
|
192
|
+
start,
|
|
193
|
+
end
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function children(der, node) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (let offset = node.start; offset < node.end;) {
|
|
199
|
+
const child = readNode(der, offset);
|
|
200
|
+
out.push(child);
|
|
201
|
+
offset = child.end;
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
function decodeObjectIdentifier(bytes) {
|
|
206
|
+
const first = bytes[0] ?? 0;
|
|
207
|
+
const arcs = [Math.min(Math.floor(first / 40), 2), first - Math.min(Math.floor(first / 40), 2) * 40];
|
|
208
|
+
let value = 0;
|
|
209
|
+
for (let i = 1; i < bytes.length; i++) {
|
|
210
|
+
const byte = bytes[i] ?? 0;
|
|
211
|
+
value = value * 128 + (byte & 127);
|
|
212
|
+
if (!(byte & 128)) {
|
|
213
|
+
arcs.push(value);
|
|
214
|
+
value = 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return arcs.join(".");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* OID de l'algorithme de signature d'un certificat DER (champ
|
|
221
|
+
* `signatureAlgorithm`, RFC 5280 §4.1.1.2). Lu dans le DER plutôt que par
|
|
222
|
+
* `X509Certificate.signatureAlgorithmOid`, absent des Node du plancher.
|
|
223
|
+
*
|
|
224
|
+
* @param der - certificat encodé DER (`X509Certificate.raw`)
|
|
225
|
+
* @throws Si le DER n'a pas la forme d'un certificat
|
|
226
|
+
*/
|
|
227
|
+
function signatureAlgorithmOid(der) {
|
|
228
|
+
const [, algorithm] = children(der, readNode(der, 0));
|
|
229
|
+
const oid = algorithm ? children(der, algorithm)[0] : void 0;
|
|
230
|
+
if (oid?.tag !== 6) throw new Error("Certificat DER sans algorithme de signature.");
|
|
231
|
+
return decodeObjectIdentifier(der.subarray(oid.start, oid.end));
|
|
232
|
+
}
|
|
233
|
+
/** Nom lisible d'un OID de signature (l'OID lui-même s'il est inconnu). */
|
|
234
|
+
function signatureAlgorithmName(oid) {
|
|
235
|
+
return SIGNATURE_NAMES[oid] ?? oid;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Entrées DNS et IP d'un `subjectAltName` tel que le rend
|
|
239
|
+
* `X509Certificate.subjectAltName` (`DNS:a, IP Address:1.2.3.4`).
|
|
240
|
+
*/
|
|
241
|
+
function parseSubjectAltName(value) {
|
|
242
|
+
const dns = [];
|
|
243
|
+
const ip = [];
|
|
244
|
+
if (!value) return {
|
|
245
|
+
dns,
|
|
246
|
+
ip
|
|
247
|
+
};
|
|
248
|
+
for (const raw of value.split(/,\s*/)) {
|
|
249
|
+
const colon = raw.indexOf(":");
|
|
250
|
+
if (colon < 0) continue;
|
|
251
|
+
const kind = raw.slice(0, colon);
|
|
252
|
+
let entry = raw.slice(colon + 1);
|
|
253
|
+
if (entry.startsWith("\"")) try {
|
|
254
|
+
entry = String(JSON.parse(entry));
|
|
255
|
+
} catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (kind === "DNS") dns.push(entry);
|
|
259
|
+
else if (kind === "IP Address") ip.push(entry);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
dns,
|
|
263
|
+
ip
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Valeur d'un champ d'un nom distinctif tel que le rend
|
|
268
|
+
* `X509Certificate.subject` / `.issuer` (une ligne `CLÉ=valeur` par champ).
|
|
269
|
+
*/
|
|
270
|
+
function distinguishedNameField(dn, key) {
|
|
271
|
+
for (const line of dn.split("\n")) if (line.startsWith(`${key}=`)) return line.slice(key.length + 1);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/** Contenu de la BIT STRING `subjectPublicKey` d'une SPKI DER. */
|
|
275
|
+
function subjectPublicKeyBits(spki) {
|
|
276
|
+
const [, key] = children(spki, readNode(spki, 0));
|
|
277
|
+
if (key?.tag !== 3) throw new Error("Clé publique SPKI sans subjectPublicKey.");
|
|
278
|
+
return spki.subarray(key.start + 1, key.end);
|
|
279
|
+
}
|
|
280
|
+
function signAsync(hash, data, key) {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
sign(hash, data, key, (error, signature) => {
|
|
283
|
+
if (error) reject(error);
|
|
284
|
+
else resolve(signature);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function toPem(der) {
|
|
289
|
+
return `-----BEGIN CERTIFICATE-----\n${der.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "")}\n-----END CERTIFICATE-----\n`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Fabrique un certificat serveur auto-signé et le rend en PEM.
|
|
293
|
+
*
|
|
294
|
+
* Contenu : version 3, sujet = émetteur, `basicConstraints` cA=false,
|
|
295
|
+
* `keyUsage` (digitalSignature, keyEncipherment — critique),
|
|
296
|
+
* `extKeyUsage` (serverAuth, clientAuth), `subjectAltName` (critique si le
|
|
297
|
+
* sujet est vide, RFC 5280 §4.2.1.6) et `subjectKeyIdentifier` (SHA-1 de la
|
|
298
|
+
* clé publique, méthode 1 de RFC 5280 §4.2.1.2).
|
|
299
|
+
*
|
|
300
|
+
* @param spec - clés, série, validité, noms et hachage
|
|
301
|
+
* @returns le certificat encodé PEM
|
|
302
|
+
* @throws Si un attribut ou une adresse IP est invalide
|
|
303
|
+
*/
|
|
304
|
+
async function createSelfSignedCertificate(spec) {
|
|
305
|
+
const spki = spec.publicKey.export({
|
|
306
|
+
type: "spki",
|
|
307
|
+
format: "der"
|
|
308
|
+
});
|
|
309
|
+
const algorithm = sequence(objectIdentifier(SIGNATURE_OIDS[spec.hash]), DER_NULL);
|
|
310
|
+
const name = distinguishedName(spec.attributes);
|
|
311
|
+
const altNames = [...spec.dns.map((dns) => tlv(130, dnsNameBytes(dns))), ...spec.ip.map((ip) => tlv(135, ipAddressBytes(ip)))];
|
|
312
|
+
const extensions = [
|
|
313
|
+
extension(OID_BASIC_CONSTRAINTS, false, sequence()),
|
|
314
|
+
extension(OID_KEY_USAGE, true, tlv(3, Buffer.from([5, 160]))),
|
|
315
|
+
extension(OID_EXT_KEY_USAGE, false, sequence(objectIdentifier(OID_SERVER_AUTH), objectIdentifier(OID_CLIENT_AUTH)))
|
|
316
|
+
];
|
|
317
|
+
if (altNames.length > 0) extensions.push(extension(OID_SUBJECT_ALT_NAME, spec.attributes.length === 0, sequence(...altNames)));
|
|
318
|
+
extensions.push(extension(OID_SUBJECT_KEY_ID, false, octetString(createHash("sha1").update(subjectPublicKeyBits(spki)).digest())));
|
|
319
|
+
const tbs = sequence(explicit(0, tlv(2, Buffer.from([2]))), positiveInteger(spec.serialHex), algorithm, name, sequence(time(spec.notBefore), time(spec.notAfter)), name, spki, explicit(3, sequence(...extensions)));
|
|
320
|
+
const signature = await signAsync(spec.hash, tbs, spec.privateKey);
|
|
321
|
+
return toPem(sequence(tbs, algorithm, bitString(signature)));
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
export { SHA1_WITH_RSA_OID, createSelfSignedCertificate, distinguishedNameField, ipAddressBytes, parseSubjectAltName, signatureAlgorithmName, signatureAlgorithmOid };
|
|
@@ -99,7 +99,7 @@ var PrebuiltUi = class {
|
|
|
99
99
|
return true;
|
|
100
100
|
}
|
|
101
101
|
kernel?.once("onReady", () => {
|
|
102
|
-
(container?.get?.("server-static"))?.addMount
|
|
102
|
+
(container?.get?.("server-static"))?.addMount(this.publicPath, this.distDir);
|
|
103
103
|
});
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
@@ -141,7 +141,7 @@ var Context = class extends Service {
|
|
|
141
141
|
scheme;
|
|
142
142
|
webSocketState = null;
|
|
143
143
|
constructor(container, type) {
|
|
144
|
-
super(
|
|
144
|
+
super(type, container);
|
|
145
145
|
if (PERF_PROBE_SUB) {
|
|
146
146
|
const p = globalThis.__nfPerfProbe;
|
|
147
147
|
if (p && p.t0 !== 0n) p.svcNs += Number(process.hrtime.bigint() - p.t0);
|
|
@@ -227,7 +227,7 @@ var Context = class extends Service {
|
|
|
227
227
|
}
|
|
228
228
|
phaseStart(name) {
|
|
229
229
|
if (!this._timingEnabled) return;
|
|
230
|
-
|
|
230
|
+
this._phaseIndex ??= /* @__PURE__ */ new Map();
|
|
231
231
|
const idx = this.phases.length;
|
|
232
232
|
this.phases.push({
|
|
233
233
|
name,
|
|
@@ -239,8 +239,8 @@ var Context = class extends Service {
|
|
|
239
239
|
if (!this._timingEnabled || this._phaseIndex === null) return;
|
|
240
240
|
const idx = this._phaseIndex.get(name);
|
|
241
241
|
if (idx === void 0) return;
|
|
242
|
-
const p = this.phases
|
|
243
|
-
if (p.endMs !== void 0) return;
|
|
242
|
+
const p = this.phases.at(idx);
|
|
243
|
+
if (p === void 0 || p.endMs !== void 0) return;
|
|
244
244
|
p.endMs = performance.now();
|
|
245
245
|
p.durationMs = p.endMs - p.startMs;
|
|
246
246
|
}
|
|
@@ -250,15 +250,28 @@ var Context = class extends Service {
|
|
|
250
250
|
Promise.resolve().then(() => boundFn(this)).catch((e) => this.log(e, "ERROR", "onAfterResponse(late)"));
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
|
-
|
|
253
|
+
this._afterResponseFns ??= [];
|
|
254
254
|
this._afterResponseFns.push(boundFn);
|
|
255
255
|
}
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Exécute les hooks `onAfterResponse`, une seule fois, dans l'ordre
|
|
258
|
+
* d'enregistrement ; un hook qui lève est journalisé sans arrêter les suivants.
|
|
259
|
+
*
|
|
260
|
+
* @returns `undefined` quand aucun hook n'est enregistré (le cas nominal) ou
|
|
261
|
+
* qu'ils ont déjà tourné : rien à attendre, le démontage continue sans
|
|
262
|
+
* suspension. Une `Promise` sinon, réglée quand le dernier hook a fini — elle
|
|
263
|
+
* ne rejette jamais.
|
|
264
|
+
*/
|
|
265
|
+
_runAfterResponse() {
|
|
266
|
+
if (this._afterResponseFired) return void 0;
|
|
258
267
|
this._afterResponseFired = true;
|
|
259
268
|
const fns = this._afterResponseFns;
|
|
260
|
-
if (fns === null || fns.length === 0) return;
|
|
269
|
+
if (fns === null || fns.length === 0) return void 0;
|
|
261
270
|
this._afterResponseFns = null;
|
|
271
|
+
return this.runAfterResponseHooks(fns);
|
|
272
|
+
}
|
|
273
|
+
/** Boucle des hooks `onAfterResponse` — séquentielle, erreurs journalisées. */
|
|
274
|
+
async runAfterResponseHooks(fns) {
|
|
262
275
|
for (const fn of fns) try {
|
|
263
276
|
await fn(this);
|
|
264
277
|
} catch (e) {
|
|
@@ -298,7 +311,7 @@ var Context = class extends Service {
|
|
|
298
311
|
* notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
|
|
299
312
|
*/
|
|
300
313
|
logEvent(event) {
|
|
301
|
-
|
|
314
|
+
lifecycleEventLogging ??= this.kernel?.environment !== "production";
|
|
302
315
|
if (!lifecycleEventLogging) return;
|
|
303
316
|
this.log(`${colorLogEvent()} ${event}`, EVENT_SEVERITY[event] || "DEBUG");
|
|
304
317
|
}
|
|
@@ -327,7 +340,9 @@ var Context = class extends Service {
|
|
|
327
340
|
if (logger.shouldSample && !logger.shouldSample(this, err)) return;
|
|
328
341
|
const entry = logger.renderHttp(this, err);
|
|
329
342
|
return this.log(entry.text, entry.severity, entry.msgid);
|
|
330
|
-
} catch {
|
|
343
|
+
} catch {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
331
346
|
}
|
|
332
347
|
/**
|
|
333
348
|
* P3.7 — Émet au teardown un log DEBUG détaillant la durée de chaque phase du
|
|
@@ -339,18 +354,18 @@ var Context = class extends Service {
|
|
|
339
354
|
logPhasesVerbose() {
|
|
340
355
|
if (!this._timingVerbose) return;
|
|
341
356
|
const phases = this.phases;
|
|
342
|
-
|
|
343
|
-
if (n === 0) return;
|
|
357
|
+
if (phases.length === 0) return;
|
|
344
358
|
let line = "";
|
|
345
359
|
let total = 0;
|
|
346
|
-
|
|
347
|
-
|
|
360
|
+
let i = 0;
|
|
361
|
+
for (const p of phases) {
|
|
348
362
|
const d = p.durationMs ?? (p.endMs !== void 0 ? p.endMs - p.startMs : void 0);
|
|
349
363
|
if (i > 0) line += " · ";
|
|
350
364
|
if (d !== void 0) {
|
|
351
365
|
line += `${p.name}=${d.toFixed(2)}ms`;
|
|
352
366
|
total += d;
|
|
353
367
|
} else line += `${p.name}=…`;
|
|
368
|
+
i++;
|
|
354
369
|
}
|
|
355
370
|
this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.type} TIMING`);
|
|
356
371
|
}
|
|
@@ -363,11 +378,12 @@ var Context = class extends Service {
|
|
|
363
378
|
}
|
|
364
379
|
}
|
|
365
380
|
getRequestCookies(name) {
|
|
366
|
-
if (name) return this.cookies[name]
|
|
381
|
+
if (name) return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
|
|
367
382
|
return this.cookies;
|
|
368
383
|
}
|
|
369
384
|
setCookie(cookie) {
|
|
370
|
-
|
|
385
|
+
const received = cookie;
|
|
386
|
+
if (received) return this.response?.addCookie(received);
|
|
371
387
|
}
|
|
372
388
|
getRequest() {
|
|
373
389
|
return this.request;
|
|
@@ -387,8 +403,7 @@ var Context = class extends Service {
|
|
|
387
403
|
return Boolean(this.cookieSession);
|
|
388
404
|
}
|
|
389
405
|
getCookieSession(name) {
|
|
390
|
-
|
|
391
|
-
return null;
|
|
406
|
+
return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
|
|
392
407
|
}
|
|
393
408
|
/**
|
|
394
409
|
* Nom effectif du cookie de session pour CE transport. Sur TLS (https/wss) on
|
|
@@ -401,7 +416,7 @@ var Context = class extends Service {
|
|
|
401
416
|
*/
|
|
402
417
|
getSessionCookieName() {
|
|
403
418
|
const base = this.sessionService?.defaultSessionName ?? "nodefony";
|
|
404
|
-
const mode = (this.sessionService?.options
|
|
419
|
+
const mode = (this.sessionService?.options.cookie)?.hostPrefix ?? "auto";
|
|
405
420
|
const tls = this.scheme === "https" || this.scheme === "wss";
|
|
406
421
|
return mode === true || mode === "auto" && tls ? `__Host-${base}` : base;
|
|
407
422
|
}
|
|
@@ -8,7 +8,7 @@ import { isIPv4 } from "node:net";
|
|
|
8
8
|
* un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
|
|
9
9
|
*/
|
|
10
10
|
function splitTopLevel(input, sep) {
|
|
11
|
-
if (input.
|
|
11
|
+
if (!input.includes("\"")) return input.split(sep);
|
|
12
12
|
const out = [];
|
|
13
13
|
let buf = "";
|
|
14
14
|
let inQuote = false;
|
|
@@ -43,7 +43,7 @@ function splitTopLevel(input, sep) {
|
|
|
43
43
|
/** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
|
|
44
44
|
function unquote(value) {
|
|
45
45
|
const v = value.trim();
|
|
46
|
-
if (v.length >= 2 && v
|
|
46
|
+
if (v.length >= 2 && v.startsWith("\"") && v.endsWith("\"")) return v.slice(1, -1).replace(/\\(.)/gu, "$1");
|
|
47
47
|
return v;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
@@ -104,11 +104,11 @@ function forwardedNodeIp(node) {
|
|
|
104
104
|
if (!node) return null;
|
|
105
105
|
const n = node.trim();
|
|
106
106
|
if (!n) return null;
|
|
107
|
-
if (n
|
|
107
|
+
if (n.startsWith("[")) {
|
|
108
108
|
const end = n.indexOf("]");
|
|
109
109
|
return end > 1 ? n.slice(1, end) : null;
|
|
110
110
|
}
|
|
111
|
-
if (n === "unknown" || n
|
|
111
|
+
if (n === "unknown" || n.startsWith("_")) return null;
|
|
112
112
|
const colon = n.indexOf(":");
|
|
113
113
|
const host = colon === -1 ? n : n.slice(0, colon);
|
|
114
114
|
return isIPv4(host) ? host : host || null;
|