EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
agentManifest.js
Go to the documentation of this file.
1/**
2 * @file agentManifest.js
3 * @module routes/agentManifest
4 * @description Agent distribution manifest and file download endpoints for auto-updates.
5 * Generates SHA-256 file manifests for agent version distribution and serves downloadable files.
6 * Supports plugin auto-sync and agent executable updates.
7 * @requires express
8 * @requires fs
9 * @requires path
10 * @requires crypto
11 * @author RMM-PSA Development Team
12 * @copyright 2026 RMM-PSA Platform
13 * @license Proprietary
14 */
15
16/**
17 * @apiDefine AgentManifest Agent Distribution
18 * Agent version manifest and file downloads for auto-updates
19 */
20
21const express = require('express');
22const router = express.Router();
23const fs = require('fs');
24const path = require('path');
25const crypto = require('crypto');
26
27const DIST_PATH = '/opt/apps/IBG_HUB_dist/agent';
28
29/**
30 * @api {get} /api/agent/manifest Get agent file manifest
31 * @apiName GetAgentManifest
32 * @apiGroup AgentManifest
33 * @apiDescription Retrieve file manifest for latest agent version. Returns SHA-256 hashes,
34 * file sizes, and modification times for all files in agent distribution (plugins, executables).
35 * Used by agents to detect changes and sync files. Manifest is cached and regenerated on demand.
36 * @apiSuccess {string} version Latest agent version (e.g., "1.0.5")
37 * @apiSuccess {DateTime} generated Manifest generation timestamp
38 * @apiSuccess {object} files File manifest dictionary (path → metadata)
39 * @apiSuccess {string} files.path File relative path (key)
40 * @apiSuccess {object} files.metadata File metadata (value)
41 * @apiSuccess {string} files.metadata.hash SHA-256 hash of file content
42 * @apiSuccess {number} files.metadata.size File size in bytes
43 * @apiSuccess {DateTime} files.metadata.modified Last modified timestamp
44 * @apiError (404) {String} error="No version available" latest_version.txt not found
45 * @apiError (404) {String} error="Version directory not found" Version folder missing
46 * @apiError (500) {String} error Error message from manifest generation
47 * @apiExample {curl} Example:
48 * curl -X GET http://localhost:3000/api/agent/manifest
49 * @apiSuccessExample {json} Success-Response:
50 * HTTP/1.1 200 OK
51 * {
52 * "version": "1.0.5",
53 * "generated": "2026-03-12T10:00:00.000Z",
54 * "files": {
55 * "agent.exe": {
56 * "hash": "a3f2c1b...",
57 * "size": 8388608,
58 * "modified": "2026-03-10T08:00:00.000Z"
59 * },
60 * "plugins/rustdesk.js": {
61 * "hash": "b4e3d2c...",
62 * "size": 2048,
63 * "modified": "2026-03-11T12:30:00.000Z"
64 * }
65 * }
66 * }
67 */
68router.get('/manifest', async (req, res) => {
69 try {
70 console.log('[Manifest] GET /api/agent/manifest called');
71 // Get latest version
72 const versionFile = path.join(DIST_PATH, 'latest_version.txt');
73 if (!fs.existsSync(versionFile)) {
74 console.error('[Manifest] latest_version.txt not found');
75 return res.status(404).json({ error: 'No version available' });
76 }
77
78 const version = fs.readFileSync(versionFile, 'utf8').trim();
79 const versionDir = path.join(DIST_PATH, version);
80
81 if (!fs.existsSync(versionDir)) {
82 return res.status(404).json({ error: 'Version directory not found' });
83 }
84
85 // Check for cached manifest
86 const manifestPath = path.join(versionDir, 'file-manifest.json');
87 if (fs.existsSync(manifestPath)) {
88 const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
89 return res.json(manifest);
90 }
91
92 // Generate manifest on the fly if not cached
93 console.log('[Manifest] Generating manifest for version:', version);
94 const manifest = await generateManifest(versionDir, version);
95
96 // Cache it
97 fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
98
99 res.json(manifest);
100 } catch (err) {
101 console.error('[Manifest] Error:', err);
102 res.status(500).json({ error: err.message });
103 }
104});
105
106/**
107 * @api {get} /api/agent/file Download agent file
108 * @apiName DownloadAgentFile
109 * @apiGroup AgentManifest
110 * @apiDescription Download specific file from agent distribution directory.
111 * File path provided via query parameter. Includes directory traversal protection
112 * and real path validation to prevent unauthorized file access. Used by agents
113 * to download updated plugins and executables during auto-update process.
114 * @apiParam {string} path Relative file path (query parameter, e.g., "plugins/rustdesk.js")
115 * @apiSuccess {File} file Binary file download with attachment disposition
116 * @apiError (400) {String} error="Invalid file path" Path contains ".." or starts with "/"
117 * @apiError (404) {String} error="No version available" latest_version.txt not found
118 * @apiError (404) {String} error="File not found" Requested file does not exist
119 * @apiError (403) {String} error="Access denied" File path outside version directory (security)
120 * @apiError (500) {String} error Error message from file system operations
121 * @apiExample {curl} Example:
122 * curl -X GET "http://localhost:3000/api/agent/file?path=plugins/rustdesk.js" \\
123 * -o rustdesk.js
124 * @apiSuccessExample {binary} Success-Response:
125 * HTTP/1.1 200 OK
126 * Content-Type: application/javascript
127 * Content-Disposition: attachment; filename="rustdesk.js"
128 *
129 * [binary file content]
130 */
131router.get('/file', async (req, res) => {
132 try {
133 // Get path from query parameter
134 const relativePath = decodeURIComponent(req.query.path || '');
135
136 // Security: prevent directory traversal
137 if (relativePath.includes('..') || relativePath.startsWith('/')) {
138 return res.status(400).json({ error: 'Invalid file path' });
139 }
140
141 // Get latest version
142 const versionFile = path.join(DIST_PATH, 'latest_version.txt');
143 if (!fs.existsSync(versionFile)) {
144 return res.status(404).json({ error: 'No version available' });
145 }
146
147 const version = fs.readFileSync(versionFile, 'utf8').trim();
148 const filePath = path.join(DIST_PATH, version, relativePath);
149
150 if (!fs.existsSync(filePath)) {
151 return res.status(404).json({ error: 'File not found' });
152 }
153
154 // Security: ensure file is within version directory
155 const realPath = fs.realpathSync(filePath);
156 const realVersionDir = fs.realpathSync(path.join(DIST_PATH, version));
157 if (!realPath.startsWith(realVersionDir)) {
158 return res.status(403).json({ error: 'Access denied' });
159 }
160
161 // Send file
162 const filename = path.basename(filePath);
163 res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
164 res.sendFile(realPath);
165 } catch (err) {
166 console.error('[Manifest] File download error:', err);
167 res.status(500).json({ error: err.message });
168 }
169});
170
171/**
172 * Generate manifest by scanning directory
173 * @param versionDir
174 * @param version
175 */
176async function generateManifest(versionDir, version) {
177 const manifest = {
178 version: version,
179 generated: new Date().toISOString(),
180 files: {}
181 };
182
183 // Files/directories to include in manifest
184 const includePaths = [
185 'plugins', // All plugin files
186 // Add other directories as needed
187 ];
188
189 // Include specific files
190 const includeFiles = [
191 'EverydayTechTray.exe', // Tray icon executable
192 'agent.exe' // Main agent executable
193 ];
194
195 for (const includePath of includePaths) {
196 const fullPath = path.join(versionDir, includePath);
197 if (fs.existsSync(fullPath)) {
198 scanDirectory(fullPath, versionDir, manifest.files);
199 }
200 }
201
202 // Add specific files to manifest
203 for (const includeFile of includeFiles) {
204 const fullPath = path.join(versionDir, includeFile);
205 if (fs.existsSync(fullPath)) {
206 const stats = fs.statSync(fullPath);
207 const hash = getFileHash(fullPath);
208
209 manifest.files[includeFile] = {
210 hash: hash,
211 size: stats.size,
212 modified: stats.mtime.toISOString()
213 };
214 }
215 }
216
217 return manifest;
218}
219
220/**
221 * Recursively scan directory and add files to manifest
222 * @param dirPath
223 * @param basePath
224 * @param files
225 */
226function scanDirectory(dirPath, basePath, files) {
227 const entries = fs.readdirSync(dirPath, { withFileTypes: true });
228
229 for (const entry of entries) {
230 const fullPath = path.join(dirPath, entry.name);
231
232 if (entry.isDirectory()) {
233 scanDirectory(fullPath, basePath, files);
234 } else if (entry.isFile()) {
235 const relativePath = path.relative(basePath, fullPath).replace(/\\/g, '/');
236 const stats = fs.statSync(fullPath);
237 const hash = getFileHash(fullPath);
238
239 files[relativePath] = {
240 hash: hash,
241 size: stats.size,
242 modified: stats.mtime.toISOString()
243 };
244 }
245 }
246}
247
248/**
249 * Calculate SHA-256 hash of file
250 * @param filePath
251 */
252function getFileHash(filePath) {
253 const hash = crypto.createHash('sha256');
254 const data = fs.readFileSync(filePath);
255 hash.update(data);
256 return hash.digest('hex');
257}
258
259module.exports = router;