kukuy 1.4.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/.ctagsd/ctagsd.json +954 -0
- package/.ctagsd/file_list.txt +100 -0
- package/.ctagsd/tags.db +0 -0
- package/CHANGELOG.md +101 -0
- package/LICENSE +680 -0
- package/README.md +251 -0
- package/captura.png +0 -0
- package/kukuy.js +23 -0
- package/kukuy.workspace +11 -0
- package/package.json +26 -0
- package/restart-balancer.sh +10 -0
- package/routes.json +14 -0
- package/scripts/load_test.py +151 -0
- package/servers.json +19 -0
- package/servers_real.json +19 -0
- package/src/algorithms/AlgorithmManager.js +85 -0
- package/src/algorithms/IPHashAlgorithm.js +131 -0
- package/src/algorithms/LoadBalancingAlgorithm.js +23 -0
- package/src/algorithms/RoundRobinAlgorithm.js +67 -0
- package/src/config/ConfigManager.js +37 -0
- package/src/config/RouteLoader.js +36 -0
- package/src/core/Balancer.js +353 -0
- package/src/core/RoundRobinAlgorithm.js +60 -0
- package/src/core/ServerPool.js +77 -0
- package/src/dashboard/WebDashboard.js +150 -0
- package/src/dashboard/WebSocketServer.js +114 -0
- package/src/extensibility/CachingFilter.js +134 -0
- package/src/extensibility/FilterChain.js +93 -0
- package/src/extensibility/HookManager.js +48 -0
- package/src/protocol/HttpBalancer.js +37 -0
- package/src/protocol/HttpsBalancer.js +47 -0
- package/src/utils/BalancerLogger.js +102 -0
- package/src/utils/HealthChecker.js +51 -0
- package/src/utils/Logger.js +39 -0
- package/src/utils/MetricsCollector.js +82 -0
- package/src/utils/ProfessionalMetrics.js +501 -0
- package/start-iphash.sh +5 -0
- package/start-roundrobin.sh +5 -0
- package/stress-test.js +190 -0
- package/webpage/README.md +17 -0
- package/webpage/index.html +549 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
class RoundRobinAlgorithm {
|
|
2
|
+
constructor(serverPool) {
|
|
3
|
+
this.serverPool = serverPool;
|
|
4
|
+
this.currentIndex = 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
selectServer(servers = null) {
|
|
8
|
+
// Obtener servidores disponibles (si se pasan servers específicos, usarlos; sino, usar todos)
|
|
9
|
+
let availableServers = servers || this.serverPool.getHealthyServers();
|
|
10
|
+
|
|
11
|
+
// Filtrar servidores activos
|
|
12
|
+
availableServers = availableServers.filter(server => server.active !== false);
|
|
13
|
+
|
|
14
|
+
if (availableServers.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Intentar encontrar un servidor disponible usando round robin
|
|
19
|
+
// Si el servidor actual no está disponible, buscar el siguiente disponible
|
|
20
|
+
for (let i = 0; i < availableServers.length; i++) {
|
|
21
|
+
const candidateIndex = (this.currentIndex + i) % availableServers.length;
|
|
22
|
+
const candidateServer = availableServers[candidateIndex];
|
|
23
|
+
|
|
24
|
+
// Verificar si el servidor candidato está realmente disponible
|
|
25
|
+
if (this.isServerAvailable(candidateServer)) {
|
|
26
|
+
this.currentIndex = candidateIndex + 1; // Preparar para la próxima selección
|
|
27
|
+
return candidateServer;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Si ningún servidor está disponible, devolver null
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Método para verificar si un servidor está disponible
|
|
36
|
+
isServerAvailable(server) {
|
|
37
|
+
// Un servidor está disponible si:
|
|
38
|
+
// 1. Está marcado como healthy
|
|
39
|
+
// 2. Está activo
|
|
40
|
+
// 3. No ha superado el número máximo de intentos fallidos
|
|
41
|
+
return server.healthy && server.active && server.failedAttempts < 5;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Método para seleccionar un servidor con reintento automático
|
|
45
|
+
async selectServerWithRetry(servers = null, maxRetries = 3) {
|
|
46
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
47
|
+
const server = this.selectServer(servers);
|
|
48
|
+
if (server) {
|
|
49
|
+
return server;
|
|
50
|
+
}
|
|
51
|
+
// Si no hay servidores disponibles, esperar un poco antes de reintentar
|
|
52
|
+
if (attempt < maxRetries - 1) {
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Esperar 100ms
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { RoundRobinAlgorithm };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const { HealthChecker } = require('../utils/HealthChecker');
|
|
2
|
+
|
|
3
|
+
class ServerPool {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.servers = [];
|
|
6
|
+
this.healthChecker = new HealthChecker();
|
|
7
|
+
this.nextId = 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
addServer(serverConfig) {
|
|
11
|
+
const server = {
|
|
12
|
+
id: this.nextId++,
|
|
13
|
+
url: serverConfig.url,
|
|
14
|
+
protocol: serverConfig.url.startsWith('https') ? 'https:' : 'http:',
|
|
15
|
+
host: new URL(serverConfig.url).host,
|
|
16
|
+
weight: serverConfig.weight || 1,
|
|
17
|
+
active: serverConfig.active !== false, // Por defecto activo
|
|
18
|
+
tags: serverConfig.tags || [],
|
|
19
|
+
failedAttempts: 0,
|
|
20
|
+
lastChecked: Date.now(),
|
|
21
|
+
healthy: true
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.servers.push(server);
|
|
25
|
+
return server;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addServers(serversConfig) {
|
|
29
|
+
serversConfig.forEach(serverConfig => {
|
|
30
|
+
this.addServer(serverConfig);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getServers() {
|
|
35
|
+
return this.servers;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getHealthyServers() {
|
|
39
|
+
return this.servers.filter(server => server.healthy && server.active);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getServersByTag(tag) {
|
|
43
|
+
return this.servers.filter(server => server.tags.includes(tag));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
markServerAsFailed(serverId) {
|
|
47
|
+
const server = this.servers.find(s => s.id === serverId);
|
|
48
|
+
if (server) {
|
|
49
|
+
server.failedAttempts++;
|
|
50
|
+
server.healthy = false;
|
|
51
|
+
|
|
52
|
+
// Programar verificación de salud después de un tiempo
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
this.healthChecker.checkServerHealth(server)
|
|
55
|
+
.then(isHealthy => {
|
|
56
|
+
if (isHealthy) {
|
|
57
|
+
server.healthy = true;
|
|
58
|
+
server.failedAttempts = 0;
|
|
59
|
+
server.lastChecked = Date.now();
|
|
60
|
+
} else {
|
|
61
|
+
server.lastChecked = Date.now();
|
|
62
|
+
// Si ha fallado demasiadas veces, mantenerlo inactivo
|
|
63
|
+
if (server.failedAttempts > 5) {
|
|
64
|
+
server.active = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}, 30000); // Reintento cada 30 segundos
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getServerById(serverId) {
|
|
73
|
+
return this.servers.find(server => server.id === serverId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { ServerPool };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const url = require('url');
|
|
5
|
+
|
|
6
|
+
class WebDashboard {
|
|
7
|
+
constructor(balancerInstance) {
|
|
8
|
+
this.balancer = balancerInstance;
|
|
9
|
+
this.port = process.env.DASHBOARD_PORT || 8082;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
start() {
|
|
13
|
+
const server = http.createServer((req, res) => {
|
|
14
|
+
const parsedUrl = url.parse(req.url, true);
|
|
15
|
+
const pathname = parsedUrl.pathname;
|
|
16
|
+
|
|
17
|
+
// Rutas de la API
|
|
18
|
+
if (pathname === '/api/status') {
|
|
19
|
+
this.handleStatusRequest(res);
|
|
20
|
+
} else if (pathname === '/api/metrics') {
|
|
21
|
+
this.handleMetricsRequest(res);
|
|
22
|
+
} else if (pathname === '/api/servers') {
|
|
23
|
+
this.handleServersRequest(res);
|
|
24
|
+
} else if (pathname === '/api/server-stats') {
|
|
25
|
+
this.handleServerStatsRequest(res);
|
|
26
|
+
} else if (pathname === '/api/config') {
|
|
27
|
+
this.handleConfigRequest(res);
|
|
28
|
+
} else {
|
|
29
|
+
// Servir archivos estáticos
|
|
30
|
+
this.serveStaticFile(req, res, pathname);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
server.listen(this.port, () => {
|
|
35
|
+
console.log(`Panel web del balanceador escuchando en puerto ${this.port}`);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handleStatusRequest(res) {
|
|
40
|
+
const status = {
|
|
41
|
+
running: true,
|
|
42
|
+
algorithm: this.balancer.algorithmManager.getCurrentAlgorithm()?.getName() || 'unknown',
|
|
43
|
+
uptime: Date.now() - this.balancer.startTime,
|
|
44
|
+
timestamp: new Date().toISOString()
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
48
|
+
res.end(JSON.stringify(status));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
handleMetricsRequest(res) {
|
|
52
|
+
const metrics = this.balancer.metricsCollector.getMetrics();
|
|
53
|
+
|
|
54
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
55
|
+
res.end(JSON.stringify(metrics));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleServersRequest(res) {
|
|
59
|
+
const servers = this.balancer.serverPool.getServers().map(server => ({
|
|
60
|
+
id: server.id,
|
|
61
|
+
url: server.url,
|
|
62
|
+
protocol: server.protocol,
|
|
63
|
+
host: server.host,
|
|
64
|
+
weight: server.weight,
|
|
65
|
+
active: server.active,
|
|
66
|
+
tags: server.tags,
|
|
67
|
+
failedAttempts: server.failedAttempts,
|
|
68
|
+
lastChecked: server.lastChecked,
|
|
69
|
+
healthy: server.healthy,
|
|
70
|
+
status: server.healthy ? 'online' : 'offline'
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
74
|
+
res.end(JSON.stringify({ servers }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
handleServerStatsRequest(res) {
|
|
78
|
+
// Obtener estadísticas detalladas por servidor
|
|
79
|
+
const serverStats = this.balancer.metricsCollector.getServerStats();
|
|
80
|
+
|
|
81
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
82
|
+
res.end(JSON.stringify({ serverStats }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
handleConfigRequest(res) {
|
|
86
|
+
// Devolver la configuración necesaria para el cliente web
|
|
87
|
+
const config = {
|
|
88
|
+
websocketPort: process.env.WEBSOCKET_PORT || 8083, // Puerto WebSocket por defecto
|
|
89
|
+
algorithm: this.balancer.algorithmManager.getCurrentAlgorithmName ?
|
|
90
|
+
this.balancer.algorithmManager.getCurrentAlgorithmName() :
|
|
91
|
+
'roundrobin',
|
|
92
|
+
httpPort: process.env.BALANCER_HTTP_PORT || 8081,
|
|
93
|
+
dashboardPort: process.env.DASHBOARD_PORT || 8082
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify(config));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
serveStaticFile(req, res, pathname) {
|
|
101
|
+
let filePath = path.join(__dirname, '../../webpage', pathname);
|
|
102
|
+
|
|
103
|
+
// Si la ruta es '/', servir index.html
|
|
104
|
+
if (pathname === '/') {
|
|
105
|
+
filePath = path.join(__dirname, '../../webpage', 'index.html');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Mapeo de extensiones a tipos MIME
|
|
109
|
+
const mimeTypes = {
|
|
110
|
+
'.html': 'text/html',
|
|
111
|
+
'.css': 'text/css',
|
|
112
|
+
'.js': 'application/javascript',
|
|
113
|
+
'.json': 'application/json',
|
|
114
|
+
'.png': 'image/png',
|
|
115
|
+
'.jpg': 'image/jpg',
|
|
116
|
+
'.gif': 'image/gif',
|
|
117
|
+
'.svg': 'image/svg+xml',
|
|
118
|
+
'.wav': 'audio/wav',
|
|
119
|
+
'.mp4': 'video/mp4',
|
|
120
|
+
'.woff': 'application/font-woff',
|
|
121
|
+
'.ttf': 'application/font-ttf',
|
|
122
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
123
|
+
'.otf': 'application/font-otf',
|
|
124
|
+
'.wasm': 'application/wasm'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const extname = String(path.extname(filePath)).toLowerCase();
|
|
128
|
+
const contentType = mimeTypes[extname] || 'application/octet-stream';
|
|
129
|
+
|
|
130
|
+
fs.readFile(filePath, (err, content) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
if (err.code === 'ENOENT') {
|
|
133
|
+
// Archivo no encontrado
|
|
134
|
+
res.writeHead(404);
|
|
135
|
+
res.end('404 Not Found');
|
|
136
|
+
} else {
|
|
137
|
+
// Otro error
|
|
138
|
+
res.writeHead(500);
|
|
139
|
+
res.end(`Error interno del servidor: ${err.code}`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Archivo encontrado
|
|
143
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
144
|
+
res.end(content, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { WebDashboard };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const { ProfessionalMetrics } = require('../utils/ProfessionalMetrics');
|
|
3
|
+
|
|
4
|
+
class WebSocketServer {
|
|
5
|
+
constructor(balancerInstance) {
|
|
6
|
+
this.balancer = balancerInstance;
|
|
7
|
+
this.wss = null;
|
|
8
|
+
this.clients = new Set();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
start(port = 8083) {
|
|
12
|
+
this.wss = new WebSocket.Server({ port });
|
|
13
|
+
|
|
14
|
+
this.wss.on('connection', (ws) => {
|
|
15
|
+
// Agregar nuevo cliente
|
|
16
|
+
this.clients.add(ws);
|
|
17
|
+
console.log(`Cliente WebSocket conectado. Total: ${this.clients.size}`);
|
|
18
|
+
|
|
19
|
+
// Enviar datos inmediatamente al conectar
|
|
20
|
+
this.sendCurrentMetrics(ws);
|
|
21
|
+
|
|
22
|
+
// Manejar desconexión
|
|
23
|
+
ws.on('close', () => {
|
|
24
|
+
this.clients.delete(ws);
|
|
25
|
+
console.log(`Cliente WebSocket desconectado. Total: ${this.clients.size}`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Manejar errores
|
|
29
|
+
ws.on('error', (error) => {
|
|
30
|
+
console.error('Error en WebSocket:', error);
|
|
31
|
+
this.clients.delete(ws);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log(`Servidor WebSocket iniciado en puerto ${port}`);
|
|
36
|
+
|
|
37
|
+
// Iniciar envío periódico de datos
|
|
38
|
+
this.startPeriodicBroadcast();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
sendCurrentMetrics(ws) {
|
|
42
|
+
try {
|
|
43
|
+
const metrics = this.balancer.metricsCollector.getMetrics();
|
|
44
|
+
const serverStats = this.balancer.metricsCollector.getServerStats();
|
|
45
|
+
const status = {
|
|
46
|
+
running: true,
|
|
47
|
+
algorithm: this.balancer.algorithmManager.getCurrentAlgorithm()?.getName() || 'unknown',
|
|
48
|
+
uptime: Date.now() - this.balancer.startTime,
|
|
49
|
+
timestamp: new Date().toISOString()
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const data = {
|
|
53
|
+
type: 'metrics_update',
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
metrics,
|
|
56
|
+
serverStats,
|
|
57
|
+
status
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
ws.send(JSON.stringify(data));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Error enviando métricas:', error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
broadcastMetrics() {
|
|
67
|
+
if (this.clients.size === 0) return;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const metrics = this.balancer.metricsCollector.getMetrics();
|
|
71
|
+
const serverStats = this.balancer.metricsCollector.getServerStats();
|
|
72
|
+
const status = {
|
|
73
|
+
running: true,
|
|
74
|
+
algorithm: this.balancer.algorithmManager.getCurrentAlgorithm()?.getName() || 'unknown',
|
|
75
|
+
uptime: Date.now() - this.balancer.startTime,
|
|
76
|
+
timestamp: new Date().toISOString()
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const data = {
|
|
80
|
+
type: 'metrics_update',
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
metrics,
|
|
83
|
+
serverStats,
|
|
84
|
+
status
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const jsonData = JSON.stringify(data);
|
|
88
|
+
|
|
89
|
+
// Enviar a todos los clientes conectados
|
|
90
|
+
for (const client of this.clients) {
|
|
91
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
92
|
+
client.send(jsonData);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Error en broadcast de métricas:', error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
startPeriodicBroadcast(interval = 2000) { // Enviar cada 2 segundos
|
|
101
|
+
setInterval(() => {
|
|
102
|
+
this.broadcastMetrics();
|
|
103
|
+
}, interval);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
close() {
|
|
107
|
+
if (this.wss) {
|
|
108
|
+
this.wss.close();
|
|
109
|
+
this.clients.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { WebSocketServer };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
class CachingFilter {
|
|
2
|
+
constructor(maxCacheSize = 100) {
|
|
3
|
+
this.cache = new Map();
|
|
4
|
+
this.maxCacheSize = maxCacheSize;
|
|
5
|
+
this.accessLog = new Map(); // Para implementar LRU
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Generar clave única para la caché basada en la solicitud
|
|
9
|
+
generateCacheKey(req) {
|
|
10
|
+
return `${req.method}:${req.url}:${JSON.stringify(req.headers)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Verificar si una solicitud es cacheable
|
|
14
|
+
isCacheable(req) {
|
|
15
|
+
// Solo GET requests son cacheables por defecto
|
|
16
|
+
return req.method === 'GET';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Aplicar el filtro de caché
|
|
20
|
+
async apply(req, res, additionalData = {}) {
|
|
21
|
+
if (!this.isCacheable(req)) {
|
|
22
|
+
// Si no es cacheable, continuar con la cadena
|
|
23
|
+
return { allowed: true, cached: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cacheKey = this.generateCacheKey(req);
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
|
|
29
|
+
// Verificar si está en caché y no ha expirado
|
|
30
|
+
if (this.cache.has(cacheKey)) {
|
|
31
|
+
const cachedData = this.cache.get(cacheKey);
|
|
32
|
+
|
|
33
|
+
// Verificar si no ha expirado (por ejemplo, 5 minutos)
|
|
34
|
+
if (now - cachedData.timestamp < 300000) { // 5 minutos en milisegundos
|
|
35
|
+
// Enviar respuesta desde caché
|
|
36
|
+
res.writeHead(cachedData.statusCode, cachedData.headers);
|
|
37
|
+
res.end(cachedData.body);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
allowed: true,
|
|
41
|
+
cached: true,
|
|
42
|
+
cacheHit: true,
|
|
43
|
+
cacheKey
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
// Eliminar entrada expirada
|
|
47
|
+
this.cache.delete(cacheKey);
|
|
48
|
+
this.accessLog.delete(cacheKey);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Si no está en caché, permitir que la solicitud continúe
|
|
53
|
+
// y almacenar la respuesta para futuras solicitudes
|
|
54
|
+
const originalWriteHead = res.writeHead;
|
|
55
|
+
const originalEnd = res.end;
|
|
56
|
+
const filter = this;
|
|
57
|
+
|
|
58
|
+
// Interceptamos la respuesta para almacenarla
|
|
59
|
+
res.writeHead = function(statusCode, headers) {
|
|
60
|
+
res.cachedStatusCode = statusCode;
|
|
61
|
+
res.cachedHeaders = headers;
|
|
62
|
+
return originalWriteHead.call(this, statusCode, headers);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
res.end = function(chunk) {
|
|
66
|
+
if (chunk && !res.finished) {
|
|
67
|
+
// Almacenar en caché antes de enviar la respuesta
|
|
68
|
+
filter.storeInCache(cacheKey, {
|
|
69
|
+
statusCode: res.cachedStatusCode || 200,
|
|
70
|
+
headers: res.cachedHeaders || {},
|
|
71
|
+
body: chunk,
|
|
72
|
+
timestamp: now
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return originalEnd.call(this, chunk);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
allowed: true,
|
|
80
|
+
cached: false,
|
|
81
|
+
cacheKey
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
storeInCache(cacheKey, data) {
|
|
86
|
+
// Si excede el tamaño máximo, eliminar el menos recientemente usado
|
|
87
|
+
if (this.cache.size >= this.maxCacheSize) {
|
|
88
|
+
const oldestKey = this.getLeastRecentlyUsedKey();
|
|
89
|
+
if (oldestKey) {
|
|
90
|
+
this.cache.delete(oldestKey);
|
|
91
|
+
this.accessLog.delete(oldestKey);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.cache.set(cacheKey, data);
|
|
96
|
+
this.accessLog.set(cacheKey, Date.now());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getLeastRecentlyUsedKey() {
|
|
100
|
+
let oldestTime = Infinity;
|
|
101
|
+
let oldestKey = null;
|
|
102
|
+
|
|
103
|
+
for (const [key, time] of this.accessLog.entries()) {
|
|
104
|
+
if (time < oldestTime) {
|
|
105
|
+
oldestTime = time;
|
|
106
|
+
oldestKey = key;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return oldestKey;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Limpiar entradas expiradas
|
|
114
|
+
clearExpiredEntries() {
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
for (const [key, data] of this.cache.entries()) {
|
|
117
|
+
if (now - data.timestamp >= 300000) { // 5 minutos
|
|
118
|
+
this.cache.delete(key);
|
|
119
|
+
this.accessLog.delete(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Obtener estadísticas de la caché
|
|
125
|
+
getStats() {
|
|
126
|
+
return {
|
|
127
|
+
size: this.cache.size,
|
|
128
|
+
maxSize: this.maxCacheSize,
|
|
129
|
+
utilization: (this.cache.size / this.maxCacheSize) * 100
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { CachingFilter };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* KUKUY
|
|
3
|
+
* Copyright (C) 2026 Desarrollador
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const HookSystem = require('jerk-hooked-lib');
|
|
20
|
+
const { CachingFilter } = require('./CachingFilter');
|
|
21
|
+
|
|
22
|
+
class FilterChain {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.hooks = new HookSystem();
|
|
25
|
+
this.cachingFilter = new CachingFilter();
|
|
26
|
+
this.setupDefaultFilters();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setupDefaultFilters() {
|
|
30
|
+
// Registrar filtros predeterminados
|
|
31
|
+
this.hooks.addFilter('request_processing', this.authenticationFilter.bind(this));
|
|
32
|
+
this.hooks.addFilter('request_processing', this.rateLimitFilter.bind(this));
|
|
33
|
+
this.hooks.addFilter('request_processing', this.loggingFilter.bind(this));
|
|
34
|
+
// El caching filter se aplica de manera especial, no en la cadena estándar
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async applyFilters(type, req, res, additionalData = {}) {
|
|
38
|
+
// Verificar si la solicitud puede ser atendida desde caché
|
|
39
|
+
const cacheResult = await this.cachingFilter.apply(req, res, additionalData);
|
|
40
|
+
|
|
41
|
+
// Si fue un hit de caché, retornar inmediatamente
|
|
42
|
+
if (cacheResult.cached && cacheResult.cacheHit) {
|
|
43
|
+
return cacheResult;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Usar el sistema de filtros de la librería para otros filtros
|
|
47
|
+
// Creamos un objeto combinado para pasar al filtro
|
|
48
|
+
const filterData = { req, res, ...additionalData, allowed: true, statusCode: 200, message: '' };
|
|
49
|
+
|
|
50
|
+
// Aplicar filtros según el tipo
|
|
51
|
+
const processedData = this.hooks.applyFilters('request_processing', filterData);
|
|
52
|
+
|
|
53
|
+
// Devolver el resultado procesado
|
|
54
|
+
return { ...processedData, ...cacheResult };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Filtro de autenticación básico
|
|
58
|
+
authenticationFilter(data) {
|
|
59
|
+
// Aquí iría la lógica de autenticación
|
|
60
|
+
// Por ahora, permitimos todas las solicitudes
|
|
61
|
+
return data; // Devolvemos el mismo objeto con posibles modificaciones
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Filtro de límite de tasa básico
|
|
65
|
+
rateLimitFilter(data) {
|
|
66
|
+
// Aquí iría la lógica de límite de tasa
|
|
67
|
+
// Por ahora, permitimos todas las solicitudes
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Filtro de logging básico
|
|
72
|
+
loggingFilter(data) {
|
|
73
|
+
console.log(`${new Date().toISOString()} - ${data.req.method} ${data.req.url}`);
|
|
74
|
+
return data;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Método para añadir nuevos filtros
|
|
78
|
+
addFilter(filterName, filterFunction) {
|
|
79
|
+
this.hooks.addFilter(filterName, filterFunction);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Método para aplicar un filtro específico
|
|
83
|
+
applySpecificFilter(filterName, data) {
|
|
84
|
+
return this.hooks.applyFilters(filterName, data);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Obtener estadísticas de la caché
|
|
88
|
+
getCacheStats() {
|
|
89
|
+
return this.cachingFilter.getStats();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { FilterChain };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const HookSystem = require('jerk-hooked-lib');
|
|
2
|
+
|
|
3
|
+
class HookManager {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.hooks = new HookSystem();
|
|
6
|
+
this.registerDefaultHooks();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
registerDefaultHooks() {
|
|
10
|
+
// Definir hooks predeterminados
|
|
11
|
+
const hookNames = [
|
|
12
|
+
'onRequestReceived',
|
|
13
|
+
'onServerSelected',
|
|
14
|
+
'onResponseSent',
|
|
15
|
+
'onServerError'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Los hooks se registran implícitamente al usar addAction
|
|
19
|
+
// No es necesario registro explícito en esta librería
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async executeHooks(hookName, data) {
|
|
23
|
+
// En esta librería, usamos doAction para ejecutar hooks
|
|
24
|
+
// Pasamos el objeto data como argumento
|
|
25
|
+
this.hooks.doAction(hookName, data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addHook(hookName, callback) {
|
|
29
|
+
// En esta librería, usamos addAction para añadir hooks
|
|
30
|
+
this.hooks.addAction(hookName, callback);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
removeHook(hookName, callback) {
|
|
34
|
+
// Para remover hooks, necesitamos un ID único
|
|
35
|
+
// Esta operación puede no estar disponible directamente en esta librería
|
|
36
|
+
console.warn(`Remoción de hook no implementada directamente para: ${hookName}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
hasHook(hookName) {
|
|
40
|
+
return this.hooks.hasAction(hookName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getHookCount(hookName) {
|
|
44
|
+
return this.hooks.actionsCount(hookName);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { HookManager };
|