jerkjs 2.5.8 → 2.6.1

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 (36) hide show
  1. package/CHANGELOG.md +162 -99
  2. package/README.md +113 -190
  3. package/RESULTADOS_WAF.md +63 -0
  4. package/doc-2.5/MANUAL_MODULOS_ADMIN.md +287 -0
  5. package/doc-2.5/QUEUE_CLI_MODULE_MANUAL.md +289 -0
  6. package/doc-2.5/QUEUE_SYSTEM_MANUAL.md +320 -0
  7. package/doc-2.5/ROUTE_CACHE_MODULE_MANUAL.md +205 -0
  8. package/doc-2.5/WAF_MODULE_MANUAL.md +229 -0
  9. package/index.js +12 -3
  10. package/jerk-admin-client/README.md +69 -0
  11. package/jerk-admin-client/package.json +23 -0
  12. package/jerk-admin-client.js +257 -0
  13. package/lib/admin/AdminExtension.js +74 -19
  14. package/lib/admin/modules/ControllerGeneratorModule.js +414 -0
  15. package/lib/admin/modules/QueueManagementModule.js +265 -0
  16. package/lib/admin/modules/RouteCacheModule.js +227 -0
  17. package/lib/admin/modules/RouteManagerModule.js +468 -0
  18. package/lib/admin/modules/STATS_MODULE_README.md +15 -0
  19. package/lib/admin/modules/ViewCacheStatsModule.js +92 -0
  20. package/lib/admin/modules/WAFModule.js +737 -0
  21. package/lib/core/server.js +72 -69
  22. package/lib/middleware/firewall.js +112 -17
  23. package/lib/mvc/viewEngine.js +69 -10
  24. package/lib/queue/GlobalQueueStorage.js +38 -0
  25. package/lib/queue/QueueSystem.js +451 -0
  26. package/lib/queue/admin_example.js +114 -0
  27. package/lib/queue/example.js +268 -0
  28. package/lib/queue/integration.js +109 -0
  29. package/lib/utils/globalViewCacheInfo.js +16 -0
  30. package/lib/utils/globalWAFStats.js +54 -0
  31. package/package.json +2 -2
  32. package/test-colors.js +46 -0
  33. package/test-help-alias.js +31 -0
  34. package/ESTADISTICAS_RENDIMIENTO.md +0 -106
  35. package/debug_hook.js +0 -11
  36. package/docs/CACHE_SYSTEM_MAP.md +0 -206
@@ -18,6 +18,7 @@ const { globalStats } = require('../utils/globalStats');
18
18
 
19
19
  // Carga anticipada de módulos comunes para evitar cargas repetidas
20
20
  let hooksInstance = null;
21
+ let errorHandlerInstance = null;
21
22
  const getHooks = () => {
22
23
  if (!hooksInstance) {
23
24
  hooksInstance = require('../../index.js').hooks;
@@ -25,6 +26,13 @@ const getHooks = () => {
25
26
  return hooksInstance;
26
27
  };
27
28
 
29
+ const getErrorHandler = () => {
30
+ if (!errorHandlerInstance) {
31
+ errorHandlerInstance = require('../utils/errorHandler');
32
+ }
33
+ return errorHandlerInstance;
34
+ };
35
+
28
36
  class APIServer {
29
37
  /**
30
38
  * Constructor del servidor
@@ -114,7 +122,8 @@ class APIServer {
114
122
  return async (req, res) => {
115
123
  try {
116
124
  // Disparar hook antes de procesar archivo estático
117
- const hooks = getHooks();
125
+ // Usar la instancia de hooks del servidor para evitar llamadas repetidas a getHooks()
126
+ const hooks = this.hooks;
118
127
  if (hooks) {
119
128
  const hookResult = hooks.applyFilters('pre_static_file_serve', {
120
129
  req,
@@ -150,8 +159,8 @@ class APIServer {
150
159
  res.writeHead(403, { 'Content-Type': 'application/json' });
151
160
  res.end(JSON.stringify({ error: 'Acceso denegado' }));
152
161
 
153
- if (hooks) {
154
- hooks.doAction('static_file_access_denied', req, res, normalizedPath);
162
+ if (this.hooks) {
163
+ this.hooks.doAction('static_file_access_denied', req, res, normalizedPath);
155
164
  }
156
165
  return;
157
166
  }
@@ -177,8 +186,8 @@ class APIServer {
177
186
  try {
178
187
  await fs.promises.access(indexPath);
179
188
  // Disparar hook antes de servir archivo índice
180
- if (hooks) {
181
- hooks.doAction('serving_index_file', indexPath, req, res);
189
+ if (this.hooks) {
190
+ this.hooks.doAction('serving_index_file', indexPath, req, res);
182
191
  }
183
192
 
184
193
  const fileContent = await fs.promises.readFile(indexPath);
@@ -194,8 +203,8 @@ class APIServer {
194
203
  res.end(fileContent);
195
204
 
196
205
  // Disparar hook después de servir archivo
197
- if (hooks) {
198
- hooks.doAction('static_file_served', indexPath, req, res);
206
+ if (this.hooks) {
207
+ this.hooks.doAction('static_file_served', indexPath, req, res);
199
208
  }
200
209
  return;
201
210
  } catch (e) {
@@ -207,8 +216,8 @@ class APIServer {
207
216
  res.writeHead(404, { 'Content-Type': 'application/json' });
208
217
  res.end(JSON.stringify({ error: 'Directorio no encontrado', path: dirPath }));
209
218
 
210
- if (hooks) {
211
- hooks.doAction('static_directory_no_index', dirPath, req, res);
219
+ if (this.hooks) {
220
+ this.hooks.doAction('static_directory_no_index', dirPath, req, res);
212
221
  }
213
222
  return;
214
223
  } else {
@@ -216,8 +225,8 @@ class APIServer {
216
225
  res.writeHead(404, { 'Content-Type': 'application/json' });
217
226
  res.end(JSON.stringify({ error: 'Archivo no encontrado', path: physicalPath }));
218
227
 
219
- if (hooks) {
220
- hooks.doAction('static_file_not_found', physicalPath, req, res);
228
+ if (this.hooks) {
229
+ this.hooks.doAction('static_file_not_found', physicalPath, req, res);
221
230
  }
222
231
  return;
223
232
  }
@@ -226,8 +235,8 @@ class APIServer {
226
235
  res.writeHead(404, { 'Content-Type': 'application/json' });
227
236
  res.end(JSON.stringify({ error: 'Archivo no encontrado', path: physicalPath }));
228
237
 
229
- if (hooks) {
230
- hooks.doAction('static_file_not_found', physicalPath, req, res);
238
+ if (this.hooks) {
239
+ this.hooks.doAction('static_file_not_found', physicalPath, req, res);
231
240
  }
232
241
  return;
233
242
  }
@@ -242,8 +251,8 @@ class APIServer {
242
251
  try {
243
252
  await fs.promises.access(indexPath);
244
253
  // Disparar hook antes de servir archivo índice
245
- if (hooks) {
246
- hooks.doAction('serving_index_file', indexPath, req, res);
254
+ if (this.hooks) {
255
+ this.hooks.doAction('serving_index_file', indexPath, req, res);
247
256
  }
248
257
 
249
258
  const fileContent = await fs.promises.readFile(indexPath);
@@ -259,8 +268,8 @@ class APIServer {
259
268
  res.end(fileContent);
260
269
 
261
270
  // Disparar hook después de servir archivo
262
- if (hooks) {
263
- hooks.doAction('static_file_served', indexPath, req, res);
271
+ if (this.hooks) {
272
+ this.hooks.doAction('static_file_served', indexPath, req, res);
264
273
  }
265
274
  return;
266
275
  } catch (e) {
@@ -272,15 +281,15 @@ class APIServer {
272
281
  res.writeHead(404, { 'Content-Type': 'application/json' });
273
282
  res.end(JSON.stringify({ error: 'Directorio no encontrado' }));
274
283
 
275
- if (hooks) {
276
- hooks.doAction('static_directory_no_index', dirPath, req, res);
284
+ if (this.hooks) {
285
+ this.hooks.doAction('static_directory_no_index', dirPath, req, res);
277
286
  }
278
287
  return;
279
288
  }
280
289
 
281
290
  // Disparar hook antes de leer archivo
282
- if (hooks) {
283
- hooks.doAction('before_reading_static_file', physicalPath, req, res);
291
+ if (this.hooks) {
292
+ this.hooks.doAction('before_reading_static_file', physicalPath, req, res);
284
293
  }
285
294
 
286
295
  // Leer y servir el archivo
@@ -300,34 +309,31 @@ class APIServer {
300
309
  res.end(fileContent);
301
310
 
302
311
  // Disparar hook después de servir archivo
303
- if (hooks) {
304
- hooks.doAction('static_file_served', physicalPath, req, res);
312
+ if (this.hooks) {
313
+ this.hooks.doAction('static_file_served', physicalPath, req, res);
305
314
  }
306
315
  } catch (error) {
307
316
  if (error.code === 'ENOENT') {
308
317
  res.writeHead(404, { 'Content-Type': 'application/json' });
309
318
  res.end(JSON.stringify({ error: 'Archivo no encontrado', path: error.path }));
310
319
 
311
- const hooks = getHooks();
312
- if (hooks) {
313
- hooks.doAction('static_file_not_found', error.path, req, res);
320
+ if (this.hooks) {
321
+ this.hooks.doAction('static_file_not_found', error.path, req, res);
314
322
  }
315
323
  } else if (error.code === 'EACCES') {
316
324
  res.writeHead(403, { 'Content-Type': 'application/json' });
317
325
  res.end(JSON.stringify({ error: 'Acceso denegado', path: error.path }));
318
326
 
319
- const hooks = getHooks();
320
- if (hooks) {
321
- hooks.doAction('static_file_access_denied', error.path, req, res);
327
+ if (this.hooks) {
328
+ this.hooks.doAction('static_file_access_denied', error.path, req, res);
322
329
  }
323
330
  } else {
324
331
  // Usar el ErrorHandler para mostrar el stacktrace en color amarillo
325
- const { ErrorHandler } = require('../utils/errorHandler');
332
+ const { ErrorHandler } = getErrorHandler();
326
333
  ErrorHandler.handle(error, req, res, this.logger);
327
334
 
328
- const hooks = getHooks();
329
- if (hooks) {
330
- hooks.doAction('static_file_error', error, req, res);
335
+ if (this.hooks) {
336
+ this.hooks.doAction('static_file_error', error, req, res);
331
337
  }
332
338
  }
333
339
  }
@@ -413,7 +419,6 @@ class APIServer {
413
419
 
414
420
  // Obtener el handler final según las configuraciones
415
421
  let finalHandler = routeConfig.handler;
416
- const path = require('path');
417
422
 
418
423
  // Si se especifica un controlador, cargarlo y obtener el handler
419
424
  if (routeConfig.controller && routeConfig.handlerName) {
@@ -445,7 +450,6 @@ class APIServer {
445
450
  if (routeConfig.auth === 'session') {
446
451
  // Verificar si el servidor tiene sessionManager
447
452
  if (this.sessionManager) {
448
- const { sessionAuth } = require('../middleware/session');
449
453
  const authMiddleware = sessionAuth(this.sessionManager, routeConfig.authOptions || {});
450
454
 
451
455
  // Crear un nuevo handler que ejecute la autenticación primero
@@ -473,7 +477,7 @@ class APIServer {
473
477
  }
474
478
  } catch (error) {
475
479
  // Usar el ErrorHandler para mostrar el stacktrace en color amarillo
476
- const { ErrorHandler } = require('../utils/errorHandler');
480
+ const { ErrorHandler } = getErrorHandler();
477
481
  if (!res.headersSent) {
478
482
  ErrorHandler.handle(error, req, res, this.logger);
479
483
  }
@@ -530,7 +534,7 @@ class APIServer {
530
534
  }
531
535
  } catch (error) {
532
536
  // Usar el ErrorHandler para mostrar el stacktrace en color amarillo
533
- const { ErrorHandler } = require('../utils/errorHandler');
537
+ const { ErrorHandler } = getErrorHandler();
534
538
  if (!res.headersSent) {
535
539
  ErrorHandler.handle(error, req, res, this.logger);
536
540
  }
@@ -603,9 +607,8 @@ class APIServer {
603
607
  */
604
608
  start() {
605
609
  // Disparar hook antes de iniciar el servidor
606
- const hooks = getHooks();
607
- if (hooks) {
608
- hooks.doAction('pre_server_start', this);
610
+ if (this.hooks) {
611
+ this.hooks.doAction('pre_server_start', this);
609
612
  }
610
613
 
611
614
  if (this.https && Object.keys(this.httpsOptions).length > 0) {
@@ -645,11 +648,11 @@ class APIServer {
645
648
  this.logger.logMemoryUsage();
646
649
 
647
650
  // Disparar hook después de iniciar el servidor
648
- if (hooks) {
649
- hooks.doAction('post_server_start', this);
650
-
651
+ if (this.hooks) {
652
+ this.hooks.doAction('post_server_start', this);
653
+
651
654
  // Disparar hook para inicializar extensiones después de que el servidor esté completamente iniciado
652
- hooks.doAction('admin_extensions_initialize', this);
655
+ this.hooks.doAction('admin_extensions_initialize', this);
653
656
  }
654
657
  });
655
658
 
@@ -695,9 +698,8 @@ class APIServer {
695
698
  const { pathname, query } = parsedUrl;
696
699
 
697
700
  // Disparar hook antes de procesar la solicitud
698
- const hooks = getHooks();
699
- if (hooks) {
700
- hooks.doAction('request_received', req, res);
701
+ if (this.hooks) {
702
+ this.hooks.doAction('request_received', req, res);
701
703
  }
702
704
 
703
705
  // Agregar propiedades útiles a la solicitud
@@ -758,16 +760,16 @@ class APIServer {
758
760
  res.writeHead(200, { 'Content-Type': 'text/html' });
759
761
  res.end(renderedHtml);
760
762
 
761
- if (hooks) {
762
- hooks.doAction('view_rendered', viewName, data, req, res);
763
+ if (this.hooks) {
764
+ this.hooks.doAction('view_rendered', viewName, data, req, res);
763
765
  }
764
766
  } catch (error) {
765
767
  // Usar el ErrorHandler para mostrar el stacktrace en color amarillo
766
768
  const { ErrorHandler } = require('../utils/errorHandler');
767
769
  ErrorHandler.handle(error, req, res, this.logger);
768
770
 
769
- if (hooks) {
770
- hooks.doAction('view_render_error', viewName, data, req, res, error);
771
+ if (this.hooks) {
772
+ this.hooks.doAction('view_render_error', viewName, data, req, res, error);
771
773
  }
772
774
  }
773
775
  };
@@ -810,8 +812,8 @@ class APIServer {
810
812
  }
811
813
 
812
814
  if (res.finished) {
813
- if (hooks) {
814
- hooks.doAction('middleware_response_finished', req, res);
815
+ if (this.hooks) {
816
+ this.hooks.doAction('middleware_response_finished', req, res);
815
817
  }
816
818
  return; // Si el middleware respondió, salir
817
819
  }
@@ -825,8 +827,8 @@ class APIServer {
825
827
  // Agregar parámetros a la solicitud
826
828
  req.params = matchedRoute.params;
827
829
 
828
- if (hooks) {
829
- hooks.doAction('route_matched', matchedRoute, req, res);
830
+ if (this.hooks) {
831
+ this.hooks.doAction('route_matched', matchedRoute, req, res);
830
832
  }
831
833
 
832
834
  // Ejecutar handler de la ruta
@@ -840,8 +842,8 @@ class APIServer {
840
842
  res.writeHead(204);
841
843
  res.end();
842
844
 
843
- if (hooks) {
844
- hooks.doAction('options_response_sent', req, res);
845
+ if (this.hooks) {
846
+ this.hooks.doAction('options_response_sent', req, res);
845
847
  }
846
848
  }
847
849
  }
@@ -853,8 +855,8 @@ class APIServer {
853
855
  // Agregar parámetros a la solicitud
854
856
  req.params = matchedRoute.params;
855
857
 
856
- if (hooks) {
857
- hooks.doAction('route_matched', matchedRoute, req, res);
858
+ if (this.hooks) {
859
+ this.hooks.doAction('route_matched', matchedRoute, req, res);
858
860
  }
859
861
 
860
862
  // Ejecutar middlewares (excepto para rutas estáticas)
@@ -885,8 +887,8 @@ class APIServer {
885
887
  }
886
888
 
887
889
  if (res.finished) {
888
- if (hooks) {
889
- hooks.doAction('middleware_response_finished', req, res);
890
+ if (this.hooks) {
891
+ this.hooks.doAction('middleware_response_finished', req, res);
890
892
  }
891
893
  return; // Si el middleware respondió, salir
892
894
  }
@@ -948,27 +950,28 @@ class APIServer {
948
950
  }
949
951
  globalStats.endpointHits.set(endpointKey, globalStats.endpointHits.get(endpointKey) + 1);
950
952
 
951
- if (hooks) {
952
- hooks.doAction('route_handler_executed', matchedRoute, req, res);
953
+ if (this.hooks) {
954
+ this.hooks.doAction('route_handler_executed', matchedRoute, req, res);
953
955
  }
954
956
  } else {
955
957
  // Ruta no encontrada
956
- if (hooks) {
957
- hooks.doAction('route_not_found', pathname, req, res);
958
+ if (this.hooks) {
959
+ this.hooks.doAction('route_not_found', pathname, req, res);
958
960
  }
959
961
  res.writeHead(404, { 'Content-Type': 'application/json' });
960
962
  res.end(JSON.stringify({ error: 'Ruta no encontrada', path: pathname }));
961
963
  }
962
964
  }
963
965
 
964
- if (hooks) {
965
- hooks.doAction('request_processed', req, res);
966
+ if (this.hooks) {
967
+ this.hooks.doAction('request_processed', req, res);
966
968
  }
967
969
  } catch (error) {
968
- if (hooks) {
969
- hooks.doAction('request_error', req, res, error);
970
+ if (this.hooks) {
971
+ this.hooks.doAction('request_error', req, res, error);
970
972
  }
971
973
 
974
+ const { ErrorHandler } = getErrorHandler();
972
975
  ErrorHandler.handle(error, req, res, this.logger);
973
976
  }
974
977
  });
@@ -170,26 +170,31 @@ class Firewall {
170
170
  // Path traversal
171
171
  const pathTraversal = /\.\.\//g;
172
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
- };
173
+ // Verificar patrones en URL solo si hay parámetros (evitar falsos positivos)
174
+ if (path.includes('?')) {
175
+ for (const pattern of sqlPatterns) {
176
+ if (pattern.test(path)) {
177
+ return {
178
+ matched: true,
179
+ rule: 'sql_injection',
180
+ action: 'block',
181
+ reason: 'Patrón de SQL Injection detectado'
182
+ };
183
+ }
182
184
  }
183
185
  }
184
186
 
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
- };
187
+ // Verificar patrones XSS solo si hay parámetros (evitar falsos positivos)
188
+ if (path.includes('?')) {
189
+ for (const pattern of xssPatterns) {
190
+ if (pattern.test(path)) {
191
+ return {
192
+ matched: true,
193
+ rule: 'xss_attack',
194
+ action: 'block',
195
+ reason: 'Patrón de XSS detectado'
196
+ };
197
+ }
193
198
  }
194
199
  }
195
200
 
@@ -202,6 +207,34 @@ class Firewall {
202
207
  };
203
208
  }
204
209
 
210
+ // Verificar reglas personalizadas de headers X-
211
+ const { globalWAFStats } = require('../utils/globalWAFStats');
212
+ for (const [ruleId, rule] of globalWAFStats.xHeaderRules) {
213
+ const headerValue = headers[rule.headerName];
214
+ if (headerValue) {
215
+ // Verificar si coincide con el patrón de la regla
216
+ if (typeof rule.pattern === 'string') {
217
+ if (headerValue.includes(rule.pattern)) {
218
+ return {
219
+ matched: true,
220
+ rule: ruleId,
221
+ action: rule.action,
222
+ reason: rule.reason
223
+ };
224
+ }
225
+ } else if (rule.pattern instanceof RegExp) {
226
+ if (rule.pattern.test(headerValue)) {
227
+ return {
228
+ matched: true,
229
+ rule: ruleId,
230
+ action: rule.action,
231
+ reason: rule.reason
232
+ };
233
+ }
234
+ }
235
+ }
236
+ }
237
+
205
238
  // Verificar patrones en body si es string
206
239
  if (typeof body === 'string') {
207
240
  for (const pattern of sqlPatterns) {
@@ -374,16 +407,35 @@ class Firewall {
374
407
 
375
408
  const clientIP = this.getClientIP(req);
376
409
 
410
+ // Actualizar estadísticas globales de solicitudes procesadas
411
+ const { globalWAFStats } = require('../utils/globalWAFStats');
412
+ globalWAFStats.requestsProcessed++;
413
+
414
+ // Capturar el tamaño del cuerpo de la solicitud si existe
415
+ if (req.body) {
416
+ const bodySize = typeof req.body === 'string' ? Buffer.byteLength(req.body, 'utf8') :
417
+ typeof req.body === 'object' ? Buffer.byteLength(JSON.stringify(req.body), 'utf8') : 0;
418
+ globalWAFStats.requestBytes += bodySize;
419
+ console.log(`[FIREWALL DEBUG] Bytes de solicitud actualizados: ${globalWAFStats.requestBytes}`);
420
+ } else {
421
+ console.log('[FIREWALL DEBUG] No hay cuerpo en la solicitud');
422
+ }
423
+
377
424
  // Verificar si la IP está bloqueada
378
425
  const blockInfo = this.isBlocked(clientIP);
379
426
  if (blockInfo.blocked) {
380
427
  this.logger.warn(`Solicitud bloqueada desde IP: ${clientIP}, razón: ${blockInfo.reason}`);
428
+ console.log(`[FIREWALL DEBUG] Solicitud bloqueada desde IP: ${clientIP}, razón: ${blockInfo.reason}`);
381
429
 
382
430
  // Disparar hook de evento de seguridad
383
431
  if (hooks) {
384
432
  hooks.doAction('firewall_ip_blocked', clientIP, blockInfo.reason, req, res);
385
433
  }
386
434
 
435
+ // Actualizar estadísticas de intentos de ataque
436
+ globalWAFStats.attackAttempts++;
437
+ console.log(`[FIREWALL DEBUG] Ataque detectado, attackAttempts ahora: ${globalWAFStats.attackAttempts}`);
438
+
387
439
  res.writeHead(403, { 'Content-Type': 'application/json' });
388
440
  res.end(JSON.stringify({
389
441
  error: 'Acceso denegado por firewall',
@@ -412,6 +464,14 @@ class Firewall {
412
464
  hooks.doAction('firewall_request_blocked', ruleMatch, clientIP, req, res);
413
465
  }
414
466
 
467
+ // Actualizar estadísticas de intentos de ataque
468
+ globalWAFStats.attackAttempts++;
469
+ console.log(`[FIREWALL DEBUG] Ataque bloqueado, attackAttempts ahora: ${globalWAFStats.attackAttempts}`);
470
+
471
+ // Registrar el tipo de ataque
472
+ globalWAFStats.attackTypes.add(ruleMatch.rule);
473
+ console.log(`[FIREWALL DEBUG] Tipo de ataque registrado: ${ruleMatch.rule}`);
474
+
415
475
  res.writeHead(403, { 'Content-Type': 'application/json' });
416
476
  res.end(JSON.stringify({
417
477
  error: 'Solicitud bloqueada por firewall',
@@ -429,11 +489,46 @@ class Firewall {
429
489
  }
430
490
  }
431
491
 
492
+ // Registrar acceso a ruta en las estadísticas globales
493
+ const routeKey = `${req.method} ${req.url}`;
494
+ if (!globalWAFStats.routeAccesses.has(routeKey)) {
495
+ globalWAFStats.routeAccesses.set(routeKey, 0);
496
+ }
497
+ globalWAFStats.routeAccesses.set(routeKey, globalWAFStats.routeAccesses.get(routeKey) + 1);
498
+ console.log(`[FIREWALL DEBUG] Acceso a ruta registrado: ${routeKey}, total accesos: ${globalWAFStats.routeAccesses.get(routeKey)}`);
499
+
500
+ // Registrar hit al endpoint en las estadísticas globales
501
+ const endpointKey = `${req.method} ${req.url}`;
502
+ if (!globalWAFStats.endpointHits.has(endpointKey)) {
503
+ globalWAFStats.endpointHits.set(endpointKey, 0);
504
+ }
505
+ globalWAFStats.endpointHits.set(endpointKey, globalWAFStats.endpointHits.get(endpointKey) + 1);
506
+ console.log(`[FIREWALL DEBUG] Hit al endpoint registrado: ${endpointKey}, total hits: ${globalWAFStats.endpointHits.get(endpointKey)}`);
507
+
432
508
  // Disparar hook antes de continuar con el siguiente middleware
433
509
  if (hooks) {
434
510
  hooks.doAction('firewall_request_allowed', req, res);
435
511
  }
436
512
 
513
+ // Capturar el tamaño de la respuesta antes de enviarla
514
+ const originalEnd = res.end;
515
+ res.end = (chunk, encoding, callback) => {
516
+ console.log(`[FIREWALL DEBUG] Enviando respuesta, tamaño del chunk: ${chunk ? Buffer.byteLength(chunk, typeof encoding === 'string' ? encoding : 'utf8') : 0} bytes`);
517
+
518
+ // Actualizar estadísticas globales de respuestas enviadas
519
+ globalWAFStats.responsesSent++;
520
+ console.log(`[FIREWALL DEBUG] responsesSent ahora: ${globalWAFStats.responsesSent}`);
521
+
522
+ // Capturar el tamaño del cuerpo de la respuesta si existe
523
+ if (chunk) {
524
+ const responseSize = Buffer.byteLength(chunk, typeof encoding === 'string' ? encoding : 'utf8');
525
+ globalWAFStats.responseBytes += responseSize;
526
+ console.log(`[FIREWALL DEBUG] responseBytes ahora: ${globalWAFStats.responseBytes}`);
527
+ }
528
+
529
+ return originalEnd.call(res, chunk, encoding, callback);
530
+ };
531
+
437
532
  // Continuar con el siguiente middleware
438
533
  next();
439
534
  };
@@ -7,24 +7,29 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const { globalViewCacheInfo } = require('../utils/globalViewCacheInfo');
10
11
 
11
12
  class ViewEngine {
12
13
  constructor(options = {}) {
13
14
  this.viewsPath = options.viewsPath || './views';
14
15
  this.defaultExtension = options.defaultExtension || '.html';
15
16
  this.cacheEnabled = options.cacheEnabled !== false; // Por defecto habilitado
16
- this.viewCache = new Map(); // Cache de vistas compiladas
17
17
  this.logger = options.logger || console;
18
-
18
+
19
+ // Usar el mapa global para el caché de vistas
20
+ this.viewCache = globalViewCacheInfo.viewCache;
21
+ this.cacheStats = globalViewCacheInfo.cacheStats;
22
+ this.viewDependencies = globalViewCacheInfo.viewDependencies;
23
+
19
24
  // Sistema de hooks para extensibilidad
20
25
  this.hooks = options.hooks || null;
21
-
26
+
22
27
  // Sistema de filtros
23
28
  this.filters = new Map();
24
29
  this.helpers = new Map(); // Sistema de helpers personalizados
25
30
  this.registerDefaultFilters();
26
31
  this.registerDefaultHelpers();
27
-
32
+
28
33
  // Asegurar que el directorio de vistas existe
29
34
  if (!fs.existsSync(this.viewsPath)) {
30
35
  fs.mkdirSync(this.viewsPath, { recursive: true });
@@ -224,6 +229,7 @@ class ViewEngine {
224
229
  // Intentar obtener del cache si está habilitado
225
230
  if (this.cacheEnabled && this.viewCache.has(viewPath)) {
226
231
  const cachedView = this.viewCache.get(viewPath);
232
+ this.cacheStats.hits++; // Incrementar contador de hits
227
233
  return this.processTemplate(cachedView, data, options);
228
234
  }
229
235
 
@@ -275,21 +281,30 @@ class ViewEngine {
275
281
  // Si el cache está habilitado, guardar la vista compilada (sin variables)
276
282
  if (this.cacheEnabled) {
277
283
  // Aplicar filter antes de cachear la vista
278
- const contentToCache = this.hooks ?
284
+ const contentToCache = this.hooks ?
279
285
  this.hooks.applyFilters('before_view_cache', viewContent, viewPath) : viewContent;
280
-
286
+
281
287
  // Verificar con hook si se debe cachear esta vista
282
- const shouldCache = this.hooks ?
288
+ const shouldCache = this.hooks ?
283
289
  this.hooks.applyFilters('should_cache_view', true, viewPath, contentToCache) : true;
284
-
290
+
285
291
  if (shouldCache) {
286
292
  this.viewCache.set(viewPath, contentToCache);
287
-
293
+ this.cacheStats.misses++; // Incrementar contador de misses (ya que se leyó del disco)
294
+ this.cacheStats.totalViews++; // Incrementar total de vistas únicas
295
+
296
+ // Registrar dependencias de inclusiones
297
+ this.trackViewDependencies(viewPath, contentToCache);
298
+
288
299
  // Disparar hook después de cachear
289
300
  if (this.hooks) {
290
301
  this.hooks.doAction('view_cached', viewPath, contentToCache);
291
302
  }
303
+ } else {
304
+ this.cacheStats.misses++; // Aún se leyó del disco aunque no se cacheó
292
305
  }
306
+ } else {
307
+ this.cacheStats.misses++; // Se leyó del disco sin cache
293
308
  }
294
309
 
295
310
  // Procesar el template con las variables
@@ -775,6 +790,42 @@ class ViewEngine {
775
790
  return value;
776
791
  }
777
792
 
793
+ /**
794
+ * Registra las dependencias de inclusiones de una vista
795
+ * @param {string} viewPath - Ruta de la vista principal
796
+ * @param {string} content - Contenido de la vista
797
+ */
798
+ trackViewDependencies(viewPath, content) {
799
+ // Patrón para encontrar bloques de inclusión como {{include:nombre_vista}}
800
+ const includePattern = /\{\{include:(.*?)\}\}/g;
801
+ let match;
802
+
803
+ while ((match = includePattern.exec(content)) !== null) {
804
+ const includePath = match[1].trim();
805
+
806
+ // Obtener la ruta completa de la vista incluida
807
+ let includedViewPath;
808
+ if (includePath.startsWith('./') || includePath.startsWith('../')) {
809
+ includedViewPath = path.resolve(path.dirname(viewPath), includePath);
810
+ if (!path.extname(includedViewPath)) {
811
+ includedViewPath += this.defaultExtension;
812
+ }
813
+ } else {
814
+ // Usar la ruta de vistas por defecto
815
+ includedViewPath = path.join(this.viewsPath, includePath.replace(/\./g, '/'));
816
+ if (!path.extname(includedViewPath)) {
817
+ includedViewPath += this.defaultExtension;
818
+ }
819
+ }
820
+
821
+ // Registrar la dependencia
822
+ if (!this.viewDependencies.has(viewPath)) {
823
+ this.viewDependencies.set(viewPath, new Set());
824
+ }
825
+ this.viewDependencies.get(viewPath).add(includedViewPath);
826
+ }
827
+ }
828
+
778
829
  /**
779
830
  * Parsea argumentos de filtros o helpers, respetando cadenas entre comillas
780
831
  * @param {string} argsString - Cadena de argumentos
@@ -836,9 +887,17 @@ class ViewEngine {
836
887
  if (this.hooks) {
837
888
  this.hooks.doAction('before_view_cache_clear', this);
838
889
  }
839
-
890
+
891
+ // Limpiar solo las vistas asociadas a esta instancia (si se desea)
892
+ // Para el sistema global, limpiar completamente
840
893
  this.viewCache.clear();
894
+ this.viewDependencies.clear();
841
895
 
896
+ // Reiniciar estadísticas
897
+ this.cacheStats.hits = 0;
898
+ this.cacheStats.misses = 0;
899
+ this.cacheStats.totalViews = 0;
900
+
842
901
  // Disparar hook después de limpiar el caché
843
902
  if (this.hooks) {
844
903
  this.hooks.doAction('view_cache_cleared');