/** * Nginx Manager for Onebox * * Manages Nginx reverse proxy configurations for services */ import * as plugins from './onebox.plugins.ts'; import { logger } from './onebox.logging.ts'; import { OneboxDatabase } from './onebox.classes.database.ts'; export class OneboxNginxManager { private oneboxRef: any; private database: OneboxDatabase; private configDir = '/etc/nginx/sites-available'; private enabledDir = '/etc/nginx/sites-enabled'; constructor(oneboxRef: any) { this.oneboxRef = oneboxRef; this.database = oneboxRef.database; // Allow custom nginx config directory const customDir = this.database.getSetting('nginxConfigDir'); if (customDir) { this.configDir = customDir; } } /** * Initialize nginx manager */ async init(): Promise { try { // Ensure directories exist await Deno.mkdir(this.configDir, { recursive: true }); await Deno.mkdir(this.enabledDir, { recursive: true }); logger.info('Nginx manager initialized'); } catch (error) { logger.error(`Failed to initialize Nginx manager: ${error.message}`); throw error; } } /** * Create nginx config for a service */ async createConfig(serviceId: number, domain: string, port: number): Promise { try { logger.info(`Creating Nginx config for ${domain}`); const service = this.database.getServiceByID(serviceId); if (!service) { throw new Error(`Service not found: ${serviceId}`); } // Get container IP (or use container name for DNS resolution within Docker network) const containerName = `onebox-${service.name}`; // Generate config const config = this.generateConfig(domain, containerName, port); // Write config file const configPath = `${this.configDir}/onebox-${service.name}.conf`; await Deno.writeTextFile(configPath, config); // Create symlink in sites-enabled const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`; try { await Deno.remove(enabledPath); } catch { // Ignore if doesn't exist } await Deno.symlink(configPath, enabledPath); logger.success(`Nginx config created: ${domain}`); } catch (error) { logger.error(`Failed to create Nginx config: ${error.message}`); throw error; } } /** * Generate nginx configuration */ private generateConfig(domain: string, upstream: string, port: number): string { return `# Onebox-managed configuration for ${domain} # Generated at ${new Date().toISOString()} upstream onebox_${domain.replace(/\./g, '_')} { server ${upstream}:${port}; } # HTTP server (redirect to HTTPS or serve directly) server { listen 80; listen [::]:80; server_name ${domain}; # ACME challenge for Let's Encrypt location /.well-known/acme-challenge/ { root /var/www/certbot; } # Redirect to HTTPS (will be enabled after SSL is configured) # location / { # return 301 https://$server_name$request_uri; # } # Proxy to container (remove after SSL is configured) location / { proxy_pass http://onebox_${domain.replace(/\./g, '_')}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } } # HTTPS server (uncomment after SSL is configured) # server { # listen 443 ssl http2; # listen [::]:443 ssl http2; # server_name ${domain}; # # ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem; # # # SSL configuration # ssl_protocols TLSv1.2 TLSv1.3; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # # location / { # proxy_pass http://onebox_${domain.replace(/\./g, '_')}; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_set_header X-Forwarded-Proto $scheme; # # # WebSocket support # proxy_http_version 1.1; # proxy_set_header Upgrade $http_upgrade; # proxy_set_header Connection "upgrade"; # # # Timeouts # proxy_connect_timeout 60s; # proxy_send_timeout 60s; # proxy_read_timeout 60s; # } # } `; } /** * Update nginx config to enable SSL */ async enableSSL(domain: string): Promise { try { logger.info(`Enabling SSL for ${domain}`); // Find service by domain const services = this.database.getAllServices(); const service = services.find((s) => s.domain === domain); if (!service) { throw new Error(`Service not found for domain: ${domain}`); } const configPath = `${this.configDir}/onebox-${service.name}.conf`; let config = await Deno.readTextFile(configPath); // Enable HTTPS redirect and HTTPS server block config = config.replace('# location / {\n # return 301', 'location / {\n return 301'); config = config.replace('# }', '}'); config = config.replace(/# server \{[\s\S]*?# \}/m, (match) => match.replace(/# /g, '') ); // Comment out HTTP proxy location config = config.replace( /# Proxy to container \(remove after SSL is configured\)[\s\S]*?location \/ \{[\s\S]*?\n \}/, (match) => `# ${match.replace(/\n/g, '\n # ')}` ); await Deno.writeTextFile(configPath, config); logger.success(`SSL enabled for ${domain}`); } catch (error) { logger.error(`Failed to enable SSL for ${domain}: ${error.message}`); throw error; } } /** * Remove nginx config for a service */ async removeConfig(serviceId: number): Promise { try { const service = this.database.getServiceByID(serviceId); if (!service) { throw new Error(`Service not found: ${serviceId}`); } logger.info(`Removing Nginx config for ${service.name}`); // Remove symlink const enabledPath = `${this.enabledDir}/onebox-${service.name}.conf`; try { await Deno.remove(enabledPath); } catch { // Ignore if doesn't exist } // Remove config file const configPath = `${this.configDir}/onebox-${service.name}.conf`; try { await Deno.remove(configPath); } catch { // Ignore if doesn't exist } logger.success(`Nginx config removed for ${service.name}`); } catch (error) { logger.error(`Failed to remove Nginx config: ${error.message}`); throw error; } } /** * Test nginx configuration */ async test(): Promise { try { const command = new Deno.Command('nginx', { args: ['-t'], stdout: 'piped', stderr: 'piped', }); const { code, stderr } = await command.output(); if (code !== 0) { const errorMsg = new TextDecoder().decode(stderr); logger.error(`Nginx config test failed: ${errorMsg}`); return false; } logger.success('Nginx configuration is valid'); return true; } catch (error) { logger.error(`Failed to test Nginx config: ${error.message}`); return false; } } /** * Reload nginx */ async reload(): Promise { try { // Test config first const isValid = await this.test(); if (!isValid) { throw new Error('Nginx configuration is invalid'); } logger.info('Reloading Nginx...'); const command = new Deno.Command('systemctl', { args: ['reload', 'nginx'], stdout: 'piped', stderr: 'piped', }); const { code, stderr } = await command.output(); if (code !== 0) { const errorMsg = new TextDecoder().decode(stderr); throw new Error(`Nginx reload failed: ${errorMsg}`); } logger.success('Nginx reloaded successfully'); } catch (error) { logger.error(`Failed to reload Nginx: ${error.message}`); throw error; } } /** * Get nginx status */ async getStatus(): Promise { try { const command = new Deno.Command('systemctl', { args: ['status', 'nginx'], stdout: 'piped', stderr: 'piped', }); const { code, stdout } = await command.output(); const output = new TextDecoder().decode(stdout); if (code === 0 || output.includes('active (running)')) { return 'running'; } else if (output.includes('inactive') || output.includes('dead')) { return 'stopped'; } else if (output.includes('failed')) { return 'failed'; } else { return 'unknown'; } } catch (error) { logger.error(`Failed to get Nginx status: ${error.message}`); return 'unknown'; } } /** * Check if nginx is installed */ async isInstalled(): Promise { try { const command = new Deno.Command('which', { args: ['nginx'], stdout: 'piped', stderr: 'piped', }); const { code } = await command.output(); return code === 0; } catch { return false; } } }