UNPKG

@kadi.build/local-remote-file-manager-ability

Version:

Local & Remote File Management System with S3-compatible container registry, HTTP server provider, file streaming, and comprehensive testing suite

1,693 lines (1,441 loc) 62.4 kB
import { createServer } from 'http'; import path from 'path'; import crypto from 'crypto'; import EventEmitter from 'events'; import { promises as fs } from 'fs'; import fsSync from 'fs'; /** * HTTP Server Provider * * Provides generic HTTP server capabilities for serving files and handling routes. * Integrates with the existing tunnel infrastructure for public access. * * Features: * - Multiple server management * - Custom route registration * - Middleware support * - Static directory serving * - Tunnel integration * - Range request support */ class HttpServerProvider extends EventEmitter { constructor(config) { super(); this.config = config || {}; // Server management this.servers = new Map(); // serverId -> server info this.routes = new Map(); // serverId -> routes array this.middleware = new Map(); // serverId -> middleware array this.staticPaths = new Map(); // serverId -> static path config this.serverCount = 0; // Configuration options this.maxServers = this.config.maxServers || 10; this.defaultPort = this.config.defaultPort || 8000; this.requestTimeout = this.config.requestTimeout || 30000; // 30 seconds // Security and performance settings this.maxRequestSize = this.config.maxRequestSize || '100mb'; this.enableCORS = this.config.enableCORS || false; this.rateLimiting = this.config.rateLimiting || false; // Static file serving options this.staticFileOptions = { dotfiles: 'ignore', etag: true, extensions: false, index: false, maxAge: '1d', redirect: false, ...this.config.staticFileOptions }; // Tunnel integration (will be set by LocalRemoteManager) this.tunnelProvider = null; console.log('🌐 HTTP Server Provider initialized'); } // ============================================================================ // SERVER LIFECYCLE MANAGEMENT // ============================================================================ /** * Create a new HTTP server * @param {number} port - Port to bind to (0 for dynamic) * @param {string} rootDirectory - Root directory for serving files * @param {object} options - Server options * @returns {Promise<object>} Server information */ async createHttpServer(port = 0, rootDirectory = process.cwd(), options = {}) { if (this.servers.size >= this.maxServers) { throw new Error(`Maximum number of servers (${this.maxServers}) reached`); } const serverId = this.generateServerId(); try { // Validate root directory const stats = await fs.stat(rootDirectory); if (!stats.isDirectory()) { throw new Error(`Root directory '${rootDirectory}' is not a valid directory`); } // Create server instance const server = createServer((req, res) => { // Track connections this.trackConnection(serverId, true); // Handle connection close res.on('close', () => { this.trackConnection(serverId, false); }); // Set request timeout if (options.requestTimeout || this.requestTimeout) { req.setTimeout(options.requestTimeout || this.requestTimeout, () => { if (!res.headersSent) { res.statusCode = 408; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Request Timeout' })); } }); } this.handleRequest(serverId, req, res); }); // Configure server server.timeout = this.requestTimeout; server.keepAliveTimeout = 5000; server.headersTimeout = 60000; // Start server const actualPort = await this.startServer(server, port); // Store server information const serverInfo = { serverId, server, config: { port: actualPort, bindAddress: options.bindAddress || '0.0.0.0', maxConnections: options.maxConnections, maxRequestSize: options.maxRequestSize, requestTimeout: options.requestTimeout, security: options.security || {} }, rootDirectory: path.resolve(rootDirectory), options, status: 'running', createdAt: new Date(), startTime: new Date(), lastActivity: new Date(), requests: 0, errors: 0, activeConnections: 0, totalConnections: 0, peakConnections: 0, tunnelId: null, tunnelUrl: null }; this.servers.set(serverId, serverInfo); this.routes.set(serverId, []); this.middleware.set(serverId, []); this.staticPaths.set(serverId, { localDirectory: rootDirectory, options: { directoryListing: false, ...options.staticOptions } }); console.log(`✅ HTTP server created: ${serverId} on port ${actualPort}`); console.log(` Root directory: ${rootDirectory}`); this.emit('serverCreated', { serverId, port: actualPort, rootDirectory, timestamp: new Date() }); return serverId; } catch (error) { console.error(`❌ Failed to create HTTP server: ${error.message}`); throw new Error(`Failed to create HTTP server: ${error.message}`); } } /** * Start server on specified port * @private */ async startServer(server, port) { return new Promise((resolve, reject) => { server.listen(port, 'localhost', () => { const actualPort = server.address().port; resolve(actualPort); }); server.on('error', (error) => { if (error.code === 'EADDRINUSE') { reject(new Error(`Port ${port} is already in use`)); } else { reject(new Error(`Server error: ${error.message}`)); } }); }); } /** * Stop a specific server * @param {string} serverId - Server ID to stop */ async stopServer(serverId) { const serverInfo = this.servers.get(serverId); if (!serverInfo) { throw new Error(`Server '${serverId}' not found`); } // Stop tunnel if it exists if (serverInfo.tunnelId || serverInfo.tunnelStatus) { try { await this.stopTunnel(serverId); } catch (error) { console.error(`Failed to stop tunnel for ${serverId}:`, error.message); // Continue with server shutdown even if tunnel cleanup fails } } return new Promise((resolve) => { console.log(`🔄 Stopping server: ${serverId}`); // Update server status serverInfo.status = 'stopping'; serverInfo.server.close(() => { // Clean up server data this.servers.delete(serverId); this.routes.delete(serverId); this.middleware.delete(serverId); this.staticPaths.delete(serverId); console.log(`🔴 Server stopped: ${serverId}`); this.emit('serverStopped', { serverId, timestamp: new Date() }); resolve(); }); }); } /** * Stop all servers */ async stopAllServers() { const serverIds = Array.from(this.servers.keys()); const stopPromises = serverIds.map(serverId => this.stopServer(serverId)); await Promise.all(stopPromises); console.log(`🔴 All HTTP servers stopped (${serverIds.length} servers)`); this.emit('allServersStopped', { serverCount: serverIds.length, timestamp: new Date() }); } /** * Get health status of a server * @param {string} serverId - Server ID */ getServerHealth(serverId) { const serverInfo = this.servers.get(serverId); if (!serverInfo) { return { status: 'not_found' }; } const uptime = Date.now() - serverInfo.createdAt.getTime(); const timeSinceActivity = Date.now() - serverInfo.lastActivity.getTime(); let health = 'healthy'; if (timeSinceActivity > 300000) { // 5 minutes health = 'stale'; } if (serverInfo.status !== 'running') { health = 'unhealthy'; } return { status: health, uptime, timeSinceActivity, requestRate: serverInfo.requests / (uptime / 1000), // requests per second errorRate: serverInfo.errors / Math.max(serverInfo.requests, 1) }; } /** * Perform health check on all servers */ async performHealthCheck() { const healthReport = { timestamp: new Date(), totalServers: this.servers.size, healthy: 0, stale: 0, unhealthy: 0, servers: {} }; for (const serverId of this.servers.keys()) { const health = this.getServerHealth(serverId); healthReport.servers[serverId] = health; healthReport[health.status]++; } this.emit('healthCheck', healthReport); return healthReport; } // ============================================================================ // ROUTE REGISTRATION SYSTEM // ============================================================================ /** * Add a custom route to a server * @param {string} serverId - Server ID * @param {string} method - HTTP method (GET, POST, etc.) * @param {string} path - Route path (supports parameters like /:bucket/:key) * @param {function} handler - Route handler function * @param {Object} options - Route options (priority, etc.) * @returns {string} Route ID */ async addCustomRoute(serverId, method, path, handler, options = {}) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } if (typeof handler !== 'function') { throw new Error('Route handler must be a function'); } const routeId = this.generateRouteId(); const routes = this.routes.get(serverId); // Check for route conflicts const existingRoute = routes.find(r => r.method === method.toUpperCase() && this.pathsMatch(r.path, path) ); if (existingRoute) { console.warn(`⚠️ Route conflict detected: ${method} ${path} (overriding existing route)`); } const route = { routeId, method: method.toUpperCase(), path, handler, pathRegex: this.createPathRegex(path), paramNames: this.extractParamNames(path), priority: options.priority || 0, // Higher numbers = higher priority createdAt: new Date(), requestCount: 0, errorCount: 0 }; routes.push(route); // Sort routes by priority (descending) to ensure high-priority routes are checked first routes.sort((a, b) => b.priority - a.priority); const priorityInfo = options.priority ? ` (priority: ${options.priority})` : ''; console.log(`➕ Route added: ${method.toUpperCase()} ${path} -> ${serverId}${priorityInfo}`); this.emit('routeAdded', { serverId, routeId, method: method.toUpperCase(), path, timestamp: new Date() }); return routeId; } /** * Remove a route from a server * @param {string} serverId - Server ID * @param {string} routeId - Route ID to remove */ async removeRoute(serverId, routeId) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } const routes = this.routes.get(serverId); const routeIndex = routes.findIndex(r => r.routeId === routeId); if (routeIndex === -1) { throw new Error(`Route '${routeId}' not found`); } const removedRoute = routes.splice(routeIndex, 1)[0]; console.log(`➖ Route removed: ${removedRoute.method} ${removedRoute.path}`); this.emit('routeRemoved', { serverId, routeId, method: removedRoute.method, path: removedRoute.path, timestamp: new Date() }); } /** * List all routes for a server * @param {string} serverId - Server ID */ listRoutes(serverId) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } const routes = this.routes.get(serverId); return routes.map(route => ({ routeId: route.routeId, method: route.method, path: route.path, requestCount: route.requestCount, errorCount: route.errorCount, createdAt: route.createdAt })); } /** * Find matching route for a request * @private */ findMatchingRoute(serverId, method, url) { const routes = this.routes.get(serverId); if (!routes) return null; for (const route of routes) { if (route.method !== method.toUpperCase()) continue; const match = url.match(route.pathRegex); if (match) { // Extract parameters const params = {}; route.paramNames.forEach((paramName, index) => { params[paramName] = match[index + 1]; }); return { route, params }; } } return null; } /** * Check for route conflicts * @private */ checkRouteConflicts(serverId, method, path) { const routes = this.routes.get(serverId); const conflicts = routes.filter(r => r.method === method.toUpperCase() && this.pathsMatch(r.path, path) ); return conflicts; } // ============================================================================ // MIDDLEWARE SUPPORT // ============================================================================ /** * Add authentication middleware to a server * @param {string} serverId - Server ID * @param {function} authFunction - Authentication function */ async addAuthMiddleware(serverId, authFunction) { return this.addMiddleware(serverId, 'authentication', async (req, res, next) => { try { const result = await authFunction(req, res, next); if (result === false) { res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Unauthorized'); return; } // If result is not false, the auth function should have called next() itself } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Authentication error'); } }); } /** * Add middleware to a server * @param {string} serverId - Server ID * @param {string} name - Middleware name * @param {function} handler - Middleware function * @param {object} options - Middleware options * @returns {string} Middleware ID */ async addMiddleware(serverId, name, handler, options = {}) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } if (typeof handler !== 'function') { throw new Error('Middleware handler must be a function'); } const middlewareId = this.generateMiddlewareId(); const middlewares = this.middleware.get(serverId); const middleware = { middlewareId, name, handler, priority: options.priority || 0, enabled: options.enabled !== false, path: options.path || '/', methods: options.methods || ['*'], createdAt: new Date(), executionCount: 0, errorCount: 0, totalDuration: 0 }; middlewares.push(middleware); // Sort by priority (higher priority executes first) middlewares.sort((a, b) => b.priority - a.priority); console.log(`🔧 Middleware added: ${name} (priority: ${middleware.priority}) -> ${serverId}`); this.emit('middlewareAdded', { serverId, middlewareId, name, priority: middleware.priority, timestamp: new Date() }); return middlewareId; } /** * Remove middleware from a server * @param {string} serverId - Server ID * @param {string} middlewareId - Middleware ID to remove */ async removeMiddleware(serverId, middlewareId) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } const middlewares = this.middleware.get(serverId); const middlewareIndex = middlewares.findIndex(m => m.middlewareId === middlewareId); if (middlewareIndex === -1) { throw new Error(`Middleware '${middlewareId}' not found`); } const removedMiddleware = middlewares.splice(middlewareIndex, 1)[0]; console.log(`🔧 Middleware removed: ${removedMiddleware.name}`); this.emit('middlewareRemoved', { serverId, middlewareId, name: removedMiddleware.name, timestamp: new Date() }); } /** * Enable/disable middleware * @param {string} serverId - Server ID * @param {string} middlewareId - Middleware ID * @param {boolean} enabled - Enable/disable state */ async toggleMiddleware(serverId, middlewareId, enabled) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } const middlewares = this.middleware.get(serverId); const middleware = middlewares.find(m => m.middlewareId === middlewareId); if (!middleware) { throw new Error(`Middleware '${middlewareId}' not found`); } middleware.enabled = enabled; console.log(`🔧 Middleware ${enabled ? 'enabled' : 'disabled'}: ${middleware.name}`); this.emit('middlewareToggled', { serverId, middlewareId, name: middleware.name, enabled, timestamp: new Date() }); } /** * List all middleware for a server * @param {string} serverId - Server ID */ listMiddleware(serverId) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } const middlewares = this.middleware.get(serverId); return middlewares.map(middleware => ({ middlewareId: middleware.middlewareId, name: middleware.name, priority: middleware.priority, enabled: middleware.enabled, path: middleware.path, methods: middleware.methods, executionCount: middleware.executionCount, errorCount: middleware.errorCount, averageDuration: middleware.executionCount > 0 ? (middleware.totalDuration / middleware.executionCount).toFixed(2) : 0, createdAt: middleware.createdAt })); } /** * Get middleware statistics * @param {string} serverId - Server ID */ getMiddlewareStats(serverId) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } const middlewares = this.middleware.get(serverId); return { total: middlewares.length, enabled: middlewares.filter(m => m.enabled).length, disabled: middlewares.filter(m => !m.enabled).length, totalExecutions: middlewares.reduce((sum, m) => sum + m.executionCount, 0), totalErrors: middlewares.reduce((sum, m) => sum + m.errorCount, 0), averageDuration: middlewares.length > 0 ? middlewares.reduce((sum, m) => sum + (m.totalDuration / Math.max(m.executionCount, 1)), 0) / middlewares.length : 0 }; } // ============================================================================ // STATIC DIRECTORY SERVING // ============================================================================ /** * Serve static files from a directory * @param {string} serverId - Server ID * @param {string} urlPath - URL path prefix * @param {string} localDirectory - Local directory to serve */ async serveStaticDirectory(serverId, urlPath, localDirectory) { // Validate directory exists const stats = await fs.stat(localDirectory); if (!stats.isDirectory()) { throw new Error(`Directory '${localDirectory}' not found`); } const resolvedDirectory = path.resolve(localDirectory); return this.addCustomRoute(serverId, 'GET', `${urlPath}/*`, async (req, res, params) => { const relativePath = params['*'] || ''; const filePath = path.join(resolvedDirectory, relativePath); // Security: prevent directory traversal if (!filePath.startsWith(resolvedDirectory)) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden: Path traversal detected'); return; } try { const fileStats = await fs.stat(filePath); if (fileStats.isFile()) { await this.serveFile(filePath, req, res); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } } catch (error) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); } // ============================================================================ // UTILITY METHODS // ============================================================================ generateServerId() { return `http_server_${++this.serverCount}_${crypto.randomBytes(4).toString('hex')}`; } generateRouteId() { return `route_${crypto.randomBytes(6).toString('hex')}`; } generateMiddlewareId() { return `middleware_${crypto.randomBytes(4).toString('hex')}`; } createPathRegex(path) { // Convert Express-style paths to regex const regexPath = path .replace(/:[^/]+/g, '([^/]+)') // :param -> capture group .replace(/\*/g, '(.*)'); // * -> capture everything return new RegExp(`^${regexPath}$`); } extractParamNames(path) { const params = []; const matches = path.match(/:([^/]+)/g); if (matches) { matches.forEach(match => { params.push(match.substring(1)); // Remove ':' }); } // Handle wildcard if (path.includes('*')) { params.push('*'); } return params; } pathsMatch(path1, path2) { return this.createPathRegex(path1).test(path2) || this.createPathRegex(path2).test(path1); } /** * Get server status information * @param {string} serverId - Server ID */ getServerStatus(serverId) { const serverInfo = this.servers.get(serverId); if (!serverInfo) { throw new Error(`Server '${serverId}' not found`); } const routes = this.routes.get(serverId) || []; const middlewares = this.middleware.get(serverId) || []; return { serverId, port: serverInfo.config.port, rootDirectory: serverInfo.rootDirectory, status: serverInfo.status, isActive: serverInfo.status === 'running', uptime: Date.now() - serverInfo.createdAt.getTime(), requests: serverInfo.requests, errors: serverInfo.errors, lastActivity: serverInfo.lastActivity, routeCount: routes.length, middlewareCount: middlewares.length, tunnelId: serverInfo.tunnelId, tunnelUrl: serverInfo.tunnelUrl, tunnelStatus: serverInfo.tunnelStatus, tunnelService: serverInfo.tunnelService, tunnelConfig: serverInfo.tunnelConfig }; } // Placeholder for request handling (will be enhanced) /** * Handle incoming HTTP requests * @private */ async handleRequest(serverId, req, res) { const startTime = Date.now(); const requestId = this.generateRequestId(); const serverInfo = this.servers.get(serverId); if (!serverInfo) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Server configuration error'); return; } // Update statistics serverInfo.requests++; serverInfo.lastActivity = new Date(); try { // Set default headers res.setHeader('X-Powered-By', 'Local-Remote-File-Manager'); res.setHeader('X-Request-ID', requestId); // Log incoming request console.log(`� ${req.method} ${req.url} [${requestId}]`); // Parse URL const urlParts = new URL(req.url, `http://${req.headers.host}`); const pathname = urlParts.pathname; // Execute middleware await this.executeMiddleware(serverId, req, res); // Check if response was already sent by middleware if (res.headersSent) { return; } // Find matching route const match = this.findMatchingRoute(serverId, req.method, pathname); if (match) { // Add route parameters to request req.params = match.params; req.query = Object.fromEntries(urlParts.searchParams); req.requestId = requestId; // Update route statistics match.route.requestCount++; try { // Execute route handler await match.route.handler(req, res); const duration = Date.now() - startTime; console.log(`✅ ${req.method} ${req.url} [${requestId}] - ${res.statusCode} (${duration}ms)`); } catch (routeError) { match.route.errorCount++; throw routeError; } } else if (this.staticPaths.has(serverId)) { // Try to serve static files await this.serveStaticFile(serverId, pathname, req, res); const duration = Date.now() - startTime; console.log(`📁 ${req.method} ${req.url} [${requestId}] - ${res.statusCode} (${duration}ms)`); } else { // No route found res.statusCode = 404; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Not Found', message: `No route found for ${req.method} ${pathname}`, requestId })); const duration = Date.now() - startTime; console.log(`❌ ${req.method} ${req.url} [${requestId}] - 404 (${duration}ms)`); } } catch (error) { const duration = Date.now() - startTime; console.error(`💥 ${req.method} ${req.url} [${requestId}] - Error (${duration}ms):`, error.message); if (!res.headersSent) { this.createErrorResponse(error, req, res, requestId); } this.emit('requestError', { serverId, requestId, method: req.method, url: req.url, error: error.message, duration, timestamp: new Date() }); } } /** * Execute middleware chain for a request * @private */ async executeMiddleware(serverId, req, res) { const middlewares = this.middleware.get(serverId); if (!middlewares || middlewares.length === 0) { return; } // Filter enabled middleware that matches the request const applicableMiddleware = middlewares.filter(middleware => { if (!middleware.enabled) return false; // Check path matching if (middleware.path !== '/' && !req.url.startsWith(middleware.path)) { return false; } // Check method matching if (!middleware.methods.includes('*') && !middleware.methods.includes(req.method)) { return false; } return true; }); let index = 0; const next = async (error) => { if (error) { throw error; } if (index >= applicableMiddleware.length) { return; } const middleware = applicableMiddleware[index++]; const startTime = Date.now(); try { middleware.executionCount++; await middleware.handler(req, res, next); const duration = Date.now() - startTime; middleware.totalDuration += duration; } catch (middlewareError) { const duration = Date.now() - startTime; middleware.totalDuration += duration; middleware.errorCount++; console.error(`Middleware error in '${middleware.name}':`, middlewareError.message); throw middlewareError; } }; await next(); } /** * Generate unique request ID * @private */ generateRequestId() { return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // ============================================================================ // STATIC FILE SERVING // ============================================================================ /** * Serve a static file * @private */ async serveStaticFile(serverId, pathname, req, res) { const staticPath = this.staticPaths.get(serverId); if (!staticPath) { res.statusCode = 404; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Static serving not configured' })); return; } try { // Security check - prevent directory traversal const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, ''); const fullPath = path.join(staticPath.localDirectory, safePath); // Ensure the file is within the static directory if (!fullPath.startsWith(path.resolve(staticPath.localDirectory))) { res.statusCode = 403; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Access denied' })); return; } const stats = await fs.stat(fullPath); if (stats.isDirectory()) { // Try to serve index.html if it exists const indexPath = path.join(fullPath, 'index.html'); try { const indexStats = await fs.stat(indexPath); if (indexStats.isFile()) { await this.serveFile(indexPath, req, res); return; } } catch (indexError) { // Index file doesn't exist, serve directory listing if enabled if (staticPath.options.directoryListing) { await this.serveDirectoryListing(fullPath, pathname, res); return; } } res.statusCode = 403; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Directory access forbidden' })); return; } if (stats.isFile()) { await this.serveFile(fullPath, req, res); } else { res.statusCode = 404; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'File not found' })); } } catch (error) { if (error.code === 'ENOENT') { res.statusCode = 404; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'File not found' })); } else { res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Server error', message: error.message })); } } } /** * Serve a file with proper headers and range support * @private */ async serveFile(filePath, req, res) { const stats = await fs.stat(filePath); const mimeType = this.getMimeType(filePath); // Set basic headers res.setHeader('Content-Type', mimeType); res.setHeader('Accept-Ranges', 'bytes'); res.setHeader('Last-Modified', stats.mtime.toUTCString()); res.setHeader('ETag', `"${stats.size}-${stats.mtime.getTime()}"`); // Handle conditional requests const ifModifiedSince = req.headers['if-modified-since']; const ifNoneMatch = req.headers['if-none-match']; if (ifModifiedSince && new Date(ifModifiedSince) >= stats.mtime) { res.statusCode = 304; res.end(); return; } if (ifNoneMatch && ifNoneMatch === res.getHeader('ETag')) { res.statusCode = 304; res.end(); return; } // Handle range requests const range = req.headers.range; if (range) { await this.serveFileRange(filePath, range, stats, res); } else { // Serve entire file res.setHeader('Content-Length', stats.size); res.statusCode = 200; const readStream = fsSync.createReadStream(filePath); readStream.pipe(res); readStream.on('error', (error) => { console.error('File stream error:', error); if (!res.headersSent) { res.statusCode = 500; res.end(); } }); } } /** * Serve file range for partial content requests * @private */ async serveFileRange(filePath, range, stats, res) { const ranges = this.parseRangeHeader(range, stats.size); if (!ranges || ranges.length === 0) { res.statusCode = 416; // Range Not Satisfiable res.setHeader('Content-Range', `bytes */${stats.size}`); res.end(); return; } if (ranges.length === 1) { // Single range const { start, end } = ranges[0]; const contentLength = end - start + 1; res.statusCode = 206; // Partial Content res.setHeader('Content-Range', `bytes ${start}-${end}/${stats.size}`); res.setHeader('Content-Length', contentLength); const readStream = fsSync.createReadStream(filePath, { start, end }); readStream.pipe(res); readStream.on('error', (error) => { console.error('Range stream error:', error); if (!res.headersSent) { res.statusCode = 500; res.end(); } }); } else { // Multiple ranges - not commonly used, but included for completeness res.statusCode = 206; const boundary = `----HttpServerProviderBoundary${Date.now()}`; res.setHeader('Content-Type', `multipart/byteranges; boundary=${boundary}`); // This would require more complex implementation for multiple ranges // For now, just serve the first range const { start, end } = ranges[0]; const contentLength = end - start + 1; res.setHeader('Content-Range', `bytes ${start}-${end}/${stats.size}`); res.setHeader('Content-Length', contentLength); const readStream = fsSync.createReadStream(filePath, { start, end }); readStream.pipe(res); } } /** * Parse HTTP Range header * @private */ parseRangeHeader(range, fileSize) { const ranges = []; const rangeSpec = range.replace(/bytes=/, '').split(','); for (const spec of rangeSpec) { const rangeParts = spec.trim().split('-'); let start = parseInt(rangeParts[0], 10); let end = parseInt(rangeParts[1], 10); if (isNaN(start) && !isNaN(end)) { // Suffix range: -500 (last 500 bytes) start = Math.max(0, fileSize - end); end = fileSize - 1; } else if (!isNaN(start) && isNaN(end)) { // Start range: 500- (from byte 500 to end) end = fileSize - 1; } else if (!isNaN(start) && !isNaN(end)) { // Full range: 500-999 if (start > end || start >= fileSize) { continue; // Invalid range } end = Math.min(end, fileSize - 1); } else { continue; // Invalid range } if (start >= 0 && end >= start && start < fileSize) { ranges.push({ start, end }); } } return ranges; } /** * Serve directory listing * @private */ async serveDirectoryListing(dirPath, urlPath, res) { try { const files = await fs.readdir(dirPath); const fileStats = await Promise.all( files.map(async (file) => { const filePath = path.join(dirPath, file); const stats = await fs.stat(filePath); return { name: file, isDirectory: stats.isDirectory(), size: stats.size, mtime: stats.mtime }; }) ); // Sort: directories first, then files, both alphabetically fileStats.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); const html = this.generateDirectoryListingHtml(urlPath, fileStats); res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.byteLength(html)); res.end(html); } catch (error) { res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Failed to read directory' })); } } /** * Generate HTML for directory listing * @private */ generateDirectoryListingHtml(urlPath, files) { const parentPath = urlPath === '/' ? '' : path.dirname(urlPath); const fileListHtml = files.map(file => { const icon = file.isDirectory ? '📁' : '📄'; const size = file.isDirectory ? '-' : this.formatFileSize(file.size); const date = file.mtime.toLocaleDateString(); const href = path.posix.join(urlPath, file.name); return ` <tr> <td><a href="${href}">${icon} ${file.name}</a></td> <td>${size}</td> <td>${date}</td> </tr> `; }).join(''); const parentLink = urlPath !== '/' ? `<tr><td><a href="${parentPath}">📁 ..</a></td><td>-</td><td>-</td></tr>` : ''; return ` <!DOCTYPE html> <html> <head> <title>Directory listing for ${urlPath}</title> <style> body { font-family: Arial, sans-serif; margin: 40px; } h1 { color: #333; } table { border-collapse: collapse; width: 100%; } th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; } th { background-color: #f2f2f2; } a { text-decoration: none; color: #0066cc; } a:hover { text-decoration: underline; } </style> </head> <body> <h1>Directory listing for ${urlPath}</h1> <table> <thead> <tr> <th>Name</th> <th>Size</th> <th>Date</th> </tr> </thead> <tbody> ${parentLink} ${fileListHtml} </tbody> </table> <hr> <p><em>Powered by Local-Remote-File-Manager</em></p> </body> </html> `; } /** * Get MIME type for a file * @private */ getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); const mimeTypes = { '.html': 'text/html', '.htm': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.txt': 'text/plain', '.md': 'text/markdown', '.pdf': 'application/pdf', '.zip': 'application/zip', '.gz': 'application/gzip', '.tar': 'application/x-tar', '.xml': 'application/xml', '.mp4': 'video/mp4', '.mp3': 'audio/mpeg', '.wav': 'audio/wav' }; return mimeTypes[ext] || 'application/octet-stream'; } /** * Format file size for display * @private */ formatFileSize(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const k = 1024; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i]}`; } // ============================================================================ // ERROR HANDLING & SECURITY // ============================================================================ /** * Create comprehensive error response * @private */ createErrorResponse(error, req, res, requestId) { const errorResponse = { error: error.name || 'Error', message: error.message || 'An error occurred', requestId, timestamp: new Date().toISOString(), path: req.url, method: req.method }; // Add stack trace in development mode if (process.env.NODE_ENV === 'development') { errorResponse.stack = error.stack; } // Set appropriate status code let statusCode = 500; if (error.code === 'ENOENT') statusCode = 404; if (error.code === 'EACCES') statusCode = 403; if (error.code === 'EMFILE' || error.code === 'ENFILE') statusCode = 503; if (error.name === 'ValidationError') statusCode = 400; if (error.name === 'AuthenticationError') statusCode = 401; if (error.name === 'AuthorizationError') statusCode = 403; if (error.name === 'RateLimitError') statusCode = 429; res.statusCode = statusCode; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(errorResponse, null, 2)); return errorResponse; } /** * Validate server creation parameters * @private */ validateServerParams(port, options = {}) { const errors = []; // Validate port if (!Number.isInteger(port) || port < 1 || port > 65535) { errors.push('Port must be an integer between 1 and 65535'); } // Validate bind address if (options.bindAddress && typeof options.bindAddress !== 'string') { errors.push('Bind address must be a string'); } // Validate request size limit if (options.maxRequestSize && (!Number.isInteger(options.maxRequestSize) || options.maxRequestSize < 0)) { errors.push('Max request size must be a positive integer'); } // Validate connection limit if (options.maxConnections && (!Number.isInteger(options.maxConnections) || options.maxConnections < 1)) { errors.push('Max connections must be a positive integer'); } // Validate timeout values if (options.requestTimeout && (!Number.isInteger(options.requestTimeout) || options.requestTimeout < 1000)) { errors.push('Request timeout must be at least 1000ms'); } if (errors.length > 0) { const error = new Error(`Validation failed: ${errors.join(', ')}`); error.name = 'ValidationError'; error.validationErrors = errors; throw error; } } /** * Apply security headers to response * @private */ applySecurityHeaders(res, options = {}) { // Prevent clickjacking res.setHeader('X-Frame-Options', options.frameOptions || 'DENY'); // Prevent MIME type sniffing res.setHeader('X-Content-Type-Options', 'nosniff'); // Enable XSS protection res.setHeader('X-XSS-Protection', '1; mode=block'); // Enforce HTTPS (if configured) if (options.enforceHttps) { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } // Content Security Policy if (options.contentSecurityPolicy) { res.setHeader('Content-Security-Policy', options.contentSecurityPolicy); } // Referrer Policy res.setHeader('Referrer-Policy', options.referrerPolicy || 'strict-origin-when-cross-origin'); // Remove server header res.removeHeader('Server'); } /** * Check request size limits * @private */ checkRequestSize(req, maxSize) { const contentLength = parseInt(req.headers['content-length'], 10); if (contentLength && contentLength > maxSize) { const error = new Error(`Request too large: ${contentLength} bytes exceeds limit of ${maxSize} bytes`); error.name = 'RequestTooLargeError'; throw error; } } /** * Rate limiting middleware * @private */ createRateLimiter(options = {}) { const windowMs = options.windowMs || 15 * 60 * 1000; // 15 minutes const maxRequests = options.maxRequests || 100; const clients = new Map(); return (req, res, next) => { const clientId = req.ip || req.connection.remoteAddress || 'unknown'; const now = Date.now(); // Clean up old entries for (const [id, data] of clients.entries()) { if (now - data.windowStart > windowMs) { clients.delete(id); } } // Get or create client record let clientData = clients.get(clientId); if (!clientData || (now - clientData.windowStart) > windowMs) { clientData = { windowStart: now, requests: 0 }; clients.set(clientId, clientData); } // Check rate limit if (clientData.requests >= maxRequests) { const error = new Error(`Rate limit exceeded: ${maxRequests} requests per ${windowMs}ms`); error.name = 'RateLimitError'; throw error; } // Increment request count clientData.requests++; // Add rate limit headers res.setHeader('X-RateLimit-Limit', maxRequests); res.setHeader('X-RateLimit-Remaining', Math.max(0, maxRequests - clientData.requests)); res.setHeader('X-RateLimit-Reset', new Date(clientData.windowStart + windowMs).toISOString()); next(); }; } /** * Handle malformed requests * @private */ handleMalformedRequest(error, req, res, requestId) { console.error(`Malformed request [${requestId}]:`, error.message); const errorResponse = { error: 'Bad Request', message: 'Malformed request', requestId, timestamp: new Date().toISOString() }; res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(errorResponse)); } // ============================================================================ // SERVER MONITORING & STATUS // ============================================================================ /** * List all active servers */ listActiveServers() { return Array.from(this.servers.keys()).map(serverId => { const status = this.getServerStatus(serverId); return { serverId: status.serverId, status: status.status, port: status.port, uptime: status.uptime, requests: status.requests, routeCount: status.routeCount, middlewareCount: status.middlewareCount }; }); } /** * Get comprehensive server metrics * @param {string} serverId - Server ID */ getServerMetrics(serverId) { if (!this.servers.has(serverId)) { throw new Error(`Server '${serverId}' not found`); } const serverInfo = this.servers.get(serverId); const routes = this.routes.get(serverId); const middlewares = this.middleware.get(serverId); const now = Date.now(); const uptimeMs = now - serverInfo.startTime.getTime(); const uptimeSeconds = Math.floor(uptimeMs / 1000); return { serverId, timestamp: new Date(), uptime: { milliseconds: uptimeMs, seconds: uptimeSeconds, minutes: Math.floor(uptimeSeconds / 60), hours: Math.floor(uptimeSeconds / 3600), days: Math.floor(uptimeSeconds / 86400), formatted: this.formatUptime(uptimeMs) }, requests: { total: serverInfo.requests, perSecond: serverInfo.requests / Math.max(uptimeSeconds, 1), perMinute: (serverInfo.requests / Math.max(uptimeSeconds, 1)) * 60, perHour: (serverInfo.requests / Math.max(uptimeSeconds, 1)) * 3600 }, connections: { active: serverInfo.activeConnections, total: serverInfo.totalConnections, peak: serverInfo.peakConnections || serverInfo.activeConnections }, routes: routes.map(route => ({ routeId: route.routeId, method: route.method, path: route.path, requests: route.requestCount, errors: route.errorCount, errorRate: route.requestCount > 0 ? (route.errorCount / route.requestCount) * 100 : 0 })), middleware: middlewares.map(middleware => ({ middlewareId: middleware.middlewareId, name: middleware.name, enabled: middleware.enabled, executions: middleware.executionCount, errors: middleware.errorCount, averageDuration: middleware.executionCount > 0 ? (middleware.totalDuration / middleware.executionCount).toFixed(2) : 0 })), memory: process.memoryUsage(), lastActivity: serverInfo.lastActivity }; } /** * Track connection count * @private */ trackConnection(serverId, increment = true) { const serverInfo = this.servers.get(serverId); if (!serverInfo) return; if (increment) { serverInfo.activeConnections++; serverInfo.totalConnections++; serverInfo.peakConnections = Math.max( serverInfo.peakConnections || 0, serverInfo.activeConnections ); } else { serverInfo.activeConnections = Math.max(0, serverInfo.activeConnections - 1); } // Emit connection events this.emit('connectionChange', { serverId, activeConnections: serverInfo.activeConnections, totalConnections: serverInfo.totalConnections, increment }); // Check connection limits if (serverInfo.config.maxConnections && serverInfo.activeConnections >= serverInfo.config.maxConnections) { this.emit('connectionLimitReached', { serverId, activeConnections: serverInfo.activeConnections, maxConnections: serverInfo.config.maxConnections }); } } /** * Check server health * @private */ checkServerHealth(serverId) { const serverInfo = this.servers.get(serverId); if (!serverInfo) { return { status: 'unknown', issues: ['Server not found'] }; } const issues = []; const warnings = []; // Check if server is running if (serverInfo.status !== 'running') { issues.push(`Server status is ${serverInfo.status}`); } // Check last activity (if no requests for 5 minutes, it might be idle) const timeSinceLastActivity = Date.now() - serverInfo.lastActivity.getTime(); if (timeSinceLastActivity > 5 * 60 * 1000) { warnings.push(`No activity for ${Math.floor(timeSinceLastActivity / 60000)} minutes`); } // Check connection count if (serverInfo.config.maxConnections) { const connectionUsage = (serverInfo.activeConnections / serverInfo.config.maxConnections) * 100; if (connectionUsage > 90) { issues.push(`High connection usage: ${connectionUsage.toFixed(1)}%`); } else if (connectionUsage > 75) { warnings.push(`Moderate connection usage: ${connectionUsage.toFixed(1)}%`); } } // Check memory usage const memUsage = process.memoryUsage(); const heapUsage = (memUsage.heapUsed / memUsage.heapTotal) * 100; if (heapUsage > 90) { issues.push(`High memory usage: ${heapUsage.toFixed(1)}%`); } else if (heapUsage > 75) { warnings.push(`Moderate memory usage: ${heapUsage.toFixed(1)}%`); } // Determine overall health status let status = 'healthy'; if (issues.length > 0) { status = 'unhealthy'; } else if (warnings.length > 0) { status = 'warning'; } return { status, issues, warnings, checkedAt: new Date() }; } /** * Create status endpoint for