jerkjs 2.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 +200 -0
- package/README.md +171 -0
- package/doc/EXTENSION_MANUAL.md +958 -0
- package/doc/FIREWALL_MANUAL.md +419 -0
- package/doc/HOOKS_REFERENCE_IMPROVED.md +599 -0
- package/doc/MANUAL_API_SDK.md +539 -0
- package/doc/MANUAL_MVC.md +397 -0
- package/doc/MARIADB_TOKENS_IMPLEMENTATION.md +113 -0
- package/doc/MIDDLEWARE_MANUAL.md +521 -0
- package/doc/OAUTH2_GOOGLE_MANUAL.md +408 -0
- package/doc/frontend-and-sessions.md +356 -0
- package/examples/advanced/controllers/productController.js +64 -0
- package/examples/advanced/controllers/userController.js +85 -0
- package/examples/advanced/routes.json +51 -0
- package/examples/advanced_example.js +93 -0
- package/examples/basic/controllers/userController.js +85 -0
- package/examples/basic_example.js +72 -0
- package/examples/frontend/README.md +71 -0
- package/examples/frontend/app.js +71 -0
- package/examples/frontend/controllers/apiController.js +39 -0
- package/examples/frontend/controllers/authController.js +220 -0
- package/examples/frontend/controllers/formController.js +47 -0
- package/examples/frontend/controllers/messageController.js +96 -0
- package/examples/frontend/controllers/pageController.js +178 -0
- package/examples/frontend/controllers/staticController.js +167 -0
- package/examples/frontend/routes.json +90 -0
- package/examples/mvc_example/app.js +138 -0
- package/examples/mvc_example/views/home/index.html +26 -0
- package/examples/mvc_example/views/home/simple.html +3 -0
- package/examples/mvc_example/views/layout.html +23 -0
- package/examples/mvc_example/views/test.html +3 -0
- package/examples/mvc_example/views/user/invalid.html +6 -0
- package/examples/mvc_example/views/user/list.html +36 -0
- package/examples/mvc_example/views/user/notfound.html +6 -0
- package/examples/mvc_example/views/user/profile.html +11 -0
- package/examples/mvc_routes_example/app.js +34 -0
- package/examples/mvc_routes_example/controllers/mainController.js +27 -0
- package/examples/mvc_routes_example/controllers/productController.js +47 -0
- package/examples/mvc_routes_example/controllers/userController.js +76 -0
- package/examples/mvc_routes_example/routes.json +30 -0
- package/examples/mvc_routes_example/views/layout.html +31 -0
- package/examples/mvc_routes_example/views/main/index.html +11 -0
- package/examples/mvc_routes_example/views/product/catalog.html +24 -0
- package/examples/mvc_routes_example/views/user/invalid.html +6 -0
- package/examples/mvc_routes_example/views/user/list.html +40 -0
- package/examples/mvc_routes_example/views/user/notfound.html +6 -0
- package/examples/mvc_routes_example/views/user/profile.html +18 -0
- package/examples/public/README.md +92 -0
- package/examples/public/app.js +72 -0
- package/examples/public/controllers/healthController.js +20 -0
- package/examples/public/controllers/mainController.js +22 -0
- package/examples/public/controllers/userController.js +139 -0
- package/examples/public/routes.json +51 -0
- package/examples/v2/README.md +72 -0
- package/examples/v2/app.js +74 -0
- package/examples/v2/app_fixed.js +74 -0
- package/examples/v2/controllers/authController.js +64 -0
- package/examples/v2/controllers/mainController.js +24 -0
- package/examples/v2/controllers/protectedController.js +12 -0
- package/examples/v2/controllers/userController.js +16 -0
- package/examples/v2/package.json +27 -0
- package/examples/v2/routes.json +30 -0
- package/examples/v2/test_api.sh +47 -0
- package/examples/v2/tokens_example.sqlite +0 -0
- package/examples/v2.1_firewall_demo/README.md +113 -0
- package/examples/v2.1_firewall_demo/app.js +182 -0
- package/examples/v2.1_firewall_demo/package.json +27 -0
- package/examples/v2.1_hooks_demo/README.md +85 -0
- package/examples/v2.1_hooks_demo/app.js +101 -0
- package/examples/v2.1_hooks_demo/controllers/hooksController.js +29 -0
- package/examples/v2.1_hooks_demo/controllers/mainController.js +18 -0
- package/examples/v2.1_hooks_demo/package.json +27 -0
- package/examples/v2.1_hooks_demo/routes.json +16 -0
- package/examples/v2.1_openapi_demo/README.md +82 -0
- package/examples/v2.1_openapi_demo/app.js +296 -0
- package/examples/v2.1_openapi_demo/package.json +26 -0
- package/examples/v2_cors/README.md +82 -0
- package/examples/v2_cors/app.js +108 -0
- package/examples/v2_cors/package.json +23 -0
- package/examples/v2_json_auth/README.md +83 -0
- package/examples/v2_json_auth/app.js +72 -0
- package/examples/v2_json_auth/controllers/authController.js +67 -0
- package/examples/v2_json_auth/controllers/mainController.js +16 -0
- package/examples/v2_json_auth/controllers/protectedController.js +12 -0
- package/examples/v2_json_auth/controllers/tokenController.js +28 -0
- package/examples/v2_json_auth/controllers/userController.js +15 -0
- package/examples/v2_json_auth/package.json +26 -0
- package/examples/v2_json_auth/routes.json +37 -0
- package/examples/v2_json_auth/tokens.json +20 -0
- package/examples/v2_mariadb_auth/README.md +94 -0
- package/examples/v2_mariadb_auth/app.js +81 -0
- package/examples/v2_mariadb_auth/controllers/authController.js +95 -0
- package/examples/v2_mariadb_auth/controllers/mainController.js +31 -0
- package/examples/v2_mariadb_auth/controllers/protectedController.js +12 -0
- package/examples/v2_mariadb_auth/controllers/userController.js +17 -0
- package/examples/v2_mariadb_auth/package.json +27 -0
- package/examples/v2_mariadb_auth/routes.json +37 -0
- package/examples/v2_no_auth/README.md +75 -0
- package/examples/v2_no_auth/app.js +72 -0
- package/examples/v2_no_auth/controllers/healthController.js +14 -0
- package/examples/v2_no_auth/controllers/mainController.js +19 -0
- package/examples/v2_no_auth/controllers/productController.js +31 -0
- package/examples/v2_no_auth/controllers/publicController.js +16 -0
- package/examples/v2_no_auth/package.json +22 -0
- package/examples/v2_no_auth/routes.json +37 -0
- package/examples/v2_oauth/README.md +70 -0
- package/examples/v2_oauth/app.js +90 -0
- package/examples/v2_oauth/controllers/mainController.js +45 -0
- package/examples/v2_oauth/controllers/oauthController.js +247 -0
- package/examples/v2_oauth/controllers/protectedController.js +13 -0
- package/examples/v2_oauth/controllers/userController.js +17 -0
- package/examples/v2_oauth/package.json +26 -0
- package/examples/v2_oauth/routes.json +44 -0
- package/examples/v2_openapi/README.md +77 -0
- package/examples/v2_openapi/app.js +222 -0
- package/examples/v2_openapi/controllers/authController.js +52 -0
- package/examples/v2_openapi/controllers/mainController.js +26 -0
- package/examples/v2_openapi/controllers/productController.js +17 -0
- package/examples/v2_openapi/controllers/userController.js +27 -0
- package/examples/v2_openapi/package.json +26 -0
- package/examples/v2_openapi/routes.json +37 -0
- package/generate_token.js +10 -0
- package/index.js +85 -0
- package/jerk.jpg +0 -0
- package/lib/core/handler.js +86 -0
- package/lib/core/hooks.js +224 -0
- package/lib/core/router.js +204 -0
- package/lib/core/securityEnhancedServer.js +752 -0
- package/lib/core/server.js +369 -0
- package/lib/loader/controllerLoader.js +175 -0
- package/lib/loader/routeLoader.js +341 -0
- package/lib/middleware/auditLogger.js +208 -0
- package/lib/middleware/authenticator.js +565 -0
- package/lib/middleware/compressor.js +218 -0
- package/lib/middleware/cors.js +135 -0
- package/lib/middleware/firewall.js +443 -0
- package/lib/middleware/rateLimiter.js +210 -0
- package/lib/middleware/session.js +301 -0
- package/lib/middleware/validator.js +193 -0
- package/lib/mvc/controllerBase.js +207 -0
- package/lib/mvc/viewEngine.js +752 -0
- package/lib/utils/configParser.js +223 -0
- package/lib/utils/logger.js +145 -0
- package/lib/utils/mariadbTokenAdapter.js +226 -0
- package/lib/utils/openapiGenerator.js +140 -0
- package/lib/utils/sqliteTokenAdapter.js +224 -0
- package/lib/utils/tokenManager.js +254 -0
- package/package.json +47 -0
- package/v2examplle/v2_json_auth/README.md +83 -0
- package/v2examplle/v2_json_auth/app.js +72 -0
- package/v2examplle/v2_json_auth/controllers/authController.js +67 -0
- package/v2examplle/v2_json_auth/controllers/mainController.js +16 -0
- package/v2examplle/v2_json_auth/controllers/protectedController.js +12 -0
- package/v2examplle/v2_json_auth/controllers/tokenController.js +28 -0
- package/v2examplle/v2_json_auth/controllers/userController.js +15 -0
- package/v2examplle/v2_json_auth/package.json +26 -0
- package/v2examplle/v2_json_auth/routes.json +37 -0
- package/v2examplle/v2_json_auth/tokens.json +20 -0
- package/v2examplle/v2_mariadb_auth/README.md +94 -0
- package/v2examplle/v2_mariadb_auth/app.js +81 -0
- package/v2examplle/v2_mariadb_auth/controllers/authController.js +95 -0
- package/v2examplle/v2_mariadb_auth/controllers/mainController.js +31 -0
- package/v2examplle/v2_mariadb_auth/controllers/protectedController.js +12 -0
- package/v2examplle/v2_mariadb_auth/controllers/userController.js +17 -0
- package/v2examplle/v2_mariadb_auth/package.json +27 -0
- package/v2examplle/v2_mariadb_auth/routes.json +37 -0
- package/v2examplle/v2_sqlite_auth/README.md +72 -0
- package/v2examplle/v2_sqlite_auth/app.js +74 -0
- package/v2examplle/v2_sqlite_auth/app_fixed.js +74 -0
- package/v2examplle/v2_sqlite_auth/controllers/authController.js +64 -0
- package/v2examplle/v2_sqlite_auth/controllers/mainController.js +24 -0
- package/v2examplle/v2_sqlite_auth/controllers/protectedController.js +12 -0
- package/v2examplle/v2_sqlite_auth/controllers/userController.js +16 -0
- package/v2examplle/v2_sqlite_auth/package.json +27 -0
- package/v2examplle/v2_sqlite_auth/routes.json +30 -0
- package/v2examplle/v2_sqlite_auth/test_api.sh +47 -0
- package/v2examplle/v2_sqlite_auth/tokens_example.sqlite +0 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware de Firewall para el framework API SDK
|
|
3
|
+
* Implementación del componente middleware/firewall.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Firewall {
|
|
7
|
+
/**
|
|
8
|
+
* Constructor del firewall
|
|
9
|
+
* @param {Object} options - Opciones de configuración
|
|
10
|
+
* @param {number} options.maxAttempts - Número máximo de intentos fallidos antes de bloquear
|
|
11
|
+
* @param {number} options.blockDuration - Duración del bloqueo en milisegundos
|
|
12
|
+
* @param {Array} options.whitelist - IPs que no deben ser bloqueadas
|
|
13
|
+
* @param {Array} options.blacklist - IPs que siempre deben ser bloqueadas
|
|
14
|
+
* @param {Array} options.rules - Reglas personalizadas de firewall
|
|
15
|
+
* @param {Object} options.logger - Instancia de logger para eventos de seguridad
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.blockedIPs = new Map(); // Map<IP, { blockedUntil, reason, attempts }>
|
|
19
|
+
this.maxAttempts = options.maxAttempts || 5;
|
|
20
|
+
this.blockDuration = options.blockDuration || 900000; // 15 minutos por defecto
|
|
21
|
+
this.logger = options.logger || console;
|
|
22
|
+
this.whitelist = options.whitelist || []; // IPs que no deben ser bloqueadas
|
|
23
|
+
this.blacklist = options.blacklist || []; // IPs que siempre deben ser bloqueadas
|
|
24
|
+
this.rules = options.rules || []; // Reglas personalizadas de firewall
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Verifica si una IP está bloqueada
|
|
29
|
+
* @param {string} ip - IP a verificar
|
|
30
|
+
* @returns {Object} - Información del bloqueo
|
|
31
|
+
*/
|
|
32
|
+
isBlocked(ip) {
|
|
33
|
+
// Verificar si está en la blacklist
|
|
34
|
+
if (this.blacklist.includes(ip)) {
|
|
35
|
+
return {
|
|
36
|
+
blocked: true,
|
|
37
|
+
reason: 'IP en lista negra',
|
|
38
|
+
permanent: true,
|
|
39
|
+
blockedUntil: null
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Verificar si está en la whitelist
|
|
44
|
+
if (this.whitelist.includes(ip)) {
|
|
45
|
+
return { blocked: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const blockInfo = this.blockedIPs.get(ip);
|
|
49
|
+
if (!blockInfo) {
|
|
50
|
+
return { blocked: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Verificar si el bloqueo ha expirado
|
|
54
|
+
if (Date.now() > blockInfo.blockedUntil) {
|
|
55
|
+
this.blockedIPs.delete(ip); // Limpiar bloqueo expirado
|
|
56
|
+
return { blocked: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
blocked: true,
|
|
61
|
+
reason: blockInfo.reason,
|
|
62
|
+
blockedUntil: blockInfo.blockedUntil,
|
|
63
|
+
attempts: blockInfo.attempts
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Bloquea una IP
|
|
69
|
+
* @param {string} ip - IP a bloquear
|
|
70
|
+
* @param {string} reason - Razón del bloqueo
|
|
71
|
+
* @returns {boolean} - True si la IP fue bloqueada
|
|
72
|
+
*/
|
|
73
|
+
blockIP(ip, reason) {
|
|
74
|
+
if (this.whitelist.includes(ip)) {
|
|
75
|
+
this.logger.info(`IP ${ip} está en la whitelist, no se bloqueará`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const blockedUntil = Date.now() + this.blockDuration;
|
|
80
|
+
const currentInfo = this.blockedIPs.get(ip);
|
|
81
|
+
const attempts = currentInfo ? currentInfo.attempts + 1 : 1;
|
|
82
|
+
|
|
83
|
+
this.blockedIPs.set(ip, {
|
|
84
|
+
blockedUntil,
|
|
85
|
+
reason,
|
|
86
|
+
attempts
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.logger.warn(`IP ${ip} bloqueada por: ${reason}. Intentos: ${attempts}`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Incrementa el contador de intentos fallidos para una IP
|
|
95
|
+
* @param {string} ip - IP a incrementar intentos
|
|
96
|
+
* @param {string} reason - Razón del intento fallido
|
|
97
|
+
*/
|
|
98
|
+
incrementFailedAttempts(ip, reason) {
|
|
99
|
+
if (this.whitelist.includes(ip)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const currentInfo = this.blockedIPs.get(ip);
|
|
104
|
+
const attempts = currentInfo ? currentInfo.attempts + 1 : 1;
|
|
105
|
+
const blockedUntil = currentInfo ? currentInfo.blockedUntil : Date.now() + this.blockDuration;
|
|
106
|
+
|
|
107
|
+
this.blockedIPs.set(ip, {
|
|
108
|
+
blockedUntil,
|
|
109
|
+
reason,
|
|
110
|
+
attempts
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Si se alcanza el límite de intentos, bloquear permanentemente
|
|
114
|
+
if (attempts >= this.maxAttempts) {
|
|
115
|
+
this.logger.warn(`IP ${ip} bloqueada permanentemente tras ${attempts} intentos fallidos`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Agrega una regla personalizada de firewall
|
|
121
|
+
* @param {string} name - Nombre de la regla
|
|
122
|
+
* @param {Function} condition - Condición que evalúa la solicitud
|
|
123
|
+
* @param {string} action - Acción a tomar ('block', 'monitor', etc.)
|
|
124
|
+
* @param {string} reason - Razón para la acción
|
|
125
|
+
*/
|
|
126
|
+
addRule(name, condition, action = 'block', reason = 'Violación de regla personalizada') {
|
|
127
|
+
this.rules.push({ name, condition, action, reason });
|
|
128
|
+
this.logger.info(`Regla de firewall agregada: ${name}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Verifica si una solicitud coincide con alguna regla de firewall
|
|
133
|
+
* @param {Object} req - Objeto de solicitud
|
|
134
|
+
* @returns {Object|null} - Regla que coincide o null si ninguna
|
|
135
|
+
*/
|
|
136
|
+
checkRules(req) {
|
|
137
|
+
const clientIP = this.getClientIP(req);
|
|
138
|
+
|
|
139
|
+
// Verificar reglas personalizadas
|
|
140
|
+
for (const rule of this.rules) {
|
|
141
|
+
if (rule.condition(req, clientIP)) {
|
|
142
|
+
return {
|
|
143
|
+
matched: true,
|
|
144
|
+
rule: rule.name,
|
|
145
|
+
action: rule.action || 'block',
|
|
146
|
+
reason: rule.reason || 'Violación de regla de firewall'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Verificar patrones comunes de ataque
|
|
152
|
+
const path = req.url;
|
|
153
|
+
const body = req.body || '';
|
|
154
|
+
const headers = req.headers;
|
|
155
|
+
|
|
156
|
+
// SQL Injection patterns
|
|
157
|
+
const sqlPatterns = [
|
|
158
|
+
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|SCRIPT)\b)/gi,
|
|
159
|
+
/('|--|#|\/\*|\*\/|;)/g
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// XSS patterns
|
|
163
|
+
const xssPatterns = [
|
|
164
|
+
/(<script|javascript:|vbscript:|onload|onerror|onmouseover|onclick|onfocus|onblur)/gi,
|
|
165
|
+
/(src|href)=["']javascript:/gi,
|
|
166
|
+
/<iframe/gi,
|
|
167
|
+
/<img[^>]*src[\\s]*=[\\s]*["'][\\s]*(javascript:|data:)/gi
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
// Path traversal
|
|
171
|
+
const pathTraversal = /\.\.\//g;
|
|
172
|
+
|
|
173
|
+
// Verificar patrones en URL
|
|
174
|
+
for (const pattern of sqlPatterns) {
|
|
175
|
+
if (pattern.test(path)) {
|
|
176
|
+
return {
|
|
177
|
+
matched: true,
|
|
178
|
+
rule: 'sql_injection',
|
|
179
|
+
action: 'block',
|
|
180
|
+
reason: 'Patrón de SQL Injection detectado'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const pattern of xssPatterns) {
|
|
186
|
+
if (pattern.test(path)) {
|
|
187
|
+
return {
|
|
188
|
+
matched: true,
|
|
189
|
+
rule: 'xss_attack',
|
|
190
|
+
action: 'block',
|
|
191
|
+
reason: 'Patrón de XSS detectado'
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (pathTraversal.test(path)) {
|
|
197
|
+
return {
|
|
198
|
+
matched: true,
|
|
199
|
+
rule: 'path_traversal',
|
|
200
|
+
action: 'block',
|
|
201
|
+
reason: 'Patrón de Path Traversal detectado'
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Verificar patrones en body si es string
|
|
206
|
+
if (typeof body === 'string') {
|
|
207
|
+
for (const pattern of sqlPatterns) {
|
|
208
|
+
if (pattern.test(body)) {
|
|
209
|
+
return {
|
|
210
|
+
matched: true,
|
|
211
|
+
rule: 'sql_injection_body',
|
|
212
|
+
action: 'block',
|
|
213
|
+
reason: 'Patrón de SQL Injection en body detectado'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const pattern of xssPatterns) {
|
|
219
|
+
if (pattern.test(body)) {
|
|
220
|
+
return {
|
|
221
|
+
matched: true,
|
|
222
|
+
rule: 'xss_attack_body',
|
|
223
|
+
action: 'block',
|
|
224
|
+
reason: 'Patrón de XSS en body detectado'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Verificar headers sospechosos
|
|
231
|
+
const suspiciousHeaders = [
|
|
232
|
+
'x-forwarded-for',
|
|
233
|
+
'x-real-ip',
|
|
234
|
+
'x-originating-ip',
|
|
235
|
+
'x-remote-ip',
|
|
236
|
+
'x-remote-addr',
|
|
237
|
+
'x-proxy-user-ip',
|
|
238
|
+
'cf-connecting-ip'
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
let suspiciousHeaderCount = 0;
|
|
242
|
+
for (const header of suspiciousHeaders) {
|
|
243
|
+
if (headers[header]) {
|
|
244
|
+
suspiciousHeaderCount++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (suspiciousHeaderCount > 3) {
|
|
249
|
+
return {
|
|
250
|
+
matched: true,
|
|
251
|
+
rule: 'suspicious_headers',
|
|
252
|
+
action: 'monitor',
|
|
253
|
+
reason: 'Cantidad sospechosa de headers de proxy detectados'
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Obtiene la IP del cliente
|
|
262
|
+
* @param {Object} req - Objeto de solicitud
|
|
263
|
+
* @returns {string} - IP del cliente
|
|
264
|
+
*/
|
|
265
|
+
getClientIP(req) {
|
|
266
|
+
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
267
|
+
req.headers['x-real-ip'] ||
|
|
268
|
+
req.connection.remoteAddress ||
|
|
269
|
+
req.socket.remoteAddress ||
|
|
270
|
+
(req.connection?.socket ? req.connection.socket.remoteAddress : 'unknown');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Agrega una IP a la lista blanca
|
|
275
|
+
* @param {string} ip - IP a agregar a la lista blanca
|
|
276
|
+
*/
|
|
277
|
+
addToWhitelist(ip) {
|
|
278
|
+
if (!this.whitelist.includes(ip)) {
|
|
279
|
+
this.whitelist.push(ip);
|
|
280
|
+
this.logger.info(`IP ${ip} agregada a la whitelist`);
|
|
281
|
+
|
|
282
|
+
// Disparar hook
|
|
283
|
+
const hooks = require('../../index.js').hooks;
|
|
284
|
+
if (hooks) {
|
|
285
|
+
hooks.doAction('firewall_whitelist_updated', ip, 'added', this.whitelist);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Remueve una IP de la lista blanca
|
|
292
|
+
* @param {string} ip - IP a remover de la lista blanca
|
|
293
|
+
*/
|
|
294
|
+
removeFromWhitelist(ip) {
|
|
295
|
+
const index = this.whitelist.indexOf(ip);
|
|
296
|
+
if (index !== -1) {
|
|
297
|
+
this.whitelist.splice(index, 1);
|
|
298
|
+
this.logger.info(`IP ${ip} removida de la whitelist`);
|
|
299
|
+
|
|
300
|
+
// Disparar hook
|
|
301
|
+
const hooks = require('../../index.js').hooks;
|
|
302
|
+
if (hooks) {
|
|
303
|
+
hooks.doAction('firewall_whitelist_updated', ip, 'removed', this.whitelist);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Agrega una IP a la lista negra
|
|
310
|
+
* @param {string} ip - IP a agregar a la lista negra
|
|
311
|
+
* @param {string} reason - Razón del bloqueo
|
|
312
|
+
*/
|
|
313
|
+
addToBlacklist(ip, reason = 'Agregada a la blacklist manualmente') {
|
|
314
|
+
if (!this.blacklist.includes(ip)) {
|
|
315
|
+
this.blacklist.push(ip);
|
|
316
|
+
this.logger.info(`IP ${ip} agregada a la blacklist: ${reason}`);
|
|
317
|
+
|
|
318
|
+
// Disparar hook
|
|
319
|
+
const hooks = require('../../index.js').hooks;
|
|
320
|
+
if (hooks) {
|
|
321
|
+
hooks.doAction('firewall_blacklist_updated', ip, 'added', this.blacklist);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Remueve una IP de la lista negra
|
|
328
|
+
* @param {string} ip - IP a remover de la lista negra
|
|
329
|
+
*/
|
|
330
|
+
removeFromBlacklist(ip) {
|
|
331
|
+
const index = this.blacklist.indexOf(ip);
|
|
332
|
+
if (index !== -1) {
|
|
333
|
+
this.blacklist.splice(index, 1);
|
|
334
|
+
this.logger.info(`IP ${ip} removida de la blacklist`);
|
|
335
|
+
|
|
336
|
+
// Disparar hook
|
|
337
|
+
const hooks = require('../../index.js').hooks;
|
|
338
|
+
if (hooks) {
|
|
339
|
+
hooks.doAction('firewall_blacklist_updated', ip, 'removed', this.blacklist);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Obtiene el estado actual del firewall
|
|
346
|
+
* @returns {Object} - Estado del firewall
|
|
347
|
+
*/
|
|
348
|
+
getStatus() {
|
|
349
|
+
return {
|
|
350
|
+
blockedIPs: Array.from(this.blockedIPs.entries()).map(([ip, info]) => ({
|
|
351
|
+
ip,
|
|
352
|
+
blockedUntil: info.blockedUntil,
|
|
353
|
+
reason: info.reason,
|
|
354
|
+
attempts: info.attempts
|
|
355
|
+
})),
|
|
356
|
+
whitelist: [...this.whitelist],
|
|
357
|
+
blacklist: [...this.blacklist],
|
|
358
|
+
totalBlocked: this.blockedIPs.size,
|
|
359
|
+
rules: this.rules.map(rule => ({ name: rule.name, action: rule.action }))
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Middleware de firewall
|
|
365
|
+
* @returns {Function} - Middleware de firewall
|
|
366
|
+
*/
|
|
367
|
+
middleware() {
|
|
368
|
+
return (req, res, next) => {
|
|
369
|
+
// Disparar hook antes de procesar la solicitud
|
|
370
|
+
const hooks = require('../../index.js').hooks;
|
|
371
|
+
if (hooks) {
|
|
372
|
+
hooks.doAction('firewall_request_received', req, res);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const clientIP = this.getClientIP(req);
|
|
376
|
+
|
|
377
|
+
// Verificar si la IP está bloqueada
|
|
378
|
+
const blockInfo = this.isBlocked(clientIP);
|
|
379
|
+
if (blockInfo.blocked) {
|
|
380
|
+
this.logger.warn(`Solicitud bloqueada desde IP: ${clientIP}, razón: ${blockInfo.reason}`);
|
|
381
|
+
|
|
382
|
+
// Disparar hook de evento de seguridad
|
|
383
|
+
if (hooks) {
|
|
384
|
+
hooks.doAction('firewall_ip_blocked', clientIP, blockInfo.reason, req, res);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
388
|
+
res.end(JSON.stringify({
|
|
389
|
+
error: 'Acceso denegado por firewall',
|
|
390
|
+
reason: blockInfo.reason,
|
|
391
|
+
blockedUntil: blockInfo.blockedUntil
|
|
392
|
+
}));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Verificar reglas de firewall
|
|
397
|
+
const ruleMatch = this.checkRules(req);
|
|
398
|
+
if (ruleMatch) {
|
|
399
|
+
// Disparar hook de evento de regla activada
|
|
400
|
+
if (hooks) {
|
|
401
|
+
hooks.doAction('firewall_rule_triggered', ruleMatch, clientIP, req);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (ruleMatch.action === 'block') {
|
|
405
|
+
this.logger.warn(`Solicitud bloqueada por regla: ${ruleMatch.rule}, IP: ${clientIP}, razón: ${ruleMatch.reason}`);
|
|
406
|
+
|
|
407
|
+
// Incrementar intentos fallidos
|
|
408
|
+
this.incrementFailedAttempts(clientIP, ruleMatch.reason);
|
|
409
|
+
|
|
410
|
+
// Disparar hook de evento de bloqueo
|
|
411
|
+
if (hooks) {
|
|
412
|
+
hooks.doAction('firewall_request_blocked', ruleMatch, clientIP, req, res);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
416
|
+
res.end(JSON.stringify({
|
|
417
|
+
error: 'Solicitud bloqueada por firewall',
|
|
418
|
+
reason: ruleMatch.reason,
|
|
419
|
+
rule: ruleMatch.rule
|
|
420
|
+
}));
|
|
421
|
+
return;
|
|
422
|
+
} else if (ruleMatch.action === 'monitor') {
|
|
423
|
+
this.logger.info(`Solicitud monitoreada por regla: ${ruleMatch.rule}, IP: ${clientIP}, razón: ${ruleMatch.reason}`);
|
|
424
|
+
|
|
425
|
+
// Disparar hook de evento de monitoreo
|
|
426
|
+
if (hooks) {
|
|
427
|
+
hooks.doAction('firewall_request_monitored', ruleMatch, clientIP, req);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Disparar hook antes de continuar con el siguiente middleware
|
|
433
|
+
if (hooks) {
|
|
434
|
+
hooks.doAction('firewall_request_allowed', req, res);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Continuar con el siguiente middleware
|
|
438
|
+
next();
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
module.exports = Firewall;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware de Rate Limiting para el framework API SDK
|
|
3
|
+
* Implementación del componente middleware/rateLimiter.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class RateLimiter {
|
|
7
|
+
/**
|
|
8
|
+
* Constructor del limitador de tasa
|
|
9
|
+
* @param {Object} options - Opciones de configuración
|
|
10
|
+
* @param {number} options.windowMs - Ventana de tiempo en milisegundos
|
|
11
|
+
* @param {number} options.maxRequests - Número máximo de solicitudes permitidas en la ventana
|
|
12
|
+
* @param {string} options.store - Tipo de almacenamiento ('memory', 'redis')
|
|
13
|
+
* @param {Object} options.redisClient - Cliente Redis (si se usa almacenamiento Redis)
|
|
14
|
+
*/
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.windowMs = options.windowMs || 900000; // 15 minutos por defecto
|
|
17
|
+
this.maxRequests = options.maxRequests || 100; // 100 solicitudes por defecto
|
|
18
|
+
this.store = options.store || 'memory';
|
|
19
|
+
this.redisClient = options.redisClient;
|
|
20
|
+
|
|
21
|
+
// Almacenamiento en memoria si no se especifica Redis
|
|
22
|
+
this.memoryStore = new Map();
|
|
23
|
+
|
|
24
|
+
// Si se especifica Redis, verificar que el cliente esté presente
|
|
25
|
+
if (this.store === 'redis' && !this.redisClient) {
|
|
26
|
+
throw new Error('Se requiere un cliente Redis cuando se usa almacenamiento Redis');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Middleware de limitación de tasa
|
|
32
|
+
* @returns {Function} - Middleware de rate limiting
|
|
33
|
+
*/
|
|
34
|
+
middleware() {
|
|
35
|
+
return async (req, res, next) => {
|
|
36
|
+
// Obtener identificador único para el cliente (puede ser IP o token de usuario)
|
|
37
|
+
const clientId = this.getClientIdentifier(req);
|
|
38
|
+
|
|
39
|
+
// Obtener el conteo actual de solicitudes para este cliente
|
|
40
|
+
const currentCount = await this.getRequestCount(clientId);
|
|
41
|
+
|
|
42
|
+
// Incrementar el conteo de solicitudes
|
|
43
|
+
await this.incrementRequestCount(clientId);
|
|
44
|
+
|
|
45
|
+
// Verificar si se ha superado el límite
|
|
46
|
+
if (currentCount >= this.maxRequests) {
|
|
47
|
+
// Calcular tiempo restante para resetear el límite
|
|
48
|
+
const timeLeft = await this.getTimeUntilReset(clientId);
|
|
49
|
+
|
|
50
|
+
// Establecer headers de rate limiting
|
|
51
|
+
res.setHeader('X-RateLimit-Limit', this.maxRequests);
|
|
52
|
+
res.setHeader('X-RateLimit-Remaining', 0);
|
|
53
|
+
res.setHeader('X-RateLimit-Reset', Math.floor(timeLeft / 1000)); // En segundos
|
|
54
|
+
|
|
55
|
+
// Devolver error 429 (Too Many Requests)
|
|
56
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(JSON.stringify({
|
|
58
|
+
error: 'Límite de solicitudes excedido',
|
|
59
|
+
retryAfter: Math.floor(timeLeft / 1000) + ' segundos'
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Establecer headers de rate limiting
|
|
66
|
+
const remaining = this.maxRequests - currentCount - 1;
|
|
67
|
+
const timeLeft = await this.getTimeUntilReset(clientId);
|
|
68
|
+
|
|
69
|
+
res.setHeader('X-RateLimit-Limit', this.maxRequests);
|
|
70
|
+
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
71
|
+
res.setHeader('X-RateLimit-Reset', Math.floor(timeLeft / 1000)); // En segundos
|
|
72
|
+
|
|
73
|
+
// Continuar con el siguiente middleware
|
|
74
|
+
if (next) {
|
|
75
|
+
next();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Obtiene el identificador único para el cliente
|
|
82
|
+
* @param {Object} req - Objeto de solicitud HTTP
|
|
83
|
+
* @returns {string} - Identificador único del cliente
|
|
84
|
+
*/
|
|
85
|
+
getClientIdentifier(req) {
|
|
86
|
+
// Prioridad: 1. Token de usuario, 2. Cabecera X-Forwarded-For, 3. IP remota
|
|
87
|
+
if (req.user && req.user.id) {
|
|
88
|
+
return `user:${req.user.id}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
92
|
+
if (forwarded) {
|
|
93
|
+
// Tomar la primera IP si hay múltiples
|
|
94
|
+
return forwarded.split(',')[0].trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return req.connection.remoteAddress ||
|
|
98
|
+
req.socket.remoteAddress ||
|
|
99
|
+
(req.connection.socket ? req.connection.socket.remoteAddress : 'unknown');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Obtiene el conteo actual de solicitudes para un cliente
|
|
104
|
+
* @param {string} clientId - Identificador del cliente
|
|
105
|
+
* @returns {Promise<number>} - Número actual de solicitudes
|
|
106
|
+
*/
|
|
107
|
+
async getRequestCount(clientId) {
|
|
108
|
+
if (this.store === 'redis') {
|
|
109
|
+
// Usar Redis para obtener el conteo
|
|
110
|
+
const key = `rate_limit:${clientId}`;
|
|
111
|
+
const count = await this.redisClient.get(key);
|
|
112
|
+
return count ? parseInt(count, 10) : 0;
|
|
113
|
+
} else {
|
|
114
|
+
// Usar almacenamiento en memoria
|
|
115
|
+
const entry = this.memoryStore.get(clientId);
|
|
116
|
+
if (!entry) {
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Verificar si el tiempo de la entrada ha expirado
|
|
121
|
+
if (Date.now() - entry.startTime > this.windowMs) {
|
|
122
|
+
// La entrada ha expirado, eliminarla y retornar 0
|
|
123
|
+
this.memoryStore.delete(clientId);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return entry.count;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Incrementa el conteo de solicitudes para un cliente
|
|
133
|
+
* @param {string} clientId - Identificador del cliente
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
async incrementRequestCount(clientId) {
|
|
137
|
+
if (this.store === 'redis') {
|
|
138
|
+
// Usar Redis para incrementar el conteo
|
|
139
|
+
const key = `rate_limit:${clientId}`;
|
|
140
|
+
const count = await this.redisClient.incr(key);
|
|
141
|
+
|
|
142
|
+
// Establecer expiración para la clave
|
|
143
|
+
await this.redisClient.expire(key, Math.ceil(this.windowMs / 1000));
|
|
144
|
+
} else {
|
|
145
|
+
// Usar almacenamiento en memoria
|
|
146
|
+
const entry = this.memoryStore.get(clientId);
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
|
|
149
|
+
if (!entry) {
|
|
150
|
+
// Crear nueva entrada
|
|
151
|
+
this.memoryStore.set(clientId, {
|
|
152
|
+
count: 1,
|
|
153
|
+
startTime: now
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
// Verificar si el tiempo ha expirado
|
|
157
|
+
if (now - entry.startTime > this.windowMs) {
|
|
158
|
+
// Reiniciar el conteo
|
|
159
|
+
this.memoryStore.set(clientId, {
|
|
160
|
+
count: 1,
|
|
161
|
+
startTime: now
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
// Incrementar el conteo
|
|
165
|
+
entry.count++;
|
|
166
|
+
entry.startTime = entry.startTime; // Mantener el tiempo de inicio
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Obtiene el tiempo restante hasta que se resetee el conteo de solicitudes
|
|
174
|
+
* @param {string} clientId - Identificador del cliente
|
|
175
|
+
* @returns {Promise<number>} - Tiempo restante en milisegundos
|
|
176
|
+
*/
|
|
177
|
+
async getTimeUntilReset(clientId) {
|
|
178
|
+
if (this.store === 'redis') {
|
|
179
|
+
// En Redis, obtenemos el TTL restante
|
|
180
|
+
const key = `rate_limit:${clientId}`;
|
|
181
|
+
const ttl = await this.redisClient.ttl(key);
|
|
182
|
+
return ttl > 0 ? ttl * 1000 : this.windowMs;
|
|
183
|
+
} else {
|
|
184
|
+
// En memoria, calcular el tiempo restante
|
|
185
|
+
const entry = this.memoryStore.get(clientId);
|
|
186
|
+
if (!entry) {
|
|
187
|
+
return this.windowMs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const elapsed = Date.now() - entry.startTime;
|
|
191
|
+
return Math.max(0, this.windowMs - elapsed);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Reinicia el conteo de solicitudes para un cliente
|
|
197
|
+
* @param {string} clientId - Identificador del cliente
|
|
198
|
+
* @returns {Promise<void>}
|
|
199
|
+
*/
|
|
200
|
+
async reset(clientId) {
|
|
201
|
+
if (this.store === 'redis') {
|
|
202
|
+
const key = `rate_limit:${clientId}`;
|
|
203
|
+
await this.redisClient.del(key);
|
|
204
|
+
} else {
|
|
205
|
+
this.memoryStore.delete(clientId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = RateLimiter;
|