/** * @file server.js * @brief Node.js Chunk Transfer Server */ const http = require('http'); const fs = require('fs'); const path = require('path'); const EventEmitter = require('events'); class ChunkServer extends EventEmitter { constructor(config) { super(); this.config = { port: 8080, rootPath: '.', chunkSize: 1024, ...config }; this.server = null; this.running = false; } /** * Calculate CRC32 (IEEE 802.3) */ static crc32(data) { const table = ChunkServer.crc32Table; let crc = 0xFFFFFFFF; for (let i = 0; i < data.length; i++) { const index = (crc ^ data[i]) & 0xFF; crc = (crc >>> 8) ^ table[index]; } return crc ^ 0xFFFFFFFF; } /** * CRC32 lookup table */ static get crc32Table() { if (!ChunkServer._crc32Table) { const table = new Uint32Array(256); const poly = 0xEDB88320; for (let i = 0; i < 256; i++) { let crc = i; for (let j = 0; j < 8; j++) { crc = (crc & 1) ? ((crc >>> 1) ^ poly) : (crc >>> 1); } table[i] = crc >>> 0; } ChunkServer._crc32Table = table; } return ChunkServer._crc32Table; } /** * Start server */ start() { return new Promise((resolve, reject) => { this.server = http.createServer((req, res) => { this.handleRequest(req, res).catch(err => { console.error('Request error:', err); res.writeHead(500); res.end('Internal Server Error'); }); }); this.server.listen(this.config.port, () => { console.log(`Chunk server running on port ${this.config.port}`); console.log(`Root path: ${this.config.rootPath}`); console.log(`Chunk size: ${this.config.chunkSize} bytes`); this.running = true; resolve(); }); this.server.on('error', reject); }); } /** * Stop server */ stop() { return new Promise((resolve, reject) => { if (!this.server) { resolve(); return; } this.running = false; this.server.close(err => { if (err) reject(err); else { console.log('Server stopped'); resolve(); } }); }); } /** * Handle HTTP request */ async handleRequest(req, res) { // Parse URL const url = new URL(req.url, `http://${req.headers.host}`); // Only handle /ota endpoint if (url.pathname !== '/ota') { res.writeHead(404); res.end('Not Found'); return; } const filename = url.searchParams.get('file'); const chunkParam = url.searchParams.get('chunk'); if (!filename) { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Missing file parameter'); return; } // Security: Prevent path traversal const safeFilename = path.basename(filename); const filePath = path.join(this.config.rootPath, safeFilename); // Check file exists let stat; try { stat = fs.statSync(filePath); if (!stat.isFile()) { res.writeHead(404); res.end('File not found'); return; } } catch (err) { res.writeHead(404); res.end('File not found'); return; } const fileSize = stat.size; const totalChunks = Math.ceil(fileSize / this.config.chunkSize); // Calculate chunk index let chunkIdx; if (chunkParam !== null) { chunkIdx = parseInt(chunkParam, 10); if (isNaN(chunkIdx) || chunkIdx < 0 || chunkIdx >= totalChunks) { res.writeHead(416, { 'Content-Type': 'text/plain', 'Content-Range': `chunks 0-${totalChunks-1}/${totalChunks}` }); res.end('Range Not Satisfiable'); return; } } else { chunkIdx = 0; } // Read chunk data const offset = chunkIdx * this.config.chunkSize; const length = Math.min(this.config.chunkSize, fileSize - offset); const buffer = Buffer.alloc(length); const fd = fs.openSync(filePath, 'r'); try { fs.readSync(fd, buffer, 0, length, offset); } finally { fs.closeSync(fd); } // Calculate CRC32 const crc32 = ChunkServer.crc32(buffer); // Build response: [chunk_idx: 2B][payload: N B][crc32: 4B] const responseBuffer = Buffer.alloc(2 + length + 4); responseBuffer.writeUInt16LE(chunkIdx, 0); buffer.copy(responseBuffer, 2); responseBuffer.writeUInt32LE(crc32 >>> 0, 2 + length); // Ensure unsigned // Set headers const headers = { 'Content-Type': 'application/octet-stream', 'Content-Length': responseBuffer.length, 'X-Chunk-Index': chunkIdx, 'X-Chunk-Size': length, 'X-Total-Chunks': totalChunks, 'X-File-Size': fileSize }; // Response status if (chunkIdx > 0) { res.writeHead(206, headers); // Partial Content for resumed chunks } else { res.writeHead(200, headers); // OK for first chunk } res.end(responseBuffer); // Emit event this.emit('chunk-sent', { file: filename, chunkIdx: chunkIdx, chunkSize: length, totalChunks: totalChunks, fileSize: fileSize }); } } // Export module.exports = ChunkServer; // CLI usage if (require.main === module) { const port = process.argv[2] || 8080; const rootPath = process.argv[3] || '.'; const server = new ChunkServer({ port: parseInt(port), rootPath }); server.on('chunk-sent', (info) => { console.log(`Sent chunk ${info.chunkIdx}/${info.totalChunks} for ${info.file} (${info.chunkSize}B)`); }); server.start().then(() => { console.log(`Server started on port ${port}`); console.log(`Root path: ${rootPath}`); }).catch(err => { console.error('Failed to start server:', err); process.exit(1); }); process.on('SIGINT', async () => { await server.stop(); process.exit(0); }); }