3 * @module PluginsRoutes
4 * @description Plugin management API routes for RMM agent extensibility. Manages plugin catalog, file downloads, agent plugin initialization, and status tracking. Supports auto-sync from plugin directories and WebSocket-based plugin deployment to connected agents.
5 * @see {@link ../services/db} for database connection
6 * @see {@link ../services/websocketManager} for agent WebSocket connections
7 * @see {@link ../middleware/auth} for authentication middleware
8 * @see {@link ../middleware/tenant} for tenant context utilities
9 * @apiDefine PluginsGroup Plugins
11 * @apiHeader {string} Authorization Bearer token required.
12 * @apiError (Error 401) Unauthorized Missing or invalid token.
13 * @apiError (Error 404) NotFound Plugin or agent not found.
14 * @apiError (Error 500) ServerError Internal server error.
17const express = require('express');
18const router = express.Router();
19const pool = require('../services/db');
20const authenticateToken = require('../middleware/auth');
21const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
22const wsManager = require('../services/websocketManager');
25// Utility: scan plugin directory and sync with DB
29async function syncPluginsWithDb() {
30 const fs = require('fs');
31 const path = require('path');
33 path.resolve(__dirname, '../../agent/plugins/agent'),
34 path.resolve(__dirname, '../../agent/plugins/windows'),
35 path.resolve(__dirname, '../../agent/plugins/linux'),
36 path.resolve(__dirname, '../../agent/plugins/mac'),
38 for (const dir of pluginDirs) {
39 if (!fs.existsSync(dir)) continue;
40 const files = fs.readdirSync(dir);
41 for (const file of files) {
42 // Only add if not already present
44 const description = 'Auto-synced from plugin directory';
45 const version = '1.0.0';
46 // Set type based on extension or default to 'binary'
48 if (file.endsWith('.sh')) type = 'script';
49 else if (file.endsWith('.js')) type = 'script';
50 else if (file.endsWith('.exe')) type = 'binary';
51 else if (file.endsWith('.pkg')) type = 'package';
54 'INSERT INTO plugins (name, type, description, version) VALUES ($1, $2, $3, $4) ON CONFLICT (name) DO NOTHING',
55 [name, type, description, version]
57 // Also insert version into agent_versions table if not present
59 `INSERT INTO agent_versions (version, release_date) VALUES ($1, NOW())
60 ON CONFLICT (version) DO NOTHING`,
64 console.error(`[Plugin Sync] Failed to insert ${name}:`, err.message);
71 * @api {get} /plugins List all available plugins
72 * @apiName ListPlugins
74 * @apiDescription Retrieve all available plugins from the database. Optionally sync with plugin directory first to discover new plugin files.
75 * @apiParam {boolean} [sync=false] Trigger plugin directory sync before returning list (query param).
76 * @apiSuccess {object[]} plugins Array of plugin objects.
77 * @apiSuccess {number} plugins.plugin_id Plugin ID.
78 * @apiSuccess {string} plugins.name Plugin name.
79 * @apiSuccess {string} plugins.type Plugin type (binary, script, package).
80 * @apiSuccess {string} plugins.description Plugin description.
81 * @apiSuccess {string} plugins.version Plugin version.
82 * @apiExample {curl} Example usage:
83 * curl -H "Authorization: Bearer <token>" https://api.example.com/plugins?sync=true
85router.get('/', authenticateToken, async (req, res) => {
87 if (req.query.sync === 'true') {
88 await syncPluginsWithDb();
90 const result = await pool.query('SELECT * FROM plugins ORDER BY name ASC');
91 res.json(result.rows);
93 console.error('Error fetching plugins:', err);
94 res.status(500).json({ error: 'Server error' });
99 * @api {get} /plugins/:agentId/list List plugins for specific agent
100 * @apiName ListAgentPlugins
102 * @apiDescription Retrieve list of plugin names available for a specific agent. Auto-syncs plugin directory to ensure current list.
103 * @apiParam {string} agentId Agent ID (URL parameter).
104 * @apiSuccess {string[]} plugins Array of plugin names.
105 * @apiExample {curl} Example usage:
106 * curl -H "Authorization: Bearer <token>" https://api.example.com/plugins/agent-123/list
108router.get('/:agentId/list', authenticateToken, async (req, res) => {
110 await syncPluginsWithDb(); // Ensure up-to-date
111 const result = await pool.query('SELECT name FROM plugins ORDER BY name ASC');
112 res.json(result.rows.map(row => row.name));
114 console.error('Error fetching agent plugins:', err);
115 res.status(500).json({ error: 'Server error' });
120 * @api {get} /plugins/download/:plugin Download plugin file
121 * @apiName DownloadPlugin
123 * @apiDescription Download plugin binary or script file. Searches plugin directories for the requested plugin file and streams it as a download.
124 * @apiParam {string} plugin Plugin file name (URL parameter).
125 * @apiSuccess {File} file Plugin file for download.
126 * @apiError (Error 404) NotFound Plugin not found in database or file not found on disk.
127 * @apiExample {curl} Example usage:
128 * curl -H "Authorization: Bearer <token>" -O https://api.example.com/plugins/download/my-plugin.exe
130router.get('/download/:plugin', authenticateToken, async (req, res) => {
132 const pluginName = req.params.plugin;
133 const result = await pool.query('SELECT name FROM plugins WHERE name = $1', [pluginName]);
134 if (result.rows.length === 0) {
135 return res.status(404).json({ error: 'Plugin not found' });
137 // Try to find file in plugins directory
138 const fs = require('fs');
139 const path = require('path');
141 path.resolve(__dirname, '../../agent/plugins/agent'),
142 path.resolve(__dirname, '../../agent/plugins/windows'),
143 path.resolve(__dirname, '../../agent/plugins/linux'),
144 path.resolve(__dirname, '../../agent/plugins/mac'),
147 for (const dir of pluginDirs) {
148 const candidate = path.join(dir, pluginName);
149 if (fs.existsSync(candidate)) {
150 filePath = candidate;
155 return res.status(404).json({ error: 'Plugin file not found' });
157 res.download(filePath);
159 console.error('Error downloading plugin:', err);
160 res.status(500).json({ error: 'Server error' });
165 * @api {post} /plugins/sync Manually trigger plugin directory sync
166 * @apiName SyncPlugins
168 * @apiDescription Scan plugin directories (agent, windows, linux, mac) and sync discovered plugins to database. Creates or updates plugin records and version entries.
169 * @apiSuccess {string} status Operation status ("ok" on success).
170 * @apiExample {curl} Example usage:
171 * curl -X POST -H "Authorization: Bearer <token>" https://api.example.com/plugins/sync
173router.post('/sync', authenticateToken, async (req, res) => {
175 await syncPluginsWithDb();
176 res.json({ status: 'ok' });
178 res.status(500).json({ error: err.message });
183 * @api {post} /plugins/initialize/:pluginName/:agentUuid Initialize plugin on agent
184 * @apiName InitializePlugin
186 * @apiDescription Initialize a specific plugin on a connected agent via WebSocket or command queue. Plugin must be in allowed_plugins list. Sends initialization command to agent and follows up with status check after 3 seconds.
187 * @apiParam {string} pluginName Plugin name (URL parameter).
188 * @apiParam {string} agentUuid Agent UUID (URL parameter).
189 * @apiSuccess {boolean} success Operation success status.
190 * @apiSuccess {string} method Delivery method (websocket, command_queue, fallback).
191 * @apiSuccess {string} message Status message.
192 * @apiSuccess {string} agentUuid Agent UUID.
193 * @apiSuccess {string} pluginName Plugin name.
194 * @apiSuccess {object} [command] Command object sent (if WebSocket method).
195 * @apiError (Error 400) BadRequest Plugin is not enabled.
196 * @apiError (Error 404) NotFound Agent not found.
197 * @apiExample {curl} Example usage:
198 * curl -X POST -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/plugins/initialize/monitoring/agent-uuid-123
200router.post('/initialize/:pluginName/:agentUuid', authenticateToken, setTenantContext, async (req, res) => {
201 const { pluginName, agentUuid } = req.params;
204 // Verify agent belongs to tenant
205 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'a');
208 SELECT a.agent_uuid, a.hostname, a.status
210 WHERE a.agent_uuid = $1
213 let queryParams = [agentUuid];
216 query += ` AND ${tenantClause}`;
217 queryParams.push(...tenantParams);
220 const agentResult = await pool.query(query, queryParams);
222 if (agentResult.rows.length === 0) {
223 return res.status(404).json({ error: 'Agent not found' });
226 const agent = agentResult.rows[0];
228 // Check if plugin is allowed
229 const pluginResult = await pool.query(
230 'SELECT * FROM allowed_plugins WHERE plugin_name = $1',
231 [pluginName + '.js'] // Add .js extension
234 if (pluginResult.rows.length === 0) {
235 return res.status(400).json({ error: `Plugin ${pluginName} is not enabled` });
238 // Try to find agent WebSocket connection
239 const ws = wsManager.agentConnections.get(agentUuid);
241 if (!ws || ws.readyState !== 1) {
242 console.log(`[Plugins] WebSocket not found for agent ${agentUuid}, attempting alternative method`);
244 // Try to queue command for agent to pick up
247 `INSERT INTO agent_commands (agent_uuid, command_type, command_data, status, created_at)
248 VALUES ($1, $2, $3, 'pending', NOW())
249 ON CONFLICT DO NOTHING`,
252 `${pluginName}_initialize`,
253 JSON.stringify({ pluginName, timestamp: Date.now() })
259 method: 'command_queue',
260 message: `${pluginName} initialization queued for agent ${agent.hostname}. Agent will process on next check.`,
265 console.log('[Plugins] Command queue not available, using direct approach');
269 // Send WebSocket command directly
271 type: `${pluginName}_initialize`,
272 timestamp: Date.now()
275 if (ws && ws.readyState === 1) {
276 ws.send(JSON.stringify(command));
277 console.log(`[Plugins] Sent ${pluginName}_initialize to agent ${agentUuid} (${agent.hostname})`);
279 // Also send status check after delay
281 if (ws.readyState === 1) {
282 ws.send(JSON.stringify({
283 type: `${pluginName}_get_status`,
284 timestamp: Date.now()
286 console.log(`[Plugins] Sent ${pluginName}_get_status to agent ${agentUuid}`);
293 message: `${pluginName} initialization sent to agent ${agent.hostname}`,
300 // Fallback: return success anyway, let agent handle it
304 message: `${pluginName} initialization requested for agent ${agent.hostname}. Check agent logs.`,
310 console.error(`[Plugins] Error initializing ${pluginName}:`, error);
311 res.status(500).json({ error: 'Failed to initialize plugin', details: error.message });
316 * @api {get} /plugins/status/:agentUuid Get plugin status for agent
317 * @apiName GetPluginStatus
319 * @apiDescription Retrieve plugin status for a specific agent including list of enabled plugins and WebSocket connection status. Used to verify agent plugin availability and connectivity.
320 * @apiParam {string} agentUuid Agent UUID (URL parameter).
321 * @apiSuccess {string} agentUuid Agent UUID.
322 * @apiSuccess {string[]} enabledPlugins List of enabled plugin names.
323 * @apiSuccess {boolean} wsConnected WebSocket connection status.
324 * @apiSuccess {string} hostname Agent hostname.
325 * @apiError (Error 404) NotFound Agent not found.
326 * @apiExample {curl} Example usage:
327 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/plugins/status/agent-uuid-123
329router.get('/status/:agentUuid', authenticateToken, setTenantContext, async (req, res) => {
330 const { agentUuid } = req.params;
333 // Verify agent belongs to tenant
334 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'a');
337 SELECT a.agent_uuid, a.hostname, a.status
339 WHERE a.agent_uuid = $1
342 let queryParams = [agentUuid];
345 query += ` AND ${tenantClause}`;
346 queryParams.push(...tenantParams);
349 const agentResult = await pool.query(query, queryParams);
351 if (agentResult.rows.length === 0) {
352 return res.status(404).json({ error: 'Agent not found' });
355 // Get enabled plugins
356 const pluginsResult = await pool.query('SELECT plugin_name FROM allowed_plugins');
357 const enabledPlugins = pluginsResult.rows.map(row => row.plugin_name);
359 // Check WebSocket connection
360 const ws = wsManager.agentConnections.get(agentUuid);
361 const wsConnected = ws && ws.readyState === 1;
365 hostname: agentResult.rows[0].hostname,
366 agentStatus: agentResult.rows[0].status,
367 websocketConnected: wsConnected,
369 connectionMethod: wsConnected ? 'websocket' : 'api_polling'
373 console.error('[Plugins] Error getting plugin status:', error);
374 res.status(500).json({ error: 'Failed to get plugin status', details: error.message });