/** * SQLite-based Certificate Manager for SmartACME * * Implements ICertManager interface to store SSL certificates in SQLite database * and write PEM files to filesystem for use by the reverse proxy. */ import * as plugins from '../plugins.ts'; import { logger } from '../logging.ts'; import { getErrorMessage } from '../utils/error.ts'; import { OneboxDatabase } from './database.ts'; export class SqliteCertManager implements plugins.smartacme.ICertManager { private database: OneboxDatabase; private certBasePath: string; constructor(database: OneboxDatabase, certBasePath = './.nogit/ssl/live') { this.database = database; this.certBasePath = certBasePath; } /** * Initialize the certificate manager */ async init(): Promise { try { // Ensure certificate directory exists await Deno.mkdir(this.certBasePath, { recursive: true }); logger.info(`Certificate manager initialized (path: ${this.certBasePath})`); } catch (error) { logger.error(`Failed to initialize certificate manager: ${getErrorMessage(error)}`); throw error; } } /** * Retrieve a certificate by domain name */ async retrieveCertificate(domainName: string): Promise { try { const dbCert = this.database.getSSLCertificate(domainName); if (!dbCert) { return null; } // Convert database format to SmartacmeCert format const cert = new plugins.smartacme.Cert({ id: dbCert.id?.toString() || domainName, domainName: dbCert.domain, created: dbCert.createdAt, privateKey: await this.readPemFile(dbCert.keyPath), publicKey: await this.readPemFile(dbCert.fullChainPath), // Full chain as public key csr: '', // CSR not stored separately validUntil: dbCert.expiryDate, }); return cert; } catch (error) { logger.warn(`Failed to retrieve certificate for ${domainName}: ${getErrorMessage(error)}`); return null; } } /** * Store a certificate */ async storeCertificate(cert: plugins.smartacme.Cert): Promise { try { const domain = cert.domainName; const domainPath = `${this.certBasePath}/${domain}`; // Create domain-specific directory await Deno.mkdir(domainPath, { recursive: true }); // Write PEM files const keyPath = `${domainPath}/privkey.pem`; const certPath = `${domainPath}/cert.pem`; const fullChainPath = `${domainPath}/fullchain.pem`; await Deno.writeTextFile(keyPath, cert.privateKey); await Deno.writeTextFile(fullChainPath, cert.publicKey); // Extract certificate from full chain (first certificate in the chain) const certOnly = this.extractCertFromChain(cert.publicKey); await Deno.writeTextFile(certPath, certOnly); // Store/update in database const existing = this.database.getSSLCertificate(domain); if (existing) { this.database.updateSSLCertificate(domain, { certPath, keyPath, fullChainPath, expiryDate: cert.validUntil, updatedAt: Date.now(), }); } else { await this.database.createSSLCertificate({ domain, certPath, keyPath, fullChainPath, expiryDate: cert.validUntil, issuer: 'Let\'s Encrypt', createdAt: cert.created, updatedAt: Date.now(), }); } logger.success(`Certificate stored for ${domain}`); } catch (error) { logger.error(`Failed to store certificate for ${cert.domainName}: ${getErrorMessage(error)}`); throw error; } } /** * Delete a certificate */ async deleteCertificate(domainName: string): Promise { try { const dbCert = this.database.getSSLCertificate(domainName); if (dbCert) { // Delete PEM files const domainPath = `${this.certBasePath}/${domainName}`; try { await Deno.remove(domainPath, { recursive: true }); } catch (error) { logger.warn(`Failed to delete PEM files for ${domainName}: ${getErrorMessage(error)}`); } // Delete from database this.database.deleteSSLCertificate(domainName); logger.info(`Certificate deleted for ${domainName}`); } } catch (error) { logger.error(`Failed to delete certificate for ${domainName}: ${getErrorMessage(error)}`); throw error; } } /** * Close the certificate manager */ async close(): Promise { // SQLite database is managed by OneboxDatabase, nothing to close here logger.info('Certificate manager closed'); } /** * Wipe all certificates (for testing) */ async wipe(): Promise { try { const certs = this.database.getAllSSLCertificates(); for (const cert of certs) { await this.deleteCertificate(cert.domain); } logger.warn('All certificates wiped'); } catch (error) { logger.error(`Failed to wipe certificates: ${getErrorMessage(error)}`); throw error; } } /** * Read PEM file from filesystem */ private async readPemFile(path: string): Promise { try { return await Deno.readTextFile(path); } catch (error) { throw new Error(`Failed to read PEM file ${path}: ${getErrorMessage(error)}`); } } /** * Extract the first certificate from a PEM chain */ private extractCertFromChain(fullChain: string): string { const certMatch = fullChain.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/); return certMatch ? certMatch[0] : fullChain; } }