feat(ssl): Add domain & certificate management, Cloudflare sync, SQLite cert manager, WebSocket realtime updates, and HTTP API SSL endpoints
This commit is contained in:
@@ -7,11 +7,13 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { OneboxDatabase } from './database.ts';
|
||||
import { SqliteCertManager } from './certmanager.ts';
|
||||
|
||||
export class OneboxSslManager {
|
||||
private oneboxRef: any;
|
||||
private database: OneboxDatabase;
|
||||
private smartacme: plugins.smartacme.SmartAcme | null = null;
|
||||
private certManager: SqliteCertManager | null = null;
|
||||
private acmeEmail: string | null = null;
|
||||
|
||||
constructor(oneboxRef: any) {
|
||||
@@ -35,14 +37,45 @@ export class OneboxSslManager {
|
||||
|
||||
this.acmeEmail = acmeEmail;
|
||||
|
||||
// Initialize SmartACME
|
||||
// Get Cloudflare API key (reuse from DNS manager)
|
||||
const cfApiKey = this.database.getSetting('cloudflareAPIKey');
|
||||
|
||||
if (!cfApiKey) {
|
||||
logger.warn('Cloudflare API key not configured. SSL certificate management will be limited.');
|
||||
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get ACME environment (default: production)
|
||||
const acmeEnvironment = this.database.getSetting('acmeEnvironment') || 'production';
|
||||
|
||||
if (!['production', 'integration'].includes(acmeEnvironment)) {
|
||||
throw new Error('acmeEnvironment must be "production" or "integration"');
|
||||
}
|
||||
|
||||
// Initialize certificate manager
|
||||
this.certManager = new SqliteCertManager(this.database);
|
||||
await this.certManager.init();
|
||||
|
||||
// Initialize Cloudflare DNS provider for DNS-01 challenge
|
||||
const cfAccount = new plugins.cloudflare.CloudflareAccount(cfApiKey);
|
||||
|
||||
// Create DNS-01 challenge handler
|
||||
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cfAccount);
|
||||
|
||||
// Initialize SmartACME with proper configuration
|
||||
this.smartacme = new plugins.smartacme.SmartAcme({
|
||||
email: acmeEmail,
|
||||
environment: 'production', // or 'staging' for testing
|
||||
dns: 'cloudflare', // Use Cloudflare DNS challenge
|
||||
accountEmail: acmeEmail,
|
||||
certManager: this.certManager,
|
||||
environment: acmeEnvironment as 'production' | 'integration',
|
||||
challengeHandlers: [dns01Handler],
|
||||
challengePriority: ['dns-01'], // Prefer DNS-01 challenges
|
||||
});
|
||||
|
||||
logger.info('SSL manager initialized with SmartACME');
|
||||
// Start SmartACME
|
||||
await this.smartacme.start();
|
||||
|
||||
logger.success('SSL manager initialized with SmartACME DNS-01 challenge');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize SSL manager: ${error.message}`);
|
||||
throw error;
|
||||
@@ -57,59 +90,33 @@ export class OneboxSslManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain SSL certificate for a domain
|
||||
* Obtain SSL certificate for a domain using SmartACME
|
||||
*/
|
||||
async obtainCertificate(domain: string): Promise<void> {
|
||||
async obtainCertificate(domain: string, includeWildcard = false): Promise<void> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('SSL manager not configured');
|
||||
}
|
||||
|
||||
logger.info(`Obtaining SSL certificate for ${domain}...`);
|
||||
logger.info(`Obtaining SSL certificate for ${domain} via SmartACME DNS-01...`);
|
||||
|
||||
// Check if certificate already exists and is valid
|
||||
const existing = this.database.getSSLCertificate(domain);
|
||||
if (existing && existing.expiryDate > Date.now()) {
|
||||
const existingCert = await this.certManager!.retrieveCertificate(domain);
|
||||
if (existingCert && existingCert.isStillValid()) {
|
||||
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);
|
||||
// Use SmartACME to obtain certificate via DNS-01 challenge
|
||||
const cert = await this.smartacme!.getCertificateForDomain(domain, {
|
||||
includeWildcard,
|
||||
});
|
||||
|
||||
// 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(),
|
||||
});
|
||||
}
|
||||
logger.success(`SSL certificate obtained for ${domain}`);
|
||||
logger.info(`Certificate valid until: ${new Date(cert.validUntil).toISOString()}`);
|
||||
|
||||
// Reload certificates in reverse proxy
|
||||
await this.oneboxRef.reverseProxy.reloadCertificates();
|
||||
|
||||
logger.success(`SSL certificate obtained for ${domain}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to obtain certificate for ${domain}: ${error.message}`);
|
||||
throw error;
|
||||
@@ -155,32 +162,21 @@ export class OneboxSslManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renew certificate for a domain
|
||||
* Renew certificate for a domain using SmartACME
|
||||
*/
|
||||
async renewCertificate(domain: string): Promise<void> {
|
||||
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}`);
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error('SSL manager not configured');
|
||||
}
|
||||
|
||||
// Update database
|
||||
const expiryDate = Date.now() + 90 * 24 * 60 * 60 * 1000;
|
||||
this.database.updateSSLCertificate(domain, {
|
||||
expiryDate,
|
||||
});
|
||||
logger.info(`Renewing SSL certificate for ${domain} via SmartACME...`);
|
||||
|
||||
// SmartACME will check if renewal is needed and obtain a new certificate
|
||||
const cert = await this.smartacme!.getCertificateForDomain(domain);
|
||||
|
||||
logger.success(`Certificate renewed for ${domain}`);
|
||||
logger.info(`Certificate valid until: ${new Date(cert.validUntil).toISOString()}`);
|
||||
|
||||
// Reload certificates in reverse proxy
|
||||
await this.oneboxRef.reverseProxy.reloadCertificates();
|
||||
@@ -209,21 +205,27 @@ export class OneboxSslManager {
|
||||
*/
|
||||
async renewExpiring(): Promise<void> {
|
||||
try {
|
||||
if (!this.isConfigured()) {
|
||||
logger.warn('SSL manager not configured, skipping renewal check');
|
||||
return;
|
||||
}
|
||||
|
||||
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...`);
|
||||
for (const dbCert of certificates) {
|
||||
try {
|
||||
// Retrieve certificate from cert manager to check if it should be renewed
|
||||
const cert = await this.certManager!.retrieveCertificate(dbCert.domain);
|
||||
|
||||
try {
|
||||
await this.renewCertificate(cert.domain);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to renew ${cert.domain}: ${error.message}`);
|
||||
// Continue with other certificates
|
||||
if (cert && cert.shouldBeRenewed()) {
|
||||
logger.info(`Certificate for ${dbCert.domain} needs renewal, renewing...`);
|
||||
await this.renewCertificate(dbCert.domain);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to renew ${dbCert.domain}: ${error.message}`);
|
||||
// Continue with other certificates
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user