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.
- package/LICENSE +21 -0
- package/README.md +734 -0
- package/bin/cli.js +39 -0
- package/bin/renew.js +149 -0
- package/bin/scan.js +124 -0
- package/bin/ssl.js +181 -0
- package/package.json +80 -0
- package/src/index.js +206 -0
- package/src/modules/acme/client.js +240 -0
- package/src/modules/acme/csr.js +172 -0
- package/src/modules/acme/detectDomains.js +132 -0
- package/src/modules/acme/index.js +211 -0
- package/src/modules/acme/jose.js +125 -0
- package/src/modules/acme/renew.js +89 -0
- package/src/modules/adaptiveRateLimiter.js +154 -0
- package/src/modules/alerts.js +168 -0
- package/src/modules/bodyLimit.js +106 -0
- package/src/modules/botDetector.js +144 -0
- package/src/modules/connectionGuard.js +128 -0
- package/src/modules/csrf.js +130 -0
- package/src/modules/dashboard.js +257 -0
- package/src/modules/dashboardHtml.js +235 -0
- package/src/modules/datacenterRanges.js +121 -0
- package/src/modules/ddosGuard.js +323 -0
- package/src/modules/firewall.js +239 -0
- package/src/modules/geoBlock.js +169 -0
- package/src/modules/headers.js +130 -0
- package/src/modules/honeypot.js +149 -0
- package/src/modules/httpsRedirect.js +67 -0
- package/src/modules/integrity.js +164 -0
- package/src/modules/ipBlocker.js +372 -0
- package/src/modules/ja3.js +155 -0
- package/src/modules/logExport.js +134 -0
- package/src/modules/nosqlGuard.js +143 -0
- package/src/modules/osFirewall.js +182 -0
- package/src/modules/rateLimiter.js +196 -0
- package/src/modules/sanitizer.js +253 -0
- package/src/modules/threatDetector.js +202 -0
- package/src/scanner/checks/dependencies.js +125 -0
- package/src/scanner/checks/environment.js +118 -0
- package/src/scanner/checks/exposure.js +99 -0
- package/src/scanner/checks/headers.js +119 -0
- package/src/scanner/checks/ssl.js +151 -0
- package/src/scanner/index.js +209 -0
- package/src/utils/cidr.js +140 -0
- package/src/utils/logger.js +194 -0
- 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;
|