import * as fs from 'fs'; import * as path from 'path'; import * as plugins from '../../plugins.js'; import type { CertificateData, Certificates } from '../models/certificate-types.js'; import { ensureCertificateDirectory } from '../utils/certificate-helpers.js'; /** * FileStorage provides file system storage for certificates */ export class FileStorage { private storageDir: string; /** * Creates a new file storage provider * @param storageDir Directory to store certificates */ constructor(storageDir: string) { this.storageDir = path.resolve(storageDir); ensureCertificateDirectory(this.storageDir); } /** * Save a certificate to the file system * @param domain Domain name * @param certData Certificate data to save */ public async saveCertificate(domain: string, certData: CertificateData): Promise { const sanitizedDomain = this.sanitizeDomain(domain); const certDir = path.join(this.storageDir, sanitizedDomain); ensureCertificateDirectory(certDir); const certPath = path.join(certDir, 'fullchain.pem'); const keyPath = path.join(certDir, 'privkey.pem'); const metaPath = path.join(certDir, 'metadata.json'); // Write certificate and private key await fs.promises.writeFile(certPath, certData.certificate, 'utf8'); await fs.promises.writeFile(keyPath, certData.privateKey, 'utf8'); // Write metadata const metadata = { domain: certData.domain, expiryDate: certData.expiryDate.toISOString(), source: certData.source || 'unknown', issuedAt: new Date().toISOString() }; await fs.promises.writeFile( metaPath, JSON.stringify(metadata, null, 2), 'utf8' ); } /** * Load a certificate from the file system * @param domain Domain name * @returns Certificate data if found, null otherwise */ public async loadCertificate(domain: string): Promise { const sanitizedDomain = this.sanitizeDomain(domain); const certDir = path.join(this.storageDir, sanitizedDomain); if (!fs.existsSync(certDir)) { return null; } const certPath = path.join(certDir, 'fullchain.pem'); const keyPath = path.join(certDir, 'privkey.pem'); const metaPath = path.join(certDir, 'metadata.json'); try { // Check if all required files exist if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) { return null; } // Read certificate and private key const certificate = await fs.promises.readFile(certPath, 'utf8'); const privateKey = await fs.promises.readFile(keyPath, 'utf8'); // Try to read metadata if available let expiryDate = new Date(); let source: 'static' | 'http01' | 'dns01' | undefined; if (fs.existsSync(metaPath)) { const metaContent = await fs.promises.readFile(metaPath, 'utf8'); const metadata = JSON.parse(metaContent); if (metadata.expiryDate) { expiryDate = new Date(metadata.expiryDate); } if (metadata.source) { source = metadata.source as 'static' | 'http01' | 'dns01'; } } return { domain, certificate, privateKey, expiryDate, source }; } catch (error) { console.error(`Error loading certificate for ${domain}:`, error); return null; } } /** * Delete a certificate from the file system * @param domain Domain name */ public async deleteCertificate(domain: string): Promise { const sanitizedDomain = this.sanitizeDomain(domain); const certDir = path.join(this.storageDir, sanitizedDomain); if (!fs.existsSync(certDir)) { return false; } try { // Recursively delete the certificate directory await this.deleteDirectory(certDir); return true; } catch (error) { console.error(`Error deleting certificate for ${domain}:`, error); return false; } } /** * List all domains with stored certificates * @returns Array of domain names */ public async listCertificates(): Promise { try { const entries = await fs.promises.readdir(this.storageDir, { withFileTypes: true }); return entries .filter(entry => entry.isDirectory()) .map(entry => entry.name); } catch (error) { console.error('Error listing certificates:', error); return []; } } /** * Check if a certificate is expiring soon * @param domain Domain name * @param thresholdDays Days threshold to consider expiring * @returns Information about expiring certificate or null */ public async isExpiringSoon( domain: string, thresholdDays: number = 30 ): Promise<{ domain: string; expiryDate: Date; daysRemaining: number } | null> { const certData = await this.loadCertificate(domain); if (!certData) { return null; } const now = new Date(); const expiryDate = certData.expiryDate; const timeRemaining = expiryDate.getTime() - now.getTime(); const daysRemaining = Math.floor(timeRemaining / (1000 * 60 * 60 * 24)); if (daysRemaining <= thresholdDays) { return { domain, expiryDate, daysRemaining }; } return null; } /** * Check all certificates for expiration * @param thresholdDays Days threshold to consider expiring * @returns List of expiring certificates */ public async getExpiringCertificates( thresholdDays: number = 30 ): Promise> { const domains = await this.listCertificates(); const expiringCerts = []; for (const domain of domains) { const expiring = await this.isExpiringSoon(domain, thresholdDays); if (expiring) { expiringCerts.push(expiring); } } return expiringCerts; } /** * Delete a directory recursively * @param directoryPath Directory to delete */ private async deleteDirectory(directoryPath: string): Promise { if (fs.existsSync(directoryPath)) { const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(directoryPath, entry.name); if (entry.isDirectory()) { await this.deleteDirectory(fullPath); } else { await fs.promises.unlink(fullPath); } } await fs.promises.rmdir(directoryPath); } } /** * Sanitize a domain name for use as a directory name * @param domain Domain name * @returns Sanitized domain name */ private sanitizeDomain(domain: string): string { // Replace wildcard and any invalid filesystem characters return domain.replace(/\*/g, '_wildcard_').replace(/[/\\:*?"<>|]/g, '_'); } }