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.
Files changed (177) hide show
  1. package/LICENSE +200 -0
  2. package/README.md +171 -0
  3. package/doc/EXTENSION_MANUAL.md +958 -0
  4. package/doc/FIREWALL_MANUAL.md +419 -0
  5. package/doc/HOOKS_REFERENCE_IMPROVED.md +599 -0
  6. package/doc/MANUAL_API_SDK.md +539 -0
  7. package/doc/MANUAL_MVC.md +397 -0
  8. package/doc/MARIADB_TOKENS_IMPLEMENTATION.md +113 -0
  9. package/doc/MIDDLEWARE_MANUAL.md +521 -0
  10. package/doc/OAUTH2_GOOGLE_MANUAL.md +408 -0
  11. package/doc/frontend-and-sessions.md +356 -0
  12. package/examples/advanced/controllers/productController.js +64 -0
  13. package/examples/advanced/controllers/userController.js +85 -0
  14. package/examples/advanced/routes.json +51 -0
  15. package/examples/advanced_example.js +93 -0
  16. package/examples/basic/controllers/userController.js +85 -0
  17. package/examples/basic_example.js +72 -0
  18. package/examples/frontend/README.md +71 -0
  19. package/examples/frontend/app.js +71 -0
  20. package/examples/frontend/controllers/apiController.js +39 -0
  21. package/examples/frontend/controllers/authController.js +220 -0
  22. package/examples/frontend/controllers/formController.js +47 -0
  23. package/examples/frontend/controllers/messageController.js +96 -0
  24. package/examples/frontend/controllers/pageController.js +178 -0
  25. package/examples/frontend/controllers/staticController.js +167 -0
  26. package/examples/frontend/routes.json +90 -0
  27. package/examples/mvc_example/app.js +138 -0
  28. package/examples/mvc_example/views/home/index.html +26 -0
  29. package/examples/mvc_example/views/home/simple.html +3 -0
  30. package/examples/mvc_example/views/layout.html +23 -0
  31. package/examples/mvc_example/views/test.html +3 -0
  32. package/examples/mvc_example/views/user/invalid.html +6 -0
  33. package/examples/mvc_example/views/user/list.html +36 -0
  34. package/examples/mvc_example/views/user/notfound.html +6 -0
  35. package/examples/mvc_example/views/user/profile.html +11 -0
  36. package/examples/mvc_routes_example/app.js +34 -0
  37. package/examples/mvc_routes_example/controllers/mainController.js +27 -0
  38. package/examples/mvc_routes_example/controllers/productController.js +47 -0
  39. package/examples/mvc_routes_example/controllers/userController.js +76 -0
  40. package/examples/mvc_routes_example/routes.json +30 -0
  41. package/examples/mvc_routes_example/views/layout.html +31 -0
  42. package/examples/mvc_routes_example/views/main/index.html +11 -0
  43. package/examples/mvc_routes_example/views/product/catalog.html +24 -0
  44. package/examples/mvc_routes_example/views/user/invalid.html +6 -0
  45. package/examples/mvc_routes_example/views/user/list.html +40 -0
  46. package/examples/mvc_routes_example/views/user/notfound.html +6 -0
  47. package/examples/mvc_routes_example/views/user/profile.html +18 -0
  48. package/examples/public/README.md +92 -0
  49. package/examples/public/app.js +72 -0
  50. package/examples/public/controllers/healthController.js +20 -0
  51. package/examples/public/controllers/mainController.js +22 -0
  52. package/examples/public/controllers/userController.js +139 -0
  53. package/examples/public/routes.json +51 -0
  54. package/examples/v2/README.md +72 -0
  55. package/examples/v2/app.js +74 -0
  56. package/examples/v2/app_fixed.js +74 -0
  57. package/examples/v2/controllers/authController.js +64 -0
  58. package/examples/v2/controllers/mainController.js +24 -0
  59. package/examples/v2/controllers/protectedController.js +12 -0
  60. package/examples/v2/controllers/userController.js +16 -0
  61. package/examples/v2/package.json +27 -0
  62. package/examples/v2/routes.json +30 -0
  63. package/examples/v2/test_api.sh +47 -0
  64. package/examples/v2/tokens_example.sqlite +0 -0
  65. package/examples/v2.1_firewall_demo/README.md +113 -0
  66. package/examples/v2.1_firewall_demo/app.js +182 -0
  67. package/examples/v2.1_firewall_demo/package.json +27 -0
  68. package/examples/v2.1_hooks_demo/README.md +85 -0
  69. package/examples/v2.1_hooks_demo/app.js +101 -0
  70. package/examples/v2.1_hooks_demo/controllers/hooksController.js +29 -0
  71. package/examples/v2.1_hooks_demo/controllers/mainController.js +18 -0
  72. package/examples/v2.1_hooks_demo/package.json +27 -0
  73. package/examples/v2.1_hooks_demo/routes.json +16 -0
  74. package/examples/v2.1_openapi_demo/README.md +82 -0
  75. package/examples/v2.1_openapi_demo/app.js +296 -0
  76. package/examples/v2.1_openapi_demo/package.json +26 -0
  77. package/examples/v2_cors/README.md +82 -0
  78. package/examples/v2_cors/app.js +108 -0
  79. package/examples/v2_cors/package.json +23 -0
  80. package/examples/v2_json_auth/README.md +83 -0
  81. package/examples/v2_json_auth/app.js +72 -0
  82. package/examples/v2_json_auth/controllers/authController.js +67 -0
  83. package/examples/v2_json_auth/controllers/mainController.js +16 -0
  84. package/examples/v2_json_auth/controllers/protectedController.js +12 -0
  85. package/examples/v2_json_auth/controllers/tokenController.js +28 -0
  86. package/examples/v2_json_auth/controllers/userController.js +15 -0
  87. package/examples/v2_json_auth/package.json +26 -0
  88. package/examples/v2_json_auth/routes.json +37 -0
  89. package/examples/v2_json_auth/tokens.json +20 -0
  90. package/examples/v2_mariadb_auth/README.md +94 -0
  91. package/examples/v2_mariadb_auth/app.js +81 -0
  92. package/examples/v2_mariadb_auth/controllers/authController.js +95 -0
  93. package/examples/v2_mariadb_auth/controllers/mainController.js +31 -0
  94. package/examples/v2_mariadb_auth/controllers/protectedController.js +12 -0
  95. package/examples/v2_mariadb_auth/controllers/userController.js +17 -0
  96. package/examples/v2_mariadb_auth/package.json +27 -0
  97. package/examples/v2_mariadb_auth/routes.json +37 -0
  98. package/examples/v2_no_auth/README.md +75 -0
  99. package/examples/v2_no_auth/app.js +72 -0
  100. package/examples/v2_no_auth/controllers/healthController.js +14 -0
  101. package/examples/v2_no_auth/controllers/mainController.js +19 -0
  102. package/examples/v2_no_auth/controllers/productController.js +31 -0
  103. package/examples/v2_no_auth/controllers/publicController.js +16 -0
  104. package/examples/v2_no_auth/package.json +22 -0
  105. package/examples/v2_no_auth/routes.json +37 -0
  106. package/examples/v2_oauth/README.md +70 -0
  107. package/examples/v2_oauth/app.js +90 -0
  108. package/examples/v2_oauth/controllers/mainController.js +45 -0
  109. package/examples/v2_oauth/controllers/oauthController.js +247 -0
  110. package/examples/v2_oauth/controllers/protectedController.js +13 -0
  111. package/examples/v2_oauth/controllers/userController.js +17 -0
  112. package/examples/v2_oauth/package.json +26 -0
  113. package/examples/v2_oauth/routes.json +44 -0
  114. package/examples/v2_openapi/README.md +77 -0
  115. package/examples/v2_openapi/app.js +222 -0
  116. package/examples/v2_openapi/controllers/authController.js +52 -0
  117. package/examples/v2_openapi/controllers/mainController.js +26 -0
  118. package/examples/v2_openapi/controllers/productController.js +17 -0
  119. package/examples/v2_openapi/controllers/userController.js +27 -0
  120. package/examples/v2_openapi/package.json +26 -0
  121. package/examples/v2_openapi/routes.json +37 -0
  122. package/generate_token.js +10 -0
  123. package/index.js +85 -0
  124. package/jerk.jpg +0 -0
  125. package/lib/core/handler.js +86 -0
  126. package/lib/core/hooks.js +224 -0
  127. package/lib/core/router.js +204 -0
  128. package/lib/core/securityEnhancedServer.js +752 -0
  129. package/lib/core/server.js +369 -0
  130. package/lib/loader/controllerLoader.js +175 -0
  131. package/lib/loader/routeLoader.js +341 -0
  132. package/lib/middleware/auditLogger.js +208 -0
  133. package/lib/middleware/authenticator.js +565 -0
  134. package/lib/middleware/compressor.js +218 -0
  135. package/lib/middleware/cors.js +135 -0
  136. package/lib/middleware/firewall.js +443 -0
  137. package/lib/middleware/rateLimiter.js +210 -0
  138. package/lib/middleware/session.js +301 -0
  139. package/lib/middleware/validator.js +193 -0
  140. package/lib/mvc/controllerBase.js +207 -0
  141. package/lib/mvc/viewEngine.js +752 -0
  142. package/lib/utils/configParser.js +223 -0
  143. package/lib/utils/logger.js +145 -0
  144. package/lib/utils/mariadbTokenAdapter.js +226 -0
  145. package/lib/utils/openapiGenerator.js +140 -0
  146. package/lib/utils/sqliteTokenAdapter.js +224 -0
  147. package/lib/utils/tokenManager.js +254 -0
  148. package/package.json +47 -0
  149. package/v2examplle/v2_json_auth/README.md +83 -0
  150. package/v2examplle/v2_json_auth/app.js +72 -0
  151. package/v2examplle/v2_json_auth/controllers/authController.js +67 -0
  152. package/v2examplle/v2_json_auth/controllers/mainController.js +16 -0
  153. package/v2examplle/v2_json_auth/controllers/protectedController.js +12 -0
  154. package/v2examplle/v2_json_auth/controllers/tokenController.js +28 -0
  155. package/v2examplle/v2_json_auth/controllers/userController.js +15 -0
  156. package/v2examplle/v2_json_auth/package.json +26 -0
  157. package/v2examplle/v2_json_auth/routes.json +37 -0
  158. package/v2examplle/v2_json_auth/tokens.json +20 -0
  159. package/v2examplle/v2_mariadb_auth/README.md +94 -0
  160. package/v2examplle/v2_mariadb_auth/app.js +81 -0
  161. package/v2examplle/v2_mariadb_auth/controllers/authController.js +95 -0
  162. package/v2examplle/v2_mariadb_auth/controllers/mainController.js +31 -0
  163. package/v2examplle/v2_mariadb_auth/controllers/protectedController.js +12 -0
  164. package/v2examplle/v2_mariadb_auth/controllers/userController.js +17 -0
  165. package/v2examplle/v2_mariadb_auth/package.json +27 -0
  166. package/v2examplle/v2_mariadb_auth/routes.json +37 -0
  167. package/v2examplle/v2_sqlite_auth/README.md +72 -0
  168. package/v2examplle/v2_sqlite_auth/app.js +74 -0
  169. package/v2examplle/v2_sqlite_auth/app_fixed.js +74 -0
  170. package/v2examplle/v2_sqlite_auth/controllers/authController.js +64 -0
  171. package/v2examplle/v2_sqlite_auth/controllers/mainController.js +24 -0
  172. package/v2examplle/v2_sqlite_auth/controllers/protectedController.js +12 -0
  173. package/v2examplle/v2_sqlite_auth/controllers/userController.js +16 -0
  174. package/v2examplle/v2_sqlite_auth/package.json +27 -0
  175. package/v2examplle/v2_sqlite_auth/routes.json +30 -0
  176. package/v2examplle/v2_sqlite_auth/test_api.sh +47 -0
  177. 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;