yusufkhon-guard 1.0.0

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +734 -0
  3. package/bin/cli.js +39 -0
  4. package/bin/renew.js +149 -0
  5. package/bin/scan.js +124 -0
  6. package/bin/ssl.js +181 -0
  7. package/package.json +80 -0
  8. package/src/index.js +206 -0
  9. package/src/modules/acme/client.js +240 -0
  10. package/src/modules/acme/csr.js +172 -0
  11. package/src/modules/acme/detectDomains.js +132 -0
  12. package/src/modules/acme/index.js +211 -0
  13. package/src/modules/acme/jose.js +125 -0
  14. package/src/modules/acme/renew.js +89 -0
  15. package/src/modules/adaptiveRateLimiter.js +154 -0
  16. package/src/modules/alerts.js +168 -0
  17. package/src/modules/bodyLimit.js +106 -0
  18. package/src/modules/botDetector.js +144 -0
  19. package/src/modules/connectionGuard.js +128 -0
  20. package/src/modules/csrf.js +130 -0
  21. package/src/modules/dashboard.js +257 -0
  22. package/src/modules/dashboardHtml.js +235 -0
  23. package/src/modules/datacenterRanges.js +121 -0
  24. package/src/modules/ddosGuard.js +323 -0
  25. package/src/modules/firewall.js +239 -0
  26. package/src/modules/geoBlock.js +169 -0
  27. package/src/modules/headers.js +130 -0
  28. package/src/modules/honeypot.js +149 -0
  29. package/src/modules/httpsRedirect.js +67 -0
  30. package/src/modules/integrity.js +164 -0
  31. package/src/modules/ipBlocker.js +372 -0
  32. package/src/modules/ja3.js +155 -0
  33. package/src/modules/logExport.js +134 -0
  34. package/src/modules/nosqlGuard.js +143 -0
  35. package/src/modules/osFirewall.js +182 -0
  36. package/src/modules/rateLimiter.js +196 -0
  37. package/src/modules/sanitizer.js +253 -0
  38. package/src/modules/threatDetector.js +202 -0
  39. package/src/scanner/checks/dependencies.js +125 -0
  40. package/src/scanner/checks/environment.js +118 -0
  41. package/src/scanner/checks/exposure.js +99 -0
  42. package/src/scanner/checks/headers.js +119 -0
  43. package/src/scanner/checks/ssl.js +151 -0
  44. package/src/scanner/index.js +209 -0
  45. package/src/utils/cidr.js +140 -0
  46. package/src/utils/logger.js +194 -0
  47. package/src/utils/privilege.js +212 -0
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file detectDomains.js
5
+ * @description VPS'da sozlangan domen(lar)ni AVTOMAT aniqlaydi — foydalanuvchi
6
+ * `--domain` bermasa, `npx yusufkhon-guard ssl` o'zi topib, admin'ga
7
+ * tasdiqlash uchun ko'rsatadi.
8
+ *
9
+ * Manbalar: nginx `server_name`, apache `ServerName`/`ServerAlias`,
10
+ * tizim hostnomi (FQDN bo'lsa). Yo'llar test uchun sozlanadi.
11
+ * @module modules/acme/detectDomains
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+
18
+ /** Standart nginx konfiguratsiya yo'llari. @type {string[]} */
19
+ const DEFAULT_NGINX = ['/etc/nginx/sites-enabled', '/etc/nginx/conf.d', '/etc/nginx/nginx.conf'];
20
+ /** Standart apache konfiguratsiya yo'llari. @type {string[]} */
21
+ const DEFAULT_APACHE = ['/etc/apache2/sites-enabled', '/etc/httpd/conf.d'];
22
+
23
+ /**
24
+ * Nomzod haqiqiy, ommaviy domenmi? (localhost, IP, wildcard, .local rad etiladi)
25
+ * @param {string} d
26
+ * @returns {boolean}
27
+ */
28
+ function isValidDomain(d) {
29
+ if (!d) return false;
30
+ const s = String(d).toLowerCase().trim().replace(/;$/, '');
31
+ if (s === 'localhost' || s === '_' || s === '""') return false;
32
+ if (s.startsWith('*.') || s.includes('*')) return false; // wildcard -> DNS-01 kerak
33
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(s)) return false; // IPv4
34
+ if (s.includes(':')) return false; // IPv6/port
35
+ if (/\.(local|lan|localdomain|internal|test|example|invalid)$/.test(s)) return false;
36
+ if (!s.includes('.')) return false; // FQDN bo'lishi shart
37
+ return /^[a-z0-9.-]+$/.test(s) && !s.startsWith('.') && !s.endsWith('.');
38
+ }
39
+
40
+ /**
41
+ * Fayl yoki papkadan barcha matn kontentini o'qiydi (xatolarni e'tiborsiz qoldiradi).
42
+ * @param {string} target
43
+ * @returns {string[]}
44
+ */
45
+ function readAll(target) {
46
+ const out = [];
47
+ try {
48
+ const st = fs.statSync(target);
49
+ if (st.isDirectory()) {
50
+ for (const name of fs.readdirSync(target)) {
51
+ try { out.push(fs.readFileSync(path.join(target, name), 'utf8')); } catch { /* skip */ }
52
+ }
53
+ } else {
54
+ out.push(fs.readFileSync(target, 'utf8'));
55
+ }
56
+ } catch { /* mavjud emas — skip */ }
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * nginx kontentidan `server_name` domenlarini ajratadi.
62
+ * @param {string} content
63
+ * @returns {string[]}
64
+ */
65
+ function parseNginx(content) {
66
+ const out = [];
67
+ const re = /server_name\s+([^;]+);/gi;
68
+ let m;
69
+ while ((m = re.exec(content))) {
70
+ for (const tok of m[1].trim().split(/\s+/)) out.push(tok);
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * apache kontentidan `ServerName`/`ServerAlias` domenlarini ajratadi.
77
+ * @param {string} content
78
+ * @returns {string[]}
79
+ */
80
+ function parseApache(content) {
81
+ const out = [];
82
+ let m;
83
+ const reName = /^\s*ServerName\s+(\S+)/gim;
84
+ while ((m = reName.exec(content))) out.push(m[1]);
85
+ const reAlias = /^\s*ServerAlias\s+([^\n]+)/gim;
86
+ while ((m = reAlias.exec(content))) for (const tok of m[1].trim().split(/\s+/)) out.push(tok);
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * @typedef {Object} DetectedDomain
92
+ * @property {string} domain
93
+ * @property {string} source - 'nginx' | 'apache' | 'hostname'
94
+ */
95
+
96
+ /**
97
+ * VPS'dagi sozlangan domenlarni aniqlaydi.
98
+ * @param {Object} [options]
99
+ * @param {string[]} [options.nginxPaths] - nginx yo'llari (test uchun).
100
+ * @param {string[]} [options.apachePaths] - apache yo'llari (test uchun).
101
+ * @param {string} [options.hostname] - tizim hostnomi (test uchun).
102
+ * @returns {DetectedDomain[]} Takrorlanmas, haqiqiy domenlar (aniqlanish tartibida).
103
+ */
104
+ function detectDomains(options = {}) {
105
+ /** @type {DetectedDomain[]} */
106
+ const found = [];
107
+ const add = (domain, source) => {
108
+ const d = String(domain).toLowerCase().trim().replace(/;$/, '');
109
+ if (isValidDomain(d) && !found.some((f) => f.domain === d)) found.push({ domain: d, source });
110
+ };
111
+
112
+ for (const p of options.nginxPaths || DEFAULT_NGINX) {
113
+ for (const content of readAll(p)) for (const d of parseNginx(content)) add(d, 'nginx');
114
+ }
115
+ for (const p of options.apachePaths || DEFAULT_APACHE) {
116
+ for (const content of readAll(p)) for (const d of parseApache(content)) add(d, 'apache');
117
+ }
118
+
119
+ const hostname = options.hostname !== undefined ? options.hostname : os.hostname();
120
+ add(hostname, 'hostname');
121
+
122
+ return found;
123
+ }
124
+
125
+ module.exports = {
126
+ detectDomains,
127
+ isValidDomain,
128
+ parseNginx,
129
+ parseApache,
130
+ DEFAULT_NGINX,
131
+ DEFAULT_APACHE,
132
+ };
@@ -0,0 +1,211 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file acme/index.js
5
+ * @description Let's Encrypt (ACME v2) uchun yuqori darajali, foydalanish oson API.
6
+ * HTTP-01 challenge'ni bajarish orqali domen uchun bepul SSL/TLS
7
+ * sertifikatini avtomat oladi. Tashqi kutubxonasiz (faqat Node ichki
8
+ * `crypto`, `fetch`, `fs`).
9
+ *
10
+ * DIQQAT: HTTP-01 uchun domen 80-portda ilovangizga yo'naltirilgan
11
+ * va internetdan ochiq bo'lishi kerak. Challenge middleware'ni
12
+ * firewall'dan OLDIN ulang.
13
+ * @module modules/acme
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const jose = require('./jose');
19
+ const { createCsr } = require('./csr');
20
+ const { AcmeClient, DIRECTORY_URLS } = require('./client');
21
+ const { detectDomains } = require('./detectDomains');
22
+ const { checkCert, needsRenewal, certDomains, daysUntilExpiry } = require('./renew');
23
+ const { defaultLogger } = require('../../utils/logger');
24
+
25
+ /**
26
+ * HTTP-01 challenge javoblarini saqlovchi oddiy xotira ombori.
27
+ * @class
28
+ */
29
+ class ChallengeStore {
30
+ constructor() {
31
+ /** @type {Map<string, string>} token -> keyAuthorization */
32
+ this.map = new Map();
33
+ }
34
+ /** @param {string} token @param {string} keyAuth @returns {void} */
35
+ set(token, keyAuth) { this.map.set(token, keyAuth); }
36
+ /** @param {string} token @returns {string|undefined} */
37
+ get(token) { return this.map.get(token); }
38
+ /** @param {string} token @returns {void} */
39
+ remove(token) { this.map.delete(token); }
40
+ }
41
+
42
+ /**
43
+ * `/.well-known/acme-challenge/<token>` so'rovlariga javob beruvchi Express
44
+ * middleware. Uni firewall/guard'dan OLDIN ulang.
45
+ *
46
+ * @example
47
+ * const acme = require('yusufkhon-guard').acme;
48
+ * const store = new acme.ChallengeStore();
49
+ * app.use(acme.challengeMiddleware(store)); // guard'dan oldin!
50
+ *
51
+ * @param {ChallengeStore} store
52
+ * @returns {import('express').RequestHandler}
53
+ */
54
+ function challengeMiddleware(store) {
55
+ const PREFIX = '/.well-known/acme-challenge/';
56
+ return function acmeChallengeMiddleware(req, res, next) {
57
+ const url = req.originalUrl || req.url || '';
58
+ if (req.method === 'GET' && url.startsWith(PREFIX)) {
59
+ const token = url.slice(PREFIX.length).split('?')[0];
60
+ const keyAuth = store.get(token);
61
+ if (keyAuth) {
62
+ res.statusCode = 200;
63
+ res.setHeader('Content-Type', 'text/plain');
64
+ res.end(keyAuth);
65
+ return;
66
+ }
67
+ res.statusCode = 404;
68
+ res.end('Not found');
69
+ return;
70
+ }
71
+ return next();
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Hisob kalitini fayldan yuklaydi yoki yangisini yaratib saqlaydi.
77
+ * @private
78
+ * @param {string|undefined} keyPath
79
+ * @param {import('../../utils/logger').Logger} logger
80
+ * @returns {import('crypto').KeyObject}
81
+ */
82
+ function loadOrCreateAccountKey(keyPath, logger) {
83
+ if (keyPath && fs.existsSync(keyPath)) {
84
+ logger.info('Mavjud ACME hisob kaliti yuklandi.');
85
+ return jose.importPrivateKeyPem(fs.readFileSync(keyPath, 'utf8'));
86
+ }
87
+ const { privateKey } = jose.generateAccountKey();
88
+ if (keyPath) {
89
+ fs.mkdirSync(path.dirname(path.resolve(keyPath)), { recursive: true });
90
+ fs.writeFileSync(keyPath, jose.exportPrivateKeyPem(privateKey), { mode: 0o600 });
91
+ logger.info('Yangi ACME hisob kaliti yaratildi va saqlandi.');
92
+ }
93
+ return privateKey;
94
+ }
95
+
96
+ /**
97
+ * @typedef {Object} ObtainOptions
98
+ * @property {string|string[]} domains - Sertifikat qamrab oladigan domen(lar).
99
+ * @property {string} [email] - Bog'lanish uchun email (tavsiya etiladi).
100
+ * @property {ChallengeStore} challengeStore - challengeMiddleware bilan bir xil ombor.
101
+ * @property {'staging'|'production'} [environment='staging'] - Sinov (staging) yoki jonli.
102
+ * @property {string} [certDir='./certs'] - Sertifikat saqlanadigan papka.
103
+ * @property {string} [accountKeyPath] - Hisob kaliti saqlanadigan fayl (qayta ishlatish uchun).
104
+ * @property {import('../../utils/logger').Logger} [logger=defaultLogger] - Logger.
105
+ */
106
+
107
+ /**
108
+ * @typedef {Object} ObtainResult
109
+ * @property {string} certificate - PEM sertifikat zanjiri (fullchain).
110
+ * @property {string} privateKey - Domen yopiq kaliti (PEM).
111
+ * @property {{ fullchain: string, privkey: string }} paths - Yozilgan fayllar yo'llari.
112
+ * @property {string} environment - Ishlatilgan muhit.
113
+ */
114
+
115
+ /**
116
+ * Domen(lar) uchun ACME (Let's Encrypt) orqali SSL sertifikatini to'liq avtomat oladi.
117
+ *
118
+ * Standart holatda **staging** muhitida ishlaydi (soxta, lekin xavfsiz sertifikat) —
119
+ * jonli sertifikat uchun `environment: 'production'` bering.
120
+ *
121
+ * @example
122
+ * const acme = require('yusufkhon-guard').acme;
123
+ * const store = new acme.ChallengeStore();
124
+ * app.use(acme.challengeMiddleware(store));
125
+ * // ... server 80-portda ishga tushgach:
126
+ * const res = await acme.obtainCertificate({
127
+ * domains: 'example.com',
128
+ * email: 'admin@example.com',
129
+ * challengeStore: store,
130
+ * environment: 'production',
131
+ * });
132
+ *
133
+ * @param {ObtainOptions} options
134
+ * @returns {Promise<ObtainResult>}
135
+ */
136
+ async function obtainCertificate(options) {
137
+ const logger = options.logger || defaultLogger;
138
+ const domains = Array.isArray(options.domains) ? options.domains : [options.domains];
139
+ const environment = options.environment || 'staging';
140
+ const certDir = path.resolve(options.certDir || './certs');
141
+ const store = options.challengeStore;
142
+
143
+ if (!store) throw new Error('obtainCertificate(): `challengeStore` majburiy.');
144
+ if (!domains.length || !domains[0]) throw new Error('obtainCertificate(): `domains` majburiy.');
145
+
146
+ logger.info(`ACME sertifikat jarayoni boshlandi [${environment}]: ${domains.join(', ')}`);
147
+
148
+ const accountKey = loadOrCreateAccountKey(options.accountKeyPath, logger);
149
+ const client = new AcmeClient({ environment, accountKey, logger });
150
+
151
+ await client.init();
152
+ await client.createAccount({ email: options.email });
153
+
154
+ const { order, orderUrl } = await client.createOrder(domains);
155
+ logger.info(`Buyurtma ochildi (${order.authorizations.length} ta avtorizatsiya).`);
156
+
157
+ // Har bir domen avtorizatsiyasini HTTP-01 orqali bajaramiz.
158
+ for (const authUrl of order.authorizations) {
159
+ const auth = await client.getAuthorization(authUrl);
160
+ const challenge = (auth.challenges || []).find((c) => c.type === 'http-01');
161
+ if (!challenge) throw new Error(`http-01 challenge topilmadi: ${auth.identifier && auth.identifier.value}`);
162
+
163
+ const keyAuth = client.keyAuthorization(challenge.token);
164
+ store.set(challenge.token, keyAuth);
165
+ logger.info(`Challenge o'rnatildi: ${auth.identifier.value}`);
166
+
167
+ await client.completeChallenge(challenge.url);
168
+ await client.poll(authUrl, ['valid'], { tries: 20, intervalMs: 3000 });
169
+ store.remove(challenge.token);
170
+ logger.success(`Domen tasdiqlandi: ${auth.identifier.value}`);
171
+ }
172
+
173
+ // CSR yaratib, buyurtmani yakunlaymiz.
174
+ const csr = createCsr(domains);
175
+ await client.finalizeOrder(order.finalize, csr.csrDerBase64Url);
176
+ const finalOrder = await client.poll(orderUrl, ['valid'], { tries: 20, intervalMs: 3000 });
177
+
178
+ const certificate = await client.downloadCertificate(finalOrder.certificate);
179
+
180
+ // Fayllarga yozamiz.
181
+ fs.mkdirSync(certDir, { recursive: true });
182
+ const fullchainPath = path.join(certDir, 'fullchain.pem');
183
+ const privkeyPath = path.join(certDir, 'privkey.pem');
184
+ fs.writeFileSync(fullchainPath, certificate);
185
+ fs.writeFileSync(privkeyPath, csr.privateKeyPem, { mode: 0o600 });
186
+
187
+ logger.success(`✅ Sertifikat olindi va saqlandi: ${certDir}`);
188
+
189
+ return {
190
+ certificate,
191
+ privateKey: csr.privateKeyPem,
192
+ paths: { fullchain: fullchainPath, privkey: privkeyPath },
193
+ environment,
194
+ };
195
+ }
196
+
197
+ module.exports = {
198
+ obtainCertificate,
199
+ challengeMiddleware,
200
+ ChallengeStore,
201
+ AcmeClient,
202
+ createCsr,
203
+ jose,
204
+ DIRECTORY_URLS,
205
+ // Auto-detect va yangilash (renewal)
206
+ detectDomains,
207
+ checkCert,
208
+ needsRenewal,
209
+ certDomains,
210
+ daysUntilExpiry,
211
+ };
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file jose.js
5
+ * @description ACME v2 (RFC 8555) uchun JOSE/JWS kriptografik yordamchilari.
6
+ * Faqat Node.js ichki `crypto` modulidan foydalanadi (zero-dependency).
7
+ * Hisob (account) kaliti sifatida ECDSA P-256 (ES256) ishlatiladi.
8
+ * @module modules/acme/jose
9
+ */
10
+
11
+ const crypto = require('crypto');
12
+
13
+ /**
14
+ * Buffer yoki matnni base64url ko'rinishiga o'giradi (to'ldiruvchi `=` siz).
15
+ * @param {Buffer|string} input
16
+ * @returns {string}
17
+ */
18
+ function base64url(input) {
19
+ const buf = Buffer.isBuffer(input) ? input : Buffer.from(String(input));
20
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
21
+ }
22
+
23
+ /**
24
+ * Obyektni JSON -> base64url ko'rinishiga o'giradi.
25
+ * @param {Object} obj
26
+ * @returns {string}
27
+ */
28
+ function base64urlJson(obj) {
29
+ return base64url(JSON.stringify(obj));
30
+ }
31
+
32
+ /**
33
+ * Yangi ECDSA P-256 (ES256) kalit juftligini yaratadi.
34
+ * @returns {{ publicKey: crypto.KeyObject, privateKey: crypto.KeyObject }}
35
+ */
36
+ function generateAccountKey() {
37
+ return crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
38
+ }
39
+
40
+ /**
41
+ * PEM matnidan yopiq (private) kalitni tiklaydi.
42
+ * @param {string} pem - PKCS#8 PEM.
43
+ * @returns {crypto.KeyObject}
44
+ */
45
+ function importPrivateKeyPem(pem) {
46
+ return crypto.createPrivateKey(pem);
47
+ }
48
+
49
+ /**
50
+ * Yopiq kalitni PKCS#8 PEM ko'rinishida eksport qiladi (faylga saqlash uchun).
51
+ * @param {crypto.KeyObject} privateKey
52
+ * @returns {string}
53
+ */
54
+ function exportPrivateKeyPem(privateKey) {
55
+ return privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
56
+ }
57
+
58
+ /**
59
+ * Ochiq kalitning ACME talab qiladigan JWK (faqat kerakli maydonlar) ko'rinishi.
60
+ * Maydonlar tartibi thumbprint uchun muhim (leksikografik).
61
+ * @param {crypto.KeyObject} publicKey
62
+ * @returns {{ crv: string, kty: string, x: string, y: string }}
63
+ */
64
+ function publicJwk(publicKey) {
65
+ const jwk = publicKey.export({ format: 'jwk' });
66
+ // RFC 7638 bo'yicha thumbprint uchun aynan shu tartib va maydonlar.
67
+ return { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y };
68
+ }
69
+
70
+ /**
71
+ * JWK thumbprint (RFC 7638) — SHA-256, base64url. HTTP-01 kalit avtorizatsiyasida
72
+ * ishlatiladi.
73
+ * @param {{ crv: string, kty: string, x: string, y: string }} jwk
74
+ * @returns {string}
75
+ */
76
+ function jwkThumbprint(jwk) {
77
+ const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
78
+ return base64url(crypto.createHash('sha256').update(canonical).digest());
79
+ }
80
+
81
+ /**
82
+ * ACME "flattened JWS" imzolangan so'rov tanasini tuzadi.
83
+ *
84
+ * @param {Object} params
85
+ * @param {crypto.KeyObject} params.privateKey - Hisob yopiq kaliti (ES256).
86
+ * @param {string} params.url - So'rov yuboriladigan ACME URL (protected header'ga kiradi).
87
+ * @param {string} params.nonce - Replay-Nonce qiymati.
88
+ * @param {Object|string} params.payload - Yuk (bo'sh POST-as-GET uchun "" bering).
89
+ * @param {string} [params.kid] - Hisob URL'i (mavjud bo'lsa `jwk` o'rniga ishlatiladi).
90
+ * @param {{crv:string,kty:string,x:string,y:string}} [params.jwk] - newAccount uchun ochiq JWK.
91
+ * @returns {string} `application/jose+json` uchun tayyor JSON matn.
92
+ */
93
+ function signedJws({ privateKey, url, nonce, payload, kid, jwk }) {
94
+ /** @type {Object} */
95
+ const protectedHeader = { alg: 'ES256', nonce, url };
96
+ if (kid) protectedHeader.kid = kid;
97
+ else protectedHeader.jwk = jwk;
98
+
99
+ const protected64 = base64urlJson(protectedHeader);
100
+ const payload64 = payload === '' ? '' : base64urlJson(payload);
101
+ const signingInput = `${protected64}.${payload64}`;
102
+
103
+ // ES256: ECDSA imzosi JOSE formatida (r||s, 64 bayt) bo'lishi shart.
104
+ const signature = crypto.sign('SHA256', Buffer.from(signingInput), {
105
+ key: privateKey,
106
+ dsaEncoding: 'ieee-p1363',
107
+ });
108
+
109
+ return JSON.stringify({
110
+ protected: protected64,
111
+ payload: payload64,
112
+ signature: base64url(signature),
113
+ });
114
+ }
115
+
116
+ module.exports = {
117
+ base64url,
118
+ base64urlJson,
119
+ generateAccountKey,
120
+ importPrivateKeyPem,
121
+ exportPrivateKeyPem,
122
+ publicJwk,
123
+ jwkThumbprint,
124
+ signedJws,
125
+ };
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file renew.js
5
+ * @description Sertifikat yangilash yordamchilari. Mavjud sertifikatning amal
6
+ * qilish muddatini va SAN domenlarini o'qiydi hamda `<N kun` qolganini
7
+ * (yangilash kerakligini) aniqlaydi. Node ichki `crypto.X509Certificate`
8
+ * dan foydalanadi.
9
+ * @module modules/acme/renew
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const crypto = require('crypto');
14
+
15
+ /**
16
+ * PEM sertifikatdan SAN (subjectAltName) domenlarini oladi.
17
+ * @param {string} pem
18
+ * @returns {string[]} DNS domenlari.
19
+ */
20
+ function certDomains(pem) {
21
+ const cert = new crypto.X509Certificate(pem);
22
+ const san = cert.subjectAltName || '';
23
+ return san
24
+ .split(',')
25
+ .map((s) => s.trim())
26
+ .filter((s) => s.toUpperCase().startsWith('DNS:'))
27
+ .map((s) => s.slice(4).trim().toLowerCase());
28
+ }
29
+
30
+ /**
31
+ * PEM sertifikat tugashiga necha kun qolganini qaytaradi (o'tgan bo'lsa manfiy).
32
+ * @param {string} pem
33
+ * @returns {number}
34
+ */
35
+ function daysUntilExpiry(pem) {
36
+ const cert = new crypto.X509Certificate(pem);
37
+ const to = new Date(cert.validTo).getTime();
38
+ return Math.floor((to - Date.now()) / (24 * 60 * 60 * 1000));
39
+ }
40
+
41
+ /**
42
+ * @typedef {Object} CertStatus
43
+ * @property {boolean} exists - Fayl mavjudmi.
44
+ * @property {number} [daysLeft] - Tugashiga qolgan kunlar.
45
+ * @property {string[]} [domains] - SAN domenlari.
46
+ * @property {boolean} needsRenewal - Yangilash kerakmi.
47
+ * @property {string} reason
48
+ */
49
+
50
+ /**
51
+ * Sertifikat holatini tekshiradi (mavjudmi, qancha qolgan, yangilash kerakmi).
52
+ * @param {string} certPath - fullchain.pem yo'li.
53
+ * @param {number} [thresholdDays=30] - Shu kundan kam qolsa — yangilanadi.
54
+ * @returns {CertStatus}
55
+ */
56
+ function checkCert(certPath, thresholdDays = 30) {
57
+ let pem;
58
+ try {
59
+ pem = fs.readFileSync(certPath, 'utf8');
60
+ } catch {
61
+ return { exists: false, needsRenewal: true, reason: 'sertifikat topilmadi' };
62
+ }
63
+ try {
64
+ const daysLeft = daysUntilExpiry(pem);
65
+ const domains = certDomains(pem);
66
+ const need = daysLeft < thresholdDays;
67
+ return {
68
+ exists: true,
69
+ daysLeft,
70
+ domains,
71
+ needsRenewal: need,
72
+ reason: need ? `${daysLeft} kun qoldi (< ${thresholdDays})` : `${daysLeft} kun qoldi — hali yangi`,
73
+ };
74
+ } catch (e) {
75
+ return { exists: true, needsRenewal: true, reason: 'sertifikatni o\'qib bo\'lmadi: ' + e.message };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Sertifikatni yangilash kerakmi (qisqa yordamchi).
81
+ * @param {string} certPath
82
+ * @param {number} [thresholdDays=30]
83
+ * @returns {boolean}
84
+ */
85
+ function needsRenewal(certPath, thresholdDays = 30) {
86
+ return checkCert(certPath, thresholdDays).needsRenewal;
87
+ }
88
+
89
+ module.exports = { certDomains, daysUntilExpiry, checkCert, needsRenewal };