/** * SSL Manager for Onebox * * Manages SSL certificates via Let's Encrypt (using smartacme) */ import * as plugins from './onebox.plugins.ts'; import { logger } from './onebox.logging.ts'; import { OneboxDatabase } from './onebox.classes.database.ts'; export class OneboxSslManager { private oneboxRef: any; private database: OneboxDatabase; private smartacme: plugins.smartacme.SmartAcme | null = null; private acmeEmail: string | null = null; constructor(oneboxRef: any) { this.oneboxRef = oneboxRef; this.database = oneboxRef.database; } /** * Initialize SSL manager */ async init(): Promise { try { // Get ACME email from settings const acmeEmail = this.database.getSetting('acmeEmail'); if (!acmeEmail) { logger.warn('ACME email not configured. SSL certificate management will be limited.'); logger.info('Configure with: onebox config set acmeEmail '); return; } this.acmeEmail = acmeEmail; // Initialize SmartACME this.smartacme = new plugins.smartacme.SmartAcme({ email: acmeEmail, environment: 'production', // or 'staging' for testing dns: 'cloudflare', // Use Cloudflare DNS challenge }); logger.info('SSL manager initialized with SmartACME'); } catch (error) { logger.error(`Failed to initialize SSL manager: ${error.message}`); throw error; } } /** * Check if SSL manager is configured */ isConfigured(): boolean { return this.smartacme !== null && this.acmeEmail !== null; } /** * Obtain SSL certificate for a domain */ async obtainCertificate(domain: string): Promise { try { if (!this.isConfigured()) { throw new Error('SSL manager not configured'); } logger.info(`Obtaining SSL certificate for ${domain}...`); // Check if certificate already exists and is valid const existing = this.database.getSSLCertificate(domain); if (existing && existing.expiryDate > Date.now()) { logger.info(`Valid certificate already exists for ${domain}`); return; } // Use certbot for now (smartacme integration would be more complex) // This is a simplified version - in production, use proper ACME client await this.obtainCertificateWithCertbot(domain); // Store in database const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`; const keyPath = `/etc/letsencrypt/live/${domain}/privkey.pem`; const fullChainPath = `/etc/letsencrypt/live/${domain}/fullchain.pem`; // Get expiry date (90 days from now for Let's Encrypt) const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000; if (existing) { this.database.updateSSLCertificate(domain, { certPath, keyPath, fullChainPath, expiryDate, }); } else { await this.database.createSSLCertificate({ domain, certPath, keyPath, fullChainPath, expiryDate, issuer: 'Let\'s Encrypt', createdAt: Date.now(), updatedAt: Date.now(), }); } // Enable SSL in nginx config await this.oneboxRef.nginx.enableSSL(domain); logger.success(`SSL certificate obtained for ${domain}`); } catch (error) { logger.error(`Failed to obtain certificate for ${domain}: ${error.message}`); throw error; } } /** * Obtain certificate using certbot */ private async obtainCertificateWithCertbot(domain: string): Promise { try { logger.info(`Running certbot for ${domain}...`); // Use webroot method (nginx serves .well-known/acme-challenge) const command = new Deno.Command('certbot', { args: [ 'certonly', '--webroot', '--webroot-path=/var/www/certbot', '--email', this.acmeEmail!, '--agree-tos', '--no-eff-email', '--domain', domain, '--non-interactive', ], stdout: 'piped', stderr: 'piped', }); const { code, stderr } = await command.output(); if (code !== 0) { const errorMsg = new TextDecoder().decode(stderr); throw new Error(`Certbot failed: ${errorMsg}`); } logger.success(`Certbot obtained certificate for ${domain}`); } catch (error) { throw new Error(`Failed to run certbot: ${error.message}`); } } /** * Renew certificate for a domain */ async renewCertificate(domain: string): Promise { try { logger.info(`Renewing SSL certificate for ${domain}...`); const command = new Deno.Command('certbot', { args: ['renew', '--cert-name', domain, '--non-interactive'], stdout: 'piped', stderr: 'piped', }); const { code, stderr } = await command.output(); if (code !== 0) { const errorMsg = new TextDecoder().decode(stderr); throw new Error(`Certbot renewal failed: ${errorMsg}`); } // Update database const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000; this.database.updateSSLCertificate(domain, { expiryDate, }); logger.success(`Certificate renewed for ${domain}`); // Reload nginx await this.oneboxRef.nginx.reload(); } catch (error) { logger.error(`Failed to renew certificate for ${domain}: ${error.message}`); throw error; } } /** * List all certificates */ listCertificates() { return this.database.getAllSSLCertificates(); } /** * Get certificate info for a domain */ getCertificate(domain: string) { return this.database.getSSLCertificate(domain); } /** * Check certificates that are expiring soon and renew them */ async renewExpiring(): Promise { try { logger.info('Checking for expiring certificates...'); const certificates = this.listCertificates(); const thirtyDaysFromNow = Date.now() + 30 * 24 * 60 * 60 * 1000; for (const cert of certificates) { if (cert.expiryDate < thirtyDaysFromNow) { logger.info(`Certificate for ${cert.domain} expires soon, renewing...`); try { await this.renewCertificate(cert.domain); } catch (error) { logger.error(`Failed to renew ${cert.domain}: ${error.message}`); // Continue with other certificates } } } logger.success('Certificate renewal check complete'); } catch (error) { logger.error(`Failed to check expiring certificates: ${error.message}`); throw error; } } /** * Force renewal of all certificates */ async renewAll(): Promise { try { logger.info('Renewing all certificates...'); const command = new Deno.Command('certbot', { args: ['renew', '--force-renewal', '--non-interactive'], stdout: 'piped', stderr: 'piped', }); const { code, stderr } = await command.output(); if (code !== 0) { const errorMsg = new TextDecoder().decode(stderr); throw new Error(`Certbot renewal failed: ${errorMsg}`); } logger.success('All certificates renewed'); // Reload nginx await this.oneboxRef.nginx.reload(); } catch (error) { logger.error(`Failed to renew all certificates: ${error.message}`); throw error; } } /** * Check if certbot is installed */ async isCertbotInstalled(): Promise { try { const command = new Deno.Command('which', { args: ['certbot'], stdout: 'piped', stderr: 'piped', }); const { code } = await command.output(); return code === 0; } catch { return false; } } /** * Get certificate expiry date from file */ async getCertificateExpiry(domain: string): Promise { try { const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`; const command = new Deno.Command('openssl', { args: ['x509', '-enddate', '-noout', '-in', certPath], stdout: 'piped', stderr: 'piped', }); const { code, stdout } = await command.output(); if (code !== 0) { return null; } const output = new TextDecoder().decode(stdout); const match = output.match(/notAfter=(.+)/); if (match) { return new Date(match[1]); } return null; } catch (error) { logger.error(`Failed to get certificate expiry for ${domain}: ${error.message}`); return null; } } }