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,128 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file connectionGuard.js
5
+ * @description Ulanish (soket) darajasidagi L7 DDoS himoyasi — Slowloris va
6
+ * slow-POST hujumlariga qarshi. Bunday hujumlar so'rovni ataylab
7
+ * juda sekin yuborib, server soketlarini uzoq band qilib qo'yadi.
8
+ *
9
+ * Himoya choralari:
10
+ * • sarlavha va butun so'rovni qabul qilish uchun timeout;
11
+ * • bo'sh turgan (idle) soketni uzish;
12
+ * • bitta IP dan ochilishi mumkin bo'lgan ulanishlar sonini cheklash.
13
+ *
14
+ * Bu — middleware EMAS, balki `http`/`https` serverga biriktiriladigan
15
+ * yordamchi funksiya.
16
+ * @module modules/connectionGuard
17
+ */
18
+
19
+ const { defaultLogger } = require('../utils/logger');
20
+
21
+ /**
22
+ * @typedef {Object} ConnectionGuardOptions
23
+ * @property {number} [headersTimeoutMs=10000] - Sarlavhalarni to'liq qabul qilish muddati (Slowloris, Node ichki).
24
+ * @property {number} [requestTimeoutMs=20000] - Butun so'rovni qabul qilish muddati, slow-POST (0 -> o'chirilgan).
25
+ * @property {number} [idleSocketMs=10000] - Soket shu vaqt davomida jim tursa (ma'lumot kelmasa) uziladi — TEZKOR Slowloris himoyasi.
26
+ * @property {number} [keepAliveTimeoutMs=5000] - Bo'sh Keep-Alive ulanishini yopish muddati.
27
+ * @property {number} [checkIntervalMs=2000] - Node'ning headersTimeout/requestTimeout ni tez-tez tekshirish oralig'i.
28
+ * @property {number} [maxConnectionsPerIp=50] - Bitta IP dan maksimal parallel ulanishlar.
29
+ * @property {string[]} [whitelist] - Cheklovsiz IP'lar.
30
+ * @property {import('./firewall').Firewall} [firewall] - Suiiste'molchini bloklash uchun (ixtiyoriy).
31
+ * @property {import('../utils/logger').Logger} [logger=defaultLogger] - Logger.
32
+ */
33
+
34
+ /** @type {ConnectionGuardOptions} */
35
+ const DEFAULTS = {
36
+ headersTimeoutMs: 10000,
37
+ requestTimeoutMs: 20000,
38
+ idleSocketMs: 10000,
39
+ keepAliveTimeoutMs: 5000,
40
+ checkIntervalMs: 2000,
41
+ maxConnectionsPerIp: 50,
42
+ whitelist: ['127.0.0.1', '::1'],
43
+ };
44
+
45
+ /**
46
+ * `http`/`https` serverga Slowloris himoyasini biriktiradi.
47
+ *
48
+ * @example
49
+ * const http = require('http');
50
+ * const { connectionGuard } = require('yusufkhon-guard');
51
+ * const server = http.createServer(app);
52
+ * connectionGuard(server, { maxConnectionsPerIp: 40 });
53
+ * server.listen(3000);
54
+ *
55
+ * @param {import('http').Server} server - HTTP/HTTPS server.
56
+ * @param {ConnectionGuardOptions} [options={}]
57
+ * @returns {import('http').Server} O'sha server (zanjirlash uchun).
58
+ */
59
+ function connectionGuard(server, options = {}) {
60
+ const opts = { ...DEFAULTS, ...options };
61
+ const logger = opts.logger || defaultLogger;
62
+ const whitelist = new Set(opts.whitelist || []);
63
+
64
+ // Node HTTP server timeout'lari (Slowloris/slow-POST himoyasi).
65
+ // headersTimeout — sarlavhalarni sekin yuborishga qarshi;
66
+ // requestTimeout — butun so'rovni sekin yuborishga qarshi;
67
+ // keepAliveTimeout — bo'sh keep-alive ulanishini yopadi.
68
+ // Node bu timeout'larni `connectionsCheckingInterval` (standart 30s!) bo'yicha
69
+ // tekshiradi — uni tezlashtiramiz, aks holda buzilish 30s kech seziladi.
70
+ server.headersTimeout = opts.headersTimeoutMs;
71
+ if (opts.requestTimeoutMs) server.requestTimeout = opts.requestTimeoutMs;
72
+ server.keepAliveTimeout = opts.keepAliveTimeoutMs;
73
+ if (opts.checkIntervalMs) {
74
+ server.connectionsCheckingInterval = opts.checkIntervalMs;
75
+ }
76
+
77
+ /** @type {Map<string, number>} IP -> ochiq ulanishlar soni. */
78
+ const perIp = new Map();
79
+
80
+ server.on('connection', (socket) => {
81
+ const ip = socket.remoteAddress ? socket.remoteAddress.replace(/^::ffff:/, '') : 'unknown';
82
+
83
+ if (!whitelist.has(ip)) {
84
+ const count = (perIp.get(ip) || 0) + 1;
85
+ perIp.set(ip, count);
86
+
87
+ // Bitta IP dan juda ko'p ulanish -> uzamiz.
88
+ if (count > opts.maxConnectionsPerIp) {
89
+ logger.blocked(ip, 'Slowloris: per-IP ulanishlar chegarasi', { connections: count, limit: opts.maxConnectionsPerIp });
90
+ if (opts.firewall) opts.firewall.blocker.addStrike(ip, 35, 'Connection flood');
91
+ socket.destroy();
92
+ const n = (perIp.get(ip) || 1) - 1;
93
+ if (n <= 0) perIp.delete(ip); else perIp.set(ip, n);
94
+ return;
95
+ }
96
+
97
+ socket.on('close', () => {
98
+ const n = (perIp.get(ip) || 1) - 1;
99
+ if (n <= 0) perIp.delete(ip); else perIp.set(ip, n);
100
+ });
101
+ }
102
+
103
+ // TEZKOR Slowloris himoyasi: soket `idleSocketMs` davomida jim tursa
104
+ // (ma'lumot kelmasa) — uzamiz. Har yangi ma'lumotda taymer qayta boshlanadi.
105
+ if (opts.idleSocketMs) {
106
+ socket.setTimeout(opts.idleSocketMs, () => {
107
+ logger.warn('Slowloris: jim/sekin soket uzildi', { ip });
108
+ socket.destroy();
109
+ });
110
+ }
111
+ });
112
+
113
+ // requestTimeout ishlaganda (sekin so'rov) 408 qaytaradi — buni loglaymiz.
114
+ server.on('timeout', (socket) => {
115
+ try { socket.destroy(); } catch { /* ignore */ }
116
+ });
117
+
118
+ logger.info('connectionGuard yoqildi (Slowloris/slow-POST himoyasi).', {
119
+ headersTimeoutMs: opts.headersTimeoutMs,
120
+ requestTimeoutMs: opts.requestTimeoutMs,
121
+ maxConnectionsPerIp: opts.maxConnectionsPerIp,
122
+ });
123
+
124
+ return server;
125
+ }
126
+
127
+ module.exports = connectionGuard;
128
+ module.exports.connectionGuard = connectionGuard;
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file csrf.js
5
+ * @description CSRF (Cross-Site Request Forgery) himoyasi. O'zgartiruvchi
6
+ * so'rovlarni (POST/PUT/PATCH/DELETE) `Origin`/`Referer` sarlavhalari
7
+ * orqali tekshiradi va begona (cross-site) manbadan kelgan
8
+ * so'rovlarni bloklaydi.
9
+ *
10
+ * Sabab: brauzerlar cross-site so'rovlarga `Origin` sarlavhasini
11
+ * qo'yadi; agar u ruxsat etilgan hostlar ro'yxatiga kirmasa —
12
+ * so'rov soxta manbadan kelgan bo'lishi mumkin.
13
+ * @module modules/csrf
14
+ */
15
+
16
+ const { defaultLogger } = require('../utils/logger');
17
+
18
+ /**
19
+ * @typedef {Object} CsrfOptions
20
+ * @property {string[]} [allowedOrigins] - Ruxsat etilgan originlar (masalan, 'https://mysite.uz'). Bo'sh -> so'rov hostiga tenglashtiriladi.
21
+ * @property {boolean} [trustSameHost=true] - Origin host === so'rov hosti bo'lsa ruxsat berish.
22
+ * @property {string[]} [methods] - Tekshiriladigan metodlar (standart: POST/PUT/PATCH/DELETE).
23
+ * @property {boolean} [requireOrigin=false] - Origin/Referer umuman bo'lmasa ham bloklash.
24
+ * @property {boolean} [allowNullOrigin=false] - `Origin: null` (sandboxed iframe/opaque) ni ochiq ruxsat berish. Standart holatda RAD etiladi.
25
+ * @property {(req: import('express').Request) => boolean} [skip] - Ba'zi so'rovlarni o'tkazib yuborish (masalan, webhook, API-key).
26
+ * @property {number} [statusCode=403] - Bloklash status kodi.
27
+ * @property {import('../utils/logger').Logger} [logger=defaultLogger] - Logger.
28
+ */
29
+
30
+ /** @type {string[]} */
31
+ const STATE_CHANGING = ['POST', 'PUT', 'PATCH', 'DELETE'];
32
+
33
+ /**
34
+ * URL'dan origin (protokol + host) ni ajratadi.
35
+ * @private
36
+ * @param {string} value
37
+ * @returns {string|null}
38
+ */
39
+ function originOf(value) {
40
+ try {
41
+ const u = new URL(value);
42
+ return `${u.protocol}//${u.host}`;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * CSRF himoya middleware'ini yaratadi.
50
+ *
51
+ * @example
52
+ * const { csrf } = require('yusufkhon-guard');
53
+ * app.use(csrf({ allowedOrigins: ['https://mysite.uz'] }));
54
+ *
55
+ * @param {CsrfOptions} [options={}]
56
+ * @returns {import('express').RequestHandler}
57
+ */
58
+ function csrf(options = {}) {
59
+ const logger = options.logger || defaultLogger;
60
+ const methods = new Set((options.methods || STATE_CHANGING).map((m) => m.toUpperCase()));
61
+ const allowed = new Set(options.allowedOrigins || []);
62
+ const trustSameHost = options.trustSameHost !== false;
63
+ const statusCode = options.statusCode || 403;
64
+
65
+ /**
66
+ * @param {import('express').Request} req
67
+ * @param {import('express').Response} res
68
+ * @param {import('express').NextFunction} next
69
+ */
70
+ return function csrfMiddleware(req, res, next) {
71
+ if (!methods.has(req.method)) return next();
72
+ if (typeof options.skip === 'function' && options.skip(req)) return next();
73
+
74
+ const headers = req.headers || {};
75
+
76
+ const reject = (reason) => {
77
+ logger.blocked(req.ip || 'unknown', `CSRF: ${reason}`, { method: req.method, path: req.originalUrl || req.url });
78
+ return res.status
79
+ ? res.status(statusCode).json({ success: false, error: 'CSRF tekshiruvidan o\'tmadi (begona manba).' })
80
+ : (res.statusCode = statusCode, res.end('Forbidden'));
81
+ };
82
+
83
+ /**
84
+ * Berilgan origin (protokol//host) ruxsatlimi — ro'yxatda yoki same-host.
85
+ * @param {string} value
86
+ * @returns {boolean}
87
+ */
88
+ const isAllowedOrigin = (value) => {
89
+ if (allowed.has(value)) return true;
90
+ if (trustSameHost) {
91
+ const host = headers['host'];
92
+ if (host && value.endsWith('//' + host)) return true;
93
+ }
94
+ return false;
95
+ };
96
+
97
+ const rawOrigin = headers['origin'];
98
+
99
+ // 1. Origin sarlavhasi MAVJUD bo'lsa — u asosiy (authoritative) manba.
100
+ if (rawOrigin !== undefined && rawOrigin !== '') {
101
+ const parsed = originOf(rawOrigin);
102
+
103
+ // `Origin: null` yoki buzuq/opaque — bu "Origin yo'q" bilan BIR XIL EMAS.
104
+ // Bu mavjud, lekin ishonchsiz (sandboxed iframe, file:) manba. Administrator
105
+ // `allowNullOrigin: true` bermasa, HAR DOIM (requireOrigin holatidan
106
+ // qat'i nazar) RAD etiladi.
107
+ if (parsed === null) {
108
+ if (options.allowNullOrigin === true) return next();
109
+ return reject('null/opaque origin');
110
+ }
111
+
112
+ if (isAllowedOrigin(parsed)) return next();
113
+ return reject(`ruxsatsiz origin ${parsed}`);
114
+ }
115
+
116
+ // 2. Origin yo'q — Referer'ga zaxira (fallback) sifatida qaraymiz.
117
+ const refOrigin = headers['referer'] ? originOf(headers['referer']) : null;
118
+ if (refOrigin) {
119
+ if (isAllowedOrigin(refOrigin)) return next();
120
+ return reject(`ruxsatsiz referer origin ${refOrigin}`);
121
+ }
122
+
123
+ // 3. Origin ham, Referer ham umuman yo'q — lenient (yoki requireOrigin bilan qattiq).
124
+ if (options.requireOrigin) return reject('Origin/Referer yo\'q');
125
+ return next();
126
+ };
127
+ }
128
+
129
+ module.exports = csrf;
130
+ module.exports.csrf = csrf;
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file dashboard.js
5
+ * @description Bloklangan IP'lar va xavfsizlik hodisalarini REAL VAQTDA
6
+ * ko'rsatuvchi web admin-panel. Express ilovasiga oddiy middleware
7
+ * sifatida ulanadi va o'zi marshrutlaydi (tashqi kutubxonasiz).
8
+ *
9
+ * Real vaqt yangilanishlari Server-Sent Events (SSE) orqali beriladi.
10
+ * Barcha /api yo'llari token bilan himoyalangan.
11
+ * @module modules/dashboard
12
+ */
13
+
14
+ const crypto = require('crypto');
15
+ const { renderDashboard } = require('./dashboardHtml');
16
+ const { defaultLogger } = require('../utils/logger');
17
+
18
+ /**
19
+ * @typedef {Object} DashboardOptions
20
+ * @property {import('./firewall').Firewall} firewall - Kuzatiladigan Firewall nusxasi (majburiy).
21
+ * @property {import('express').RequestHandler & { ddos: import('events').EventEmitter, metrics: Object }} [ddos] - L7 DDoS middleware (ixtiyoriy) — Attack Mode indikatori uchun.
22
+ * @property {string} [token] - Kirish tokeni. Berilmasa — avtomat yaratiladi va log qilinadi.
23
+ * @property {string} [title='yusufkhon-guard'] - Panel sarlavhasi.
24
+ * @property {import('../utils/logger').Logger} [logger=defaultLogger] - Logger.
25
+ */
26
+
27
+ /**
28
+ * Firewall hodisasi yukini (payload) tarmoq orqali yuborishga XAVFSIZ
29
+ * (JSON'ga aylantiriladigan) ko'rinishga keltiradi — `req` kabi aylanma
30
+ * (circular) obyektlarni chiqarib tashlaydi.
31
+ * @private
32
+ * @param {string} type
33
+ * @param {Object} payload
34
+ * @returns {Object}
35
+ */
36
+ function safeEvent(type, payload = {}) {
37
+ const base = { type, at: Date.now() };
38
+ switch (type) {
39
+ case 'ban':
40
+ return { ...base, ip: payload.ip, reason: payload.reason };
41
+ case 'blocked':
42
+ return { ...base, ip: payload.ip, path: payload.path };
43
+ case 'threat':
44
+ return {
45
+ ...base,
46
+ ip: payload.ip,
47
+ severity: payload.report && payload.report.severity,
48
+ score: payload.report && payload.report.score,
49
+ types: payload.report && payload.report.signals
50
+ ? payload.report.signals.map((s) => s.type)
51
+ : [],
52
+ };
53
+ case 'tick':
54
+ return { ...base, stats: payload };
55
+ default:
56
+ return base;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * JSON javob yuboradi (Express `res.json` bo'lmasa ham ishlaydi).
62
+ * @private
63
+ * @param {import('http').ServerResponse} res
64
+ * @param {number} status
65
+ * @param {Object} body
66
+ * @returns {void}
67
+ */
68
+ function sendJson(res, status, body) {
69
+ res.statusCode = status;
70
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
71
+ res.end(JSON.stringify(body));
72
+ }
73
+
74
+ /**
75
+ * Real vaqtli web admin-panelni Express middleware sifatida yaratadi.
76
+ *
77
+ * @example
78
+ * const guard = require('yusufkhon-guard');
79
+ * const shield = guard({ firewall: {} });
80
+ * app.use(shield);
81
+ * app.use('/admin', guard.dashboard({ firewall: shield.firewall, token: 'maxfiy' }));
82
+ * // Brauzer: http://localhost:3000/admin/?token=maxfiy
83
+ *
84
+ * @param {DashboardOptions} options
85
+ * @returns {import('express').RequestHandler & { token: string, close: () => void }}
86
+ */
87
+ function dashboard(options = {}) {
88
+ const firewall = options.firewall;
89
+ const logger = options.logger || defaultLogger;
90
+ const title = options.title || 'yusufkhon-guard';
91
+ const token = options.token || crypto.randomBytes(16).toString('hex');
92
+
93
+ if (!firewall || typeof firewall.on !== 'function') {
94
+ throw new Error('dashboard(): `firewall` nusxasi majburiy (guard({...}).firewall).');
95
+ }
96
+
97
+ if (!options.token) {
98
+ logger.warn(`Dashboard tokeni avtomat yaratildi: ${token} (uni saqlab qo'ying)`);
99
+ }
100
+
101
+ /** @type {Set<import('http').ServerResponse>} SSE ga ulangan mijozlar. */
102
+ const clients = new Set();
103
+
104
+ /**
105
+ * Barcha ulangan SSE mijozlariga hodisa yuboradi.
106
+ * @param {Object} event - safeEvent() natijasi.
107
+ */
108
+ function broadcast(event) {
109
+ const line = `data: ${JSON.stringify(event)}\n\n`;
110
+ for (const res of clients) {
111
+ try {
112
+ res.write(line);
113
+ } catch {
114
+ clients.delete(res);
115
+ }
116
+ }
117
+ }
118
+
119
+ // Firewall hodisalariga obuna bo'lamiz.
120
+ const listeners = {
121
+ ban: (p) => broadcast(safeEvent('ban', p)),
122
+ blocked: (p) => broadcast(safeEvent('blocked', p)),
123
+ threat: (p) => broadcast(safeEvent('threat', p)),
124
+ tick: (p) => broadcast(safeEvent('tick', p)),
125
+ };
126
+ for (const [ev, fn] of Object.entries(listeners)) firewall.on(ev, fn);
127
+
128
+ // L7 DDoS Attack Mode hodisalari (ixtiyoriy).
129
+ const ddos = options.ddos;
130
+ if (ddos && ddos.ddos && typeof ddos.ddos.on === 'function') {
131
+ ddos.ddos.on('attack-start', (m) => broadcast({ type: 'attack', at: Date.now(), active: true, ...m }));
132
+ ddos.ddos.on('attack-end', (m) => broadcast({ type: 'attack', at: Date.now(), active: false, ...m }));
133
+ }
134
+
135
+ // Har 2 soniyada jonli statistikani yuboramiz (mijoz bo'lsa).
136
+ const statTimer = setInterval(() => {
137
+ if (clients.size === 0) return;
138
+ broadcast(safeEvent('tick', { ...firewall.metrics, ...firewall.blocker.stats() }));
139
+ }, 2000);
140
+ if (typeof statTimer.unref === 'function') statTimer.unref();
141
+
142
+ /**
143
+ * Joriy holat statistikasi.
144
+ * @returns {Object}
145
+ */
146
+ function getStats() {
147
+ return {
148
+ metrics: firewall.metrics,
149
+ store: firewall.blocker.stats(),
150
+ ddos: ddos && ddos.metrics ? ddos.metrics : null,
151
+ uptimeSec: Math.floor(process.uptime()),
152
+ startedAt: Date.now() - Math.floor(process.uptime() * 1000),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * @param {import('express').Request} req
158
+ * @param {import('express').Response} res
159
+ * @param {import('express').NextFunction} next
160
+ */
161
+ function dashboardMiddleware(req, res, next) {
162
+ const parsed = new URL(req.url, 'http://localhost');
163
+ const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
164
+ const qToken = parsed.searchParams.get('token');
165
+ const authed = qToken === token || req.headers['x-guard-token'] === token;
166
+
167
+ // 1. Panel sahifasi (HTML).
168
+ if (req.method === 'GET' && pathname === '/') {
169
+ res.statusCode = 200;
170
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
171
+ res.end(renderDashboard({ title, token: authed ? token : '', authed }));
172
+ return;
173
+ }
174
+
175
+ // 2. Real vaqt oqimi (SSE) — token bilan.
176
+ if (req.method === 'GET' && pathname === '/api/events') {
177
+ if (!authed) return sendJson(res, 401, { error: 'unauthorized' });
178
+ res.statusCode = 200;
179
+ res.setHeader('Content-Type', 'text/event-stream');
180
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
181
+ res.setHeader('Connection', 'keep-alive');
182
+ res.write('retry: 3000\n\n');
183
+ res.write(`data: ${JSON.stringify(safeEvent('hello', {}))}\n\n`);
184
+ clients.add(res);
185
+ req.on('close', () => clients.delete(res));
186
+ return;
187
+ }
188
+
189
+ // 3. Boshqa barcha /api yo'llari — token talab qiladi.
190
+ if (pathname.startsWith('/api/')) {
191
+ if (!authed) return sendJson(res, 401, { error: 'unauthorized' });
192
+
193
+ if (req.method === 'GET' && pathname === '/api/stats') {
194
+ return sendJson(res, 200, { success: true, ...getStats() });
195
+ }
196
+
197
+ if (req.method === 'GET' && pathname === '/api/bans') {
198
+ return sendJson(res, 200, { success: true, banned: firewall.blocker.listBanned() });
199
+ }
200
+
201
+ // /api/unban/:ip
202
+ const unbanMatch = pathname.match(/^\/api\/unban\/(.+)$/);
203
+ if (req.method === 'POST' && unbanMatch) {
204
+ const ip = decodeURIComponent(unbanMatch[1]);
205
+ const ok = firewall.blocker.unban(ip);
206
+ logger.info(`Dashboard: unban ${ip} (${ok ? 'bajarildi' : 'topilmadi'})`);
207
+ return sendJson(res, 200, { success: ok, ip });
208
+ }
209
+
210
+ // /api/block/:ip
211
+ const blockMatch = pathname.match(/^\/api\/block\/(.+)$/);
212
+ if (req.method === 'POST' && blockMatch) {
213
+ const ip = decodeURIComponent(blockMatch[1]);
214
+ const reason = 'Dashboard orqali qo\'lda bloklandi';
215
+ // `ban()` haqiqiy BanRecord qaytaradi — 'ban' hodisasini record bilan
216
+ // emit qilamiz, shunda Alerter/SSE oqimi va custom handler'lar qo'lda
217
+ // bloklashdan ham xabardor bo'ladi (honeypot/ddosGuard bilan izchil).
218
+ const record = firewall.blocker.ban(ip, reason);
219
+ if (typeof firewall.emit === 'function') {
220
+ firewall.emit('ban', { ip, reason, record });
221
+ }
222
+ logger.info(`Dashboard: manual block ${ip}`);
223
+ return sendJson(res, 200, { success: true, ip });
224
+ }
225
+
226
+ // /api/allow/:ip (oq ro'yxatga qo'shish)
227
+ const allowMatch = pathname.match(/^\/api\/allow\/(.+)$/);
228
+ if (req.method === 'POST' && allowMatch) {
229
+ const ip = decodeURIComponent(allowMatch[1]);
230
+ firewall.blocker.allow(ip);
231
+ logger.info(`Dashboard: allow (whitelist) ${ip}`);
232
+ return sendJson(res, 200, { success: true, ip });
233
+ }
234
+
235
+ return sendJson(res, 404, { error: 'not_found' });
236
+ }
237
+
238
+ return next();
239
+ }
240
+
241
+ // Tashqaridan boshqarish uchun qo'shimchalar.
242
+ dashboardMiddleware.token = token;
243
+ dashboardMiddleware.close = () => {
244
+ clearInterval(statTimer);
245
+ for (const [ev, fn] of Object.entries(listeners)) firewall.removeListener(ev, fn);
246
+ for (const res of clients) {
247
+ try { res.end(); } catch { /* ignore */ }
248
+ }
249
+ clients.clear();
250
+ };
251
+
252
+ return dashboardMiddleware;
253
+ }
254
+
255
+ module.exports = dashboard;
256
+ module.exports.dashboard = dashboard;
257
+ module.exports.safeEvent = safeEvent;