kukuy 1.5.0 → 1.6.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.
@@ -18,8 +18,8 @@
18
18
 
19
19
  const fs = require('fs');
20
20
  const url = require('url');
21
- const { HookManager } = require('../extensibility/HookManager');
22
- const { FilterChain } = require('../extensibility/FilterChain');
21
+ const { ExtendedHookManager } = require('../extensibility/ExtendedHookManager');
22
+ const { ExtendedFilterChain } = require('../extensibility/ExtendedFilterChain');
23
23
  const { ServerPool } = require('./ServerPool');
24
24
  const { AlgorithmManager } = require('../algorithms/AlgorithmManager');
25
25
  const { ConfigManager } = require('../config/ConfigManager');
@@ -31,12 +31,14 @@ const { HttpsBalancer } = require('../protocol/HttpsBalancer');
31
31
  const { ProfessionalMetrics } = require('../utils/ProfessionalMetrics');
32
32
  const { WebDashboard } = require('../dashboard/WebDashboard');
33
33
  const { WebSocketServer } = require('../dashboard/WebSocketServer');
34
+ const { PostStartupExtension } = require('../extensibility/PostStartupExtension');
35
+ const { PluginManager } = require('../plugins/PluginManager');
34
36
 
35
37
  class Balancer {
36
38
  constructor() {
37
39
  this.config = ConfigManager.getInstance();
38
- this.hookManager = new HookManager();
39
- this.filterChain = new FilterChain();
40
+ this.hookManager = new ExtendedHookManager();
41
+ this.filterChain = new ExtendedFilterChain();
40
42
  this.serverPool = new ServerPool();
41
43
  this.algorithmManager = new AlgorithmManager();
42
44
  this.routeLoader = new RouteLoader();
@@ -56,6 +58,64 @@ class Balancer {
56
58
 
57
59
  // Inicializar servidor WebSocket para datos en tiempo real
58
60
  this.webSocketServer = new WebSocketServer(this);
61
+
62
+ // Inicializar sistema de extensión post-inicio
63
+ this.postStartupExtension = new PostStartupExtension(this);
64
+
65
+ // Activar plugin de depuración si está habilitado
66
+ this.initializeDebugPlugin();
67
+
68
+ // Inicializar sistema de plugins
69
+ this.pluginManager = new PluginManager(this);
70
+ }
71
+
72
+ initializeDebugPlugin() {
73
+ // Verificar si el plugin de depuración está habilitado
74
+ const debugEnabled = process.env.DEBUG_REQUEST_HEADERS === 'true' ||
75
+ process.env.KUKUY_DEBUG_HEADERS === 'true';
76
+
77
+ if (debugEnabled) {
78
+ // Registrar hooks para mostrar encabezados y servidor de destino
79
+
80
+ // Hook para mostrar encabezados cuando se recibe una solicitud
81
+ this.hookManager.addHook('onRequestReceived', ({ req, res }) => {
82
+ console.log('\n--- Nueva Solicitud Recibida ---');
83
+ console.log(`Método: ${req.method}`);
84
+ console.log(`URL: ${req.url}`);
85
+ console.log('Encabezados:');
86
+
87
+ // Mostrar todos los encabezados de la solicitud
88
+ for (const [header, value] of Object.entries(req.headers)) {
89
+ console.log(` ${header}: ${value}`);
90
+ }
91
+
92
+ console.log('-----------------------------\n');
93
+ });
94
+
95
+ // Hook para mostrar el servidor de destino asignado
96
+ this.hookManager.addHook('onServerSelected', ({ req, res, server }) => {
97
+ console.log('\n--- Servidor de Destino Asignado ---');
98
+ console.log(`Solicitud: ${req.method} ${req.url}`);
99
+ console.log(`Servidor destino: ${server.url}`);
100
+ console.log(`ID del servidor: ${server.id}`);
101
+ console.log(`Protocolo: ${server.protocol}`);
102
+ console.log(`Host: ${server.host}`);
103
+ console.log(`Etiquetas: ${server.tags.join(', ') || 'ninguna'}`);
104
+ console.log('----------------------------------\n');
105
+ });
106
+
107
+ // Hook para mostrar información cuando se envía la respuesta
108
+ this.hookManager.addHook('onResponseSent', ({ req, res, serverRes, responseTime }) => {
109
+ console.log('\n--- Respuesta Enviada ---');
110
+ console.log(`Solicitud: ${req.method} ${req.url}`);
111
+ console.log(`Código de estado: ${serverRes.statusCode}`);
112
+ console.log(`Tiempo de respuesta: ${responseTime}ms`);
113
+ console.log('------------------------\n');
114
+ });
115
+
116
+ console.log('Plugin de depuración de solicitud activado');
117
+ console.log('Mostrando encabezados de solicitud y servidores de destino en stdout');
118
+ }
59
119
  }
60
120
 
61
121
  loadConfiguration() {
@@ -88,7 +148,7 @@ class Balancer {
88
148
  }
89
149
  }
90
150
 
91
- start() {
151
+ async start() {
92
152
  // Crear servidor HTTP usando HttpBalancer
93
153
  if (this.config.httpPort) {
94
154
  this.httpBalancer = new HttpBalancer(this.config.httpPort, this.handleRequest.bind(this));
@@ -112,6 +172,9 @@ class Balancer {
112
172
  // Iniciar servidor WebSocket para datos en tiempo real
113
173
  const wsPort = process.env.WEBSOCKET_PORT || 8083;
114
174
  this.webSocketServer.start(wsPort);
175
+
176
+ // Cargar plugins después de iniciar componentes principales
177
+ await this.pluginManager.loadPlugins();
115
178
  }
116
179
 
117
180
  async handleRequest(clientReq, clientRes) {
@@ -350,6 +413,14 @@ class Balancer {
350
413
  this.httpsBalancer.stop();
351
414
  }
352
415
  }
416
+
417
+ /**
418
+ * Get the post-startup extension API
419
+ * @returns {PostStartupExtension} The extension API
420
+ */
421
+ getPostStartupExtension() {
422
+ return this.postStartupExtension;
423
+ }
353
424
  }
354
425
 
355
426
  module.exports = { Balancer };
@@ -0,0 +1,90 @@
1
+ const { FilterChain } = require('./FilterChain');
2
+
3
+ /**
4
+ * Extended FilterChain that allows registering filters after startup
5
+ */
6
+ class ExtendedFilterChain {
7
+ constructor() {
8
+ // Create a new instance of the original FilterChain
9
+ this.originalFilterChain = new FilterChain();
10
+
11
+ // Storage for post-startup filters
12
+ this.postStartupFilters = {
13
+ request_processing: [] // Using the same hook type as original
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Register a filter after startup
19
+ * @param {string} type - Type of filter ('request_processing' for now)
20
+ * @param {Function} filterFunction - The filter function to register
21
+ * @param {number} priority - Priority of the filter (lower numbers execute first)
22
+ */
23
+ registerFilter(type, filterFunction, priority = 0) {
24
+ if (!this.postStartupFilters[type]) {
25
+ this.postStartupFilters[type] = [];
26
+ }
27
+
28
+ // Insert filter according to priority
29
+ const filterObj = {
30
+ fn: filterFunction,
31
+ priority,
32
+ id: this.generateId()
33
+ };
34
+
35
+ this.postStartupFilters[type].push(filterObj);
36
+ this.postStartupFilters[type].sort((a, b) => a.priority - b.priority);
37
+
38
+ // Also add to the original hook system
39
+ this.originalFilterChain.addFilter(type, filterFunction);
40
+
41
+ console.log(`Filter registered for ${type} with priority ${priority}`);
42
+ }
43
+
44
+ /**
45
+ * Unregister a specific filter
46
+ * @param {string} type - Type of filter
47
+ * @param {string} filterId - ID of the filter to remove
48
+ */
49
+ unregisterFilter(type, filterId) {
50
+ if (this.postStartupFilters[type]) {
51
+ this.postStartupFilters[type] = this.postStartupFilters[type].filter(f => f.id !== filterId);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Apply all filters of a specific type
57
+ * @param {string} type - Type of filter
58
+ * @param {Object} req - Request object
59
+ * @param {Object} res - Response object
60
+ * @param {Object} additionalData - Additional data to pass to filters
61
+ * @returns {Object} Result of filter processing
62
+ */
63
+ async applyFilters(type, req, res, additionalData = {}) {
64
+ // Apply filters using the original system which now includes post-startup filters
65
+ return await this.originalFilterChain.applyFilters(type, req, res, additionalData);
66
+ }
67
+
68
+ /**
69
+ * Generate a unique ID
70
+ * @returns {string} Unique identifier
71
+ */
72
+ generateId() {
73
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
74
+ }
75
+
76
+ // Proxy other methods to the original FilterChain
77
+ addFilter(filterName, filterFunction) {
78
+ this.originalFilterChain.addFilter(filterName, filterFunction);
79
+ }
80
+
81
+ applySpecificFilter(filterName, data) {
82
+ return this.originalFilterChain.applySpecificFilter(filterName, data);
83
+ }
84
+
85
+ getCacheStats() {
86
+ return this.originalFilterChain.getCacheStats();
87
+ }
88
+ }
89
+
90
+ module.exports = { ExtendedFilterChain };
@@ -0,0 +1,87 @@
1
+ const { HookManager } = require('./HookManager');
2
+
3
+ /**
4
+ * Extended HookManager that allows registering hooks after startup
5
+ */
6
+ class ExtendedHookManager {
7
+ constructor() {
8
+ // Create a new instance of the original HookManager
9
+ this.originalHookManager = new HookManager();
10
+
11
+ // Storage for post-startup hooks
12
+ this.postStartupHooks = {};
13
+ }
14
+
15
+ /**
16
+ * Register a hook after startup
17
+ * @param {string} hookName - Name of the hook
18
+ * @param {Function} callback - The callback function to register
19
+ * @param {number} priority - Priority of the hook (lower numbers execute first)
20
+ */
21
+ registerHook(hookName, callback, priority = 0) {
22
+ if (!this.postStartupHooks[hookName]) {
23
+ this.postStartupHooks[hookName] = [];
24
+ }
25
+
26
+ const hookObj = {
27
+ callback,
28
+ priority,
29
+ id: this.generateId()
30
+ };
31
+
32
+ this.postStartupHooks[hookName].push(hookObj);
33
+ this.postStartupHooks[hookName].sort((a, b) => a.priority - b.priority);
34
+
35
+ // Add to the original hook system
36
+ this.originalHookManager.addHook(hookName, callback);
37
+
38
+ console.log(`Hook registered: ${hookName} with priority ${priority}`);
39
+ }
40
+
41
+ /**
42
+ * Unregister a specific hook
43
+ * @param {string} hookName - Name of the hook
44
+ * @param {string} hookId - ID of the hook to remove
45
+ */
46
+ unregisterHook(hookName, hookId) {
47
+ if (this.postStartupHooks[hookName]) {
48
+ this.postStartupHooks[hookName] = this.postStartupHooks[hookName].filter(h => h.id !== hookId);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Execute all hooks for a specific event
54
+ * @param {string} hookName - Name of the hook
55
+ * @param {Object} data - Data to pass to the hooks
56
+ */
57
+ async executeHooks(hookName, data) {
58
+ // Execute hooks using the original system which now includes post-startup hooks
59
+ return await this.originalHookManager.executeHooks(hookName, data);
60
+ }
61
+
62
+ /**
63
+ * Generate a unique ID
64
+ * @returns {string} Unique identifier
65
+ */
66
+ generateId() {
67
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
68
+ }
69
+
70
+ // Proxy other methods to the original HookManager
71
+ addHook(hookName, callback) {
72
+ this.originalHookManager.addHook(hookName, callback);
73
+ }
74
+
75
+ executeHooks(hookName, data) {
76
+ return this.originalHookManager.executeHooks(hookName, data);
77
+ }
78
+
79
+ getRegisteredHooks() {
80
+ return Object.keys(this.postStartupHooks).reduce((acc, hookName) => {
81
+ acc[hookName] = this.postStartupHooks[hookName].length;
82
+ return acc;
83
+ }, {});
84
+ }
85
+ }
86
+
87
+ module.exports = { ExtendedHookManager };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * API for registering extensions after Kukuy startup
3
+ */
4
+ class PostStartupExtension {
5
+ constructor(balancer) {
6
+ this.balancer = balancer;
7
+ }
8
+
9
+ /**
10
+ * Register a filter after startup
11
+ * @param {string} type - Type of filter ('request_processing' for now)
12
+ * @param {Function} filterFunction - The filter function to register
13
+ * @param {number} priority - Priority of the filter (lower numbers execute first)
14
+ */
15
+ registerFilter(type, filterFunction, priority = 0) {
16
+ if (this.balancer.filterChain && typeof this.balancer.filterChain.registerFilter === 'function') {
17
+ this.balancer.filterChain.registerFilter(type, filterFunction, priority);
18
+ } else {
19
+ console.error('FilterChain no disponible o registerFilter no es una función');
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Unregister a filter
25
+ * @param {string} type - Type of filter
26
+ * @param {string} filterId - ID of the filter to remove
27
+ */
28
+ unregisterFilter(type, filterId) {
29
+ if (this.balancer.filterChain && typeof this.balancer.filterChain.unregisterFilter === 'function') {
30
+ this.balancer.filterChain.unregisterFilter(type, filterId);
31
+ } else {
32
+ console.error('FilterChain no disponible o unregisterFilter no es una función');
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Register a hook after startup
38
+ * @param {string} hookName - Name of the hook
39
+ * @param {Function} callback - The callback function to register
40
+ * @param {number} priority - Priority of the hook (lower numbers execute first)
41
+ */
42
+ registerHook(hookName, callback, priority = 0) {
43
+ if (this.balancer.hookManager && typeof this.balancer.hookManager.registerHook === 'function') {
44
+ this.balancer.hookManager.registerHook(hookName, callback, priority);
45
+ } else {
46
+ console.error('HookManager no disponible o registerHook no es una función');
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Unregister a hook
52
+ * @param {string} hookName - Name of the hook
53
+ * @param {string} hookId - ID of the hook to remove
54
+ */
55
+ unregisterHook(hookName, hookId) {
56
+ if (this.balancer.hookManager && typeof this.balancer.hookManager.unregisterHook === 'function') {
57
+ this.balancer.hookManager.unregisterHook(hookName, hookId);
58
+ } else {
59
+ console.error('HookManager no disponible o unregisterHook no es una función');
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get access to system components
65
+ * @param {string} componentName - Name of the component to access
66
+ * @returns {Object} The requested component or null
67
+ */
68
+ getComponent(componentName) {
69
+ switch(componentName) {
70
+ case 'serverPool':
71
+ return this.balancer.serverPool;
72
+ case 'algorithmManager':
73
+ return this.balancer.algorithmManager;
74
+ case 'metricsCollector':
75
+ return this.balancer.metricsCollector;
76
+ case 'config':
77
+ return this.balancer.config;
78
+ case 'routeLoader':
79
+ return this.balancer.routeLoader;
80
+ default:
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get information about registered hooks
87
+ * @returns {Object} Information about registered hooks
88
+ */
89
+ getRegisteredHooks() {
90
+ if (this.balancer.hookManager && typeof this.balancer.hookManager.getRegisteredHooks === 'function') {
91
+ return this.balancer.hookManager.getRegisteredHooks();
92
+ }
93
+ return {};
94
+ }
95
+ }
96
+
97
+ module.exports = { PostStartupExtension };
@@ -0,0 +1,183 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * PluginInstance represents a loaded plugin instance
6
+ */
7
+ class PluginInstance {
8
+ constructor(name, manifest, module, balancer) {
9
+ this.name = name;
10
+ this.manifest = manifest;
11
+ this.module = module;
12
+ this.balancer = balancer;
13
+ this.isActive = false;
14
+ }
15
+
16
+ async activate() {
17
+ if (typeof this.module.init === 'function') {
18
+ await this.module.init(this.balancer);
19
+ }
20
+ this.isActive = true;
21
+ }
22
+
23
+ async deactivate() {
24
+ if (typeof this.module.deinit === 'function') {
25
+ await this.module.deinit(this.balancer);
26
+ }
27
+ this.isActive = false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * PluginManager handles the discovery and loading of plugins
33
+ */
34
+ class PluginManager {
35
+ constructor(balancer) {
36
+ this.balancer = balancer;
37
+ this.plugins = new Map();
38
+ this.pluginPath = process.env.PLUGINS_DIR || './kukuy-plugins';
39
+ }
40
+
41
+ /**
42
+ * Check if a path is a directory
43
+ * @param {string} filePath - Path to check
44
+ * @returns {boolean} - True if path is a directory
45
+ */
46
+ isDirectory(filePath) {
47
+ try {
48
+ const stat = fs.statSync(filePath);
49
+ return stat.isDirectory();
50
+ } catch (error) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Load all plugins from the plugins directory
57
+ */
58
+ async loadPlugins() {
59
+ if (!fs.existsSync(this.pluginPath)) {
60
+ console.log(`Plugin directory not found: ${this.pluginPath}`);
61
+ return;
62
+ }
63
+
64
+ const pluginDirs = fs.readdirSync(this.pluginPath)
65
+ .filter(item => this.isDirectory(path.join(this.pluginPath, item)));
66
+
67
+ console.log(`Found ${pluginDirs.length} potential plugins in ${this.pluginPath}`);
68
+
69
+ for (const pluginDir of pluginDirs) {
70
+ await this.loadPlugin(pluginDir);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Load a single plugin
76
+ * @param {string} pluginDir - Name of the plugin directory
77
+ */
78
+ async loadPlugin(pluginDir) {
79
+ const pluginPath = path.join(this.pluginPath, pluginDir);
80
+ const manifestPath = path.join(pluginPath, 'manifest.json');
81
+
82
+ if (!fs.existsSync(manifestPath)) {
83
+ console.warn(`Plugin ${pluginDir} does not have manifest.json, skipping...`);
84
+ return;
85
+ }
86
+
87
+ try {
88
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
89
+
90
+ if (!this.validateManifest(manifest)) {
91
+ console.error(`Invalid manifest for plugin ${pluginDir}`);
92
+ return;
93
+ }
94
+
95
+ // Check Kukuy version compatibility
96
+ if (!this.isCompatibleVersion(manifest.kukuyVersion)) {
97
+ console.error(`Plugin ${pluginDir} requires incompatible Kukuy version: ${manifest.kukuyVersion}`);
98
+ return;
99
+ }
100
+
101
+ if (!manifest.enabled) {
102
+ console.log(`Plugin ${pluginDir} is disabled`);
103
+ return;
104
+ }
105
+
106
+ const mainPath = path.join(pluginPath, manifest.main || 'index.js');
107
+ if (!fs.existsSync(mainPath)) {
108
+ console.error(`Main file not found for plugin ${pluginDir}: ${mainPath}`);
109
+ return;
110
+ }
111
+
112
+ // Resolve the path to avoid conflicts with other requires
113
+ const pluginModule = require(path.resolve(mainPath));
114
+ const pluginInstance = new PluginInstance(pluginDir, manifest, pluginModule, this.balancer);
115
+
116
+ this.plugins.set(pluginDir, pluginInstance);
117
+ await pluginInstance.activate();
118
+
119
+ console.log(`Plugin ${pluginDir} loaded successfully`);
120
+ } catch (error) {
121
+ console.error(`Error loading plugin ${pluginDir}:`, error);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Validate the plugin manifest
127
+ * @param {Object} manifest - The manifest object to validate
128
+ * @returns {boolean} - True if manifest is valid
129
+ */
130
+ validateManifest(manifest) {
131
+ const requiredFields = ['name', 'version', 'description', 'main'];
132
+ const isValid = requiredFields.every(field => manifest.hasOwnProperty(field));
133
+
134
+ if (!isValid) {
135
+ console.error('Missing required fields in manifest:', requiredFields.filter(field => !manifest.hasOwnProperty(field)));
136
+ }
137
+
138
+ return isValid;
139
+ }
140
+
141
+ /**
142
+ * Check if the plugin is compatible with current Kukuy version
143
+ * @param {string} requiredVersion - Version requirement from manifest
144
+ * @returns {boolean} - True if compatible
145
+ */
146
+ isCompatibleVersion(requiredVersion) {
147
+ if (!requiredVersion) {
148
+ // If no version specified, assume compatibility
149
+ return true;
150
+ }
151
+
152
+ // Simple version check - in a real implementation, you'd want to use semver
153
+ const currentVersion = require('../../package.json').version;
154
+
155
+ // For now, just check if the major version matches or if it's a compatible range
156
+ if (requiredVersion.startsWith('^')) {
157
+ const requiredMajor = requiredVersion.slice(1).split('.')[0];
158
+ const currentMajor = currentVersion.split('.')[0];
159
+ return currentMajor === requiredMajor;
160
+ } else if (requiredVersion.startsWith('~')) {
161
+ const [reqMajor, reqMinor] = requiredVersion.slice(1).split('.');
162
+ const [curMajor, curMinor] = currentVersion.split('.');
163
+ return currentMajor === reqMajor && currentMinor === reqMinor;
164
+ } else {
165
+ return currentVersion === requiredVersion;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get information about loaded plugins
171
+ * @returns {Array} - Array of plugin information
172
+ */
173
+ getLoadedPlugins() {
174
+ return Array.from(this.plugins.values()).map(plugin => ({
175
+ name: plugin.name,
176
+ version: plugin.manifest.version,
177
+ description: plugin.manifest.description,
178
+ isActive: plugin.isActive
179
+ }));
180
+ }
181
+ }
182
+
183
+ module.exports = { PluginManager };