@nodefony/http 10.0.0-beta.1 → 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 +5 -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 +2 -2
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +4 -3
- package/dist/nodefony/service/certificates.js +74 -143
- package/dist/nodefony/service/http-kernel.js +2 -2
- 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-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- 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/http/Request.js +17 -4
- package/dist/nodefony/src/context/http/Response.js +30 -6
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/service/certificates.d.ts +32 -47
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/docs/cookies.md +10 -10
- package/docs/index.md +4 -2
- package/docs/observabilite.md +13 -13
- package/docs/rate-limit.md +19 -19
- package/docs/servers.md +50 -47
- package/docs/session.md +17 -17
- package/docs/upload.md +29 -29
- package/package.json +7 -10
|
@@ -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 };
|
|
@@ -52,10 +52,21 @@ function perfMark(field) {
|
|
|
52
52
|
const p = globalThis.__nfPerfProbe;
|
|
53
53
|
if (p && p.t0 !== 0n) p[field] += Number(process.hrtime.bigint() - p.t0);
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Nom d'hôte d'une autorité brute (`host[:port]`) : tout ce qui précède le
|
|
57
|
+
* premier `:`. Même résultat que `host.split(":")[0]`, sans tableau (#508).
|
|
58
|
+
*/
|
|
59
|
+
function hostnameOf(host) {
|
|
60
|
+
if (!host) return "";
|
|
61
|
+
const colon = host.indexOf(":");
|
|
62
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
63
|
+
}
|
|
55
64
|
var HttpRequest = class {
|
|
56
65
|
context;
|
|
57
66
|
request;
|
|
58
67
|
#url = null;
|
|
68
|
+
#rawHostname;
|
|
69
|
+
#rawHostSource;
|
|
59
70
|
headers = {};
|
|
60
71
|
host = "";
|
|
61
72
|
method;
|
|
@@ -162,7 +173,9 @@ var HttpRequest = class {
|
|
|
162
173
|
if (PERF_PROBE_SUB) perfMark("reqProxyNs");
|
|
163
174
|
this.method = this.getMethod();
|
|
164
175
|
this.host = this.getHost();
|
|
165
|
-
this
|
|
176
|
+
this.#rawHostSource = this.host;
|
|
177
|
+
this.#rawHostname = hostnameOf(this.host);
|
|
178
|
+
this.hostname = this.#rawHostname;
|
|
166
179
|
this.sUrl = this.getFullUrl(request);
|
|
167
180
|
const fast = (this.scheme === "http" || this.scheme === "https") && isCanonicalAuthority(this.host, this.scheme) ? splitTarget(this.getRawTarget()) : null;
|
|
168
181
|
if (fast !== null) {
|
|
@@ -534,9 +547,9 @@ var HttpRequest = class {
|
|
|
534
547
|
}
|
|
535
548
|
getHostName(host) {
|
|
536
549
|
if (this.#url?.hostname) return this.#url.hostname;
|
|
537
|
-
if (host) return host
|
|
538
|
-
|
|
539
|
-
return
|
|
550
|
+
if (host) return hostnameOf(host);
|
|
551
|
+
host = this.getHost();
|
|
552
|
+
return host === this.#rawHostSource ? this.#rawHostname : hostnameOf(host);
|
|
540
553
|
}
|
|
541
554
|
getHost() {
|
|
542
555
|
return this.request.headers.host;
|
|
@@ -15,6 +15,29 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
|
15
15
|
308
|
|
16
16
|
]);
|
|
17
17
|
const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
|
|
18
|
+
const CONTENT_TYPE_CACHE_MAX = 64;
|
|
19
|
+
const contentTypeCache = /* @__PURE__ */ new Map();
|
|
20
|
+
/**
|
|
21
|
+
* Résout `type` (extension ou type MIME) comme `mime.contentType`, avec
|
|
22
|
+
* mémorisation bornée des résolutions positives.
|
|
23
|
+
*
|
|
24
|
+
* @param type - extension (`json`) ou type MIME (`text/html`).
|
|
25
|
+
* @returns la résolution, ou `null` si `mime-types` ne connaît pas le type.
|
|
26
|
+
*/
|
|
27
|
+
function resolveContentType(type) {
|
|
28
|
+
const hit = contentTypeCache.get(type);
|
|
29
|
+
if (hit !== void 0) return hit;
|
|
30
|
+
const full = mime.contentType(type);
|
|
31
|
+
if (!full) return null;
|
|
32
|
+
const semi = full.indexOf(";");
|
|
33
|
+
const resolved = {
|
|
34
|
+
full,
|
|
35
|
+
bare: semi === -1 ? full : full.slice(0, semi),
|
|
36
|
+
charset: mime.charset(full)
|
|
37
|
+
};
|
|
38
|
+
if (contentTypeCache.size < CONTENT_TYPE_CACHE_MAX) contentTypeCache.set(type, resolved);
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
18
41
|
var HttpResponse = class {
|
|
19
42
|
context;
|
|
20
43
|
response;
|
|
@@ -177,9 +200,9 @@ var HttpResponse = class {
|
|
|
177
200
|
}
|
|
178
201
|
setContentType(type, encoding) {
|
|
179
202
|
if (type && encoding) {
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
const mytype =
|
|
203
|
+
const resolved = resolveContentType(type);
|
|
204
|
+
if (resolved) {
|
|
205
|
+
const mytype = resolved.bare;
|
|
183
206
|
this.contentType = mytype;
|
|
184
207
|
this.encoding = encoding;
|
|
185
208
|
if (mytype === "application/json" || mytype.endsWith("+json")) return this.setHeader("Content-Type", mytype);
|
|
@@ -187,10 +210,11 @@ var HttpResponse = class {
|
|
|
187
210
|
}
|
|
188
211
|
}
|
|
189
212
|
if (type && !encoding) {
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
213
|
+
const resolved = resolveContentType(type);
|
|
214
|
+
if (resolved) {
|
|
215
|
+
const mytype = resolved.full;
|
|
192
216
|
this.contentType = mytype;
|
|
193
|
-
|
|
217
|
+
const charset = resolved.charset;
|
|
194
218
|
if (charset) this.encoding = charset;
|
|
195
219
|
return this.setHeader("Content-Type", mytype);
|
|
196
220
|
}
|
|
@@ -79,20 +79,45 @@ function isCanonicalAuthority(host, scheme) {
|
|
|
79
79
|
if (port.length === 0 || port.charCodeAt(0) === 48) return false;
|
|
80
80
|
if (scheme === "https" && port === "443" || scheme === "http" && port === "80") return false;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
let labels = 1;
|
|
83
|
+
let start = 0;
|
|
84
|
+
for (let i = 0; i < name.length; i++) if (name.charCodeAt(i) === DOT) {
|
|
85
|
+
if (i === start) return false;
|
|
86
|
+
labels++;
|
|
87
|
+
start = i + 1;
|
|
88
|
+
}
|
|
89
|
+
if (start === name.length) return false;
|
|
90
|
+
if (isDigitsFrom(name, start)) {
|
|
91
|
+
if (labels !== 4) return false;
|
|
92
|
+
return isCanonicalDottedQuad(name);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Les quatre labels (non vides, déjà comptés) forment-ils une dotted-quad
|
|
98
|
+
* canonique ? Chacun : chiffres seuls, 3 au plus, sans zéro de tête, ≤ 255.
|
|
99
|
+
*/
|
|
100
|
+
function isCanonicalDottedQuad(name) {
|
|
101
|
+
let start = 0;
|
|
102
|
+
for (let i = 0; i <= name.length; i++) {
|
|
103
|
+
if (i < name.length && name.charCodeAt(i) !== DOT) continue;
|
|
104
|
+
const len = i - start;
|
|
105
|
+
if (len > 3) return false;
|
|
106
|
+
if (len > 1 && name.charCodeAt(start) === 48) return false;
|
|
107
|
+
let value = 0;
|
|
108
|
+
for (let k = start; k < i; k++) {
|
|
109
|
+
const c = name.charCodeAt(k);
|
|
110
|
+
if (c < 48 || c > 57) return false;
|
|
111
|
+
value = value * 10 + (c - 48);
|
|
90
112
|
}
|
|
113
|
+
if (value > 255) return false;
|
|
114
|
+
start = i + 1;
|
|
91
115
|
}
|
|
92
116
|
return true;
|
|
93
117
|
}
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
/** `s` ne contient-elle que des chiffres ASCII à partir de `from` ? */
|
|
119
|
+
function isDigitsFrom(s, from) {
|
|
120
|
+
for (let i = from; i < s.length; i++) {
|
|
96
121
|
const c = s.charCodeAt(i);
|
|
97
122
|
if (c < 48 || c > 57) return false;
|
|
98
123
|
}
|
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
import { isPortListening, resolvePortPolicy } from "nodefony";
|
|
2
|
+
import cluster from "node:cluster";
|
|
2
3
|
//#region nodefony/src/servers/portBinder.ts
|
|
4
|
+
/**
|
|
5
|
+
* **portBinder** — écoute sur le port désiré, ou sur le prochain port libre.
|
|
6
|
+
*
|
|
7
|
+
* Pourquoi ce fichier existe : lancer deux apps Nodefony côte à côte (le repo et
|
|
8
|
+
* une app générée, deux projets, un banc et une démo…) faisait mourir la seconde
|
|
9
|
+
* en `EADDRINUSE`. Le port désiré est une PRÉFÉRENCE en développement ; il reste
|
|
10
|
+
* un CONTRAT en production.
|
|
11
|
+
*
|
|
12
|
+
* ## Pourquoi retenter au `listen()` plutôt que sonder d'abord
|
|
13
|
+
*
|
|
14
|
+
* La tentation est d'appeler « le port 5151 est-il libre ? » puis de binder. C'est
|
|
15
|
+
* une **course** (TOCTOU) : entre la réponse et le bind, un autre process peut
|
|
16
|
+
* prendre le port — et on échoue quand même, après avoir cru le contraire. Le
|
|
17
|
+
* `listen()` est, lui, **atomique** : soit il réussit, soit le noyau dit
|
|
18
|
+
* `EADDRINUSE`. On retente donc sur l'échec réel, jamais sur une prédiction.
|
|
19
|
+
*
|
|
20
|
+
* ## Pourquoi une SONDE malgré cela
|
|
21
|
+
*
|
|
22
|
+
* Le `listen()` est atomique, mais il ne dit pas tout : un port peut être **déjà
|
|
23
|
+
* servi** et le noyau l'accorder quand même. Lier `0.0.0.0:P` alors qu'un autre
|
|
24
|
+
* processus tient `127.0.0.1:P` **réussit** sur macOS et les BSD — ce sont deux
|
|
25
|
+
* liaisons distinctes, et les connexions locales partent à la PLUS SPÉCIFIQUE.
|
|
26
|
+
* Entre familles d'adresses (`::1` tenu, `0.0.0.0` demandé), même linux
|
|
27
|
+
* l'accorde. L'application annonce alors `READY`, publie ses ports… et ne reçoit
|
|
28
|
+
* rien : une panne muette, et un banc qui interroge le serveur du VOISIN.
|
|
29
|
+
*
|
|
30
|
+
* On constate donc, AVANT de binder, qu'aucun serveur ne répond déjà là où notre
|
|
31
|
+
* liaison va recevoir (`isPortListening` — une connexion en boucle locale, aucun
|
|
32
|
+
* outil système, même verdict sur les trois plateformes). Ce n'est pas la sonde
|
|
33
|
+
* « ce port est-il libre ? » écartée plus haut : elle ne remplace pas le repli
|
|
34
|
+
* atomique, qui reste le seul à trancher la course — elle ajoute le seul conflit
|
|
35
|
+
* que le noyau n'exprime JAMAIS. La course résiduelle (un tiers prend le port
|
|
36
|
+
* entre la sonde et le bind) retombe donc sur `EADDRINUSE`, inchangé.
|
|
37
|
+
*
|
|
38
|
+
* ## Pourquoi sauter les ports réservés
|
|
39
|
+
*
|
|
40
|
+
* HTTP veut 5151, HTTPS veut 5152. Si 5151 est pris, incrémenter naïvement ferait
|
|
41
|
+
* voler 5152 à HTTPS — qui se décalerait à son tour, en cascade. Les ports que les
|
|
42
|
+
* AUTRES serveurs convoitent sont donc sautés d'emblée.
|
|
43
|
+
*
|
|
44
|
+
* ## Fail-loud
|
|
45
|
+
*
|
|
46
|
+
* Un décalage est TOUJOURS annoncé (`onShift`) : une app qui écoute ailleurs que
|
|
47
|
+
* là où on l'attend sans le dire est une dégradation silencieuse.
|
|
48
|
+
*/
|
|
3
49
|
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
4
50
|
const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
5
51
|
/** Port configuré d'un serveur, ou `0` (désactivé / non précisé → choix noyau). */
|
|
@@ -127,12 +173,21 @@ function isPortUnavailable(code, port) {
|
|
|
127
173
|
* handler d'erreur durable s'installe APRÈS, sur le serveur qui écoute — sinon il
|
|
128
174
|
* verrait passer les `EADDRINUSE` de repli et croirait à une panne).
|
|
129
175
|
*
|
|
176
|
+
* @param server - serveur à mettre en écoute.
|
|
177
|
+
* @param host - hôte passé à `listen`.
|
|
178
|
+
* @param plan - port désiré et nombre de replis permis.
|
|
179
|
+
* @param probe - sonde de présence (injectée par le banc).
|
|
180
|
+
* @param sharedListener - vrai dans un worker `node:cluster` : le port y est
|
|
181
|
+
* PARTAGÉ par le maître, et un voisin qui y répond est un frère, pas un
|
|
182
|
+
* conflit. Sans cette exception, `nodefony cluster -w N` ne gardait qu'un
|
|
183
|
+
* worker en vie — les autres mouraient sur « port déjà servi » et
|
|
184
|
+
* bouclaient en relance.
|
|
130
185
|
* @returns l'adresse obtenue + le port désiré si un décalage a eu lieu.
|
|
131
186
|
* @throws l'erreur de `listen` : soit un code qui ne dit pas « ce port est pris »
|
|
132
187
|
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
133
188
|
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
134
189
|
*/
|
|
135
|
-
async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
190
|
+
async function bindWithFallback(server, host, plan, probe = isPortListening, sharedListener = cluster.isWorker) {
|
|
136
191
|
let candidate = plan.desired;
|
|
137
192
|
let used = 0;
|
|
138
193
|
/**
|
|
@@ -154,7 +209,7 @@ async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
|
154
209
|
server.listen(port, host);
|
|
155
210
|
});
|
|
156
211
|
for (;;) {
|
|
157
|
-
const conflict = plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
|
|
212
|
+
const conflict = sharedListener || plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
|
|
158
213
|
const failure = conflict ? portConflictError(candidate, conflict) : await listenOnce(candidate);
|
|
159
214
|
if (failure === null) return {
|
|
160
215
|
address: server.address(),
|
|
@@ -87,6 +87,7 @@ export declare const httpConfigSchema: z.ZodObject<{
|
|
|
87
87
|
name: z.ZodOptional<z.ZodString>;
|
|
88
88
|
value: z.ZodOptional<z.ZodString>;
|
|
89
89
|
shortName: z.ZodOptional<z.ZodString>;
|
|
90
|
+
type: z.ZodOptional<z.ZodString>;
|
|
90
91
|
}, z.core.$loose>>>;
|
|
91
92
|
}, z.core.$strict>>;
|
|
92
93
|
}, z.core.$strict>>;
|